diff --git a/Cargo.lock b/Cargo.lock index 402de6995..9f92329cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -693,6 +693,7 @@ dependencies = [ "markdown", "serde", "tempfile", + "templates", "test-case", "time", "utils", diff --git a/components/content/Cargo.toml b/components/content/Cargo.toml index df99263b1..8de332824 100644 --- a/components/content/Cargo.toml +++ b/components/content/Cargo.toml @@ -18,3 +18,4 @@ markdown = { path = "../markdown" } [dev-dependencies] test-case = "3" # TODO: can we solve that usecase in src/page.rs in a simpler way? A custom macro_rules! maybe tempfile = "3.3.0" +templates = { path = "../templates" } diff --git a/components/content/src/page.rs b/components/content/src/page.rs index 041ab981d..a860ee50d 100644 --- a/components/content/src/page.rs +++ b/components/content/src/page.rs @@ -302,8 +302,8 @@ mod tests { use std::path::{Path, PathBuf}; use libs::globset::{Glob, GlobSetBuilder}; - use libs::tera::Tera; use tempfile::tempdir; + use templates::ZOLA_TERA; use crate::Page; use config::{Config, LanguageOptions}; @@ -325,7 +325,7 @@ Hello world"#; let mut page = res.unwrap(); page.render_markdown( &HashMap::default(), - &Tera::default(), + &ZOLA_TERA, &config, InsertAnchor::None, &HashMap::new(), @@ -353,7 +353,7 @@ Hello world"#; let mut page = res.unwrap(); page.render_markdown( &HashMap::default(), - &Tera::default(), + &ZOLA_TERA, &config, InsertAnchor::None, &HashMap::new(), @@ -523,13 +523,13 @@ Hello world let mut page = res.unwrap(); page.render_markdown( &HashMap::default(), - &Tera::default(), + &ZOLA_TERA, &config, InsertAnchor::None, &HashMap::new(), ) .unwrap(); - assert_eq!(page.summary, Some("

Hello world

\n".to_string())); + assert_eq!(page.summary, Some("

Hello world

".to_string())); } #[test] @@ -557,7 +557,7 @@ And here's another. [^3] let mut page = res.unwrap(); page.render_markdown( &HashMap::default(), - &Tera::default(), + &ZOLA_TERA, &config, InsertAnchor::None, &HashMap::new(), @@ -565,7 +565,7 @@ And here's another. [^3] .unwrap(); assert_eq!( page.summary, - Some("

This page use 1.5 and has footnotes, here\'s one.

\n

Here's another.

\n".to_string()) + Some("

This page use 1.5 and has footnotes, here\'s one.

\n

Here's another.

".to_string()) ); } diff --git a/components/markdown/src/markdown.rs b/components/markdown/src/markdown.rs index 8bdc778e2..735a5abd2 100644 --- a/components/markdown/src/markdown.rs +++ b/components/markdown/src/markdown.rs @@ -24,6 +24,7 @@ use crate::codeblock::{CodeBlock, FenceSettings}; use crate::shortcode::{Shortcode, SHORTCODE_PLACEHOLDER}; const CONTINUE_READING: &str = ""; +const SUMMARY_CUTOFF_TEMPLATE: &str = "summary-cutoff.html"; const ANCHOR_LINK_TEMPLATE: &str = "anchor-link.html"; static EMOJI_REPLACER: Lazy = Lazy::new(EmojiReplacer::new); @@ -684,7 +685,9 @@ pub fn markdown_to_html( event }); } - Event::Html(text) if !has_summary && MORE_DIVIDER_RE.is_match(text.as_ref()) => { + 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())); } @@ -787,6 +790,19 @@ pub fn markdown_to_html( .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 @@ -794,8 +810,30 @@ pub fn markdown_to_html( if has_summary { // remove footnotes - let summary_html = FOOTNOTES_RE.replace_all(&html, "").into_owned(); - summary = Some(summary_html) + let mut summary_html = FOOTNOTES_RE.replace_all(&html, "").into_owned(); + + // truncate trailing whitespace + summary_html.truncate(summary_html.trim_end().len()); + + // add cutoff template + if !tags.is_empty() { + let mut c = tera::Context::new(); + c.insert("summary", &summary_html); + c.insert("lang", &context.lang); + let summary_cutoff = utils::templates::render_template( + SUMMARY_CUTOFF_TEMPLATE, + &context.tera, + c, + &None, + ) + .context("Failed to render summary cutoff template")?; + 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 @@ -820,6 +858,7 @@ mod tests { use super::*; use config::Config; use insta::assert_snapshot; + use templates::ZOLA_TERA; #[test] fn insert_many_works() { @@ -875,7 +914,8 @@ mod tests { let mores = ["", "", "", "", ""]; let config = Config::default(); - let context = RenderContext::from_config(&config); + let mut context = RenderContext::from_config(&config); + context.tera.to_mut().extend(&ZOLA_TERA).unwrap(); for more in mores { let content = format!("{top}\n\n{more}\n\n{bottom}"); let rendered = markdown_to_html(&content, &context, vec![]).unwrap(); diff --git a/components/markdown/tests/markdown.rs b/components/markdown/tests/markdown.rs index e8cdcd42c..e286a48df 100644 --- a/components/markdown/tests/markdown.rs +++ b/components/markdown/tests/markdown.rs @@ -125,6 +125,25 @@ fn can_customise_anchor_template() { insta::assert_snapshot!(body); } +#[test] +fn can_customise_summary_template() { + let mut tera = Tera::default(); + tera.extend(&ZOLA_TERA).unwrap(); + tera.add_raw_template("summary-cutoff.html", " (in {{ lang }})").unwrap(); + let permalinks_ctx = HashMap::new(); + let config = Config::default_for_test(); + let context = RenderContext::new( + &tera, + &config, + &config.default_language, + "", + &permalinks_ctx, + InsertAnchor::Right, + ); + let summary = render_content("Hello World!", &context).unwrap().summary.unwrap(); + insta::assert_snapshot!(summary); +} + #[test] fn can_use_smart_punctuation() { let mut config = Config::default_for_test(); diff --git a/components/markdown/tests/snapshots/markdown__can_customise_summary_template.snap b/components/markdown/tests/snapshots/markdown__can_customise_summary_template.snap new file mode 100644 index 000000000..491852d4a --- /dev/null +++ b/components/markdown/tests/snapshots/markdown__can_customise_summary_template.snap @@ -0,0 +1,5 @@ +--- +source: components/markdown/tests/markdown.rs +expression: summary +--- +

Hello (in en)

diff --git a/components/markdown/tests/snapshots/summary__no_truncated_summary.snap b/components/markdown/tests/snapshots/summary__no_truncated_summary.snap deleted file mode 100644 index 70c632d63..000000000 --- a/components/markdown/tests/snapshots/summary__no_truncated_summary.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: components/markdown/tests/summary.rs -expression: rendered.body ---- -

Things to do:

- diff --git a/components/markdown/tests/snapshots/summary__truncated_summary.snap b/components/markdown/tests/snapshots/summary__truncated_summary.snap new file mode 100644 index 000000000..7f8574a48 --- /dev/null +++ b/components/markdown/tests/snapshots/summary__truncated_summary.snap @@ -0,0 +1,9 @@ +--- +source: components/markdown/tests/summary.rs +expression: body +--- +

Things to do:

+ diff --git a/components/markdown/tests/summary.rs b/components/markdown/tests/summary.rs index 39dae4099..eb8aa04f9 100644 --- a/components/markdown/tests/summary.rs +++ b/components/markdown/tests/summary.rs @@ -48,8 +48,8 @@ And some content after } #[test] -fn no_truncated_summary() { - let rendered = get_rendered( +fn truncated_summary() { + let body = get_summary( r#" Things to do: * Program something @@ -57,8 +57,7 @@ Things to do: * Sleep "#, ); - assert!(rendered.summary.is_none()); - insta::assert_snapshot!(rendered.body); + insta::assert_snapshot!(body); } #[test] diff --git a/components/templates/src/builtins/summary-cutoff.html b/components/templates/src/builtins/summary-cutoff.html new file mode 100644 index 000000000..eeefc9d59 --- /dev/null +++ b/components/templates/src/builtins/summary-cutoff.html @@ -0,0 +1 @@ +{% if summary is matching("\PP$") %}…{% endif %} diff --git a/components/templates/src/lib.rs b/components/templates/src/lib.rs index 59ab2abaf..04e7d1da3 100644 --- a/components/templates/src/lib.rs +++ b/components/templates/src/lib.rs @@ -23,6 +23,7 @@ pub static ZOLA_TERA: Lazy = Lazy::new(|| { include_str!("builtins/split_sitemap_index.xml"), ), ("__zola_builtins/anchor-link.html", include_str!("builtins/anchor-link.html")), + ("__zola_builtins/summary-cutoff.html", include_str!("builtins/summary-cutoff.html")), ("internal/alias.html", include_str!("builtins/internal/alias.html")), ]) .unwrap(); diff --git a/docs/content/documentation/content/page.md b/docs/content/documentation/content/page.md index aed2de14b..a172d5eb1 100644 --- a/docs/content/documentation/content/page.md +++ b/docs/content/documentation/content/page.md @@ -162,3 +162,7 @@ available separately in the A span element in this position with a `continue-reading` id is created, so you can link directly to it if needed. For example: `Continue Reading`. + +The `` marker can also exist in the middle of a line, and it will ensure that this does not emit unclosed HTML tags. +You can use the `summary-cutoff.html` to show an ellipsis or other text after the summary (but before these closing tags) based +upon the summary before the cutoff. By default, it will show an ellipsis (…) if the summary does not end in any punctuation.