mod all;
mod backlinks;
mod collection;
mod dir;
mod dirtree;
mod doc;
mod entries;
mod filter_in_dir;
mod macros;
mod markdown;
mod omit_docs;
mod related;
mod taxonomy;
mod text;
mod url;
use crate::build::markup::wikilink::build_stem_index;
use crate::config::{self, Config};
use crate::doc::DocMeta;
use crate::doc_index::DocIndex;
use anyhow::{Context, Result};
use comrak::plugins::syntect::{SyntectAdapter, SyntectAdapterBuilder};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, LazyLock};
use tera::Tera;
static SYNTECT: LazyLock<Arc<SyntectAdapter>> =
LazyLock::new(|| Arc::new(SyntectAdapterBuilder::new().theme("InspiredGitHub").build()));
#[derive(Clone)]
pub struct MarkupEnv {
pub tera: Tera,
pub macro_preamble: String,
pub options: comrak::Options<'static>,
pub syntect: Arc<SyntectAdapter>,
pub stem_index: HashMap<String, Vec<DocMeta>>,
pub hashtags: bool,
}
fn markup_options() -> comrak::Options<'static> {
let mut options = comrak::Options::default();
options.render.r#unsafe = true;
options.extension.wikilinks_title_after_pipe = true;
options.extension.table = true;
options.extension.strikethrough = true;
options.extension.tasklist = true;
options.extension.footnotes = true;
options.extension.autolink = true;
options
}
pub fn build_markup_env(config: &Config, docs: Arc<Vec<DocMeta>>) -> Result<MarkupEnv> {
let (mut tera, template_names) = load_templates(config)?;
let stem_index = build_stem_index(&docs);
let options = markup_options();
url::register(
&mut tera,
url::lookup_from_metas(docs),
config.site_url.clone(),
config.base_path.clone(),
);
text::register(&mut tera);
entries::register(&mut tera);
dir::register(&mut tera);
dirtree::register(&mut tera);
filter_in_dir::register(&mut tera);
omit_docs::register(&mut tera);
markdown::register(&mut tera, options.clone(), SYNTECT.clone());
let macro_preamble = macros::macro_preamble(&template_names);
Ok(MarkupEnv {
tera,
macro_preamble,
options,
syntect: SYNTECT.clone(),
stem_index,
hashtags: config.hashtags,
})
}
pub fn build_template_env(config: &Config, index: Arc<DocIndex>) -> Result<Tera> {
let (mut env, _template_names) = load_templates(config)?;
all::register(&mut env, index.clone());
collection::register(&mut env, index.clone());
doc::register(&mut env, index.clone());
taxonomy::register(&mut env, index.clone());
backlinks::register(&mut env, index.clone());
related::register(&mut env, index.clone(), config.related.clone());
url::register(
&mut env,
url::lookup_from_index(index),
config.site_url.clone(),
config.base_path.clone(),
);
text::register(&mut env);
entries::register(&mut env);
dir::register(&mut env);
dirtree::register(&mut env);
filter_in_dir::register(&mut env);
omit_docs::register(&mut env);
markdown::register(&mut env, markup_options(), SYNTECT.clone());
Ok(env)
}
fn load_templates(config: &Config) -> Result<(Tera, Vec<String>)> {
let files: Vec<(String, PathBuf)> =
config::overlay_files(&config.template_roots(), is_html_or_xml)?
.into_iter()
.map(|(rel, path)| (rel_template_name(&rel), path))
.collect();
if files.is_empty() {
return Ok((Tera::default(), Vec::new()));
}
let mut tera = Tera::default();
let pairs: Vec<(PathBuf, Option<&str>)> = files
.iter()
.map(|(name, path)| (path.clone(), Some(name.as_str())))
.collect();
tera.add_template_files(pairs)
.context("loading templates")?;
let names = files.into_iter().map(|(name, _)| name).collect();
Ok((tera, names))
}
fn is_html_or_xml(path: &std::path::Path) -> bool {
matches!(
path.extension().and_then(|s| s.to_str()),
Some("html") | Some("xml")
)
}
fn rel_template_name(rel: &std::path::Path) -> String {
rel.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::doc::Doc;
use std::path::PathBuf;
fn cfg_without_templates() -> Config {
Config {
templates_dir: PathBuf::from("/definitely/does/not/exist/templates"),
..Config::default()
}
}
fn cfg_with_urls(site_url: Option<&str>, base_path: &str) -> Config {
Config {
templates_dir: PathBuf::from("/definitely/does/not/exist/templates"),
site_url: site_url.map(String::from),
base_path: base_path.to_string(),
..Config::default()
}
}
fn empty_snapshot() -> Arc<DocIndex> {
Arc::new(DocIndex::new())
}
fn empty_meta_snapshot() -> Arc<Vec<DocMeta>> {
Arc::new(Vec::new())
}
#[test]
fn markup_env_does_not_register_collection() {
let mut env = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
let ctx = tera::Context::new();
assert!(
env.tera
.render_str("{{ collection(name=\"posts\") }}", &ctx)
.is_err()
);
}
#[test]
fn markup_env_does_not_register_taxonomy() {
let mut env = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
let ctx = tera::Context::new();
assert!(
env.tera
.render_str("{{ taxonomy(name=\"tags\") }}", &ctx)
.is_err()
);
}
#[test]
fn markup_env_does_not_register_backlinks() {
let mut env = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
let ctx = tera::Context::new();
assert!(env.tera.render_str("{{ backlinks() }}", &ctx).is_err());
}
#[test]
fn markup_env_does_not_register_backlinks_as_filter() {
let mut env = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
let ctx = tera::Context::new();
assert!(
env.tera
.render_str("{{ 'a.md' | backlinks }}", &ctx)
.is_err()
);
}
#[test]
fn missing_templates_dir_is_not_an_error() {
let env = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
assert!(env.macro_preamble.is_empty());
assert!(build_template_env(&cfg_without_templates(), empty_snapshot()).is_ok());
}
#[test]
fn template_env_registers_collection() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
let out = env
.render_str(
"{{ collection(name=\"posts\") | length }}",
&tera::Context::new(),
)
.unwrap();
assert_eq!(out, "0");
}
#[test]
fn template_env_registers_all() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
let out = env
.render_str("{{ all() | length }}", &tera::Context::new())
.unwrap();
assert_eq!(out, "0");
}
#[test]
fn all_lists_every_doc_in_id_path_order() {
let mut idx = DocIndex::new();
idx.insert(one_doc_at("posts/b.md"));
idx.insert(one_doc_at("posts/a.md"));
idx.insert(one_doc_at("pages/c.md"));
let mut env = build_template_env(&cfg_without_templates(), Arc::new(idx)).unwrap();
let out = env
.render_str(
"{% for d in all() %}{{ d.id_path }} {% endfor %}",
&tera::Context::new(),
)
.unwrap();
assert_eq!(out, "pages/c.md posts/a.md posts/b.md ");
}
#[test]
fn all_rejects_any_argument() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
assert!(
env.render_str("{{ all(limit=5) }}", &tera::Context::new())
.is_err()
);
}
#[test]
fn markup_env_does_not_register_all() {
let mut env = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
let ctx = tera::Context::new();
assert!(env.tera.render_str("{{ all() }}", &ctx).is_err());
}
#[test]
fn template_env_registers_doc() {
let mut env = build_template_env(&cfg_without_templates(), one_doc_snapshot()).unwrap();
let out = env
.render_str(
"{% set d = doc(id_path=\"posts/hello.md\") %}{{ d.output_path }}",
&tera::Context::new(),
)
.unwrap();
assert_eq!(out, "posts/hello.html");
}
#[test]
fn doc_unknown_id_path_renders_null_not_error() {
let mut env = build_template_env(&cfg_without_templates(), one_doc_snapshot()).unwrap();
let out = env
.render_str(
"{% set d = doc(id_path=\"missing.md\") %}{% if d %}yes{% else %}no{% endif %}",
&tera::Context::new(),
)
.unwrap();
assert_eq!(out, "no");
}
#[test]
fn doc_missing_id_path_argument_is_an_error() {
let mut env = build_template_env(&cfg_without_templates(), one_doc_snapshot()).unwrap();
assert!(
env.render_str("{{ doc() }}", &tera::Context::new())
.is_err()
);
}
#[test]
fn markup_env_does_not_register_doc() {
let mut env = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
let ctx = tera::Context::new();
assert!(
env.tera
.render_str("{{ doc(id_path=\"posts/hello.md\") }}", &ctx)
.is_err()
);
}
#[test]
fn template_env_registers_taxonomy() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
let out = env
.render_str(
"{{ taxonomy(name=\"tags\") | length }}",
&tera::Context::new(),
)
.unwrap();
assert_eq!(out, "0");
}
#[test]
fn template_env_registers_backlinks() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
let out = env
.render_str(
"{{ 'missing.md' | backlinks | length }}",
&tera::Context::new(),
)
.unwrap();
assert_eq!(out, "0");
}
#[test]
fn template_env_registers_related() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
let out = env
.render_str(
"{{ 'missing.md' | related | length }}",
&tera::Context::new(),
)
.unwrap();
assert_eq!(out, "0");
}
#[test]
fn markup_env_does_not_register_related() {
let mut env = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
let ctx = tera::Context::new();
assert!(env.tera.render_str("{{ 'a.md' | related }}", &ctx).is_err());
}
fn one_doc_at(id_path: &str) -> Doc {
Doc {
id_path: PathBuf::from(id_path),
output_path: PathBuf::from(id_path).with_extension("html"),
..Default::default()
}
}
fn one_doc() -> Doc {
Doc {
id_path: PathBuf::from("posts/hello.md"),
output_path: PathBuf::from("posts/hello.html"),
..Default::default()
}
}
fn one_doc_snapshot() -> Arc<DocIndex> {
let mut idx = DocIndex::new();
idx.insert(one_doc());
Arc::new(idx)
}
fn one_doc_meta_snapshot() -> Arc<Vec<DocMeta>> {
Arc::new(vec![DocMeta::from(&one_doc())])
}
#[test]
fn markup_env_registers_permalink() {
let mut env = build_markup_env(&cfg_without_templates(), one_doc_meta_snapshot()).unwrap();
let out = env
.tera
.render_str("{{ 'posts/hello.md' | permalink }}", &tera::Context::new())
.unwrap();
assert_eq!(out, "/posts/hello.html");
}
#[test]
fn template_env_registers_permalink() {
let mut env = build_template_env(&cfg_without_templates(), one_doc_snapshot()).unwrap();
let out = env
.render_str("{{ 'posts/hello.md' | permalink }}", &tera::Context::new())
.unwrap();
assert_eq!(out, "/posts/hello.html");
}
#[test]
fn permalink_filter_errors_on_unknown_id_path() {
let mut env = build_markup_env(&cfg_without_templates(), one_doc_meta_snapshot()).unwrap();
let res = env
.tera
.render_str("{{ 'missing.md' | permalink }}", &tera::Context::new());
assert!(res.is_err());
}
#[test]
fn permalink_with_site_url_and_base_path_is_absolute() {
let mut env = build_template_env(
&cfg_with_urls(Some("https://example.com"), "/blog"),
one_doc_snapshot(),
)
.unwrap();
let out = env
.render_str("{{ 'posts/hello.md' | permalink }}", &tera::Context::new())
.unwrap();
assert_eq!(out, "https://example.com/blog/posts/hello.html");
}
#[test]
fn permalink_without_site_url_falls_back_to_root_relative() {
let mut env =
build_template_env(&cfg_with_urls(None, "/blog"), one_doc_snapshot()).unwrap();
let out = env
.render_str("{{ 'posts/hello.md' | permalink }}", &tera::Context::new())
.unwrap();
assert_eq!(out, "/blog/posts/hello.html");
}
#[test]
fn permalink_no_config_preserves_root_relative_output() {
let mut env = build_template_env(&cfg_without_templates(), one_doc_snapshot()).unwrap();
let out = env
.render_str("{{ 'posts/hello.md' | permalink }}", &tera::Context::new())
.unwrap();
assert_eq!(out, "/posts/hello.html");
}
#[test]
fn link_filter_returns_root_relative_with_base_path() {
let mut env = build_template_env(
&cfg_with_urls(Some("https://example.com"), "/blog"),
one_doc_snapshot(),
)
.unwrap();
let out = env
.render_str("{{ 'posts/hello.md' | link }}", &tera::Context::new())
.unwrap();
assert_eq!(out, "/blog/posts/hello.html");
}
#[test]
fn link_filter_errors_on_unknown_id_path() {
let mut env = build_template_env(&cfg_without_templates(), one_doc_snapshot()).unwrap();
assert!(
env.render_str("{{ 'missing.md' | link }}", &tera::Context::new())
.is_err()
);
}
#[test]
fn relative_url_prepends_base_path() {
let mut env =
build_template_env(&cfg_with_urls(None, "/blog"), one_doc_snapshot()).unwrap();
let out = env
.render_str("{{ 'css/site.css' | relative_url }}", &tera::Context::new())
.unwrap();
assert_eq!(out, "/blog/css/site.css");
}
#[test]
fn relative_url_handles_leading_slash_in_input() {
let mut env =
build_template_env(&cfg_with_urls(None, "/blog"), one_doc_snapshot()).unwrap();
let out = env
.render_str(
"{{ '/css/site.css' | relative_url }}",
&tera::Context::new(),
)
.unwrap();
assert_eq!(out, "/blog/css/site.css");
}
#[test]
fn relative_url_with_empty_base_path() {
let mut env = build_template_env(&cfg_without_templates(), one_doc_snapshot()).unwrap();
let out = env
.render_str("{{ 'css/site.css' | relative_url }}", &tera::Context::new())
.unwrap();
assert_eq!(out, "/css/site.css");
}
#[test]
fn absolute_url_prepends_site_url_and_base_path() {
let mut env = build_template_env(
&cfg_with_urls(Some("https://example.com"), "/blog"),
one_doc_snapshot(),
)
.unwrap();
let out = env
.render_str("{{ 'feed.xml' | absolute_url }}", &tera::Context::new())
.unwrap();
assert_eq!(out, "https://example.com/blog/feed.xml");
}
#[test]
fn absolute_url_falls_back_without_site_url() {
let mut env =
build_template_env(&cfg_with_urls(None, "/blog"), one_doc_snapshot()).unwrap();
let out = env
.render_str("{{ 'feed.xml' | absolute_url }}", &tera::Context::new())
.unwrap();
assert_eq!(out, "/blog/feed.xml");
}
#[test]
fn markup_env_registers_all_four_url_filters() {
let mut env = build_markup_env(
&cfg_with_urls(Some("https://example.com"), "/blog"),
one_doc_meta_snapshot(),
)
.unwrap();
for body in [
"{{ 'posts/hello.md' | permalink }}",
"{{ 'posts/hello.md' | link }}",
"{{ 'css/x' | relative_url }}",
"{{ 'css/x' | absolute_url }}",
] {
assert!(
env.tera.render_str(body, &tera::Context::new()).is_ok(),
"markup env missing filter for body: {}",
body,
);
}
}
struct TempTemplatesDir(PathBuf);
impl TempTemplatesDir {
fn new(name: &str) -> Self {
let dir = std::env::temp_dir().join(format!(
"italic-tera_env-test-{}-{}",
name,
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
Self(dir)
}
fn cfg(&self) -> Config {
Config {
templates_dir: self.0.clone(),
..Config::default()
}
}
fn write_macros_file(&self, name: &str, body: &str) {
let macros = self.0.join("macros");
std::fs::create_dir_all(¯os).unwrap();
std::fs::write(macros.join(name), body).unwrap();
}
}
impl Drop for TempTemplatesDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn markup_env_imports_macros_from_macros_dir() {
let tmp = TempTemplatesDir::new("import");
tmp.write_macros_file(
"youtube.html",
"{% macro embed(id) %}<iframe src=\"https://youtube.com/embed/{{ id }}\"></iframe>{% endmacro embed %}",
);
let mut env = build_markup_env(&tmp.cfg(), empty_meta_snapshot()).unwrap();
let body = format!("{}{{{{ youtube::embed(id=\"abc\") }}}}", env.macro_preamble);
let out = env.tera.render_str(&body, &tera::Context::new()).unwrap();
assert_eq!(
out,
"<iframe src=\"https://youtube.com/embed/abc\"></iframe>"
);
}
#[test]
fn markup_env_handles_missing_macros_dir() {
let tmp = TempTemplatesDir::new("no-macros");
let env = build_markup_env(&tmp.cfg(), empty_meta_snapshot()).unwrap();
assert!(env.macro_preamble.is_empty());
}
#[test]
fn markup_env_handles_empty_macros_dir() {
let tmp = TempTemplatesDir::new("empty-macros");
std::fs::create_dir_all(tmp.0.join("macros")).unwrap();
let env = build_markup_env(&tmp.cfg(), empty_meta_snapshot()).unwrap();
assert!(env.macro_preamble.is_empty());
}
#[test]
fn markup_env_macro_preamble_is_deterministic() {
let tmp = TempTemplatesDir::new("sorted");
tmp.write_macros_file("twitter.html", "{% macro x() %}t{% endmacro x %}");
tmp.write_macros_file("aside.html", "{% macro x() %}a{% endmacro x %}");
let env = build_markup_env(&tmp.cfg(), empty_meta_snapshot()).unwrap();
assert_eq!(
env.macro_preamble,
"{% import \"macros/aside.html\" as aside %}\n\
{% import \"macros/twitter.html\" as twitter %}\n"
);
}
#[test]
fn markup_env_ignores_non_html_files_in_macros_dir() {
let tmp = TempTemplatesDir::new("non-html");
tmp.write_macros_file("notes.md", "not a macro");
tmp.write_macros_file("real.html", "{% macro hi() %}hi{% endmacro hi %}");
let env = build_markup_env(&tmp.cfg(), empty_meta_snapshot()).unwrap();
assert_eq!(
env.macro_preamble,
"{% import \"macros/real.html\" as real %}\n"
);
}
struct TempOverlay(PathBuf);
impl TempOverlay {
fn new(name: &str) -> Self {
let dir = std::env::temp_dir().join(format!(
"italic-tera_env-overlay-{}-{}",
name,
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
Self(dir)
}
fn write_template(&self, layer: &str, rel: &str, body: &str) {
let path = self.0.join(layer).join("templates").join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, body).unwrap();
}
fn cfg(&self) -> Config {
Config {
templates_dir: self.0.join("site").join("templates"),
theme: Some(self.0.join("theme")),
..Config::default()
}
}
}
impl Drop for TempOverlay {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn site_template_overrides_theme_template_of_same_name() {
let o = TempOverlay::new("override");
o.write_template("theme", "base.html", "THEME");
o.write_template("site", "base.html", "SITE");
let mut env = build_template_env(&o.cfg(), empty_snapshot()).unwrap();
let out = env
.render_str("{% include \"base.html\" %}", &tera::Context::new())
.unwrap();
assert_eq!(out, "SITE");
}
#[test]
fn theme_only_template_falls_through_when_site_lacks_it() {
let o = TempOverlay::new("fallthrough");
o.write_template(
"theme",
"base.html",
"[THEME {% block body %}{% endblock %}]",
);
o.write_template("site", "base.html", "[SITE {% block body %}{% endblock %}]");
o.write_template(
"theme",
"post.html",
"{% extends \"base.html\" %}{% block body %}hi{% endblock %}",
);
let env = build_template_env(&o.cfg(), empty_snapshot()).unwrap();
let out = env.render("post.html", &tera::Context::new()).unwrap();
assert_eq!(out, "[SITE hi]");
}
#[test]
fn site_macro_overrides_theme_macro_of_same_stem() {
let o = TempOverlay::new("macro-override");
o.write_template(
"theme",
"macros/note.html",
"{% macro show() %}THEME{% endmacro show %}",
);
o.write_template(
"site",
"macros/note.html",
"{% macro show() %}SITE{% endmacro show %}",
);
let mut env = build_markup_env(&o.cfg(), empty_meta_snapshot()).unwrap();
assert_eq!(
env.macro_preamble,
"{% import \"macros/note.html\" as note %}\n"
);
let body = format!("{}{{{{ note::show() }}}}", env.macro_preamble);
let out = env.tera.render_str(&body, &tera::Context::new()).unwrap();
assert_eq!(out, "SITE");
}
#[test]
fn truncate_words_filter_on_markup_env() {
let mut env = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
let out = env
.tera
.render_str(
"{{ 'hello world foo bar baz' | truncate_words(length=15) }}",
&tera::Context::new(),
)
.unwrap();
assert_eq!(out, "hello worldโฆ");
}
#[test]
fn truncate_words_filter_on_template_env() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
let out = env
.render_str(
"{{ 'hello world foo bar baz' | truncate_words(length=15) }}",
&tera::Context::new(),
)
.unwrap();
assert_eq!(out, "hello worldโฆ");
}
#[test]
fn truncate_words_filter_default_length_is_250() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
let short = "a".repeat(100);
let body = format!("{{{{ '{}' | truncate_words }}}}", short);
let out = env.render_str(&body, &tera::Context::new()).unwrap();
assert_eq!(out, short);
}
#[test]
fn truncate_words_composes_with_striptags() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
let out = env
.render_str(
"{{ '<p>hello world foo bar baz</p>' | striptags | truncate_words(length=15) }}",
&tera::Context::new(),
)
.unwrap();
assert_eq!(out, "hello worldโฆ");
}
fn ctx_with_map() -> tera::Context {
let mut ctx = tera::Context::new();
let map: HashMap<&str, i32> = [("b", 2), ("a", 1), ("c", 3)].into_iter().collect();
ctx.insert("m", &map);
ctx
}
#[test]
fn entries_sorts_map_by_key_ascending_by_default() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
let out = env
.render_str(
"{% for e in m | entries %}{{ e.key }}={{ e.value }} {% endfor %}",
&ctx_with_map(),
)
.unwrap();
assert_eq!(out, "a=1 b=2 c=3 ");
}
#[test]
fn entries_desc_reverses_key_order() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
let out = env
.render_str(
"{% for e in m | entries(sort=\"desc\") %}{{ e.key }} {% endfor %}",
&ctx_with_map(),
)
.unwrap();
assert_eq!(out, "c b a ");
}
#[test]
fn entries_is_registered_on_markup_env() {
let mut env = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
let out = env
.tera
.render_str(
"{% for e in m | entries %}{{ e.key }} {% endfor %}",
&ctx_with_map(),
)
.unwrap();
assert_eq!(out, "a b c ");
}
#[test]
fn entries_rejects_non_map_input() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
assert!(
env.render_str("{{ [1, 2, 3] | entries }}", &tera::Context::new())
.is_err()
);
}
#[test]
fn entries_rejects_unknown_sort_direction() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
assert!(
env.render_str("{{ m | entries(sort=\"sideways\") }}", &ctx_with_map())
.is_err()
);
}
#[test]
fn markdown_block_filter_on_markup_env() {
let mut env = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
let out = env
.tera
.render_str(
"{% filter markdown %}# Hi{% endfilter %}",
&tera::Context::new(),
)
.unwrap();
assert!(out.contains("<h1>"), "expected an <h1>, got: {out}");
}
#[test]
fn markdown_pipe_filter_on_template_env() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
let out = env
.render_str("{{ \"*hi*\" | markdown }}", &tera::Context::new())
.unwrap();
assert!(
out.contains("<em>hi</em>"),
"expected <em>hi</em>, got: {out}"
);
}
#[test]
fn markdown_filter_registered_on_both_envs() {
let mut markup = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
assert!(
markup
.tera
.render_str("{{ \"*x*\" | markdown }}", &tera::Context::new())
.is_ok()
);
let mut template = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
assert!(
template
.render_str("{{ \"*x*\" | markdown }}", &tera::Context::new())
.is_ok()
);
}
#[test]
fn dir_function_registered_on_both_envs() {
let mut markup = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
assert_eq!(
markup
.tera
.render_str("{{ dir(path=\"a/b/c.md\") }}", &tera::Context::new())
.unwrap(),
"a/b"
);
let mut template = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
assert_eq!(
template
.render_str("{{ dir(path=\"a/b/c.md\") }}", &tera::Context::new())
.unwrap(),
"a/b"
);
}
#[test]
fn filter_in_dir_registered_on_both_envs() {
let body = "{{ [] | filter_in_dir(dir=\"foo\") | length }}";
let mut markup = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
assert_eq!(
markup.tera.render_str(body, &tera::Context::new()).unwrap(),
"0"
);
let mut template = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
assert_eq!(
template.render_str(body, &tera::Context::new()).unwrap(),
"0"
);
}
#[test]
fn omit_docs_registered_on_both_envs() {
let body = "{{ [] | omit_docs(omit=[]) | length }}";
let mut markup = build_markup_env(&cfg_without_templates(), empty_meta_snapshot()).unwrap();
assert_eq!(
markup.tera.render_str(body, &tera::Context::new()).unwrap(),
"0"
);
let mut template = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
assert_eq!(
template.render_str(body, &tera::Context::new()).unwrap(),
"0"
);
}
#[test]
fn markdown_filter_rejects_non_string_input() {
let mut env = build_template_env(&cfg_without_templates(), empty_snapshot()).unwrap();
assert!(
env.render_str("{{ 5 | markdown }}", &tera::Context::new())
.is_err()
);
}
#[test]
fn template_env_does_not_auto_import_macros() {
let tmp = TempTemplatesDir::new("template-no-auto");
tmp.write_macros_file("youtube.html", "{% macro embed(id) %}x{% endmacro embed %}");
let mut env = build_template_env(&tmp.cfg(), empty_snapshot()).unwrap();
assert!(
env.render_str("{{ youtube::embed(id=\"a\") }}", &tera::Context::new())
.is_err()
);
}
}