diff --git a/components/rendering/src/markdown.rs b/components/rendering/src/markdown.rs index dc409ce7..01e31af0 100644 --- a/components/rendering/src/markdown.rs +++ b/components/rendering/src/markdown.rs @@ -1,22 +1,25 @@ use std::borrow::Cow::{Borrowed, Owned}; -use self::cmark::{Event, Options, Parser, Tag}; use pulldown_cmark as cmark; use slug::slugify; use syntect::easy::HighlightLines; use syntect::html::{ - start_highlighted_html_snippet, styled_line_to_highlighted_html, IncludeBackground, + IncludeBackground, start_highlighted_html_snippet, styled_line_to_highlighted_html, }; use config::highlighting::{get_highlighter, SYNTAX_SET, THEME_SET}; -use errors::Result; -use link_checker::check_url; -use utils::site::resolve_internal_link; - use context::RenderContext; -use table_of_contents::{make_table_of_contents, Header, TempHeader}; +use errors::Result; +use front_matter::InsertAnchor; +use link_checker::check_url; +use table_of_contents::{Header, make_table_of_contents, TempHeader}; +use utils::site::resolve_internal_link; +use utils::vec::InsertMany; + +use self::cmark::{Event, Options, Parser, Tag}; const CONTINUE_READING: &str = "
\n"; +const ANCHOR_LINK_TEMPLATE: &str = "anchor-link.html"; #[derive(Debug)] pub struct Rendered { @@ -25,6 +28,20 @@ pub struct Rendered { pub toc: Vec"),
- // Tag::Link is handled in `markdown_to_html`
- _ => return false,
+/// get only text in a slice of events
+fn get_text(parser_slice: &[Event]) -> String {
+ let mut title = String::new();
+
+ for event in parser_slice.iter() {
+ if let Event::Text(text) = event {
+ title += text;
+ }
}
- true
+
+ title
}
-fn push_end_tag(temp_header: &mut TempHeader, tag: &Tag) -> bool {
- match tag {
- Tag::Emphasis => temp_header.add_html("
"),
- Tag::Strong => temp_header.add_html(""),
- Tag::Code => temp_header.add_html(""),
- Tag::Link(_, _) => temp_header.add_html(""),
- _ => return false,
- }
- true
-}
+fn get_header_refs(events: &[Event]) -> Vec or other things already there
- temp_header.add_text(&text);
- header_created = true;
- return Event::Html(Borrowed(""));
- }
-
// if we are in the middle of a code block
if let Some((ref mut highlighter, in_extra)) = highlighter {
let highlighted = if in_extra {
@@ -217,47 +211,55 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result", fixed_link)
- } else {
- format!("", fixed_link, title)
- };
- temp_header.add_html(&html);
- return Event::Html(Borrowed(""));
- }
-
Event::Start(Tag::Link(Owned(fixed_link), title))
}
- Event::Start(Tag::Header(num)) => {
- in_header = true;
- temp_header = TempHeader::new(num);
- Event::Html(Borrowed(""))
- }
- Event::End(Tag::Header(_)) => {
- // End of a header, reset all the things and return the header string
-
- let id = find_anchor(&anchors, slugify(&temp_header.title), 0);
- anchors.push(id.clone());
- temp_header.permalink = format!("{}#{}", context.current_page_permalink, id);
- temp_header.id = id;
-
- in_header = false;
- header_created = false;
- let val = temp_header.to_string(context.tera, context.insert_anchor);
- headers.push(temp_header.clone());
- temp_header = TempHeader::default();
- Event::Html(Owned(val))
- }
Event::Html(ref markup) if markup.contains("") => {
has_summary = true;
Event::Html(Borrowed(CONTINUE_READING))
}
_ => event,
}
- });
+ }).collect::>(); // We need to collect the events to make a second pass
- cmark::html::push_html(&mut html, parser);
+ let header_refs = get_header_refs(&events);
+
+ let mut anchors_to_insert = vec![];
+
+ for header_ref in header_refs {
+ let start_idx = header_ref.start_idx;
+ let end_idx = header_ref.end_idx;
+ let title = get_text(&events[start_idx + 1 .. end_idx]);
+ let id = find_anchor(&inserted_anchors, slugify(&title), 0);
+ inserted_anchors.push(id.clone());
+
+ // insert `id` to the tag
+ let html = format!("", lvl = header_ref.level, id = id);
+ events[start_idx] = Event::Html(Owned(html));
+
+ // generate anchors and places to insert them
+ if context.insert_anchor != InsertAnchor::None {
+ let anchor_idx = match context.insert_anchor {
+ InsertAnchor::Left => start_idx + 1,
+ InsertAnchor::Right => end_idx,
+ InsertAnchor::None => 0, // Not important
+ };
+ let mut c = tera::Context::new();
+ c.insert("id", &id);
+ let anchor_link = context.tera.render(ANCHOR_LINK_TEMPLATE, &c).unwrap();
+ anchors_to_insert.push((anchor_idx, Event::Html(Owned(anchor_link))));
+ }
+
+ // record header to make table of contents
+ let permalink = format!("{}#{}", context.current_page_permalink, id);
+ let temp_header = TempHeader { level: header_ref.level, id, permalink, title };
+ headers.push(temp_header);
+ }
+
+ if context.insert_anchor != InsertAnchor::None {
+ events.insert_many(anchors_to_insert);
+ }
+
+ cmark::html::push_html(&mut html, events.into_iter());
}
if let Some(e) = error {
diff --git a/components/rendering/src/table_of_contents.rs b/components/rendering/src/table_of_contents.rs
index 5cc115e0..777d5f24 100644
--- a/components/rendering/src/table_of_contents.rs
+++ b/components/rendering/src/table_of_contents.rs
@@ -1,6 +1,3 @@
-use front_matter::InsertAnchor;
-use tera::{Context as TeraContext, Tera};
-
#[derive(Debug, PartialEq, Clone, Serialize)]
pub struct Header {
#[serde(skip_serializing)]
@@ -30,7 +27,6 @@ pub struct TempHeader {
pub id: String,
pub permalink: String,
pub title: String,
- pub html: String,
}
impl TempHeader {
@@ -40,50 +36,6 @@ impl TempHeader {
id: String::new(),
permalink: String::new(),
title: String::new(),
- html: String::new(),
- }
- }
-
- pub fn add_html(&mut self, val: &str) {
- self.html += val;
- }
-
- pub fn add_text(&mut self, val: &str) {
- self.html += val;
- self.title += val;
- }
-
- /// Transform all the information we have about this header into the HTML string for it
- pub fn to_string(&self, tera: &Tera, insert_anchor: InsertAnchor) -> String {
- let anchor_link = if insert_anchor != InsertAnchor::None {
- let mut c = TeraContext::new();
- c.insert("id", &self.id);
- tera.render("anchor-link.html", &c).unwrap()
- } else {
- String::new()
- };
-
- match insert_anchor {
- InsertAnchor::None => format!(
- "{t} \n",
- lvl = self.level,
- t = self.html,
- id = self.id
- ),
- InsertAnchor::Left => format!(
- "{a}{t} \n",
- lvl = self.level,
- a = anchor_link,
- t = self.html,
- id = self.id
- ),
- InsertAnchor::Right => format!(
- "{t}{a} \n",
- lvl = self.level,
- a = anchor_link,
- t = self.html,
- id = self.id
- ),
}
}
}
diff --git a/components/rendering/tests/markdown.rs b/components/rendering/tests/markdown.rs
index ebd74942..dbceae87 100644
--- a/components/rendering/tests/markdown.rs
+++ b/components/rendering/tests/markdown.rs
@@ -375,6 +375,19 @@ fn can_insert_anchor_right() {
);
}
+#[test]
+fn can_insert_anchor_for_multi_header() {
+ let permalinks_ctx = HashMap::new();
+ let config = Config::default();
+ let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::Right);
+ let res = render_content("# Hello\n# World", &context).unwrap();
+ assert_eq!(
+ res.body,
+ "Hello🔗\n
\n\
+World🔗\n
\n"
+ );
+}
+
// See https://github.com/Keats/gutenberg/issues/42
#[test]
fn can_insert_anchor_with_exclamation_mark() {
@@ -528,7 +541,7 @@ fn can_understand_emphasis_in_header() {
let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("# *Emphasis* text", &context).unwrap();
- assert_eq!(res.body, "Emphasis text
\n")
+ assert_eq!(res.body, "Emphasis text
\n");
}
#[test]
@@ -537,7 +550,7 @@ fn can_understand_strong_in_header() {
let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("# **Strong** text", &context).unwrap();
- assert_eq!(res.body, "Strong text
\n")
+ assert_eq!(res.body, "Strong text
\n");
}
#[test]
@@ -546,7 +559,21 @@ fn can_understand_code_in_header() {
let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("# `Code` text", &context).unwrap();
- assert_eq!(res.body, "Code
text
\n")
+ assert_eq!(res.body, "Code
text
\n");
+}
+
+// See https://github.com/getzola/zola/issues/569
+#[test]
+fn can_understand_footnote_in_header() {
+ let permalinks_ctx = HashMap::new();
+ let config = Config::default();
+ let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
+ let res = render_content("# text [^1] there\n[^1]: footnote", &context).unwrap();
+ assert_eq!(res.body, r##"text 1 there
+1
+footnote
+
+"##);
}
#[test]
diff --git a/components/utils/src/lib.rs b/components/utils/src/lib.rs
index 25581e80..8e462ccf 100644
--- a/components/utils/src/lib.rs
+++ b/components/utils/src/lib.rs
@@ -14,3 +14,4 @@ pub mod fs;
pub mod net;
pub mod site;
pub mod templates;
+pub mod vec;
diff --git a/components/utils/src/vec.rs b/components/utils/src/vec.rs
new file mode 100644
index 00000000..778de4a7
--- /dev/null
+++ b/components/utils/src/vec.rs
@@ -0,0 +1,44 @@
+pub trait InsertMany {
+ type Element;
+ fn insert_many(&mut self, elem_to_insert: Vec<(usize, Self::Element)>);
+}
+
+impl InsertMany for Vec {
+ type Element = T;
+
+ /// Efficiently insert multiple element in their specified index.
+ /// The elements should sorted in ascending order by their index.
+ ///
+ /// This is done in O(n) time.
+ fn insert_many(&mut self, elem_to_insert: Vec<(usize, T)>) {
+ let mut inserted = vec![];
+ let mut last_idx = 0;
+
+ for (idx, elem) in elem_to_insert.into_iter() {
+ let head_len = idx - last_idx;
+ inserted.extend(self.splice(0 .. head_len, std::iter::empty()));
+ inserted.push(elem);
+ last_idx = idx;
+ }
+ let len = self.len();
+ inserted.extend(self.drain(0..len));
+
+ *self = inserted;
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::InsertMany;
+
+ #[test]
+ fn insert_many_works() {
+ let mut v = vec![1, 2, 3, 4, 5];
+ v.insert_many(vec![(0, 0), (2, -1), (5, 6)]);
+ assert_eq!(v, &[0, 1, 2, -1, 3, 4, 5, 6]);
+
+ let mut v2 = vec![1, 2, 3, 4, 5];
+ v2.insert_many(vec![(0, 0), (2, -1)]);
+ assert_eq!(v2, &[0, 1, 2, -1, 3, 4, 5]);
+ }
+}
\ No newline at end of file