From 6d6df45f2377e2e319a85e40ea37b93b553d3998 Mon Sep 17 00:00:00 2001 From: Hanno Braun Date: Fri, 19 Feb 2021 20:51:08 +0100 Subject: [PATCH] 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. --- components/site/src/lib.rs | 6 +- components/site/src/tpls.rs | 60 ++++--------------- components/templates/src/filters.rs | 52 ++++++++++------ components/templates/src/lib.rs | 46 +++++++++++++- .../documentation/templates/overview.md | 3 +- 5 files changed, 93 insertions(+), 74 deletions(-) diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 48932742..bc374134 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -20,7 +20,7 @@ use front_matter::InsertAnchor; use library::{find_taxonomies, Library, Page, Paginator, Section, Taxonomy}; use relative_path::RelativePathBuf; use std::time::Instant; -use templates::render_redirect_template; +use templates::{load_tera, render_redirect_template}; use utils::fs::{ 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)?; } - let tera = tpls::load_tera(path, &config)?; + let tera = load_tera(path, &config)?; let content_path = path.join("content"); let static_path = path.join("static"); @@ -295,7 +295,7 @@ impl Site { // taxonomy Tera fns are loaded in `register_early_global_fns` // so we do need to populate it first. self.populate_taxonomies()?; - tpls::register_early_global_fns(self); + tpls::register_early_global_fns(self)?; self.populate_sections(); self.render_markdown()?; tpls::register_tera_global_fns(self); diff --git a/components/site/src/tpls.rs b/components/site/src/tpls.rs index 0377695e..19c6f7ff 100644 --- a/components/site/src/tpls.rs +++ b/components/site/src/tpls.rs @@ -1,58 +1,16 @@ -use std::path::Path; - -use tera::Tera; - use crate::Site; -use config::Config; -use errors::{bail, Error, Result}; -use templates::{filters, global_fns, ZOLA_TERA}; -use utils::templates::rewrite_theme_paths; - -pub fn load_tera(path: &Path, config: &Config) -> Result { - 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) -} +use templates::{filters, global_fns}; +use tera::Result as TeraResult; /// 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( "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( @@ -87,6 +45,8 @@ pub fn register_early_global_fns(site: &mut Site) { site.content_path.clone(), ]), ); + + Ok(()) } /// Functions filled once we have parsed all the pages/sections only, so not available in shortcodes diff --git a/components/templates/src/filters.rs b/components/templates/src/filters.rs index 51ecca47..5e8575bd 100644 --- a/components/templates/src/filters.rs +++ b/components/templates/src/filters.rs @@ -1,21 +1,27 @@ use std::borrow::Cow; use std::collections::HashMap; use std::hash::BuildHasher; +use std::path::PathBuf; use base64::{decode, encode}; use config::Config; 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)] pub struct MarkdownFilter { config: Config, permalinks: HashMap, + tera: Tera, } impl MarkdownFilter { - pub fn new(config: Config, permalinks: HashMap) -> Self { - Self { config, permalinks } + pub fn new(path: PathBuf, config: Config, permalinks: HashMap) -> TeraResult { + 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) -> TeraResult { let mut context = RenderContext::from_config(&self.config); context.permalinks = Cow::Borrowed(&self.permalinks); + context.tera = Cow::Borrowed(&self.tera); + let s = try_get_value!("markdown", "value", String, value); let inline = match args.get("inline") { Some(val) => try_get_value!("markdown", "inline", bool, val), @@ -63,7 +71,7 @@ pub fn base64_decode( #[cfg(test)] mod tests { - use std::collections::HashMap; + use std::{collections::HashMap, path::PathBuf}; use tera::{to_value, Filter}; @@ -72,7 +80,8 @@ mod tests { #[test] 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()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(&"

Hey

\n").unwrap()); @@ -82,10 +91,11 @@ mod tests { fn markdown_filter_inline() { let mut args = HashMap::new(); args.insert("inline".to_string(), to_value(true).unwrap()); - let result = MarkdownFilter::new(Config::default(), HashMap::new()).filter( - &to_value(&"Using `map`, `filter`, and `fold` instead of `for`").unwrap(), - &args, - ); + let result = + MarkdownFilter::new(PathBuf::new(), Config::default(), HashMap::new()).unwrap().filter( + &to_value(&"Using `map`, `filter`, and `fold` instead of `for`").unwrap(), + &args, + ); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(&"Using map, filter, and fold instead of for").unwrap()); } @@ -95,18 +105,19 @@ mod tests { fn markdown_filter_inline_tables() { let mut args = HashMap::new(); args.insert("inline".to_string(), to_value(true).unwrap()); - let result = MarkdownFilter::new(Config::default(), HashMap::new()).filter( - &to_value( - &r#" + let result = + MarkdownFilter::new(PathBuf::new(), Config::default(), HashMap::new()).unwrap().filter( + &to_value( + &r#" |id|author_id| timestamp_created|title |content | |-:|--------:|-----------------------:|:---------------------|:-----------------| | 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| "#, - ) - .unwrap(), - &args, - ); + ) + .unwrap(), + &args, + ); assert!(result.is_ok()); assert!(result.unwrap().as_str().unwrap().contains("")); } @@ -120,13 +131,15 @@ mod tests { config.markdown.external_links_target_blank = true; let md = "Hello :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()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(&"

Hello https://google.com 😄 …

\n").unwrap()); 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()); assert!(result.is_ok()); assert!(result.unwrap().as_str().unwrap().contains("
 Result {
     tera.render("internal/alias.html", &context)
         .map_err(|e| Error::chain(format!("Failed to render alias for '{}'", url), e))
 }
+
+pub fn load_tera(path: &Path, config: &Config) -> Result {
+    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)
+}
diff --git a/docs/content/documentation/templates/overview.md b/docs/content/documentation/templates/overview.md
index 086294d5..118eeb4d 100644
--- a/docs/content/documentation/templates/overview.md
+++ b/docs/content/documentation/templates/overview.md
@@ -64,7 +64,8 @@ Zola adds a few filters in addition to [those](https://tera.netlify.com/docs/#fi
 in Tera.
 
 ### 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
 pass `true` to the inline argument: