Skip to content

Commit

Permalink
Fix shortcode/continue-reading parsing with inline HTML
Browse files Browse the repository at this point in the history
  • Loading branch information
clarfonthey committed Aug 9, 2024
1 parent c666ee1 commit de5b94c
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 33 deletions.
13 changes: 3 additions & 10 deletions components/content/src/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ static RFC3339_DATE: Lazy<Regex> = Lazy::new(|| {
).unwrap()
});

static FOOTNOTES_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"<sup class="footnote-reference"><a href=\s*.*?>\s*.*?</a></sup>"#).unwrap()
});

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Page {
/// All info about the actual file
Expand Down Expand Up @@ -232,10 +228,7 @@ impl Page {
let res = render_content(&self.raw_content, &context)
.with_context(|| format!("Failed to render content of {}", self.file.path.display()))?;

self.summary = res
.summary_len
.map(|l| &res.body[0..l])
.map(|s| FOOTNOTES_RE.replace_all(s, "").into_owned());
self.summary = res.summary;
self.content = res.body;
self.toc = res.toc;
self.external_links = res.external_links;
Expand Down Expand Up @@ -536,7 +529,7 @@ Hello world
&HashMap::new(),
)
.unwrap();
assert_eq!(page.summary, Some("<p>Hello world</p>\n".to_string()));
assert_eq!(page.summary, Some("<p>Hello world</p>".to_string()));
}

#[test]
Expand Down Expand Up @@ -572,7 +565,7 @@ And here's another. [^3]
.unwrap();
assert_eq!(
page.summary,
Some("<p>This page use <sup>1.5</sup> and has footnotes, here\'s one. </p>\n<p>Here's another. </p>\n".to_string())
Some("<p>This page use <sup>1.5</sup> and has footnotes, here\'s one. </p>\n<p>Here's another. </p>".to_string())
);
}

Expand Down
83 changes: 65 additions & 18 deletions components/markdown/src/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::codeblock::{CodeBlock, FenceSettings};
use crate::shortcode::{Shortcode, SHORTCODE_PLACEHOLDER};

const CONTINUE_READING: &str = "<span id=\"continue-reading\"></span>";
const SUMMARY_CUTOFF: &str = "<span class=\"summary-cutoff\"></span>";
const ANCHOR_LINK_TEMPLATE: &str = "anchor-link.html";
static EMOJI_REPLACER: Lazy<EmojiReplacer> = Lazy::new(EmojiReplacer::new);

Expand All @@ -36,6 +37,10 @@ static MORE_DIVIDER_RE: Lazy<Regex> = Lazy::new(|| {
.unwrap()
});

static FOOTNOTES_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"<sup class="footnote-reference"><a href=\s*.*?>\s*.*?</a></sup>"#).unwrap()
});

/// Although there exists [a list of registered URI schemes][uri-schemes], a link may use arbitrary,
/// private schemes. This regex checks if the given string starts with something that just looks
/// like a scheme, i.e., a case-insensitive identifier followed by a colon.
Expand Down Expand Up @@ -78,7 +83,7 @@ fn is_colocated_asset_link(link: &str) -> bool {
#[derive(Debug)]
pub struct Rendered {
pub body: String,
pub summary_len: Option<usize>,
pub summary: Option<String>,
pub toc: Vec<Heading>,
/// Links to site-local pages: relative path plus optional anchor target.
pub internal_links: Vec<(String, Option<String>)>,
Expand Down Expand Up @@ -405,6 +410,7 @@ pub fn markdown_to_html(
.map(|x| x.as_object().unwrap().get("relative_path").unwrap().as_str().unwrap());
// the rendered html
let mut html = String::with_capacity(content.len());
let mut summary = None;
// Set while parsing
let mut error = None;

Expand Down Expand Up @@ -679,17 +685,15 @@ pub fn markdown_to_html(
event
});
}
Event::Html(text) => {
if !has_summary && MORE_DIVIDER_RE.is_match(&text) {
has_summary = true;
events.push(Event::Html(CONTINUE_READING.into()));
continue;
}
if !contains_shortcode(text.as_ref()) {
events.push(Event::Html(text));
continue;
}

Event::Html(text) | Event::InlineHtml(text)
if !has_summary && MORE_DIVIDER_RE.is_match(text.as_ref()) =>
{
has_summary = true;
events.push(Event::Html(CONTINUE_READING.into()));
}
Event::Html(text) | Event::InlineHtml(text)
if contains_shortcode(text.as_ref()) =>
{
render_shortcodes!(false, text, range);
}
_ => events.push(event),
Expand Down Expand Up @@ -781,14 +785,57 @@ pub fn markdown_to_html(
convert_footnotes_to_github_style(&mut events);
}

cmark::html::push_html(&mut html, events.into_iter());
let continue_reading = events
.iter()
.position(|e| matches!(e, Event::Html(CowStr::Borrowed(CONTINUE_READING))))
.unwrap_or(events.len());

// determine closing tags missing from summary
let mut tags = Vec::new();
for event in &events[..continue_reading] {
match event {
Event::Start(Tag::HtmlBlock) | Event::End(TagEnd::HtmlBlock) => (),
Event::Start(tag) => tags.push(tag.to_end()),
Event::End(tag) => {
tags.truncate(tags.iter().rposition(|t| *t == *tag).unwrap_or(0));
}
_ => (),
}
}

let mut events = events.into_iter();

// emit everything up to summary
cmark::html::push_html(&mut html, events.by_ref().take(continue_reading));

if has_summary {
// remove footnotes
let mut summary_html = FOOTNOTES_RE.replace_all(&html, "").into_owned();

// truncate trailing whitespace
summary_html.truncate(summary_html.trim_end().len());

// add cutoff placeholder
if !tags.is_empty() {
dbg!(&tags);
summary_html.push_str(SUMMARY_CUTOFF);
}

// close remaining tags
cmark::html::push_html(&mut summary_html, tags.into_iter().rev().map(Event::End));

summary = Some(summary_html)
}

// emit everything after summary
cmark::html::push_html(&mut html, events);
}

if let Some(e) = error {
Err(e)
} else {
Ok(Rendered {
summary_len: if has_summary { html.find(CONTINUE_READING) } else { None },
summary,
body: html,
toc: make_table_of_contents(headings),
internal_links,
Expand Down Expand Up @@ -861,10 +908,10 @@ mod tests {
for more in mores {
let content = format!("{top}\n\n{more}\n\n{bottom}");
let rendered = markdown_to_html(&content, &context, vec![]).unwrap();
assert!(rendered.summary_len.is_some(), "no summary when splitting on {more}");
let summary_len = rendered.summary_len.unwrap();
let summary = &rendered.body[..summary_len].trim();
let body = &rendered.body[summary_len..].trim();
assert!(rendered.summary.is_some(), "no summary when splitting on {more}");
let summary = rendered.summary.unwrap();
let summary = summary.trim();
let body = rendered.body[summary.len()..].trim();
let continue_reading = &body[..CONTINUE_READING.len()];
let body = &body[CONTINUE_READING.len()..].trim();
assert_eq!(summary, &top_rendered);
Expand Down
12 changes: 12 additions & 0 deletions components/markdown/tests/shortcodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,15 @@ fn can_use_shortcodes_in_quotes() {
.body;
insta::assert_snapshot!(body);
}

#[test]
fn can_render_with_inline_html() {
let body = common::render(
r#"
Here is <span>{{ ex1(page="") }}</span> example.
"#,
)
.unwrap()
.body;
insta::assert_snapshot!(body);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: components/markdown/tests/shortcodes.rs
expression: body
---
<p>Here is <span>1</span> example.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: components/markdown/tests/summary.rs
expression: body
---
<p>Hello world.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: components/markdown/tests/summary.rs
expression: body
---
<p>Things to do:</p>
<ul>
<li>Program<span class="summary-cutoff"></span></li>
</ul>
34 changes: 30 additions & 4 deletions components/markdown/tests/summary.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
mod common;

fn get_summary(content: &str) -> String {
let rendered = common::render(content).unwrap();
assert!(rendered.summary_len.is_some());
let summary_len = rendered.summary_len.unwrap();
rendered.body[..summary_len].to_owned()
common::render(content).expect("couldn't render").summary.expect("had no summary")
}

#[test]
Expand Down Expand Up @@ -45,3 +42,32 @@ And some content after
);
insta::assert_snapshot!(body);
}

#[test]
fn truncated_summary() {
let body = get_summary(
r#"
Things to do:
* Program <!-- more --> something
* Eat
* Sleep
"#,
);
insta::assert_snapshot!(body);
}

#[test]
fn foontnotes_summary() {
let body = get_summary(
r#"
Hello world[^1].
<!-- more -->
Good bye.
[^1]: "World" is a placeholder.
"#,
);
insta::assert_snapshot!(body);
}
10 changes: 9 additions & 1 deletion docs/content/documentation/content/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,18 @@ template = "page.html"
You can ask Zola to create a summary if, for example, you only want to show the first
paragraph of the page content in a list.

To do so, add <code>&lt;!-- more --&gt;</code> in your content at the point
To do so, add `<!-- more -->` in your content at the point
where you want the summary to end. The content up to that point will be
available separately in the
[template](@/documentation/templates/pages-sections.md#page-variables) via `page.summary`.

A span element in this position with a `continue-reading` id is created, so you can link directly to it if needed. For example:
`<a href="{{ page.permalink }}#continue-reading">Continue Reading</a>`.

If the `<!-- more -->` marker exists in the middle of a line, the summary will contain a span with a `summary-cutoff` class so you can configure the way the cutoff is rendered. For example, this CSS will display it as an ellipsis:

```css
.summary-cutoff::before {
content: "\2027";
}
```

0 comments on commit de5b94c

Please sign in to comment.