Allow emitting newlines and whitespace in shortcodes and introduce markdown shortcodes (#1085)

* Replace hack for newline support in shortcodes with new hack

* Be a bit more space efficient/accurate with naming

* Boil newline/whitespace shortcode test down to the essentials

* Make sure the new \n and \s chars in old tests are properly represented

* Support markdown templates and shortcodes

* Refactoring .md/.html shortcode behaviour

* Add test for markdown shortcodes

* Add an html output test for markdown based shortcodes

* Add documentation for Markdown based shortcodes
This commit is contained in:
eaon 2020-07-29 14:20:43 -04:00 committed by GitHub
parent b003a47d54
commit 28523ac9ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 126 additions and 28 deletions

View file

@ -14,7 +14,9 @@ pub fn render_content(content: &str, context: &RenderContext) -> Result<markdown
// Don't do shortcodes if there is nothing like a shortcode in the content // Don't do shortcodes if there is nothing like a shortcode in the content
if content.contains("{{") || content.contains("{%") { if content.contains("{{") || content.contains("{%") {
let rendered = render_shortcodes(content, context)?; let rendered = render_shortcodes(content, context)?;
return markdown_to_html(&rendered, context); let mut html = markdown_to_html(&rendered, context)?;
html.body = html.body.replace("<!--\\n-->", "\n");
return Ok(html);
} }
markdown_to_html(&content, context) markdown_to_html(&content, context)

View file

@ -17,7 +17,6 @@ const _GRAMMAR: &str = include_str!("content.pest");
pub struct ContentParser; pub struct ContentParser;
lazy_static! { 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(); 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()); 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) 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))?; .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, ""); 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
// "<!--\n-->" 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', "<!--\\n-->").to_string())
} else {
Ok(res.to_string())
}
} }
pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<String> { pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<String> {
@ -413,8 +420,8 @@ Some body {{ hello() }}{%/* end */%}"#,
fn shortcodes_with_body_do_not_eat_newlines() { fn shortcodes_with_body_do_not_eat_newlines() {
let mut tera = Tera::default(); let mut tera = Tera::default();
tera.add_raw_template("shortcodes/youtube.html", "{{body | safe}}").unwrap(); tera.add_raw_template("shortcodes/youtube.html", "{{body | safe}}").unwrap();
let res = render_shortcodes("Body\n {% youtube() %}\nHello \n World{% end %}", &tera); let res = render_shortcodes("Body\n {% youtube() %}\nHello \n \n\n World{% end %}", &tera);
assert_eq!(res, "Body\n Hello \n World"); assert_eq!(res, "Body\n Hello <!--\\n--> <!--\\n--><!--\\n--> World");
} }
#[test] #[test]
@ -432,4 +439,12 @@ Some body {{ hello() }}{%/* end */%}"#,
let res = render_shortcodes("\n{{ youtube() }}\n", &tera); let res = render_shortcodes("\n{{ youtube() }}\n", &tera);
assert_eq!(res, "\n Hello, Zola. \n"); 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");
}
} }

View file

@ -788,10 +788,7 @@ fn doesnt_try_to_highlight_content_from_shortcode() {
let markdown_string = r#"{{ figure(src="spherecluster.png", caption="Some spheres.") }}"#; let markdown_string = r#"{{ figure(src="spherecluster.png", caption="Some spheres.") }}"#;
let expected = r#"<figure> let expected = "<figure>\n \n <img src=\"/images/spherecluster.png\" alt=\"Some spheres.\" />\n \n\n <figcaption>Some spheres.</figcaption>\n</figure>";
<img src="/images/spherecluster.png" alt="Some spheres." />
<figcaption>Some spheres.</figcaption>
</figure>"#;
tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap(); tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap();
let config = Config::default(); let config = Config::default();
@ -801,6 +798,28 @@ fn doesnt_try_to_highlight_content_from_shortcode() {
assert_eq!(res.body, expected); 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#"<pre>
{{ body }}
</pre>"#;
let markdown_string = "{% preformatted() %}\nHello\n \n Zola\n \n !\n{% end %}";
let expected = "<pre>\nHello\n \n Zola\n \n !\n</pre>";
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 // TODO: re-enable once it's fixed in Tera
// https://github.com/Keats/tera/issues/373 // https://github.com/Keats/tera/issues/373
//#[test] //#[test]
@ -885,3 +904,44 @@ fn stops_with_an_error_on_an_empty_link() {
assert!(res.is_err()); assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), expected); 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#"<p>Hello</p>
<blockquote>
<h1 id="passing-through">Passing through</h1>
<p><em>to</em> <strong>the</strong> document</p>
</blockquote>
<p>-- Vincent</p>
<p>Bla bla</p>
"#;
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);
}

View file

@ -10,7 +10,7 @@ use utils::templates::rewrite_theme_paths;
pub fn load_tera(path: &Path, config: &Config) -> Result<Tera> { pub fn load_tera(path: &Path, config: &Config) -> Result<Tera> {
let tpl_glob = 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 // Only parsing as we might be extending templates from themes and that would error
// as we haven't loaded them yet // as we haven't loaded them yet
@ -27,7 +27,7 @@ pub fn load_tera(path: &Path, config: &Config) -> Result<Tera> {
let theme_tpl_glob = format!( let theme_tpl_glob = format!(
"{}/{}", "{}/{}",
path.to_string_lossy().replace("\\", "/"), 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) let mut tera_theme = Tera::parse(&theme_tpl_glob)
.map_err(|e| Error::chain("Error parsing templates from themes", e))?; .map_err(|e| Error::chain("Error parsing templates from themes", e))?;

View file

@ -3,14 +3,20 @@ title = "Shortcodes"
weight = 40 weight = 40
+++ +++
Although Markdown is good for writing, it isn't great when you need write inline Zola borrows the concept of [shortcodes](https://codex.wordpress.org/Shortcode_API) from WordPress.
HTML to add some styling for example.
To solve this, 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 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 ## Writing a shortcode
Let's write a shortcode to embed YouTube videos as an example. Let's write a shortcode to embed YouTube videos as an example.
@ -37,9 +43,24 @@ That's it. Zola will now recognise this template as a shortcode named `youtube`
The Markdown renderer will wrap an inline HTML node such as `<a>` or `<span>` into a paragraph. The Markdown renderer will wrap an inline HTML node such as `<a>` or `<span>` into a paragraph.
If you want to disable this behaviour, wrap your shortcode in a `<div>`. If you want to disable this behaviour, wrap your shortcode in a `<div>`.
Shortcodes are rendered before the Markdown is parsed so they don't have access to the table of contents. Because of that, A Markdown based shortcode in turn will be treated as if what it returned was part of the page's body. If we create
you also cannot use the `get_page`/`get_section`/`get_taxonomy` global functions. It might work while running `books.md` in `templates/shortcodes` for example:
`zola serve` because it has been loaded but it will fail during `zola build`.
```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 ## Using shortcodes

View file

@ -3,7 +3,7 @@ title = "Table of Contents"
weight = 60 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. 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) You can view the [template variables](@/documentation/templates/pages-sections.md#table-of-contents)