diff --git a/components/rendering/src/lib.rs b/components/rendering/src/lib.rs index d6a6cd3e..dd896f93 100644 --- a/components/rendering/src/lib.rs +++ b/components/rendering/src/lib.rs @@ -14,7 +14,9 @@ pub fn render_content(content: &str, context: &RenderContext) -> Result", "\n"); + return Ok(html); } markdown_to_html(&content, context) diff --git a/components/rendering/src/shortcode.rs b/components/rendering/src/shortcode.rs index 5133a453..308da13c 100644 --- a/components/rendering/src/shortcode.rs +++ b/components/rendering/src/shortcode.rs @@ -17,7 +17,6 @@ const _GRAMMAR: &str = include_str!("content.pest"); pub struct ContentParser; lazy_static! { - static ref MULTIPLE_NEWLINE_RE: Regex = Regex::new(r"\n\s*\n").unwrap(); static ref OUTER_NEWLINE_RE: Regex = Regex::new(r"^\s*\n|\n\s*$").unwrap(); } @@ -115,19 +114,27 @@ fn render_shortcode( } tera_context.extend(context.tera_context.clone()); - let template_name = format!("shortcodes/{}.html", name); + let mut template_name = format!("shortcodes/{}.md", name); + if !context.tera.templates.contains_key(&template_name) { + template_name = format!("shortcodes/{}.html", name); + } let res = utils::templates::render_template(&template_name, &context.tera, tera_context, &None) .map_err(|e| Error::chain(format!("Failed to render {} shortcode", name), e))?; - // Small hack to avoid having multiple blank lines because of Tera tags for example - // A blank like will cause the markdown parser to think we're out of HTML and start looking - // at indentation, making the output a code block. - let res = MULTIPLE_NEWLINE_RE.replace_all(&res, "\n"); - let res = OUTER_NEWLINE_RE.replace_all(&res, ""); - Ok(res.to_string()) + // A blank line will cause the markdown parser to think we're out of HTML and start looking + // at indentation, making the output a code block. To avoid this, newlines are replaced with + // "" at this stage, which will be undone after markdown rendering in lib.rs. Since + // that is an HTML comment, it shouldn't be rendered anyway. and not cause problems unless + // someone wants to include that comment in their content. This behaviour is unwanted in when + // rendering markdown shortcodes. + if template_name.ends_with(".html") { + Ok(res.replace('\n', "").to_string()) + } else { + Ok(res.to_string()) + } } pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result { @@ -413,8 +420,8 @@ Some body {{ hello() }}{%/* end */%}"#, fn shortcodes_with_body_do_not_eat_newlines() { let mut tera = Tera::default(); tera.add_raw_template("shortcodes/youtube.html", "{{body | safe}}").unwrap(); - let res = render_shortcodes("Body\n {% youtube() %}\nHello \n World{% end %}", &tera); - assert_eq!(res, "Body\n Hello \n World"); + let res = render_shortcodes("Body\n {% youtube() %}\nHello \n \n\n World{% end %}", &tera); + assert_eq!(res, "Body\n Hello World"); } #[test] @@ -432,4 +439,12 @@ Some body {{ hello() }}{%/* end */%}"#, let res = render_shortcodes("\n{{ youtube() }}\n", &tera); assert_eq!(res, "\n Hello, Zola. \n"); } + + #[test] + fn shortcodes_that_emit_markdown() { + let mut tera = Tera::default(); + tera.add_raw_template("shortcodes/youtube.md", "{% for i in [1,2,3] %}\n* {{ i }}\n{%- endfor %}").unwrap(); + let res = render_shortcodes("{{ youtube() }}", &tera); + assert_eq!(res, "* 1\n* 2\n* 3"); + } } diff --git a/components/rendering/tests/markdown.rs b/components/rendering/tests/markdown.rs index e3d48c92..4b11b397 100644 --- a/components/rendering/tests/markdown.rs +++ b/components/rendering/tests/markdown.rs @@ -788,10 +788,7 @@ fn doesnt_try_to_highlight_content_from_shortcode() { let markdown_string = r#"{{ figure(src="spherecluster.png", caption="Some spheres.") }}"#; - let expected = r#"
- Some spheres. -
Some spheres.
-
"#; + let expected = "
\n \n \"Some\n \n\n
Some spheres.
\n
"; tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap(); let config = Config::default(); @@ -801,6 +798,28 @@ fn doesnt_try_to_highlight_content_from_shortcode() { assert_eq!(res.body, expected); } +#[test] +fn can_emit_newlines_and_whitespace_with_shortcode() { + let permalinks_ctx = HashMap::new(); + let mut tera = Tera::default(); + tera.extend(&ZOLA_TERA).unwrap(); + + let shortcode = r#"
+{{ body }}
+
"#; + + let markdown_string = "{% preformatted() %}\nHello\n \n Zola\n \n !\n{% end %}"; + + let expected = "
\nHello\n    \n    Zola\n   \n  !\n
"; + + tera.add_raw_template(&format!("shortcodes/{}.html", "preformatted"), shortcode).unwrap(); + let config = Config::default(); + let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); + + let res = render_content(markdown_string, &context).unwrap(); + assert_eq!(res.body, expected); +} + // TODO: re-enable once it's fixed in Tera // https://github.com/Keats/tera/issues/373 //#[test] @@ -885,3 +904,44 @@ fn stops_with_an_error_on_an_empty_link() { assert!(res.is_err()); assert_eq!(res.unwrap_err().to_string(), expected); } + +#[test] +fn can_passthrough_markdown_from_shortcode() { + let permalinks_ctx = HashMap::new(); + let mut tera = Tera::default(); + tera.extend(&ZOLA_TERA).unwrap(); + + let shortcode = r#"{% for line in body | split(pat="\n") %} +> {{ line }} +{%- endfor %} + +-- {{ author }} +"#; + let markdown_string = r#" +Hello + +{% quote(author="Vincent") %} +# Passing through + +*to* **the** document +{% end %} + +Bla bla"#; + + let expected = r#"

Hello

+
+

Passing through

+

to the document

+
+

-- Vincent

+

Bla bla

+"#; + + tera.add_raw_template(&format!("shortcodes/{}.md", "quote"), shortcode).unwrap(); + let config = Config::default(); + let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); + + let res = render_content(markdown_string, &context).unwrap(); + println!("{:?}", res); + assert_eq!(res.body, expected); +} diff --git a/components/site/src/tpls.rs b/components/site/src/tpls.rs index 12582610..5d3a9027 100644 --- a/components/site/src/tpls.rs +++ b/components/site/src/tpls.rs @@ -10,7 +10,7 @@ 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"); + 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 @@ -27,7 +27,7 @@ pub fn load_tera(path: &Path, config: &Config) -> Result { let theme_tpl_glob = format!( "{}/{}", path.to_string_lossy().replace("\\", "/"), - format!("themes/{}/templates/**/*.*ml", theme) + 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))?; diff --git a/docs/content/documentation/content/shortcodes.md b/docs/content/documentation/content/shortcodes.md index 22bf6aba..b0fba7f9 100644 --- a/docs/content/documentation/content/shortcodes.md +++ b/docs/content/documentation/content/shortcodes.md @@ -3,14 +3,20 @@ title = "Shortcodes" weight = 40 +++ -Although Markdown is good for writing, it isn't great when you need write inline -HTML to add some styling for example. - -To solve this, Zola borrows the concept of [shortcodes](https://codex.wordpress.org/Shortcode_API) -from WordPress. +Zola borrows the concept of [shortcodes](https://codex.wordpress.org/Shortcode_API) from WordPress. In our case, a shortcode corresponds to a template defined in the `templates/shortcodes` directory or -a built-in one that can be used in a Markdown file. If you want to use something similar to shortcodes in your templates, try [Tera macros](https://tera.netlify.com/docs#macros). +a built-in one that can be used in a Markdown file. If you want to use something similar to shortcodes in your templates, +try [Tera macros](https://tera.netlify.com/docs#macros). +Broadly speaking, Zola's shortcodes cover two distinct use cases: + +* Inject more complex HTML: Markdown is good for writing, but it isn't great when you need add inline HTML or styling. +* Ease repetitive data based tasks: when you have [external data](@/documentation/templates/overview.md#load-data) that you + want to display in your page's body. + +The latter may also be solved by writing HTML, however Zola allows the use of Markdown based shortcodes which end in `.md` +rather than `.html`. This may be particularly useful if you want to include headings generated by the shortcode in the +[table of contents](@/documentation/content/table-of-contents.md). ## Writing a shortcode Let's write a shortcode to embed YouTube videos as an example. @@ -34,12 +40,27 @@ are in an `if` statement, they are optional. That's it. Zola will now recognise this template as a shortcode named `youtube` (the filename minus the `.html` extension). -The Markdown renderer will wrap an inline HTML node such as `` or `` into a paragraph. +The Markdown renderer will wrap an inline HTML node such as `` or `` into a paragraph. If you want to disable this behaviour, wrap your shortcode in a `
`. -Shortcodes are rendered before the Markdown is parsed so they don't have access to the table of contents. Because of that, -you also cannot use the `get_page`/`get_section`/`get_taxonomy` global functions. It might work while running -`zola serve` because it has been loaded but it will fail during `zola build`. +A Markdown based shortcode in turn will be treated as if what it returned was part of the page's body. If we create +`books.md` in `templates/shortcodes` for example: + +```jinja2 +{% set data = load_data(path=path) -%} +{% for book in data.books %} +### {{ book.title }} + +{{ book.description | safe }} +{% endfor %} +``` + +This will create a shortcode `books` with the argument `path` pointing to a `.toml` file where it loads lists of books with +titles and descriptions. They will flow with the rest of the document in which `books` is called. + +Shortcodes are rendered before the page's Markdown is parsed so they don't have access to the page's table of contents. +Because of that, you also cannot use the `get_page`/`get_section`/`get_taxonomy` global functions. It might work while +running `zola serve` because it has been loaded but it will fail during `zola build`. ## Using shortcodes diff --git a/docs/content/documentation/content/table-of-contents.md b/docs/content/documentation/content/table-of-contents.md index d1924011..b42a4dfd 100644 --- a/docs/content/documentation/content/table-of-contents.md +++ b/docs/content/documentation/content/table-of-contents.md @@ -3,7 +3,7 @@ title = "Table of Contents" weight = 60 +++ -Each page/section will automatically generate a table of contents for itself based on the headers present. +Each page/section will automatically generate a table of contents for itself based on the headers generated with markdown. It is available in the template through the `page.toc` or `section.toc` variable. You can view the [template variables](@/documentation/templates/pages-sections.md#table-of-contents)