Add shortcode 'invocation' variable to allow a shortcode to track how… (#1236)

* add shortcode 'invocation' variable to allow a shortcode to track how many times it has been invoked in a given Markdown file

* use closure (implicit struct) instead of explicit struct for invocation tracking

* update variable name to "nth"
This commit is contained in:
Nathanael Lane 2020-12-14 10:29:10 -05:00 committed by Vincent Prouillet
parent a93063ba4a
commit a210abc5a3
3 changed files with 71 additions and 2 deletions

View file

@ -3,6 +3,7 @@ use pest::iterators::Pair;
use pest::Parser; use pest::Parser;
use pest_derive::Parser; use pest_derive::Parser;
use regex::Regex; use regex::Regex;
use std::collections::HashMap;
use tera::{to_value, Context, Map, Value}; use tera::{to_value, Context, Map, Value};
use crate::context::RenderContext; use crate::context::RenderContext;
@ -102,6 +103,7 @@ fn render_shortcode(
name: &str, name: &str,
args: &Map<String, Value>, args: &Map<String, Value>,
context: &RenderContext, context: &RenderContext,
invocation_count: u32,
body: Option<&str>, body: Option<&str>,
) -> Result<String> { ) -> Result<String> {
let mut tera_context = Context::new(); let mut tera_context = Context::new();
@ -112,6 +114,7 @@ fn render_shortcode(
// Trimming right to avoid most shortcodes with bodies ending up with a HTML new line // Trimming right to avoid most shortcodes with bodies ending up with a HTML new line
tera_context.insert("body", b.trim_end()); tera_context.insert("body", b.trim_end());
} }
tera_context.insert("nth", &invocation_count);
tera_context.extend(context.tera_context.clone()); tera_context.extend(context.tera_context.clone());
let mut template_name = format!("shortcodes/{}.md", name); let mut template_name = format!("shortcodes/{}.md", name);
@ -139,6 +142,12 @@ fn render_shortcode(
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());
let mut invocation_map: HashMap<String, u32> = HashMap::new();
let mut get_invocation_count = |name: &str| {
let invocation_number = invocation_map.entry(String::from(name)).or_insert(0);
*invocation_number += 1;
*invocation_number
};
let mut pairs = match ContentParser::parse(Rule::page, content) { let mut pairs = match ContentParser::parse(Rule::page, content) {
Ok(p) => p, Ok(p) => p,
@ -184,7 +193,13 @@ pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<Strin
Rule::text => res.push_str(p.as_span().as_str()), Rule::text => res.push_str(p.as_span().as_str()),
Rule::inline_shortcode => { Rule::inline_shortcode => {
let (name, args) = parse_shortcode_call(p); let (name, args) = parse_shortcode_call(p);
res.push_str(&render_shortcode(&name, &args, context, None)?); res.push_str(&render_shortcode(
&name,
&args,
context,
get_invocation_count(&name),
None,
)?);
} }
Rule::shortcode_with_body => { Rule::shortcode_with_body => {
let mut inner = p.into_inner(); let mut inner = p.into_inner();
@ -192,7 +207,13 @@ pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<Strin
// we don't care about the closing tag // we don't care about the closing tag
let (name, args) = parse_shortcode_call(inner.next().unwrap()); let (name, args) = parse_shortcode_call(inner.next().unwrap());
let body = inner.next().unwrap().as_span().as_str(); let body = inner.next().unwrap().as_span().as_str();
res.push_str(&render_shortcode(&name, &args, context, Some(body))?); res.push_str(&render_shortcode(
&name,
&args,
context,
get_invocation_count(&name),
Some(body),
)?);
} }
Rule::ignored_inline_shortcode => { Rule::ignored_inline_shortcode => {
res.push_str( res.push_str(

View file

@ -1059,6 +1059,36 @@ fn emoji_aliases_are_ignored_when_disabled_in_config() {
assert_eq!(res.body, "<p>Hello, World! :smile:</p>\n"); assert_eq!(res.body, "<p>Hello, World! :smile:</p>\n");
} }
#[test]
fn invocation_count_increments_in_shortcode() {
let permalinks_ctx = HashMap::new();
let mut tera = Tera::default();
tera.extend(&ZOLA_TERA).unwrap();
let shortcode_template_a = r#"<p>a: {{ nth }}</p>"#;
let shortcode_template_b = r#"<p>b: {{ nth }}</p>"#;
let markdown_string = r#"{{ a() }}
{{ b() }}
{{ a() }}
{{ b() }}
"#;
let expected = r#"<p>a: 1</p>
<p>b: 1</p>
<p>a: 2</p>
<p>b: 2</p>
"#;
tera.add_raw_template("shortcodes/a.html", shortcode_template_a).unwrap();
tera.add_raw_template("shortcodes/b.html", shortcode_template_b).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);
}
#[test] #[test]
fn basic_external_links_unchanged() { fn basic_external_links_unchanged() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();

View file

@ -134,6 +134,24 @@ If you want to have some content that looks like a shortcode but not have Zola t
you will need to escape it by using `{%/*` and `*/%}` instead of `{%` and `%}`. You won't need to escape you will need to escape it by using `{%/*` and `*/%}` instead of `{%` and `%}`. You won't need to escape
anything else until the closing tag. anything else until the closing tag.
### Invocation Count
Every shortcode context is passed in a variable named `nth` that tracks how many times a particular shortcode has
been invoked in a Markdown file. Given a shortcode `true_statement.html` template:
```jinja2
<p id="number{{ nth }}">{{ value }} is equal to {{ nth }}.</p>
```
It could be used in our Markdown as follows:
```md
{{/* true_statement(value=1) */}}
{{/* true_statement(value=2) */}}
```
This is useful when implementing custom markup for features such as sidenotes or end notes.
## Built-in shortcodes ## Built-in shortcodes
Zola comes with a few built-in shortcodes. If you want to override a default shortcode template, Zola comes with a few built-in shortcodes. If you want to override a default shortcode template,