From c2b190513ed277200c051dec7142149e336e08bc Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Wed, 27 Sep 2017 23:09:13 +0900 Subject: [PATCH] Refactor markdown header rendering --- CHANGELOG.md | 2 + components/rendering/src/markdown.rs | 78 +++++++++---------- components/rendering/src/table_of_contents.rs | 29 ++++++- components/rendering/tests/markdown.rs | 22 ++++++ 4 files changed, 89 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef6edfc..db28eab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - Page and Section `path` field is not starting with a `/` anymore - All Tera global fns are now rebuilt on changes - Use flags for port/interface in `gutenberg serve` +- Fix various issues with headers markdown rendering + ## 0.1.3 (2017-08-31) diff --git a/components/rendering/src/markdown.rs b/components/rendering/src/markdown.rs index ccc5d20..4209cd3 100644 --- a/components/rendering/src/markdown.rs +++ b/components/rendering/src/markdown.rs @@ -5,11 +5,9 @@ use self::cmark::{Parser, Event, Tag, Options, OPTION_ENABLE_TABLES, OPTION_ENAB use slug::slugify; use syntect::easy::HighlightLines; use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, IncludeBackground}; -use tera::{Context as TeraContext}; use errors::Result; use utils::site::resolve_internal_link; -use front_matter::InsertAnchor; use context::Context; use highlighting::{SYNTAX_SET, THEME_SET}; use short_code::{SHORTCODE_RE, ShortCode, parse_shortcode, render_simple_shortcode}; @@ -40,7 +38,7 @@ pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec let mut in_header = false; // pulldown_cmark can send several text events for a title if there are markdown // specific characters like `!` in them. We only want to insert the anchor the first time - let mut header_already_inserted = false; + let mut header_created = false; let mut anchors: Vec = vec![]; // the rendered html @@ -75,6 +73,23 @@ pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec { let parser = Parser::new_ext(content, opts).map(|event| match event { Event::Text(text) => { + // Header first + if in_header { + if header_created { + temp_header.push(&text); + return Event::Html(Owned(String::new())); + } + let id = find_anchor(&anchors, slugify(&text), 0); + anchors.push(id.clone()); + // update the header and add it to the list + temp_header.id = id.clone(); + // += as we might have some or other things already there + temp_header.title += &text; + temp_header.permalink = format!("{}#{}", context.current_page_permalink, id); + header_created = true; + return Event::Html(Owned(String::new())); + } + // if we are in the middle of a code block if let Some(ref mut highlighter) = highlighter { let highlighted = &highlighter.highlight(&text); @@ -94,10 +109,9 @@ pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec Ok(s) => return Event::Html(Owned(format!("

{}", s))), Err(e) => { error = Some(e); - return Event::Html(Owned("".to_string())); + return Event::Html(Owned(String::new())); } } - // non-matching will be returned normally below } // Shortcode with a body @@ -107,7 +121,7 @@ pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec shortcode_block = Some(ShortCode::new(&name, args)); } // Don't return anything - return Event::Text(Owned("".to_string())); + return Event::Text(Owned(String::new())); } // If we have some text while in a shortcode, it's either the body @@ -120,45 +134,16 @@ pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec Ok(s) => return Event::Html(Owned(format!("

{}", s))), Err(e) => { error = Some(e); - return Event::Html(Owned("".to_string())); + return Event::Html(Owned(String::new())); } } } else { shortcode.append(&text); - return Event::Html(Owned("".to_string())); + return Event::Html(Owned(String::new())); } } } - if in_header { - if header_already_inserted { - return Event::Text(text); - } - let id = find_anchor(&anchors, slugify(&text), 0); - anchors.push(id.clone()); - let anchor_link = if context.should_insert_anchor() { - let mut c = TeraContext::new(); - c.add("id", &id); - context.tera.render("anchor-link.html", &c).unwrap() - } else { - String::new() - }; - // update the header and add it to the list - temp_header.id = id.clone(); - temp_header.title = text.clone().into_owned(); - temp_header.permalink = format!("{}#{}", context.current_page_permalink, id); - headers.push(temp_header.clone()); - temp_header = TempHeader::default(); - - header_already_inserted = true; - let event = match context.insert_anchor { - InsertAnchor::Left => Event::Html(Owned(format!(r#"id="{}">{}{}"#, id, anchor_link, text))), - InsertAnchor::Right => Event::Html(Owned(format!(r#"id="{}">{}{}"#, id, text, anchor_link))), - InsertAnchor::None => Event::Html(Owned(format!(r#"id="{}">{}"#, id, text))) - }; - return event; - } - // Business as usual Event::Text(text) }, @@ -216,22 +201,33 @@ pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec // need to know when we are in a code block to disable shortcodes in them Event::Start(Tag::Code) => { in_code_block = true; + if in_header { + temp_header.push(""); + return Event::Html(Owned(String::new())); + } event }, Event::End(Tag::Code) => { in_code_block = false; + if in_header { + temp_header.push(""); + return Event::Html(Owned(String::new())); + } event }, Event::Start(Tag::Header(num)) => { in_header = true; temp_header = TempHeader::new(num); - // ugly eh - Event::Html(Owned(format!(" { + // End of a header, reset all the things and return the stringified version of the header in_header = false; - header_already_inserted = false; - event + header_created = false; + let val = temp_header.to_string(context); + headers.push(temp_header.clone()); + temp_header = TempHeader::default(); + Event::Html(Owned(val)) }, // If we added shortcodes, don't close a paragraph since there's none Event::End(Tag::Paragraph) => { diff --git a/components/rendering/src/table_of_contents.rs b/components/rendering/src/table_of_contents.rs index ba25bd7..70f7595 100644 --- a/components/rendering/src/table_of_contents.rs +++ b/components/rendering/src/table_of_contents.rs @@ -1,3 +1,8 @@ +use tera::{Context as TeraContext}; +use front_matter::InsertAnchor; + +use context::Context; + #[derive(Debug, PartialEq, Clone, Serialize)] pub struct Header { @@ -21,6 +26,7 @@ impl Header { } } +/// Populated while receiving events from the markdown parser #[derive(Debug, PartialEq, Clone)] pub struct TempHeader { pub level: i32, @@ -38,6 +44,27 @@ impl TempHeader { title: String::new(), } } + + pub fn push(&mut self, val: &str) { + self.title += val; + } + + /// Transform all the information we have about this header into the HTML string for it + pub fn to_string(&self, context: &Context) -> String { + let anchor_link = if context.should_insert_anchor() { + let mut c = TeraContext::new(); + c.add("id", &self.id); + context.tera.render("anchor-link.html", &c).unwrap() + } else { + String::new() + }; + + match context.insert_anchor { + InsertAnchor::None => format!("{t}\n", lvl=self.level, t=self.title, id=self.id), + InsertAnchor::Left => format!("{a}{t}\n", lvl=self.level, a=anchor_link, t=self.title, id=self.id), + InsertAnchor::Right => format!("{t}{a}\n", lvl=self.level, a=anchor_link, t=self.title, id=self.id), + } + } } impl Default for TempHeader { @@ -102,7 +129,7 @@ pub fn make_table_of_contents(temp_headers: &[TempHeader]) -> Vec
{ if i < start_idx { continue; } - let (end_idx, children) = find_children(h.level, start_idx + 1, &temp_headers); + let (end_idx, children) = find_children(h.level, start_idx + 1, temp_headers); start_idx = end_idx; toc.push(Header::from_temp_header(h, children)); } diff --git a/components/rendering/tests/markdown.rs b/components/rendering/tests/markdown.rs index eac918c..3e5a682 100644 --- a/components/rendering/tests/markdown.rs +++ b/components/rendering/tests/markdown.rs @@ -284,3 +284,25 @@ fn can_make_toc() { assert_eq!(toc[0].children[1].children.len(), 1); } + +#[test] +fn can_understand_backtick_in_titles() { + let permalinks_ctx = HashMap::new(); + let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); + let res = markdown_to_html("# `Hello`", &context).unwrap(); + assert_eq!( + res.0, + "

Hello

\n" + ); +} + +#[test] +fn can_understand_backtick_in_paragraphs() { + let permalinks_ctx = HashMap::new(); + let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); + let res = markdown_to_html("Hello `world`", &context).unwrap(); + assert_eq!( + res.0, + "

Hello world

\n" + ); +}