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:
parent
b003a47d54
commit
28523ac9ad
|
@ -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)
|
||||||
|
|
|
@ -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,20 +114,28 @@ 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, "");
|
||||||
|
|
||||||
|
// 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())
|
Ok(res.to_string())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<String> {
|
pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<String> {
|
||||||
let mut res = String::with_capacity(content.len());
|
let mut res = String::with_capacity(content.len());
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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))?;
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue