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:
parent
3ba2d33564
commit
6d6df45f23
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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!(
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue