mirror of
https://git.madhouse-project.org/algernon/iocaine.git
synced 2025-03-10 09:18:49 +01:00
Make the generated HTML templatable
Use Handlebars to generate the HTML output, to make it easier to change. Signed-off-by: Gergely Nagy <me@gergo.csillger.hu>
This commit is contained in:
parent
b66a60ab30
commit
5d60b84300
7 changed files with 342 additions and 11 deletions
264
Cargo.lock
generated
264
Cargo.lock
generated
|
@ -278,6 +278,16 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.20.0"
|
||||
|
@ -430,6 +440,72 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
@ -566,6 +642,19 @@ version = "0.31.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.7"
|
||||
|
@ -585,6 +674,24 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "handlebars"
|
||||
version = "6.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6b224b95c1e668ac0270325ad563b2eef1469fbbb8959bc7c692c844b813d9"
|
||||
dependencies = [
|
||||
"derive_builder",
|
||||
"log",
|
||||
"num-order",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
|
@ -721,6 +828,12 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
|
@ -757,8 +870,10 @@ dependencies = [
|
|||
"console-subscriber",
|
||||
"figment",
|
||||
"figment_file_provider_adapter",
|
||||
"handlebars",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"sha2",
|
||||
"tokio",
|
||||
|
@ -885,6 +1000,21 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-modular"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
|
||||
|
||||
[[package]]
|
||||
name = "num-order"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
|
||||
dependencies = [
|
||||
"num-modular",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
|
@ -944,6 +1074,51 @@ version = "2.3.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"pest",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.7"
|
||||
|
@ -1122,6 +1297,41 @@ version = "0.8.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
|
||||
dependencies = [
|
||||
"globset",
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
|
@ -1140,6 +1350,15 @@ version = "1.0.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.217"
|
||||
|
@ -1280,6 +1499,26 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.8"
|
||||
|
@ -1558,6 +1797,12 @@ version = "1.17.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||
|
||||
[[package]]
|
||||
name = "uncased"
|
||||
version = "0.9.10"
|
||||
|
@ -1591,6 +1836,16 @@ version = "0.9.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
|
@ -1622,6 +1877,15 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
|
|
|
@ -19,8 +19,10 @@
|
|||
console-subscriber = { version = "0.4.1", optional = true }
|
||||
figment = { version = "0.10.19", features = ["toml", "env"] }
|
||||
figment_file_provider_adapter = "0.1.1"
|
||||
handlebars = { version = "6.3.0", features = ["dir_source", "rust-embed"] }
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rust-embed = "8.5.0"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
sha2 = "0.10.8"
|
||||
tokio = { version = "1.42.0", features = [
|
||||
|
|
|
@ -21,7 +21,7 @@ SPDX-PackageDownloadLocation = "https://git.madhouse-project.org/algernon/iocain
|
|||
SPDX-License-Identifier = "MIT"
|
||||
|
||||
[[annotations]]
|
||||
path = ["docs/templates/**", "docs/sass/**"]
|
||||
path = ["docs/templates/**", "docs/sass/**", "templates/*.hbs"]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2025 Gergely Nagy"
|
||||
SPDX-License-Identifier = "MIT"
|
||||
|
|
|
@ -53,8 +53,13 @@ min = 2
|
|||
max = 5
|
||||
backlink = true
|
||||
|
||||
[generator.template]
|
||||
#directory =
|
||||
|
||||
[generator]
|
||||
initial_seed = ""
|
||||
```
|
||||
|
||||
When configuring through environment variables, these settings are available via `IOCAINE__GENERATOR__MARKOV__PARAGRAPHS__MIN`, `IOCAINE__GENERATOR__MARKOV__PARAGRAPHS_MAX`, `IOCAINE__GENERATOR__MARKOV__WORDS__MIN`, `IOCAINE__GENERATOR__MARKOV__WORDS__MAX`, `IOCAINE__GENERATOR__LINKS__MIN`, `IOCAINE__GENERATOR__LINKS__MAX`, and `IOCAINE__GENERATOR__LINKS__BACKLINK`, `IOCAINE__GENERATOR__INITIAL_SEED` respectively.
|
||||
The `[generator.template].directory` property can be set to a directory containing custom templates. If not set (the default), `iocaine` will use its own [default template](https://git.madhouse-project.org/algernon/iocaine/src/branch/main/templates/main.hbs). If configured, the directory **must** contain a `main.hbs` file, which will be used as the template for all generated pages. See the default template for hints, for now.
|
||||
|
||||
When configuring through environment variables, these settings are available via `IOCAINE__GENERATOR__MARKOV__PARAGRAPHS__MIN`, `IOCAINE__GENERATOR__MARKOV__PARAGRAPHS_MAX`, `IOCAINE__GENERATOR__MARKOV__WORDS__MIN`, `IOCAINE__GENERATOR__MARKOV__WORDS__MAX`, `IOCAINE__GENERATOR__LINKS__MIN`, `IOCAINE__GENERATOR__LINKS__MAX`, and `IOCAINE__GENERATOR__LINKS__BACKLINK`, `IOCAINE__GENERATOR__TEMPLATE__DIRECTORY`, `IOCAINE__GENERATOR__INITIAL_SEED`, respectively.
|
||||
|
|
|
@ -10,7 +10,10 @@ use axum::{
|
|||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use handlebars::Handlebars;
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
use rust_embed::Embed;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::iocaine::{
|
||||
config::Config,
|
||||
|
@ -26,6 +29,11 @@ pub struct Iocaine {
|
|||
words: GargleBargle,
|
||||
}
|
||||
|
||||
#[derive(Embed)]
|
||||
#[folder = "templates/"]
|
||||
#[include = "*.hbs"]
|
||||
struct Asset;
|
||||
|
||||
impl Iocaine {
|
||||
pub fn new(config: Config) -> Result<Self> {
|
||||
let mut chain = WurstsalatGeneratorPro::new();
|
||||
|
@ -87,6 +95,20 @@ fn poison(iocaine: &Iocaine, headers: axum::http::HeaderMap, path: &str) -> Html
|
|||
.unwrap();
|
||||
let initial_seed = &iocaine.config.generator.initial_seed;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Link<'a> {
|
||||
href: &'a str,
|
||||
title: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Page<'a> {
|
||||
pub request_uri: &'a str,
|
||||
pub backlink: &'static str,
|
||||
pub garbage: Vec<String>,
|
||||
pub links: Vec<Link<'a>>,
|
||||
}
|
||||
|
||||
// Generate paragraphs
|
||||
|
||||
let mut rng = GobbledyGook::for_url(format!("markov://{}/{}#{}", host, path, &initial_seed));
|
||||
|
@ -116,14 +138,13 @@ fn poison(iocaine: &Iocaine, headers: axum::http::HeaderMap, path: &str) -> Html
|
|||
.clone()
|
||||
.skip(i * iocaine.config.generator.markov.words.max as usize)
|
||||
.take(word_count as usize);
|
||||
format!("<p>{}</p>", join_words(words))
|
||||
join_words(words)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
// Generate links
|
||||
|
||||
let mut links = String::new();
|
||||
let mut links = Vec::new();
|
||||
let rng = GobbledyGook::for_url(format!("titles://{}/{}#{}", host, path, initial_seed));
|
||||
let titles = iocaine
|
||||
.chain
|
||||
|
@ -139,7 +160,7 @@ fn poison(iocaine: &Iocaine, headers: axum::http::HeaderMap, path: &str) -> Html
|
|||
|
||||
let title = titles.clone().skip(i as usize * 5).take(5);
|
||||
let title = join_words(title);
|
||||
links.push_str(&format!("<li><a href=\"{}/\">{}</a></li>", word, title))
|
||||
links.push(Link { href: word, title });
|
||||
}
|
||||
|
||||
let backlink = if iocaine.config.generator.links.backlink {
|
||||
|
@ -148,10 +169,25 @@ fn poison(iocaine: &Iocaine, headers: axum::http::HeaderMap, path: &str) -> Html
|
|||
""
|
||||
};
|
||||
|
||||
Html(format!(
|
||||
"<!doctype html><html><head><title>{}</title></head><body>{}{}<ul>{}</ul></body></html>",
|
||||
path, backlink, markov, links
|
||||
))
|
||||
let mut handlebars = Handlebars::new();
|
||||
if let Some(dir) = &iocaine.config.generator.template.directory {
|
||||
handlebars
|
||||
.register_templates_directory(dir, handlebars::DirectorySourceOptions::default())
|
||||
.unwrap();
|
||||
} else {
|
||||
handlebars
|
||||
.register_embed_templates_with_extension::<Asset>(".hbs")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let data = Page {
|
||||
request_uri: path,
|
||||
backlink,
|
||||
garbage: markov,
|
||||
links,
|
||||
};
|
||||
|
||||
Html(handlebars.render("main", &data).unwrap())
|
||||
}
|
||||
|
||||
async fn poison_root(
|
||||
|
|
|
@ -40,6 +40,8 @@ pub struct GeneratorConfig {
|
|||
pub links: LinkGeneratorConfig,
|
||||
#[serde(default)]
|
||||
pub initial_seed: String,
|
||||
#[serde(default)]
|
||||
pub template: GeneratorTemplateConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
|
@ -133,3 +135,8 @@ impl LinkGeneratorConfig {
|
|||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct GeneratorTemplateConfig {
|
||||
pub directory: Option<String>,
|
||||
}
|
||||
|
|
17
templates/main.hbs
Normal file
17
templates/main.hbs
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ request_uri }}</title>
|
||||
</head>
|
||||
<body>
|
||||
{{{ backlink }}}
|
||||
{{#each garbage}}
|
||||
<p>{{{ this }}}</p>
|
||||
{{/each}}
|
||||
<ul>
|
||||
{{#each links}}
|
||||
<li><a href=\"{{ this.href }}\">{{ this.title }}</a></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue