Make shortcodes work in markdown filter (#1358)

* Move `load_tera` to `templates`

I don't know if this is a good place for it, conceptually. I'm moving it
there because I need to use it from `templates`, and `templates` can't
depend on `site`, because there's already a dependency in the opposite
direction.

* Load templates in `markdown` filter

This enables the `markdown` filter to handle shortcodes, as long as
those shortcodes don't access any context variables.

Addresses #1350

* Update documentation of `markdown` filter

* Only load templates for `markdown` filter once

* Clarify `markdown` filter documentation

This is a lightly edited version of what @southerntofu suggested.
This commit is contained in:
Hanno Braun 2021-02-19 20:51:08 +01:00 committed by GitHub
parent 3ba2d33564
commit 6d6df45f23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 93 additions and 74 deletions

View file

@ -20,7 +20,7 @@ use front_matter::InsertAnchor;
use library::{find_taxonomies, Library, Page, Paginator, Section, Taxonomy}; use library::{find_taxonomies, Library, Page, Paginator, Section, Taxonomy};
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use std::time::Instant; use std::time::Instant;
use templates::render_redirect_template; use templates::{load_tera, render_redirect_template};
use utils::fs::{ use utils::fs::{
copy_directory, copy_file_if_needed, create_directory, create_file, ensure_directory_exists, copy_directory, copy_file_if_needed, create_directory, create_file, ensure_directory_exists,
}; };
@ -80,7 +80,7 @@ impl Site {
config.merge_with_theme(&path.join("themes").join(&theme).join("theme.toml"), &theme)?; config.merge_with_theme(&path.join("themes").join(&theme).join("theme.toml"), &theme)?;
} }
let tera = tpls::load_tera(path, &config)?; let tera = load_tera(path, &config)?;
let content_path = path.join("content"); let content_path = path.join("content");
let static_path = path.join("static"); let static_path = path.join("static");
@ -295,7 +295,7 @@ impl Site {
// taxonomy Tera fns are loaded in `register_early_global_fns` // taxonomy Tera fns are loaded in `register_early_global_fns`
// so we do need to populate it first. // so we do need to populate it first.
self.populate_taxonomies()?; self.populate_taxonomies()?;
tpls::register_early_global_fns(self); tpls::register_early_global_fns(self)?;
self.populate_sections(); self.populate_sections();
self.render_markdown()?; self.render_markdown()?;
tpls::register_tera_global_fns(self); tpls::register_tera_global_fns(self);

View file

@ -1,58 +1,16 @@
use std::path::Path;
use tera::Tera;
use crate::Site; use crate::Site;
use config::Config; use templates::{filters, global_fns};
use errors::{bail, Error, Result}; use tera::Result as TeraResult;
use templates::{filters, global_fns, ZOLA_TERA};
use utils::templates::rewrite_theme_paths;
pub fn load_tera(path: &Path, config: &Config) -> Result<Tera> {
let tpl_glob =
format!("{}/{}", path.to_string_lossy().replace("\\", "/"), "templates/**/*.{*ml,md}");
// Only parsing as we might be extending templates from themes and that would error
// as we haven't loaded them yet
let mut tera =
Tera::parse(&tpl_glob).map_err(|e| Error::chain("Error parsing templates", e))?;
if let Some(ref theme) = config.theme {
// Test that the templates folder exist for that theme
let theme_path = path.join("themes").join(&theme);
if !theme_path.join("templates").exists() {
bail!("Theme `{}` is missing a templates folder", theme);
}
let theme_tpl_glob = format!(
"{}/{}",
path.to_string_lossy().replace("\\", "/"),
format!("themes/{}/templates/**/*.{{*ml,md}}", theme)
);
let mut tera_theme = Tera::parse(&theme_tpl_glob)
.map_err(|e| Error::chain("Error parsing templates from themes", e))?;
rewrite_theme_paths(&mut tera_theme, &theme);
if theme_path.join("templates").join("robots.txt").exists() {
tera_theme.add_template_file(theme_path.join("templates").join("robots.txt"), None)?;
}
tera.extend(&tera_theme)?;
}
tera.extend(&ZOLA_TERA)?;
tera.build_inheritance_chains()?;
if path.join("templates").join("robots.txt").exists() {
tera.add_template_file(path.join("templates").join("robots.txt"), Some("robots.txt"))?;
}
Ok(tera)
}
/// Adds global fns that are to be available to shortcodes while rendering markdown /// Adds global fns that are to be available to shortcodes while rendering markdown
pub fn register_early_global_fns(site: &mut Site) { pub fn register_early_global_fns(site: &mut Site) -> TeraResult<()> {
site.tera.register_filter( site.tera.register_filter(
"markdown", "markdown",
filters::MarkdownFilter::new(site.config.clone(), site.permalinks.clone()), filters::MarkdownFilter::new(
site.base_path.clone(),
site.config.clone(),
site.permalinks.clone(),
)?,
); );
site.tera.register_function( site.tera.register_function(
@ -87,6 +45,8 @@ pub fn register_early_global_fns(site: &mut Site) {
site.content_path.clone(), site.content_path.clone(),
]), ]),
); );
Ok(())
} }
/// Functions filled once we have parsed all the pages/sections only, so not available in shortcodes /// Functions filled once we have parsed all the pages/sections only, so not available in shortcodes

View file

@ -1,21 +1,27 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::hash::BuildHasher; use std::hash::BuildHasher;
use std::path::PathBuf;
use base64::{decode, encode}; use base64::{decode, encode};
use config::Config; use config::Config;
use rendering::{render_content, RenderContext}; use rendering::{render_content, RenderContext};
use tera::{to_value, try_get_value, Filter as TeraFilter, Result as TeraResult, Value}; use tera::{Filter as TeraFilter, Result as TeraResult, Tera, Value, to_value, try_get_value};
use crate::load_tera;
#[derive(Debug)] #[derive(Debug)]
pub struct MarkdownFilter { pub struct MarkdownFilter {
config: Config, config: Config,
permalinks: HashMap<String, String>, permalinks: HashMap<String, String>,
tera: Tera,
} }
impl MarkdownFilter { impl MarkdownFilter {
pub fn new(config: Config, permalinks: HashMap<String, String>) -> Self { pub fn new(path: PathBuf, config: Config, permalinks: HashMap<String, String>) -> TeraResult<Self> {
Self { config, permalinks } let tera = load_tera(&path, &config)
.map_err(|err| tera::Error::msg(err))?;
Ok(Self { config, permalinks, tera })
} }
} }
@ -23,6 +29,8 @@ impl TeraFilter for MarkdownFilter {
fn filter(&self, value: &Value, args: &HashMap<String, Value>) -> TeraResult<Value> { fn filter(&self, value: &Value, args: &HashMap<String, Value>) -> TeraResult<Value> {
let mut context = RenderContext::from_config(&self.config); let mut context = RenderContext::from_config(&self.config);
context.permalinks = Cow::Borrowed(&self.permalinks); context.permalinks = Cow::Borrowed(&self.permalinks);
context.tera = Cow::Borrowed(&self.tera);
let s = try_get_value!("markdown", "value", String, value); let s = try_get_value!("markdown", "value", String, value);
let inline = match args.get("inline") { let inline = match args.get("inline") {
Some(val) => try_get_value!("markdown", "inline", bool, val), Some(val) => try_get_value!("markdown", "inline", bool, val),
@ -63,7 +71,7 @@ pub fn base64_decode<S: BuildHasher>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap; use std::{collections::HashMap, path::PathBuf};
use tera::{to_value, Filter}; use tera::{to_value, Filter};
@ -72,7 +80,8 @@ mod tests {
#[test] #[test]
fn markdown_filter() { fn markdown_filter() {
let result = MarkdownFilter::new(Config::default(), HashMap::new()) let result = MarkdownFilter::new(PathBuf::new(), Config::default(), HashMap::new())
.unwrap()
.filter(&to_value(&"# Hey").unwrap(), &HashMap::new()); .filter(&to_value(&"# Hey").unwrap(), &HashMap::new());
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), to_value(&"<h1 id=\"hey\">Hey</h1>\n").unwrap()); assert_eq!(result.unwrap(), to_value(&"<h1 id=\"hey\">Hey</h1>\n").unwrap());
@ -82,10 +91,11 @@ mod tests {
fn markdown_filter_inline() { fn markdown_filter_inline() {
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("inline".to_string(), to_value(true).unwrap()); args.insert("inline".to_string(), to_value(true).unwrap());
let result = MarkdownFilter::new(Config::default(), HashMap::new()).filter( let result =
&to_value(&"Using `map`, `filter`, and `fold` instead of `for`").unwrap(), MarkdownFilter::new(PathBuf::new(), Config::default(), HashMap::new()).unwrap().filter(
&args, &to_value(&"Using `map`, `filter`, and `fold` instead of `for`").unwrap(),
); &args,
);
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), to_value(&"Using <code>map</code>, <code>filter</code>, and <code>fold</code> instead of <code>for</code>").unwrap()); assert_eq!(result.unwrap(), to_value(&"Using <code>map</code>, <code>filter</code>, and <code>fold</code> instead of <code>for</code>").unwrap());
} }
@ -95,18 +105,19 @@ mod tests {
fn markdown_filter_inline_tables() { fn markdown_filter_inline_tables() {
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("inline".to_string(), to_value(true).unwrap()); args.insert("inline".to_string(), to_value(true).unwrap());
let result = MarkdownFilter::new(Config::default(), HashMap::new()).filter( let result =
&to_value( MarkdownFilter::new(PathBuf::new(), Config::default(), HashMap::new()).unwrap().filter(
&r#" &to_value(
&r#"
|id|author_id| timestamp_created|title |content | |id|author_id| timestamp_created|title |content |
|-:|--------:|-----------------------:|:---------------------|:-----------------| |-:|--------:|-----------------------:|:---------------------|:-----------------|
| 1| 1|2018-09-05 08:03:43.141Z|How to train your ORM |Badly written blog| | 1| 1|2018-09-05 08:03:43.141Z|How to train your ORM |Badly written blog|
| 2| 1|2018-08-22 13:11:50.050Z|How to bake a nice pie|Badly written blog| | 2| 1|2018-08-22 13:11:50.050Z|How to bake a nice pie|Badly written blog|
"#, "#,
) )
.unwrap(), .unwrap(),
&args, &args,
); );
assert!(result.is_ok()); assert!(result.is_ok());
assert!(result.unwrap().as_str().unwrap().contains("<table>")); assert!(result.unwrap().as_str().unwrap().contains("<table>"));
} }
@ -120,13 +131,15 @@ mod tests {
config.markdown.external_links_target_blank = true; config.markdown.external_links_target_blank = true;
let md = "Hello <https://google.com> :smile: ..."; let md = "Hello <https://google.com> :smile: ...";
let result = MarkdownFilter::new(config.clone(), HashMap::new()) let result = MarkdownFilter::new(PathBuf::new(), config.clone(), HashMap::new())
.unwrap()
.filter(&to_value(&md).unwrap(), &HashMap::new()); .filter(&to_value(&md).unwrap(), &HashMap::new());
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), to_value(&"<p>Hello <a rel=\"noopener\" target=\"_blank\" href=\"https://google.com\">https://google.com</a> 😄 …</p>\n").unwrap()); assert_eq!(result.unwrap(), to_value(&"<p>Hello <a rel=\"noopener\" target=\"_blank\" href=\"https://google.com\">https://google.com</a> 😄 …</p>\n").unwrap());
let md = "```py\ni=0\n```"; let md = "```py\ni=0\n```";
let result = MarkdownFilter::new(config, HashMap::new()) let result = MarkdownFilter::new(PathBuf::new(), config, HashMap::new())
.unwrap()
.filter(&to_value(&md).unwrap(), &HashMap::new()); .filter(&to_value(&md).unwrap(), &HashMap::new());
assert!(result.is_ok()); assert!(result.is_ok());
assert!(result.unwrap().as_str().unwrap().contains("<pre style")); assert!(result.unwrap().as_str().unwrap().contains("<pre style"));
@ -137,7 +150,8 @@ mod tests {
let mut permalinks = HashMap::new(); let mut permalinks = HashMap::new();
permalinks.insert("blog/_index.md".to_string(), "/foo/blog".to_string()); permalinks.insert("blog/_index.md".to_string(), "/foo/blog".to_string());
let md = "Hello. Check out [my blog](@/blog/_index.md)!"; let md = "Hello. Check out [my blog](@/blog/_index.md)!";
let result = MarkdownFilter::new(Config::default(), permalinks) let result = MarkdownFilter::new(PathBuf::new(), Config::default(), permalinks)
.unwrap()
.filter(&to_value(&md).unwrap(), &HashMap::new()); .filter(&to_value(&md).unwrap(), &HashMap::new());
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(

View file

@ -1,10 +1,14 @@
pub mod filters; pub mod filters;
pub mod global_fns; pub mod global_fns;
use std::path::Path;
use config::Config;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use tera::{Context, Tera}; use tera::{Context, Tera};
use errors::{Error, Result}; use errors::{Error, Result, bail};
use utils::templates::rewrite_theme_paths;
lazy_static! { lazy_static! {
pub static ref ZOLA_TERA: Tera = { pub static ref ZOLA_TERA: Tera = {
@ -51,3 +55,43 @@ pub fn render_redirect_template(url: &str, tera: &Tera) -> Result<String> {
tera.render("internal/alias.html", &context) tera.render("internal/alias.html", &context)
.map_err(|e| Error::chain(format!("Failed to render alias for '{}'", url), e)) .map_err(|e| Error::chain(format!("Failed to render alias for '{}'", url), e))
} }
pub fn load_tera(path: &Path, config: &Config) -> Result<Tera> {
let tpl_glob =
format!("{}/{}", path.to_string_lossy().replace("\\", "/"), "templates/**/*.{*ml,md}");
// Only parsing as we might be extending templates from themes and that would error
// as we haven't loaded them yet
let mut tera =
Tera::parse(&tpl_glob).map_err(|e| Error::chain("Error parsing templates", e))?;
if let Some(ref theme) = config.theme {
// Test that the templates folder exist for that theme
let theme_path = path.join("themes").join(&theme);
if !theme_path.join("templates").exists() {
bail!("Theme `{}` is missing a templates folder", theme);
}
let theme_tpl_glob = format!(
"{}/{}",
path.to_string_lossy().replace("\\", "/"),
format!("themes/{}/templates/**/*.{{*ml,md}}", theme)
);
let mut tera_theme = Tera::parse(&theme_tpl_glob)
.map_err(|e| Error::chain("Error parsing templates from themes", e))?;
rewrite_theme_paths(&mut tera_theme, &theme);
if theme_path.join("templates").join("robots.txt").exists() {
tera_theme.add_template_file(theme_path.join("templates").join("robots.txt"), None)?;
}
tera.extend(&tera_theme)?;
}
tera.extend(&ZOLA_TERA)?;
tera.build_inheritance_chains()?;
if path.join("templates").join("robots.txt").exists() {
tera.add_template_file(path.join("templates").join("robots.txt"), Some("robots.txt"))?;
}
Ok(tera)
}

View file

@ -64,7 +64,8 @@ Zola adds a few filters in addition to [those](https://tera.netlify.com/docs/#fi
in Tera. in Tera.
### markdown ### markdown
Converts the given variable to HTML using Markdown. Shortcodes won't work within this filter. Converts the given variable to HTML using Markdown. Please note that shortcodes evaluated by this filter cannot access the current rendering context. `config` will be available, but accessing `section` or `page` (among others) from a shortcode called within the `markdown` filter will prevent your site from building. See [this discussion](https://github.com/getzola/zola/pull/1358).
By default, the filter will wrap all text in a paragraph. To disable this behaviour, you can By default, the filter will wrap all text in a paragraph. To disable this behaviour, you can
pass `true` to the inline argument: pass `true` to the inline argument: