@@ -1113,12 +1113,13 @@ dependencies = [ | |||
name = "rendering" | |||
version = "0.1.0" | |||
dependencies = [ | |||
"config 0.1.0", | |||
"errors 0.1.0", | |||
"front_matter 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)", | |||
"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_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)", | |||
@@ -52,7 +52,7 @@ If you want a feature added or modified, please open an issue to discuss it befo | |||
Syntax highlighting depends on submodules so ensure you load them first: | |||
```bash | |||
$ git submodule update --init | |||
$ git submodule update --init | |||
``` | |||
Gutenberg only works with syntaxes in the `.sublime-syntax` format. If your syntax | |||
@@ -75,7 +75,7 @@ You can check for any updates to the current packages by running: | |||
$ 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 | |||
$ cargo run --example generate_sublime synpack ../../sublime_syntaxes ../../sublime_syntaxes/newlines.packdump ../../sublime_syntaxes/nonewlines.packdump | |||
@@ -14,7 +14,7 @@ use utils::fs::{read_file, find_related_assets}; | |||
use utils::site::get_reading_analytics; | |||
use utils::templates::render_template; | |||
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; | |||
@@ -162,21 +162,23 @@ impl Page { | |||
/// We need access to all pages url to render links relative to content | |||
/// 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<()> { | |||
let context = Context::new( | |||
let context = RenderContext::new( | |||
tera, | |||
config.highlight_code, | |||
config.highlight_theme.clone(), | |||
config, | |||
&self.permalink, | |||
permalinks, | |||
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.toc = res.1; | |||
if self.raw_content.contains("<!-- more -->") { | |||
self.summary = Some({ | |||
let summary = self.raw_content.splitn(2, "<!-- more -->").collect::<Vec<&str>>()[0]; | |||
markdown_to_html(summary, &context)?.0 | |||
render_content(summary, &context)?.0 | |||
}) | |||
} | |||
@@ -11,7 +11,7 @@ use errors::{Result, ResultExt}; | |||
use utils::fs::read_file; | |||
use utils::templates::render_template; | |||
use utils::site::get_reading_analytics; | |||
use rendering::{Context, Header, markdown_to_html}; | |||
use rendering::{RenderContext, Header, render_content}; | |||
use page::Page; | |||
use file_info::FileInfo; | |||
@@ -98,15 +98,14 @@ impl Section { | |||
/// We need access to all pages url to render links relative to content | |||
/// 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<()> { | |||
let context = Context::new( | |||
let context = RenderContext::new( | |||
tera, | |||
config.highlight_code, | |||
config.highlight_theme.clone(), | |||
config, | |||
&self.permalink, | |||
permalinks, | |||
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.toc = res.1; | |||
Ok(()) | |||
@@ -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 { | |||
let mut new = Map::new(); | |||
for (k, v) in table.into_iter() { | |||
for (k, v) in table { | |||
if k == "$__toml_private_datetime" { | |||
return v; | |||
} | |||
@@ -4,7 +4,8 @@ extern crate syntect; | |||
use syntect::dumps::from_binary; | |||
use syntect::parsing::SyntaxSet; | |||
use syntect::highlighting::ThemeSet; | |||
use syntect::highlighting::{ThemeSet, Theme}; | |||
use syntect::easy::HighlightLines; | |||
thread_local!{ | |||
pub static SYNTAX_SET: SyntaxSet = { | |||
@@ -17,3 +18,15 @@ thread_local!{ | |||
lazy_static!{ | |||
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) | |||
}) | |||
} |
@@ -5,8 +5,6 @@ authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||
[dependencies] | |||
tera = "0.11" | |||
regex = "1" | |||
lazy_static = "1" | |||
syntect = "2" | |||
pulldown-cmark = "0" | |||
slug = "0.1" | |||
@@ -9,7 +9,7 @@ extern crate front_matter; | |||
use std::collections::HashMap; | |||
use tera::Tera; | |||
use rendering::{Context, markdown_to_html, render_shortcodes}; | |||
use rendering::{RenderContext, render_content, render_shortcodes}; | |||
use front_matter::InsertAnchor; | |||
use config::Config; | |||
@@ -86,34 +86,43 @@ if __name__ == "__main__": | |||
"#; | |||
#[bench] | |||
fn bench_markdown_to_html_with_highlighting(b: &mut test::Bencher) { | |||
let tera_ctx = Tera::default(); | |||
fn bench_render_content_with_highlighting(b: &mut test::Bencher) { | |||
let mut tera = Tera::default(); | |||
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap(); | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); | |||
b.iter(|| markdown_to_html(CONTENT, &context)); | |||
let config = Config::default(); | |||
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
b.iter(|| render_content(CONTENT, &context).unwrap()); | |||
} | |||
#[bench] | |||
fn bench_markdown_to_html_without_highlighting(b: &mut test::Bencher) { | |||
let tera_ctx = Tera::default(); | |||
fn bench_render_content_without_highlighting(b: &mut test::Bencher) { | |||
let mut tera = Tera::default(); | |||
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap(); | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&tera_ctx, false, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); | |||
b.iter(|| markdown_to_html(CONTENT, &context)); | |||
let mut config = Config::default(); | |||
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_shortcodes_one_present(b: &mut test::Bencher) { | |||
let mut tera = Tera::default(); | |||
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap(); | |||
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_shortcodes(CONTENT, &tera, &Config::default())); | |||
b.iter(|| render_content(&content2, &context).unwrap()); | |||
} | |||
#[bench] | |||
fn bench_render_shortcodes_none(b: &mut test::Bencher) { | |||
fn bench_render_shortcodes_one_present(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())); | |||
b.iter(|| render_shortcodes(CONTENT, &tera, &Config::default())); | |||
} | |||
@@ -1,41 +1,35 @@ | |||
use std::collections::HashMap; | |||
use tera::Tera; | |||
use front_matter::InsertAnchor; | |||
use config::Config; | |||
/// All the information from the gutenberg site that is needed to render HTML from markdown | |||
#[derive(Debug)] | |||
pub struct Context<'a> { | |||
pub struct RenderContext<'a> { | |||
pub tera: &'a Tera, | |||
pub highlight_code: bool, | |||
pub highlight_theme: String, | |||
pub current_page_permalink: String, | |||
pub config: &'a Config, | |||
pub current_page_permalink: &'a str, | |||
pub permalinks: &'a HashMap<String, String>, | |||
pub insert_anchor: InsertAnchor, | |||
} | |||
impl<'a> Context<'a> { | |||
impl<'a> RenderContext<'a> { | |||
pub fn new( | |||
tera: &'a Tera, | |||
highlight_code: bool, | |||
highlight_theme: String, | |||
current_page_permalink: &str, | |||
config: &'a Config, | |||
current_page_permalink: &'a str, | |||
permalinks: &'a HashMap<String, String>, | |||
insert_anchor: InsertAnchor, | |||
) -> Context<'a> { | |||
Context { | |||
) -> RenderContext<'a> { | |||
RenderContext { | |||
tera, | |||
current_page_permalink: current_page_permalink.to_string(), | |||
current_page_permalink, | |||
permalinks, | |||
insert_anchor, | |||
highlight_code, | |||
highlight_theme, | |||
config, | |||
} | |||
} | |||
pub fn should_insert_anchor(&self) -> bool { | |||
self.insert_anchor != InsertAnchor::None | |||
} | |||
} |
@@ -1,6 +1,3 @@ | |||
#[macro_use] | |||
extern crate lazy_static; | |||
extern crate regex; | |||
extern crate tera; | |||
extern crate syntect; | |||
extern crate pulldown_cmark; | |||
@@ -12,7 +9,7 @@ extern crate pest; | |||
#[macro_use] | |||
extern crate pest_derive; | |||
#[macro_use] | |||
extern crate errors; | |||
extern crate front_matter; | |||
extern crate highlighting; | |||
@@ -26,9 +23,20 @@ mod context; | |||
mod markdown; | |||
mod table_of_contents; | |||
mod shortcode; | |||
mod short_code; | |||
pub use context::Context; | |||
pub use markdown::markdown_to_html; | |||
use errors::Result; | |||
use markdown::markdown_to_html; | |||
pub use table_of_contents::Header; | |||
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) | |||
} |
@@ -8,37 +8,36 @@ use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, Includ | |||
use errors::Result; | |||
use utils::site::resolve_internal_link; | |||
use context::Context; | |||
use highlighting::{SYNTAX_SET, THEME_SET}; | |||
use short_code::{SHORTCODE_RE, ShortCode, parse_shortcode, render_simple_shortcode}; | |||
use highlighting::{get_highlighter, THEME_SET}; | |||
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 | |||
// 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) { | |||
return name.to_string(); | |||
} | |||
let new_anchor = format!("{}-{}", name, level + 1); | |||
if !anchors.contains(&new_anchor) { | |||
return new_anchor; | |||
} | |||
find_anchor(anchors, name, level + 1) | |||
} | |||
pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec<Header>)> { | |||
// We try to be smart about highlighting code as it can be time-consuming | |||
// If the global config disables it, then we do nothing. However, | |||
// if we see a code block in the content, we assume that this page needs | |||
// 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 | |||
}; | |||
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; | |||
// 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 | |||
@@ -46,254 +45,128 @@ pub fn markdown_to_html(content: &str, context: &Context) -> Result<(String, Vec | |||
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) { | |||
return name.to_string(); | |||
} | |||
let new_anchor = format!("{}-{}", name, level + 1); | |||
if !anchors.contains(&new_anchor) { | |||
return new_anchor; | |||
} | |||
find_anchor(anchors, name, level + 1) | |||
} | |||
let mut headers = vec![]; | |||
// Defaults to a 0 level so not a real header | |||
// It should be an Option ideally but not worth the hassle to update | |||
let mut temp_header = TempHeader::default(); | |||
let mut clear_shortcode_block = false; | |||
let mut opts = Options::empty(); | |||
opts.insert(OPTION_ENABLE_TABLES); | |||
opts.insert(OPTION_ENABLE_FOOTNOTES); | |||
{ | |||
let parser = Parser::new_ext(content, opts).map(|event| { | |||
if clear_shortcode_block { | |||
clear_shortcode_block = false; | |||
shortcode_block = None; | |||
} | |||
match event { | |||
Event::Text(mut text) => { | |||
// Header first | |||
if in_header { | |||
if header_created { | |||
temp_header.push(&text); | |||
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 <code> 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())); | |||
} | |||
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 <code> 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); | |||
let html = styles_to_coloured_html(highlighted, IncludeBackground::Yes); | |||
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; | |||
// if we are in the middle of a code block | |||
if let Some(ref mut highlighter) = highlighter { | |||
let highlighted = &highlighter.highlight(&text); | |||
let html = styles_to_coloured_html(highlighted, IncludeBackground::Yes); | |||
return Event::Html(Owned(html)); | |||
} | |||
} 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())); | |||
} | |||
// Business as usual | |||
Event::Text(text) | |||
}, | |||
Event::Start(Tag::CodeBlock(ref info)) => { | |||
if !context.config.highlight_code { | |||
return Event::Html(Owned("<pre><code>".to_owned())); | |||
} | |||
} | |||
// 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)); | |||
let theme = &THEME_SET.themes[&context.config.highlight_theme]; | |||
highlighter = Some(get_highlighter(&theme, info)); | |||
let snippet = start_coloured_html_snippet(theme); | |||
Event::Html(Owned(snippet)) | |||
}, | |||
Event::End(Tag::CodeBlock(_)) => { | |||
if !context.config.highlight_code { | |||
return Event::Html(Owned("</code></pre>\n".to_owned())) | |||
} | |||
// 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())); | |||
} | |||
// reset highlight and close the code block | |||
highlighter = None; | |||
Event::Html(Owned("</pre>".to_owned())) | |||
}, | |||
// Need to handle relative links | |||
Event::Start(Tag::Link(ref link, ref title)) => { | |||
if in_header { | |||
return Event::Html(Owned("".to_owned())); | |||
} | |||
if link.starts_with("./") { | |||
match resolve_internal_link(link, context.permalinks) { | |||
Ok(url) => { | |||
return Event::Start(Tag::Link(Owned(url), title.clone())); | |||
}, | |||
Err(_) => { | |||
error = Some(format!("Relative link {} not found.", link).into()); | |||
return Event::Html(Owned("".to_string())); | |||
} | |||
} else { | |||
shortcode.append(&text); | |||
return Event::Html(Owned(String::new())); | |||
} | |||
}; | |||
} | |||
} | |||
// Business as usual | |||
Event::Text(text) | |||
}, | |||
Event::Start(Tag::CodeBlock(ref info)) => { | |||
in_code_block = true; | |||
if !should_highlight { | |||
return Event::Html(Owned("<pre><code>".to_owned())); | |||
} | |||
let theme = &THEME_SET.themes[&context.highlight_theme]; | |||
highlighter = 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()); | |||
Some(HighlightLines::new(syntax, theme)) | |||
}); | |||
let snippet = start_coloured_html_snippet(theme); | |||
Event::Html(Owned(snippet)) | |||
}, | |||
Event::End(Tag::CodeBlock(_)) => { | |||
in_code_block = false; | |||
if !should_highlight{ | |||
return Event::Html(Owned("</code></pre>\n".to_owned())) | |||
} | |||
// reset highlight and close the code block | |||
highlighter = None; | |||
Event::Html(Owned("</pre>".to_owned())) | |||
}, | |||
// Need to handle relative links | |||
Event::Start(Tag::Link(ref link, ref title)) => { | |||
if in_header { | |||
return Event::Html(Owned("".to_owned())); | |||
} | |||
if link.starts_with("./") { | |||
match resolve_internal_link(link, context.permalinks) { | |||
Ok(url) => { | |||
return Event::Start(Tag::Link(Owned(url), title.clone())); | |||
}, | |||
Err(_) => { | |||
error = Some(format!("Relative link {} not found.", link).into()); | |||
return Event::Html(Owned("".to_string())); | |||
} | |||
}; | |||
} | |||
Event::Start(Tag::Link(link.clone(), title.clone())) | |||
}, | |||
Event::End(Tag::Link(_, _)) => { | |||
if in_header { | |||
return Event::Html(Owned("".to_owned())); | |||
Event::Start(Tag::Link(link.clone(), title.clone())) | |||
}, | |||
Event::End(Tag::Link(_, _)) => { | |||
if in_header { | |||
return Event::Html(Owned("".to_owned())); | |||
} | |||
event | |||
} | |||
event | |||
Event::Start(Tag::Code) => { | |||
if in_header { | |||
temp_header.push("<code>"); | |||
return Event::Html(Owned(String::new())); | |||
} | |||
event | |||
}, | |||
Event::End(Tag::Code) => { | |||
if in_header { | |||
temp_header.push("</code>"); | |||
return Event::Html(Owned(String::new())); | |||
} | |||
event | |||
}, | |||
Event::Start(Tag::Header(num)) => { | |||
in_header = true; | |||
temp_header = TempHeader::new(num); | |||
Event::Html(Owned(String::new())) | |||
}, | |||
Event::End(Tag::Header(_)) => { | |||
// End of a header, reset all the things and return the stringified version of the header | |||
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, | |||
} | |||
// 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("<code>"); | |||
return Event::Html(Owned(String::new())); | |||
} | |||
event | |||
}, | |||
Event::End(Tag::Code) => { | |||
in_code_block = false; | |||
if in_header { | |||
temp_header.push("</code>"); | |||
return Event::Html(Owned(String::new())); | |||
} | |||
event | |||
}, | |||
Event::Start(Tag::Header(num)) => { | |||
in_header = true; | |||
temp_header = TempHeader::new(num); | |||
Event::Html(Owned(String::new())) | |||
}, | |||
Event::End(Tag::Header(_)) => { | |||
// End of a header, reset all the things and return the stringified version of the header | |||
in_header = false; | |||
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) => { | |||
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); | |||
} | |||
if !current_shortcode.is_empty() { | |||
return Err(format!("A shortcode was not closed properly:\n{:?}", current_shortcode).into()); | |||
} | |||
match error { | |||
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) | |||
)), | |||
} | |||
} |
@@ -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"); | |||
} | |||
} |
@@ -90,7 +90,8 @@ fn render_shortcode(name: String, args: Map<String, Value>, tera: &Tera, config: | |||
context.insert(key, value); | |||
} | |||
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); | |||
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> { | |||
// 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 pairs = match ContentParser::parse(Rule::page, content) { | |||
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 | |||
@@ -333,4 +343,10 @@ Hello World | |||
let res = render_shortcodes("Body\n {% youtube() %}Hey!{% end %}", &tera, &Config::default()).unwrap(); | |||
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()); | |||
} | |||
} |
@@ -1,8 +1,6 @@ | |||
use tera::{Context as TeraContext}; | |||
use tera::{Tera, Context as TeraContext}; | |||
use front_matter::InsertAnchor; | |||
use context::Context; | |||
#[derive(Debug, PartialEq, Clone, Serialize)] | |||
pub struct Header { | |||
@@ -50,16 +48,16 @@ impl TempHeader { | |||
} | |||
/// 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() { | |||
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.add("id", &self.id); | |||
context.tera.render("anchor-link.html", &c).unwrap() | |||
tera.render("anchor-link.html", &c).unwrap() | |||
} else { | |||
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::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), | |||
@@ -2,22 +2,25 @@ extern crate tera; | |||
extern crate front_matter; | |||
extern crate templates; | |||
extern crate rendering; | |||
extern crate config; | |||
use std::collections::HashMap; | |||
use tera::Tera; | |||
use config::Config; | |||
use front_matter::InsertAnchor; | |||
use templates::GUTENBERG_TERA; | |||
use rendering::{Context, markdown_to_html}; | |||
use rendering::{RenderContext, render_content}; | |||
#[test] | |||
fn can_do_markdown_to_html_simple() { | |||
fn can_do_render_content_simple() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("hello", &context).unwrap(); | |||
let config = Config::default(); | |||
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"); | |||
} | |||
@@ -25,9 +28,10 @@ fn can_do_markdown_to_html_simple() { | |||
fn doesnt_highlight_code_block_with_highlighting_off() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let mut context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); | |||
context.highlight_code = false; | |||
let res = markdown_to_html("```\n$ gutenberg server\n```", &context).unwrap(); | |||
let mut config = Config::default(); | |||
config.highlight_code = false; | |||
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
let res = render_content("```\n$ gutenberg server\n```", &context).unwrap(); | |||
assert_eq!( | |||
res.0, | |||
"<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() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", &context).unwrap(); | |||
let config = Config::default(); | |||
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!( | |||
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>" | |||
@@ -50,8 +55,9 @@ fn can_highlight_code_block_no_lang() { | |||
fn can_highlight_code_block_with_lang() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("```python\nlist.append(1)\n```", &context).unwrap(); | |||
let config = Config::default(); | |||
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
let res = render_content("```python\nlist.append(1)\n```", &context).unwrap(); | |||
assert_eq!( | |||
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>" | |||
@@ -62,8 +68,9 @@ fn can_highlight_code_block_with_lang() { | |||
fn can_higlight_code_block_with_unknown_lang() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("```yolo\nlist.append(1)\n```", &context).unwrap(); | |||
let config = Config::default(); | |||
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 | |||
assert_eq!( | |||
res.0, | |||
@@ -74,8 +81,9 @@ fn can_higlight_code_block_with_unknown_lang() { | |||
#[test] | |||
fn can_render_shortcode() { | |||
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#" | |||
let config = Config::default(); | |||
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
let res = render_content(r#" | |||
Hello | |||
{{ youtube(id="ub36ffWAqgQ") }} | |||
@@ -87,7 +95,8 @@ Hello | |||
#[test] | |||
fn can_render_shortcode_with_markdown_char_in_args_name() { | |||
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![ | |||
"name", | |||
"na_me", | |||
@@ -95,7 +104,7 @@ fn can_render_shortcode_with_markdown_char_in_args_name() { | |||
"n1", | |||
]; | |||
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""#)); | |||
} | |||
} | |||
@@ -103,7 +112,8 @@ fn can_render_shortcode_with_markdown_char_in_args_name() { | |||
#[test] | |||
fn can_render_shortcode_with_markdown_char_in_args_value() { | |||
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![ | |||
"ub36ffWAqgQ-hey", | |||
"ub36ffWAqgQ_hey", | |||
@@ -112,7 +122,7 @@ fn can_render_shortcode_with_markdown_char_in_args_value() { | |||
"ub36ffWAqgQ#hey", | |||
]; | |||
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))); | |||
} | |||
} | |||
@@ -126,12 +136,13 @@ fn can_render_body_shortcode_with_markdown_char_in_name() { | |||
"quo_te", | |||
"qu_o_te", | |||
]; | |||
let config = Config::default(); | |||
for i in input { | |||
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); | |||
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(); | |||
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); | |||
assert_eq!(res.0, expected); | |||
} | |||
@@ -189,9 +201,10 @@ Here is another paragraph. | |||
"; | |||
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); | |||
assert_eq!(res.0, expected); | |||
} | |||
@@ -199,8 +212,9 @@ Here is another paragraph. | |||
#[test] | |||
fn can_render_several_shortcode_in_row() { | |||
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#" | |||
let config = Config::default(); | |||
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
let res = render_content(r#" | |||
Hello | |||
{{ youtube(id="ub36ffWAqgQ") }} | |||
@@ -222,18 +236,12 @@ Hello | |||
} | |||
#[test] | |||
fn errors_if_unterminated_shortcode() { | |||
fn doesnt_render_ignored_shortcodes() { | |||
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="w7Ft2ym_a"#, &context); | |||
assert!(res.is_err()); | |||
} | |||
#[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(); | |||
let mut config = Config::default(); | |||
config.highlight_code = false; | |||
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
let res = render_content(r#"```{{/* youtube(id="w7Ft2ymGmfc") */}}```"#, &context).unwrap(); | |||
assert_eq!(res.0, "<p><code>{{ youtube(id="w7Ft2ymGmfc") }}</code></p>\n"); | |||
} | |||
@@ -243,23 +251,25 @@ fn can_render_shortcode_with_body() { | |||
tera.extend(&GUTENBERG_TERA).unwrap(); | |||
tera.add_raw_template("shortcodes/quote.html", "<blockquote>{{ body }} - {{ author }}</blockquote>").unwrap(); | |||
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 | |||
{% quote(author="Keats") %} | |||
A quote | |||
{% end %} | |||
"#, &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] | |||
fn errors_rendering_unknown_shortcode() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("{{ hello(flash=true) }}", &context); | |||
let config = Config::default(); | |||
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
let res = render_content("{{ hello(flash=true) }}", &context); | |||
assert!(res.is_err()); | |||
} | |||
@@ -268,8 +278,9 @@ fn can_make_valid_relative_link() { | |||
let mut permalinks = HashMap::new(); | |||
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string()); | |||
let tera_ctx = Tera::default(); | |||
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks, InsertAnchor::None); | |||
let res = markdown_to_html( | |||
let config = Config::default(); | |||
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)"#, | |||
&context | |||
).unwrap(); | |||
@@ -284,8 +295,9 @@ fn can_make_relative_links_with_anchors() { | |||
let mut permalinks = HashMap::new(); | |||
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string()); | |||
let tera_ctx = Tera::default(); | |||
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks, InsertAnchor::None); | |||
let res = markdown_to_html(r#"[rel link](./pages/about.md#cv)"#, &context).unwrap(); | |||
let config = Config::default(); | |||
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None); | |||
let res = render_content(r#"[rel link](./pages/about.md#cv)"#, &context).unwrap(); | |||
assert!( | |||
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() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("[rel link](./pages/about.md)", &context); | |||
let config = Config::default(); | |||
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()); | |||
} | |||
@@ -305,8 +318,9 @@ fn errors_relative_link_inexistant() { | |||
fn can_add_id_to_headers() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html(r#"# Hello"#, &context).unwrap(); | |||
let config = Config::default(); | |||
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"); | |||
} | |||
@@ -314,16 +328,18 @@ fn can_add_id_to_headers() { | |||
fn can_add_id_to_headers_same_slug() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&tera_ctx, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("# Hello\n# Hello", &context).unwrap(); | |||
let config = Config::default(); | |||
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"); | |||
} | |||
#[test] | |||
fn can_insert_anchor_left() { | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::Left); | |||
let res = markdown_to_html("# Hello", &context).unwrap(); | |||
let config = Config::default(); | |||
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left); | |||
let res = render_content("# Hello", &context).unwrap(); | |||
assert_eq!( | |||
res.0, | |||
"<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] | |||
fn can_insert_anchor_right() { | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::Right); | |||
let res = markdown_to_html("# Hello", &context).unwrap(); | |||
let config = Config::default(); | |||
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Right); | |||
let res = render_content("# Hello", &context).unwrap(); | |||
assert_eq!( | |||
res.0, | |||
"<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] | |||
fn can_insert_anchor_with_exclamation_mark() { | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::Left); | |||
let res = markdown_to_html("# Hello!", &context).unwrap(); | |||
let config = Config::default(); | |||
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left); | |||
let res = render_content("# Hello!", &context).unwrap(); | |||
assert_eq!( | |||
res.0, | |||
"<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] | |||
fn can_insert_anchor_with_link() { | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::Left); | |||
let res = markdown_to_html("## [](#xresources)Xresources", &context).unwrap(); | |||
let config = Config::default(); | |||
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left); | |||
let res = render_content("## [](#xresources)Xresources", &context).unwrap(); | |||
assert_eq!( | |||
res.0, | |||
"<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] | |||
fn can_insert_anchor_with_other_special_chars() { | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new(&GUTENBERG_TERA, true, "base16-ocean-dark".to_string(), "", &permalinks_ctx, InsertAnchor::Left); | |||
let res = markdown_to_html("# Hello*_()", &context).unwrap(); | |||
let config = Config::default(); | |||
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left); | |||
let res = render_content("# Hello*_()", &context).unwrap(); | |||
assert_eq!( | |||
res.0, | |||
"<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] | |||
fn can_make_toc() { | |||
let permalinks_ctx = HashMap::new(); | |||
let context = Context::new( | |||
let config = Config::default(); | |||
let context = RenderContext::new( | |||
&GUTENBERG_TERA, | |||
true, | |||
"base16-ocean-dark".to_string(), | |||
&config, | |||
"https://mysite.com/something", | |||
&permalinks_ctx, | |||
InsertAnchor::Left | |||
); | |||
let res = markdown_to_html(r#" | |||
let res = render_content(r#" | |||
# Header 1 | |||
## Header 2 | |||
@@ -408,8 +428,9 @@ fn can_make_toc() { | |||
#[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(); | |||
let config = Config::default(); | |||
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
let res = render_content("# `Hello`", &context).unwrap(); | |||
assert_eq!( | |||
res.0, | |||
"<h1 id=\"hello\"><code>Hello</code></h1>\n" | |||
@@ -419,8 +440,9 @@ fn can_understand_backtick_in_titles() { | |||
#[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(); | |||
let config = Config::default(); | |||
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
let res = render_content("Hello `world`", &context).unwrap(); | |||
assert_eq!( | |||
res.0, | |||
"<p>Hello <code>world</code></p>\n" | |||