Skip to content

Commit c957ffa

Browse files
committed
Indent shortcodes as much as their starting line
1 parent e613aa8 commit c957ffa

File tree

3 files changed

+83
-3
lines changed

3 files changed

+83
-3
lines changed

Diff for: components/markdown/src/shortcode/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ mod tests {
7575
args: to_value(&HashMap::<u8, u8>::new()).unwrap(),
7676
span: 0..SHORTCODE_PLACEHOLDER.len(),
7777
body: None,
78+
indent: String::new(),
7879
nth: 1,
7980
inner: Vec::new(),
8081
tera_name: "shortcodes/a.md".to_owned(),
@@ -84,6 +85,7 @@ mod tests {
8485
args: to_value(&HashMap::<u8, u8>::new()).unwrap(),
8586
span: SHORTCODE_PLACEHOLDER.len()..(2 * SHORTCODE_PLACEHOLDER.len()),
8687
body: None,
88+
indent: String::new(),
8789
nth: 2,
8890
inner: Vec::new(),
8991
tera_name: "shortcodes/a.md".to_owned(),
@@ -105,6 +107,7 @@ mod tests {
105107
args: to_value(&HashMap::<u8, u8>::new()).unwrap(),
106108
span: 9..(9 + SHORTCODE_PLACEHOLDER.len()),
107109
body: Some("Content of the body".to_owned()),
110+
indent: String::new(),
108111
nth: 1,
109112
inner: Vec::new(),
110113
tera_name: "shortcodes/bodied.md".to_owned(),
@@ -125,12 +128,14 @@ mod tests {
125128
args: to_value(&HashMap::<u8, u8>::new()).unwrap(),
126129
span: 9..(9 + SHORTCODE_PLACEHOLDER.len()),
127130
body: Some(format!("Content of {SHORTCODE_PLACEHOLDER}")),
131+
indent: String::new(),
128132
nth: 1,
129133
inner: vec![Shortcode {
130134
name: "bodied".to_string(),
131135
args: to_value(&HashMap::<u8, u8>::new()).unwrap(),
132136
span: 11..(11 + SHORTCODE_PLACEHOLDER.len()),
133137
body: Some("the body".to_owned()),
138+
indent: String::new(),
134139
nth: 1,
135140
inner: Vec::new(),
136141
tera_name: "shortcodes/bodied.md".to_owned(),

Diff for: components/markdown/src/shortcode/parser.rs

+33-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub struct Shortcode {
1616
// In practice, span.len() is always equal to SHORTCODE_PLACEHOLDER.len()
1717
pub(crate) span: Range<usize>,
1818
pub(crate) body: Option<String>,
19+
pub(crate) indent: String,
1920
pub(crate) nth: usize,
2021
pub(crate) inner: Vec<Shortcode>,
2122
// set later down the line, for quick access without needing the definitions
@@ -88,9 +89,20 @@ impl Shortcode {
8889
new_context.insert("nth", &self.nth);
8990
new_context.extend(context.clone());
9091

91-
let res = utils::templates::render_template(&tpl_name, tera, new_context, &None)
92-
.with_context(|| format!("Failed to render {} shortcode", name))?
93-
.replace("\r\n", "\n");
92+
let rendered = utils::templates::render_template(&tpl_name, tera, new_context, &None)
93+
.with_context(|| format!("Failed to render {} shortcode", name))?;
94+
// Append the rendered text, but indenting each line after the first one as much as the line in which the shortcode was called.
95+
let mut res = String::with_capacity(rendered.len());
96+
let mut lines = rendered.split_terminator('\n');
97+
if let Some(first_line) = lines.next() {
98+
res.push_str(first_line.trim_end_matches('\r'));
99+
res.push('\n');
100+
for line in lines {
101+
res.push_str(&self.indent);
102+
res.push_str(line.trim_end_matches('\r'));
103+
res.push('\n');
104+
}
105+
}
94106

95107
Ok(res)
96108
}
@@ -246,17 +258,31 @@ pub fn parse_for_shortcodes(
246258

247259
// We have at least a `page` pair
248260
for p in pairs.next().unwrap().into_inner() {
261+
fn current_indent(text: &str) -> &str {
262+
let current_line = match text.rsplit_once('\n') {
263+
Some((_, line)) => line,
264+
None => text,
265+
};
266+
// Stop at the first character that is not considered indentation by the CommonMark spec.
267+
match current_line.split_once(|ch| ch != ' ' && ch != '\t') {
268+
Some((whitespace, _)) => whitespace,
269+
None => current_line,
270+
}
271+
}
272+
249273
match p.as_rule() {
250274
Rule::text => output.push_str(p.as_span().as_str()),
251275
Rule::inline_shortcode => {
252276
let start = output.len();
277+
let indent = current_indent(&output).into();
253278
let (name, args) = parse_shortcode_call(p);
254279
let nth = invocation_counter.get(&name);
255280
shortcodes.push(Shortcode {
256281
name,
257282
args,
258283
span: start..(start + SHORTCODE_PLACEHOLDER.len()),
259284
body: None,
285+
indent,
260286
nth,
261287
inner: Vec::new(),
262288
tera_name: String::new(),
@@ -265,6 +291,7 @@ pub fn parse_for_shortcodes(
265291
}
266292
Rule::shortcode_with_body => {
267293
let start = output.len();
294+
let indent = current_indent(&output).into();
268295
let mut inner = p.into_inner();
269296
// 3 items in inner: call, body, end
270297
// we don't care about the closing tag
@@ -279,6 +306,7 @@ pub fn parse_for_shortcodes(
279306
args,
280307
span: start..(start + SHORTCODE_PLACEHOLDER.len()),
281308
body: Some(body),
309+
indent,
282310
nth,
283311
inner,
284312
tera_name: String::new(),
@@ -422,6 +450,7 @@ mod tests {
422450
args: Value::Null,
423451
span: 10..20,
424452
body: None,
453+
indent: String::new(),
425454
nth: 0,
426455
inner: Vec::new(),
427456
tera_name: String::new(),
@@ -442,6 +471,7 @@ mod tests {
442471
args: Value::Null,
443472
span: 42..65,
444473
body: None,
474+
indent: String::new(),
445475
nth: 0,
446476
inner: Vec::new(),
447477
tera_name: String::new(),

Diff for: docs/content/documentation/content/shortcodes.md

+45
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,51 @@ If you want to have some content that looks like a shortcode but not have Zola t
167167
you will need to escape it by using `{%/*` and `*/%}` instead of `{%` and `%}`. You won't need to escape
168168
anything else until the closing tag.
169169

170+
### Indentation
171+
172+
If the shortcode expands to multiple lines, the lines after the first one will be indented as much as the first one:
173+
174+
```jinja2
175+
- Second, heed this ancient wisdom:
176+
{%/* quote(author=Vincent) */%}A quote{%/* end */%}
177+
```
178+
179+
becomes...
180+
181+
```md
182+
- Second, heed this ancient wisdom:
183+
> A quote
184+
> -- *Vincent*
185+
```
186+
187+
But sometimes, the first line is not indented, and this breaks rendering:
188+
189+
```jinja2
190+
- {%/* quote(author=Vincent) */%}A quote{%/* end */%}
191+
192+
...are some wise words
193+
```
194+
195+
becomes...
196+
197+
```md
198+
- > A quote
199+
> -- *Vincent*
200+
201+
...are some wise words
202+
```
203+
204+
...which renders improperly (the second line of the quote onwards are not part of the list item).
205+
206+
Since the first line of a list item can be empty, the fix is simply to add a new line:
207+
208+
```jinja2
209+
-
210+
{%/* quote(author=Vincent) */%}A quote{%/* end */%}
211+
212+
...are some wise words
213+
```
214+
170215
## Shortcode context
171216

172217
Every shortcode can access some variables, beyond what you explicitly passed as parameter. These variables are explained in the following subsections:

0 commit comments

Comments
 (0)