Use the new shortcode parser

This commit is contained in:
Vincent Prouillet 2018-05-06 22:58:39 +02:00
parent 204f514c0e
commit 85e13483eb
18 changed files with 322 additions and 579 deletions

5
Cargo.lock generated
View file

@ -1113,12 +1113,13 @@ dependencies = [
name = "rendering" name = "rendering"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"config 0.1.0",
"errors 0.1.0", "errors 0.1.0",
"front_matter 0.1.0", "front_matter 0.1.0",
"highlighting 0.1.0", "highlighting 0.1.0",
"lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "pest 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"pest_derive 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"pulldown-cmark 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "pulldown-cmark 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)",
"slug 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "slug 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -75,7 +75,7 @@ You can check for any updates to the current packages by running:
$ git submodule update --remote --merge $ git submodule update --remote --merge
``` ```
And finally from the root of the components/rendering crate run the following command: And finally from the root of the components/highlighting crate run the following command:
```bash ```bash
$ cargo run --example generate_sublime synpack ../../sublime_syntaxes ../../sublime_syntaxes/newlines.packdump ../../sublime_syntaxes/nonewlines.packdump $ cargo run --example generate_sublime synpack ../../sublime_syntaxes ../../sublime_syntaxes/newlines.packdump ../../sublime_syntaxes/nonewlines.packdump

View file

@ -14,7 +14,7 @@ use utils::fs::{read_file, find_related_assets};
use utils::site::get_reading_analytics; use utils::site::get_reading_analytics;
use utils::templates::render_template; use utils::templates::render_template;
use front_matter::{PageFrontMatter, InsertAnchor, split_page_content}; use front_matter::{PageFrontMatter, InsertAnchor, split_page_content};
use rendering::{Context, Header, markdown_to_html}; use rendering::{RenderContext, Header, render_content};
use file_info::FileInfo; use file_info::FileInfo;
@ -162,21 +162,23 @@ impl Page {
/// We need access to all pages url to render links relative to content /// We need access to all pages url to render links relative to content
/// so that can't happen at the same time as parsing /// so that can't happen at the same time as parsing
pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config, anchor_insert: InsertAnchor) -> Result<()> { pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config, anchor_insert: InsertAnchor) -> Result<()> {
let context = Context::new( let context = RenderContext::new(
tera, tera,
config.highlight_code, config,
config.highlight_theme.clone(),
&self.permalink, &self.permalink,
permalinks, permalinks,
anchor_insert anchor_insert
); );
let res = markdown_to_html(&self.raw_content.replacen("<!-- more -->", "<a name=\"continue-reading\"></a>", 1), &context)?; let res = render_content(
&self.raw_content.replacen("<!-- more -->", "<a name=\"continue-reading\"></a>", 1),
&context
)?;
self.content = res.0; self.content = res.0;
self.toc = res.1; self.toc = res.1;
if self.raw_content.contains("<!-- more -->") { if self.raw_content.contains("<!-- more -->") {
self.summary = Some({ self.summary = Some({
let summary = self.raw_content.splitn(2, "<!-- more -->").collect::<Vec<&str>>()[0]; let summary = self.raw_content.splitn(2, "<!-- more -->").collect::<Vec<&str>>()[0];
markdown_to_html(summary, &context)?.0 render_content(summary, &context)?.0
}) })
} }

View file

@ -11,7 +11,7 @@ use errors::{Result, ResultExt};
use utils::fs::read_file; use utils::fs::read_file;
use utils::templates::render_template; use utils::templates::render_template;
use utils::site::get_reading_analytics; use utils::site::get_reading_analytics;
use rendering::{Context, Header, markdown_to_html}; use rendering::{RenderContext, Header, render_content};
use page::Page; use page::Page;
use file_info::FileInfo; use file_info::FileInfo;
@ -98,15 +98,14 @@ impl Section {
/// We need access to all pages url to render links relative to content /// We need access to all pages url to render links relative to content
/// so that can't happen at the same time as parsing /// so that can't happen at the same time as parsing
pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config) -> Result<()> { pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config) -> Result<()> {
let context = Context::new( let context = RenderContext::new(
tera, tera,
config.highlight_code, config,
config.highlight_theme.clone(),
&self.permalink, &self.permalink,
permalinks, permalinks,
self.meta.insert_anchor_links, self.meta.insert_anchor_links,
); );
let res = markdown_to_html(&self.raw_content, &context)?; let res = render_content(&self.raw_content, &context)?;
self.content = res.0; self.content = res.0;
self.toc = res.1; self.toc = res.1;
Ok(()) Ok(())

View file

@ -21,7 +21,7 @@ fn from_toml_datetime<'de, D>(deserializer: D) -> StdResult<Option<String>, D::E
fn convert_toml_date(table: Map<String, Value>) -> Value { fn convert_toml_date(table: Map<String, Value>) -> Value {
let mut new = Map::new(); let mut new = Map::new();
for (k, v) in table.into_iter() { for (k, v) in table {
if k == "$__toml_private_datetime" { if k == "$__toml_private_datetime" {
return v; return v;
} }

View file

@ -4,7 +4,8 @@ extern crate syntect;
use syntect::dumps::from_binary; use syntect::dumps::from_binary;
use syntect::parsing::SyntaxSet; use syntect::parsing::SyntaxSet;
use syntect::highlighting::ThemeSet; use syntect::highlighting::{ThemeSet, Theme};
use syntect::easy::HighlightLines;
thread_local!{ thread_local!{
pub static SYNTAX_SET: SyntaxSet = { pub static SYNTAX_SET: SyntaxSet = {
@ -17,3 +18,15 @@ thread_local!{
lazy_static!{ lazy_static!{
pub static ref THEME_SET: ThemeSet = from_binary(include_bytes!("../../../sublime_themes/all.themedump")); pub static ref THEME_SET: ThemeSet = from_binary(include_bytes!("../../../sublime_themes/all.themedump"));
} }
pub fn get_highlighter<'a>(theme: &'a Theme, info: &str) -> HighlightLines<'a> {
SYNTAX_SET.with(|ss| {
let syntax = info
.split(' ')
.next()
.and_then(|lang| ss.find_syntax_by_token(lang))
.unwrap_or_else(|| ss.find_syntax_plain_text());
HighlightLines::new(syntax, theme)
})
}

View file

@ -5,8 +5,6 @@ authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
[dependencies] [dependencies]
tera = "0.11" tera = "0.11"
regex = "1"
lazy_static = "1"
syntect = "2" syntect = "2"
pulldown-cmark = "0" pulldown-cmark = "0"
slug = "0.1" slug = "0.1"

View file

@ -9,7 +9,7 @@ extern crate front_matter;
use std::collections::HashMap; use std::collections::HashMap;
use tera::Tera; use tera::Tera;
use rendering::{Context, markdown_to_html, render_shortcodes}; use rendering::{RenderContext, render_content, render_shortcodes};
use front_matter::InsertAnchor; use front_matter::InsertAnchor;
use config::Config; use config::Config;
@ -86,19 +86,36 @@ if __name__ == "__main__":
"#; "#;
#[bench] #[bench]
fn bench_markdown_to_html_with_highlighting(b: &mut test::Bencher) { fn bench_render_content_with_highlighting(b: &mut test::Bencher) {
let tera_ctx = Tera::default(); let mut tera = Tera::default();
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
b.iter(|| markdown_to_html(CONTENT, &context)); let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
b.iter(|| render_content(CONTENT, &context).unwrap());
} }
#[bench] #[bench]
fn bench_markdown_to_html_without_highlighting(b: &mut test::Bencher) { fn bench_render_content_without_highlighting(b: &mut test::Bencher) {
let tera_ctx = Tera::default(); let mut tera = Tera::default();
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&tera_ctx, false, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let mut config = Config::default();
b.iter(|| markdown_to_html(CONTENT, &context)); config.highlight_code = false;
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
b.iter(|| render_content(CONTENT, &context).unwrap());
}
#[bench]
fn bench_render_content_no_shortcode(b: &mut test::Bencher) {
let tera = Tera::default();
let content2 = CONTENT.replace(r#"{{ youtube(id="my_youtube_id") }}"#, "");
let mut config = Config::default();
config.highlight_code = false;
let permalinks_ctx = HashMap::new();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
b.iter(|| render_content(&content2, &context).unwrap());
} }
#[bench] #[bench]
@ -109,11 +126,3 @@ fn bench_render_shortcodes_one_present(b: &mut test::Bencher) {
b.iter(|| render_shortcodes(CONTENT, &tera, &Config::default())); b.iter(|| render_shortcodes(CONTENT, &tera, &Config::default()));
} }
#[bench]
fn bench_render_shortcodes_none(b: &mut test::Bencher) {
let mut tera = Tera::default();
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap();
let content2 = CONTENT.replace(r#"{{ youtube(id="my_youtube_id") }}"#, "");
b.iter(|| render_shortcodes(&content2, &tera, &Config::default()));
}

View file

@ -1,41 +1,35 @@
use std::collections::HashMap; use std::collections::HashMap;
use tera::Tera; use tera::Tera;
use front_matter::InsertAnchor; use front_matter::InsertAnchor;
use config::Config;
/// All the information from the gutenberg site that is needed to render HTML from markdown /// All the information from the gutenberg site that is needed to render HTML from markdown
#[derive(Debug)] #[derive(Debug)]
pub struct Context<'a> { pub struct RenderContext<'a> {
pub tera: &'a Tera, pub tera: &'a Tera,
pub highlight_code: bool, pub config: &'a Config,
pub highlight_theme: String, pub current_page_permalink: &'a str,
pub current_page_permalink: String,
pub permalinks: &'a HashMap<String, String>, pub permalinks: &'a HashMap<String, String>,
pub insert_anchor: InsertAnchor, pub insert_anchor: InsertAnchor,
} }
impl<'a> Context<'a> { impl<'a> RenderContext<'a> {
pub fn new( pub fn new(
tera: &'a Tera, tera: &'a Tera,
highlight_code: bool, config: &'a Config,
highlight_theme: String, current_page_permalink: &'a str,
current_page_permalink: &str,
permalinks: &'a HashMap<String, String>, permalinks: &'a HashMap<String, String>,
insert_anchor: InsertAnchor, insert_anchor: InsertAnchor,
) -> Context<'a> { ) -> RenderContext<'a> {
Context { RenderContext {
tera, tera,
current_page_permalink: current_page_permalink.to_string(), current_page_permalink,
permalinks, permalinks,
insert_anchor, insert_anchor,
highlight_code, config,
highlight_theme,
} }
} }
pub fn should_insert_anchor(&self) -> bool {
self.insert_anchor != InsertAnchor::None
}
} }

View file

@ -1,6 +1,3 @@
#[macro_use]
extern crate lazy_static;
extern crate regex;
extern crate tera; extern crate tera;
extern crate syntect; extern crate syntect;
extern crate pulldown_cmark; extern crate pulldown_cmark;
@ -12,7 +9,7 @@ extern crate pest;
#[macro_use] #[macro_use]
extern crate pest_derive; extern crate pest_derive;
#[macro_use]
extern crate errors; extern crate errors;
extern crate front_matter; extern crate front_matter;
extern crate highlighting; extern crate highlighting;
@ -26,9 +23,20 @@ mod context;
mod markdown; mod markdown;
mod table_of_contents; mod table_of_contents;
mod shortcode; mod shortcode;
mod short_code;
pub use context::Context; use errors::Result;
pub use markdown::markdown_to_html;
use markdown::markdown_to_html;
pub use table_of_contents::Header; pub use table_of_contents::Header;
pub use shortcode::render_shortcodes; pub use shortcode::render_shortcodes;
pub use context::RenderContext;
pub fn render_content(content: &str, context: &RenderContext) -> Result<(String, Vec<Header>)> {
// Don't do anything if there is nothing like a shortcode in the content
if content.contains("{{") || content.contains("{%") {
let rendered = render_shortcodes(content, context.tera, context.config)?;
return markdown_to_html(&rendered, context);
}
markdown_to_html(&content, context)
}

View file

@ -8,52 +8,16 @@ use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, Includ
use errors::Result; use errors::Result;
use utils::site::resolve_internal_link; use utils::site::resolve_internal_link;
use context::Context; use highlighting::{get_highlighter, THEME_SET};
use highlighting::{SYNTAX_SET, THEME_SET};
use short_code::{SHORTCODE_RE, ShortCode, parse_shortcode, render_simple_shortcode};
use table_of_contents::{TempHeader, Header, make_table_of_contents}; use table_of_contents::{TempHeader, Header, make_table_of_contents};
use context::RenderContext;
// We might have cases where the slug is already present in our list of anchor
pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec<Header>)> { // for example an article could have several titles named Example
// We try to be smart about highlighting code as it can be time-consuming // We add a counter after the slug if the slug is already present, which
// If the global config disables it, then we do nothing. However, // means we will have example, example-1, example-2 etc
// if we see a code block in the content, we assume that this page needs fn find_anchor(anchors: &[String], name: String, level: u8) -> String {
// to be highlighted. It could potentially have false positive if the content
// has ``` in it but that seems kind of unlikely
let should_highlight = if context.highlight_code {
content.contains("```")
} else {
false
};
// Set while parsing
let mut error = None;
let mut highlighter: Option<HighlightLines> = None;
// the markdown parser will send several Text event if a markdown character
// is present in it, for example `hello_test` will be split in 2: hello and _test.
// Since we can use those chars in shortcode arguments, we need to collect
// the full shortcode somehow first
let mut current_shortcode = String::new();
let mut shortcode_block = None;
// shortcodes live outside of paragraph so we need to ensure we don't close
// a paragraph that has already been closed
let mut added_shortcode = false;
// Don't transform things that look like shortcodes in code blocks
let mut in_code_block = false;
// If we get text in header, we need to insert the id and a anchor
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_created = false;
let mut anchors: Vec<String> = vec![];
// the rendered html
let mut html = String::new();
// We might have cases where the slug is already present in our list of anchor
// for example an article could have several titles named Example
// We add a counter after the slug if the slug is already present, which
// means we will have example, example-1, example-2 etc
fn find_anchor(anchors: &[String], name: String, level: u8) -> String {
if level == 0 && !anchors.contains(&name) { if level == 0 && !anchors.contains(&name) {
return name.to_string(); return name.to_string();
} }
@ -64,28 +28,36 @@ pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec
} }
find_anchor(anchors, name, level + 1) find_anchor(anchors, name, level + 1)
} }
pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(String, Vec<Header>)> {
// the rendered html
let mut html = String::new();
// Set while parsing
let mut error = None;
let mut highlighter: Option<HighlightLines> = None;
// If we get text in header, we need to insert the id and a anchor
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_created = false;
let mut anchors: Vec<String> = vec![];
let mut headers = vec![]; let mut headers = vec![];
// Defaults to a 0 level so not a real header // Defaults to a 0 level so not a real header
// It should be an Option ideally but not worth the hassle to update // It should be an Option ideally but not worth the hassle to update
let mut temp_header = TempHeader::default(); let mut temp_header = TempHeader::default();
let mut clear_shortcode_block = false;
let mut opts = Options::empty(); let mut opts = Options::empty();
opts.insert(OPTION_ENABLE_TABLES); opts.insert(OPTION_ENABLE_TABLES);
opts.insert(OPTION_ENABLE_FOOTNOTES); opts.insert(OPTION_ENABLE_FOOTNOTES);
{ {
let parser = Parser::new_ext(content, opts).map(|event| { let parser = Parser::new_ext(content, opts).map(|event| {
if clear_shortcode_block {
clear_shortcode_block = false;
shortcode_block = None;
}
match event { match event {
Event::Text(mut text) => { Event::Text(text) => {
// Header first // Header first
if in_header { if in_header {
if header_created { if header_created {
@ -110,98 +82,20 @@ pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec
return Event::Html(Owned(html)); return Event::Html(Owned(html));
} }
if in_code_block {
return Event::Text(text);
}
// Are we in the middle of a shortcode that somehow got cut off
// by the markdown parser?
if current_shortcode.is_empty() {
if text.starts_with("{{") && !text.ends_with("}}") {
current_shortcode += &text;
} else if text.starts_with("{%") && !text.ends_with("%}") {
current_shortcode += &text;
}
} else {
current_shortcode += &text;
}
if current_shortcode.ends_with("}}") || current_shortcode.ends_with("%}") {
text = Owned(current_shortcode.clone());
current_shortcode = String::new();
}
// Shortcode without body
if shortcode_block.is_none() && text.starts_with("{{") && text.ends_with("}}") && SHORTCODE_RE.is_match(&text) {
let (name, args) = parse_shortcode(&text);
added_shortcode = true;
match render_simple_shortcode(context.tera, &name, &args) {
// Make before and after cleaning up of extra <p> / </p> tags more parallel.
// Or, in other words:
// TERRIBLE HORRIBLE NO GOOD VERY BAD HACK
Ok(s) => return Event::Html(Owned(format!("</p>{}<p>", s))),
Err(e) => {
error = Some(e);
return Event::Html(Owned(String::new()));
}
}
}
// Shortcode with a body
if shortcode_block.is_none() && text.starts_with("{%") && text.ends_with("%}") {
if SHORTCODE_RE.is_match(&text) {
let (name, args) = parse_shortcode(&text);
shortcode_block = Some(ShortCode::new(&name, args));
}
// Don't return anything
return Event::Text(Owned(String::new()));
}
// If we have some text while in a shortcode, it's either the body
// or the end tag
if shortcode_block.is_some() {
if let Some(ref mut shortcode) = shortcode_block {
if text.trim() == "{% end %}" {
added_shortcode = true;
clear_shortcode_block = true;
match shortcode.render(context.tera) {
Ok(s) => return Event::Html(Owned(format!("</p>{}", s))),
Err(e) => {
error = Some(e);
return Event::Html(Owned(String::new()));
}
}
} else {
shortcode.append(&text);
return Event::Html(Owned(String::new()));
}
}
}
// Business as usual // Business as usual
Event::Text(text) Event::Text(text)
}, },
Event::Start(Tag::CodeBlock(ref info)) => { Event::Start(Tag::CodeBlock(ref info)) => {
in_code_block = true; if !context.config.highlight_code {
if !should_highlight {
return Event::Html(Owned("<pre><code>".to_owned())); return Event::Html(Owned("<pre><code>".to_owned()));
} }
let theme = &THEME_SET.themes[&context.highlight_theme]; let theme = &THEME_SET.themes[&context.config.highlight_theme];
highlighter = SYNTAX_SET.with(|ss| { highlighter = Some(get_highlighter(&theme, info));
let syntax = info
.split(' ')
.next()
.and_then(|lang| ss.find_syntax_by_token(lang))
.unwrap_or_else(|| ss.find_syntax_plain_text());
Some(HighlightLines::new(syntax, theme))
});
let snippet = start_coloured_html_snippet(theme); let snippet = start_coloured_html_snippet(theme);
Event::Html(Owned(snippet)) Event::Html(Owned(snippet))
}, },
Event::End(Tag::CodeBlock(_)) => { Event::End(Tag::CodeBlock(_)) => {
in_code_block = false; if !context.config.highlight_code {
if !should_highlight{
return Event::Html(Owned("</code></pre>\n".to_owned())) return Event::Html(Owned("</code></pre>\n".to_owned()))
} }
// reset highlight and close the code block // reset highlight and close the code block
@ -233,9 +127,7 @@ pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec
} }
event event
} }
// need to know when we are in a code block to disable shortcodes in them
Event::Start(Tag::Code) => { Event::Start(Tag::Code) => {
in_code_block = true;
if in_header { if in_header {
temp_header.push("<code>"); temp_header.push("<code>");
return Event::Html(Owned(String::new())); return Event::Html(Owned(String::new()));
@ -243,7 +135,6 @@ pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec
event event
}, },
Event::End(Tag::Code) => { Event::End(Tag::Code) => {
in_code_block = false;
if in_header { if in_header {
temp_header.push("</code>"); temp_header.push("</code>");
return Event::Html(Owned(String::new())); return Event::Html(Owned(String::new()));
@ -259,41 +150,23 @@ pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec
// End of a header, reset all the things and return the stringified version of the header // End of a header, reset all the things and return the stringified version of the header
in_header = false; in_header = false;
header_created = false; header_created = false;
let val = temp_header.to_string(context); let val = temp_header.to_string(context.tera, context.insert_anchor);
headers.push(temp_header.clone()); headers.push(temp_header.clone());
temp_header = TempHeader::default(); temp_header = TempHeader::default();
Event::Html(Owned(val)) Event::Html(Owned(val))
}, },
// If we added shortcodes, don't close a paragraph since there's none _ => event,
Event::End(Tag::Paragraph) => {
if added_shortcode {
added_shortcode = false;
return Event::Html(Owned("".to_owned()));
} }
event });
},
// Ignore softbreaks inside shortcodes
Event::SoftBreak => {
if shortcode_block.is_some() {
return Event::Html(Owned("".to_owned()));
}
event
},
_ => {
// println!("event = {:?}", event);
event
},
}});
cmark::html::push_html(&mut html, parser); cmark::html::push_html(&mut html, parser);
} }
if !current_shortcode.is_empty() {
return Err(format!("A shortcode was not closed properly:\n{:?}", current_shortcode).into());
}
match error { match error {
Some(e) => Err(e), Some(e) => Err(e),
None => Ok((html.replace("<p></p>", "").replace("</p></p>", "</p>"), make_table_of_contents(&headers))), None => Ok((
html.replace("<p></p>", "").replace("</p></p>", "</p>"),
make_table_of_contents(&headers)
)),
} }
} }

View file

@ -1,190 +0,0 @@
use std::collections::HashMap;
use regex::Regex;
use tera::{Tera, Context, Value, to_value};
use errors::{Result, ResultExt};
lazy_static!{
// Does this look like a shortcode?
pub static ref SHORTCODE_RE: Regex = Regex::new(
r#"\{(?:%|\{)\s+(\w+?)\((\w+?="?(?:.|\n)+?"?)?\)\s+(?:%|\})\}"#
).unwrap();
// Parse the shortcode args with capture groups named after their type
pub static ref SHORTCODE_ARGS_RE: Regex = Regex::new(
r#"(?P<name>\w+)=\s*((?P<str>".*?")|(?P<float>[-+]?[0-9]+\.[0-9]+)|(?P<int>[-+]?[0-9]+)|(?P<bool>true|false))"#
).unwrap();
}
/// A shortcode that has a body
/// Called by having some content like {% ... %} body {% end %}
/// We need the struct to hold the data while we're processing the markdown
#[derive(Debug)]
pub struct ShortCode {
name: String,
args: HashMap<String, Value>,
body: String,
}
impl ShortCode {
pub fn new(name: &str, args: HashMap<String, Value>) -> ShortCode {
ShortCode {
name: name.to_string(),
args,
body: String::new(),
}
}
pub fn append(&mut self, text: &str) {
self.body.push_str(text)
}
pub fn render(&self, tera: &Tera) -> Result<String> {
let mut context = Context::new();
for (key, value) in &self.args {
context.add(key, value);
}
context.add("body", &self.body);
let tpl_name = format!("shortcodes/{}.html", self.name);
tera.render(&tpl_name, &context)
.chain_err(|| format!("Failed to render {} shortcode", self.name))
}
}
/// Parse a shortcode without a body
pub fn parse_shortcode(input: &str) -> (String, HashMap<String, Value>) {
let mut args = HashMap::new();
let caps = SHORTCODE_RE.captures(input).unwrap();
// caps[0] is the full match
let name = &caps[1];
if let Some(arg_list) = caps.get(2) {
for arg_cap in SHORTCODE_ARGS_RE.captures_iter(arg_list.as_str()) {
let arg_name = arg_cap["name"].trim().to_string();
if let Some(arg_val) = arg_cap.name("str") {
args.insert(arg_name, to_value(arg_val.as_str().replace("\"", "")).unwrap());
continue;
}
if let Some(arg_val) = arg_cap.name("int") {
args.insert(arg_name, to_value(arg_val.as_str().parse::<i64>().unwrap()).unwrap());
continue;
}
if let Some(arg_val) = arg_cap.name("float") {
args.insert(arg_name, to_value(arg_val.as_str().parse::<f64>().unwrap()).unwrap());
continue;
}
if let Some(arg_val) = arg_cap.name("bool") {
args.insert(arg_name, to_value(arg_val.as_str() == "true").unwrap());
continue;
}
}
}
(name.to_string(), args)
}
/// Renders a shortcode or return an error
pub fn render_simple_shortcode(tera: &Tera, name: &str, args: &HashMap<String, Value>) -> Result<String> {
let mut context = Context::new();
for (key, value) in args.iter() {
context.add(key, value);
}
let tpl_name = format!("shortcodes/{}.html", name);
tera.render(&tpl_name, &context).chain_err(|| format!("Failed to render {} shortcode", name))
}
#[cfg(test)]
mod tests {
use super::{parse_shortcode, SHORTCODE_RE};
#[test]
fn can_match_all_kinds_of_shortcode() {
let inputs = vec![
"{{ basic() }}",
"{{ basic(ho=1) }}",
"{{ basic(ho=\"hey\") }}",
"{{ basic(ho=\"hey_underscore\") }}",
"{{ basic(ho=\"hey-dash\") }}",
"{% basic(ho=\"hey-dash\") %}",
"{% basic(ho=\"hey_underscore\") %}",
"{% basic() %}",
"{% quo_te(author=\"Bob\") %}",
"{{ quo_te(author=\"Bob\") }}",
// https://github.com/Keats/gutenberg/issues/229
r#"{{ youtube(id="dQw4w9WgXcQ",
autoplay=true) }}"#,
];
for i in inputs {
println!("{}", i);
assert!(SHORTCODE_RE.is_match(i));
}
}
// https://github.com/Keats/gutenberg/issues/228
#[test]
fn doesnt_panic_on_invalid_shortcode() {
let (name, args) = parse_shortcode(r#"{{ youtube(id="dQw4w9WgXcQ", autoplay) }}"#);
assert_eq!(name, "youtube");
assert_eq!(args["id"], "dQw4w9WgXcQ");
assert!(args.get("autoplay").is_none());
}
#[test]
fn can_parse_simple_shortcode_no_arg() {
let (name, args) = parse_shortcode(r#"{{ basic() }}"#);
assert_eq!(name, "basic");
assert!(args.is_empty());
}
#[test]
fn can_parse_simple_shortcode_one_arg() {
let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc") }}"#);
assert_eq!(name, "youtube");
assert_eq!(args["id"], "w7Ft2ymGmfc");
}
#[test]
fn can_parse_simple_shortcode_several_arg() {
let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc", autoplay=true) }}"#);
assert_eq!(name, "youtube");
assert_eq!(args["id"], "w7Ft2ymGmfc");
assert_eq!(args["autoplay"], true);
}
#[test]
fn can_parse_block_shortcode_several_arg() {
let (name, args) = parse_shortcode(r#"{% youtube(id="w7Ft2ymGmfc", autoplay=true) %}"#);
assert_eq!(name, "youtube");
assert_eq!(args["id"], "w7Ft2ymGmfc");
assert_eq!(args["autoplay"], true);
}
#[test]
fn can_parse_shortcode_number() {
let (name, args) = parse_shortcode(r#"{% test(int=42, float=42.0, autoplay=false) %}"#);
assert_eq!(name, "test");
assert_eq!(args["int"], 42);
assert_eq!(args["float"], 42.0);
assert_eq!(args["autoplay"], false);
}
// https://github.com/Keats/gutenberg/issues/249
#[test]
fn can_parse_shortcode_with_comma_in_it() {
let (name, args) = parse_shortcode(
r#"{% quote(author="C++ Standard Core Language Defect Reports and Accepted Issues, Revision 82, delete and user-written deallocation function", href="http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#348") %}"#
);
assert_eq!(name, "quote");
assert_eq!(args["author"], "C++ Standard Core Language Defect Reports and Accepted Issues, Revision 82, delete and user-written deallocation function");
assert_eq!(args["href"], "http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#348");
}
}

View file

@ -90,7 +90,8 @@ fn render_shortcode(name: String, args: Map<String, Value>, tera: &Tera, config:
context.insert(key, value); context.insert(key, value);
} }
if let Some(ref b) = body { if let Some(ref b) = body {
context.insert("body", b); // Trimming right to avoid most shortcodes with bodies ending up with a HTML new line
context.insert("body", b.trim_right());
} }
context.insert("config", config); context.insert("config", config);
let tpl_name = format!("shortcodes/{}.html", name); let tpl_name = format!("shortcodes/{}.html", name);
@ -99,16 +100,25 @@ fn render_shortcode(name: String, args: Map<String, Value>, tera: &Tera, config:
} }
pub fn render_shortcodes(content: &str, tera: &Tera, config: &Config) -> Result<String> { pub fn render_shortcodes(content: &str, tera: &Tera, config: &Config) -> Result<String> {
// Don't do anything if there is nothing like a shortcode in the content
if !content.contains("{{") && !content.contains("{%") {
return Ok(content.to_string());
}
let mut res = String::with_capacity(content.len()); let mut res = String::with_capacity(content.len());
let mut pairs = match ContentParser::parse(Rule::page, content) { let mut pairs = match ContentParser::parse(Rule::page, content) {
Ok(p) => p, Ok(p) => p,
Err(_) => panic!("TODO"), // TODO: error handling Err(e) => {
let fancy_e = e.renamed_rules(|rule| {
match *rule {
Rule::int => "an integer".to_string(),
Rule::float => "a float".to_string(),
Rule::string => "a string".to_string(),
Rule::literal => "a literal (int, float, string, bool)".to_string(),
Rule::array => "an array".to_string(),
Rule::kwarg => "a keyword argument".to_string(),
Rule::ident => "an identifier".to_string(),
_ => format!("TODO error: {:?}", rule).to_string(),
}
});
bail!("{}", fancy_e);
},
}; };
// We have at least a `page` pair // We have at least a `page` pair
@ -333,4 +343,10 @@ Hello World
let res = render_shortcodes("Body\n {% youtube() %}Hey!{% end %}", &tera, &Config::default()).unwrap(); let res = render_shortcodes("Body\n {% youtube() %}Hey!{% end %}", &tera, &Config::default()).unwrap();
assert_eq!(res, "Body\n Hey!"); assert_eq!(res, "Body\n Hey!");
} }
#[test]
fn errors_on_unterminated_shortcode() {
let res = render_shortcodes("{{ youtube(", &Tera::default(), &Config::default());
assert!(res.is_err());
}
} }

View file

@ -1,8 +1,6 @@
use tera::{Context as TeraContext}; use tera::{Tera, Context as TeraContext};
use front_matter::InsertAnchor; use front_matter::InsertAnchor;
use context::Context;
#[derive(Debug, PartialEq, Clone, Serialize)] #[derive(Debug, PartialEq, Clone, Serialize)]
pub struct Header { pub struct Header {
@ -50,16 +48,16 @@ impl TempHeader {
} }
/// Transform all the information we have about this header into the HTML string for it /// Transform all the information we have about this header into the HTML string for it
pub fn to_string(&self, context: &Context) -> String { pub fn to_string(&self, tera: &Tera, insert_anchor: InsertAnchor) -> String {
let anchor_link = if context.should_insert_anchor() { let anchor_link = if insert_anchor != InsertAnchor::None {
let mut c = TeraContext::new(); let mut c = TeraContext::new();
c.add("id", &self.id); c.add("id", &self.id);
context.tera.render("anchor-link.html", &c).unwrap() tera.render("anchor-link.html", &c).unwrap()
} else { } else {
String::new() String::new()
}; };
match context.insert_anchor { match insert_anchor {
InsertAnchor::None => format!("<h{lvl} id=\"{id}\">{t}</h{lvl}>\n", lvl=self.level, t=self.title, id=self.id), InsertAnchor::None => format!("<h{lvl} id=\"{id}\">{t}</h{lvl}>\n", lvl=self.level, t=self.title, id=self.id),
InsertAnchor::Left => format!("<h{lvl} id=\"{id}\">{a}{t}</h{lvl}>\n", lvl=self.level, a=anchor_link, t=self.title, id=self.id), InsertAnchor::Left => format!("<h{lvl} id=\"{id}\">{a}{t}</h{lvl}>\n", lvl=self.level, a=anchor_link, t=self.title, id=self.id),
InsertAnchor::Right => format!("<h{lvl} id=\"{id}\">{t}{a}</h{lvl}>\n", lvl=self.level, a=anchor_link, t=self.title, id=self.id), InsertAnchor::Right => format!("<h{lvl} id=\"{id}\">{t}{a}</h{lvl}>\n", lvl=self.level, a=anchor_link, t=self.title, id=self.id),

View file

@ -2,22 +2,25 @@ extern crate tera;
extern crate front_matter; extern crate front_matter;
extern crate templates; extern crate templates;
extern crate rendering; extern crate rendering;
extern crate config;
use std::collections::HashMap; use std::collections::HashMap;
use tera::Tera; use tera::Tera;
use config::Config;
use front_matter::InsertAnchor; use front_matter::InsertAnchor;
use templates::GUTENBERG_TERA; use templates::GUTENBERG_TERA;
use rendering::{Context, markdown_to_html}; use rendering::{RenderContext, render_content};
#[test] #[test]
fn can_do_markdown_to_html_simple() { fn can_do_render_content_simple() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html("hello", &context).unwrap(); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("hello", &context).unwrap();
assert_eq!(res.0, "<p>hello</p>\n"); assert_eq!(res.0, "<p>hello</p>\n");
} }
@ -25,9 +28,10 @@ fn can_do_markdown_to_html_simple() {
fn doesnt_highlight_code_block_with_highlighting_off() { fn doesnt_highlight_code_block_with_highlighting_off() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let mut context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let mut config = Config::default();
context.highlight_code = false; config.highlight_code = false;
let res = markdown_to_html("```\n$ gutenberg server\n```", &context).unwrap(); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("```\n$ gutenberg server\n```", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.0,
"<pre><code>$ gutenberg server\n</code></pre>\n" "<pre><code>$ gutenberg server\n</code></pre>\n"
@ -38,8 +42,9 @@ fn doesnt_highlight_code_block_with_highlighting_off() {
fn can_highlight_code_block_no_lang() { fn can_highlight_code_block_no_lang() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", &context).unwrap(); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("```\n$ gutenberg server\n$ ping\n```", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.0,
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">$ gutenberg server\n</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">$ ping\n</span></pre>" "<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">$ gutenberg server\n</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">$ ping\n</span></pre>"
@ -50,8 +55,9 @@ fn can_highlight_code_block_no_lang() {
fn can_highlight_code_block_with_lang() { fn can_highlight_code_block_with_lang() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html("```python\nlist.append(1)\n```", &context).unwrap(); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("```python\nlist.append(1)\n```", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.0,
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list.</span><span style=\"background-color:#2b303b;color:#bf616a;\">append</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">(</span><span style=\"background-color:#2b303b;color:#d08770;\">1</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">)\n</span></pre>" "<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list.</span><span style=\"background-color:#2b303b;color:#bf616a;\">append</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">(</span><span style=\"background-color:#2b303b;color:#d08770;\">1</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">)\n</span></pre>"
@ -62,8 +68,9 @@ fn can_highlight_code_block_with_lang() {
fn can_higlight_code_block_with_unknown_lang() { fn can_higlight_code_block_with_unknown_lang() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html("```yolo\nlist.append(1)\n```", &context).unwrap(); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("```yolo\nlist.append(1)\n```", &context).unwrap();
// defaults to plain text // defaults to plain text
assert_eq!( assert_eq!(
res.0, res.0,
@ -74,8 +81,9 @@ fn can_higlight_code_block_with_unknown_lang() {
#[test] #[test]
fn can_render_shortcode() { fn can_render_shortcode() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html(r#" let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content(r#"
Hello Hello
{{ youtube(id="ub36ffWAqgQ") }} {{ youtube(id="ub36ffWAqgQ") }}
@ -87,7 +95,8 @@ Hello
#[test] #[test]
fn can_render_shortcode_with_markdown_char_in_args_name() { fn can_render_shortcode_with_markdown_char_in_args_name() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let input = vec![ let input = vec![
"name", "name",
"na_me", "na_me",
@ -95,7 +104,7 @@ fn can_render_shortcode_with_markdown_char_in_args_name() {
"n1", "n1",
]; ];
for i in input { for i in input {
let res = markdown_to_html(&format!("{{{{ youtube(id=\"hey\", {}=1) }}}}", i), &context).unwrap(); let res = render_content(&format!("{{{{ youtube(id=\"hey\", {}=1) }}}}", i), &context).unwrap();
assert!(res.0.contains(r#"<iframe src="https://www.youtube.com/embed/hey""#)); assert!(res.0.contains(r#"<iframe src="https://www.youtube.com/embed/hey""#));
} }
} }
@ -103,7 +112,8 @@ fn can_render_shortcode_with_markdown_char_in_args_name() {
#[test] #[test]
fn can_render_shortcode_with_markdown_char_in_args_value() { fn can_render_shortcode_with_markdown_char_in_args_value() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let input = vec![ let input = vec![
"ub36ffWAqgQ-hey", "ub36ffWAqgQ-hey",
"ub36ffWAqgQ_hey", "ub36ffWAqgQ_hey",
@ -112,7 +122,7 @@ fn can_render_shortcode_with_markdown_char_in_args_value() {
"ub36ffWAqgQ#hey", "ub36ffWAqgQ#hey",
]; ];
for i in input { for i in input {
let res = markdown_to_html(&format!("{{{{ youtube(id=\"{}\") }}}}", i), &context).unwrap(); let res = render_content(&format!("{{{{ youtube(id=\"{}\") }}}}", i), &context).unwrap();
assert!(res.0.contains(&format!(r#"<iframe src="https://www.youtube.com/embed/{}""#, i))); assert!(res.0.contains(&format!(r#"<iframe src="https://www.youtube.com/embed/{}""#, i)));
} }
} }
@ -126,12 +136,13 @@ fn can_render_body_shortcode_with_markdown_char_in_name() {
"quo_te", "quo_te",
"qu_o_te", "qu_o_te",
]; ];
let config = Config::default();
for i in input { for i in input {
tera.add_raw_template(&format!("shortcodes/{}.html", i), "<blockquote>{{ body }} - {{ author}}</blockquote>").unwrap(); tera.add_raw_template(&format!("shortcodes/{}.html", i), "<blockquote>{{ body }} - {{ author}}</blockquote>").unwrap();
let context = Context::new(&tera, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html(&format!("{{% {}(author=\"Bob\") %}}\nhey\n{{% end %}}", i), &context).unwrap(); let res = render_content(&format!("{{% {}(author=\"Bob\") %}}\nhey\n{{% end %}}", i), &context).unwrap();
println!("{:?}", res); println!("{:?}", res);
assert!(res.0.contains("<blockquote>hey - Bob</blockquote>")); assert!(res.0.contains("<blockquote>hey - Bob</blockquote>"));
} }
@ -157,9 +168,10 @@ Here is another paragraph.
"; ";
tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap(); tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap();
let context = Context::new(&tera, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html(markdown_string, &context).unwrap(); let res = render_content(markdown_string, &context).unwrap();
println!("{:?}", res); println!("{:?}", res);
assert_eq!(res.0, expected); assert_eq!(res.0, expected);
} }
@ -189,9 +201,10 @@ Here is another paragraph.
"; ";
tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap(); tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap();
let context = Context::new(&tera, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html(markdown_string, &context).unwrap(); let res = render_content(markdown_string, &context).unwrap();
println!("{:?}", res); println!("{:?}", res);
assert_eq!(res.0, expected); assert_eq!(res.0, expected);
} }
@ -199,8 +212,9 @@ Here is another paragraph.
#[test] #[test]
fn can_render_several_shortcode_in_row() { fn can_render_several_shortcode_in_row() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html(r#" let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content(r#"
Hello Hello
{{ youtube(id="ub36ffWAqgQ") }} {{ youtube(id="ub36ffWAqgQ") }}
@ -222,18 +236,12 @@ Hello
} }
#[test] #[test]
fn errors_if_unterminated_shortcode() { fn doesnt_render_ignored_shortcodes() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let mut config = Config::default();
let res = markdown_to_html(r#"{{ youtube(id="w7Ft2ym_a"#, &context); config.highlight_code = false;
assert!(res.is_err()); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
} let res = render_content(r#"```{{/* youtube(id="w7Ft2ymGmfc") */}}```"#, &context).unwrap();
#[test]
fn doesnt_render_shortcode_in_code_block() {
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(r#"```{{ youtube(id="w7Ft2ymGmfc") }}```"#, &context).unwrap();
assert_eq!(res.0, "<p><code>{{ youtube(id=&quot;w7Ft2ymGmfc&quot;) }}</code></p>\n"); assert_eq!(res.0, "<p><code>{{ youtube(id=&quot;w7Ft2ymGmfc&quot;) }}</code></p>\n");
} }
@ -243,23 +251,25 @@ fn can_render_shortcode_with_body() {
tera.extend(&GUTENBERG_TERA).unwrap(); tera.extend(&GUTENBERG_TERA).unwrap();
tera.add_raw_template("shortcodes/quote.html", "<blockquote>{{ body }} - {{ author }}</blockquote>").unwrap(); tera.add_raw_template("shortcodes/quote.html", "<blockquote>{{ body }} - {{ author }}</blockquote>").unwrap();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&tera, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html(r#" let res = render_content(r#"
Hello Hello
{% quote(author="Keats") %} {% quote(author="Keats") %}
A quote A quote
{% end %} {% end %}
"#, &context).unwrap(); "#, &context).unwrap();
assert_eq!(res.0, "<p>Hello\n</p><blockquote>A quote - Keats</blockquote>"); assert_eq!(res.0, "<p>Hello</p>\n<blockquote>A quote - Keats</blockquote>\n");
} }
#[test] #[test]
fn errors_rendering_unknown_shortcode() { fn errors_rendering_unknown_shortcode() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html("{{ hello(flash=true) }}", &context); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("{{ hello(flash=true) }}", &context);
assert!(res.is_err()); assert!(res.is_err());
} }
@ -268,8 +278,9 @@ fn can_make_valid_relative_link() {
let mut permalinks = HashMap::new(); let mut permalinks = HashMap::new();
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string()); permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string());
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html( let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None);
let res = render_content(
r#"[rel link](./pages/about.md), [abs link](https://vincent.is/about)"#, r#"[rel link](./pages/about.md), [abs link](https://vincent.is/about)"#,
&context &context
).unwrap(); ).unwrap();
@ -284,8 +295,9 @@ fn can_make_relative_links_with_anchors() {
let mut permalinks = HashMap::new(); let mut permalinks = HashMap::new();
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string()); permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string());
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html(r#"[rel link](./pages/about.md#cv)"#, &context).unwrap(); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None);
let res = render_content(r#"[rel link](./pages/about.md#cv)"#, &context).unwrap();
assert!( assert!(
res.0.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#) res.0.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#)
@ -296,8 +308,9 @@ fn can_make_relative_links_with_anchors() {
fn errors_relative_link_inexistant() { fn errors_relative_link_inexistant() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html("[rel link](./pages/about.md)", &context); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("[rel link](./pages/about.md)", &context);
assert!(res.is_err()); assert!(res.is_err());
} }
@ -305,8 +318,9 @@ fn errors_relative_link_inexistant() {
fn can_add_id_to_headers() { fn can_add_id_to_headers() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html(r#"# Hello"#, &context).unwrap(); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content(r#"# Hello"#, &context).unwrap();
assert_eq!(res.0, "<h1 id=\"hello\">Hello</h1>\n"); assert_eq!(res.0, "<h1 id=\"hello\">Hello</h1>\n");
} }
@ -314,16 +328,18 @@ fn can_add_id_to_headers() {
fn can_add_id_to_headers_same_slug() { fn can_add_id_to_headers_same_slug() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html("# Hello\n# Hello", &context).unwrap(); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("# Hello\n# Hello", &context).unwrap();
assert_eq!(res.0, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n"); assert_eq!(res.0, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n");
} }
#[test] #[test]
fn can_insert_anchor_left() { fn can_insert_anchor_left() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::Left); let config = Config::default();
let res = markdown_to_html("# Hello", &context).unwrap(); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left);
let res = render_content("# Hello", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.0,
"<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello</h1>\n" "<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello</h1>\n"
@ -333,8 +349,9 @@ fn can_insert_anchor_left() {
#[test] #[test]
fn can_insert_anchor_right() { fn can_insert_anchor_right() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::Right); let config = Config::default();
let res = markdown_to_html("# Hello", &context).unwrap(); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Right);
let res = render_content("# Hello", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.0,
"<h1 id=\"hello\">Hello<a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\n</h1>\n" "<h1 id=\"hello\">Hello<a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\n</h1>\n"
@ -345,8 +362,9 @@ fn can_insert_anchor_right() {
#[test] #[test]
fn can_insert_anchor_with_exclamation_mark() { fn can_insert_anchor_with_exclamation_mark() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::Left); let config = Config::default();
let res = markdown_to_html("# Hello!", &context).unwrap(); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left);
let res = render_content("# Hello!", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.0,
"<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello!</h1>\n" "<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello!</h1>\n"
@ -357,8 +375,9 @@ fn can_insert_anchor_with_exclamation_mark() {
#[test] #[test]
fn can_insert_anchor_with_link() { fn can_insert_anchor_with_link() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::Left); let config = Config::default();
let res = markdown_to_html("## [](#xresources)Xresources", &context).unwrap(); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left);
let res = render_content("## [](#xresources)Xresources", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.0,
"<h2 id=\"xresources\"><a class=\"gutenberg-anchor\" href=\"#xresources\" aria-label=\"Anchor link for: xresources\">🔗</a>\nXresources</h2>\n" "<h2 id=\"xresources\"><a class=\"gutenberg-anchor\" href=\"#xresources\" aria-label=\"Anchor link for: xresources\">🔗</a>\nXresources</h2>\n"
@ -368,8 +387,9 @@ fn can_insert_anchor_with_link() {
#[test] #[test]
fn can_insert_anchor_with_other_special_chars() { fn can_insert_anchor_with_other_special_chars() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::Left); let config = Config::default();
let res = markdown_to_html("# Hello*_()", &context).unwrap(); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left);
let res = render_content("# Hello*_()", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.0,
"<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello*_()</h1>\n" "<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello*_()</h1>\n"
@ -379,16 +399,16 @@ fn can_insert_anchor_with_other_special_chars() {
#[test] #[test]
fn can_make_toc() { fn can_make_toc() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new( let config = Config::default();
let context = RenderContext::new(
&GUTENBERG_TERA, &GUTENBERG_TERA,
true, &config,
"base16-ocean-dark".to_string(),
"https://mysite.com/something", "https://mysite.com/something",
&permalinks_ctx, &permalinks_ctx,
InsertAnchor::Left InsertAnchor::Left
); );
let res = markdown_to_html(r#" let res = render_content(r#"
# Header 1 # Header 1
## Header 2 ## Header 2
@ -408,8 +428,9 @@ fn can_make_toc() {
#[test] #[test]
fn can_understand_backtick_in_titles() { fn can_understand_backtick_in_titles() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html("# `Hello`", &context).unwrap(); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("# `Hello`", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.0,
"<h1 id=\"hello\"><code>Hello</code></h1>\n" "<h1 id=\"hello\"><code>Hello</code></h1>\n"
@ -419,8 +440,9 @@ fn can_understand_backtick_in_titles() {
#[test] #[test]
fn can_understand_backtick_in_paragraphs() { fn can_understand_backtick_in_paragraphs() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); let config = Config::default();
let res = markdown_to_html("Hello `world`", &context).unwrap(); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("Hello `world`", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.0,
"<p>Hello <code>world</code></p>\n" "<p>Hello <code>world</code></p>\n"

Binary file not shown.

Binary file not shown.