2017-03-07 12:34:31 +00:00
|
|
|
use std::borrow::Cow::Owned;
|
2017-03-27 14:17:33 +00:00
|
|
|
use std::collections::HashMap;
|
2017-03-07 12:34:31 +00:00
|
|
|
|
|
|
|
use pulldown_cmark as cmark;
|
2017-04-19 13:16:21 +00:00
|
|
|
use self::cmark::{Parser, Event, Tag, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
|
2017-03-27 14:17:33 +00:00
|
|
|
use regex::Regex;
|
2017-04-06 07:07:18 +00:00
|
|
|
use slug::slugify;
|
2017-03-20 08:30:50 +00:00
|
|
|
use syntect::dumps::from_binary;
|
2017-03-07 12:34:31 +00:00
|
|
|
use syntect::easy::HighlightLines;
|
|
|
|
use syntect::parsing::SyntaxSet;
|
|
|
|
use syntect::highlighting::ThemeSet;
|
|
|
|
use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, IncludeBackground};
|
2017-03-27 14:17:33 +00:00
|
|
|
use tera::{Tera, Context};
|
|
|
|
|
|
|
|
use config::Config;
|
|
|
|
use errors::{Result, ResultExt};
|
2017-03-07 12:34:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
// We need to put those in a struct to impl Send and sync
|
2017-03-20 08:30:50 +00:00
|
|
|
pub struct Setup {
|
2017-04-21 07:21:44 +00:00
|
|
|
pub syntax_set: SyntaxSet,
|
2017-03-20 08:30:50 +00:00
|
|
|
pub theme_set: ThemeSet,
|
2017-03-07 12:34:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
unsafe impl Send for Setup {}
|
|
|
|
unsafe impl Sync for Setup {}
|
|
|
|
|
|
|
|
lazy_static!{
|
2017-03-27 14:17:33 +00:00
|
|
|
static ref SHORTCODE_RE: Regex = Regex::new(r#"\{(?:%|\{)\s+([[:alnum:]]+?)\(([[:alnum:]]+?="?.+?"?)\)\s+(?:%|\})\}"#).unwrap();
|
2017-03-20 08:30:50 +00:00
|
|
|
pub static ref SETUP: Setup = Setup {
|
2017-04-21 07:21:44 +00:00
|
|
|
syntax_set: {
|
|
|
|
let mut ps: SyntaxSet = from_binary(include_bytes!("../sublime_syntaxes/newlines.packdump"));
|
|
|
|
ps.link_syntaxes();
|
|
|
|
ps
|
|
|
|
},
|
2017-03-20 08:30:50 +00:00
|
|
|
theme_set: from_binary(include_bytes!("../sublime_themes/all.themedump"))
|
2017-03-07 12:34:31 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-03-27 14:17:33 +00:00
|
|
|
/// 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)]
|
|
|
|
struct ShortCode {
|
|
|
|
name: String,
|
|
|
|
args: HashMap<String, String>,
|
|
|
|
body: String,
|
2017-03-07 12:34:31 +00:00
|
|
|
}
|
|
|
|
|
2017-03-27 14:17:33 +00:00
|
|
|
impl ShortCode {
|
|
|
|
pub fn new(name: &str, args: HashMap<String, String>) -> ShortCode {
|
|
|
|
ShortCode {
|
|
|
|
name: name.to_string(),
|
|
|
|
args: args,
|
|
|
|
body: String::new(),
|
2017-03-07 12:34:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-27 14:17:33 +00:00
|
|
|
pub fn append(&mut self, text: &str) {
|
|
|
|
self.body.push_str(text)
|
|
|
|
}
|
2017-03-07 12:34:31 +00:00
|
|
|
|
2017-03-27 14:17:33 +00:00
|
|
|
pub fn render(&self, tera: &Tera) -> Result<String> {
|
|
|
|
let mut context = Context::new();
|
|
|
|
for (key, value) in self.args.iter() {
|
|
|
|
context.add(key, value);
|
2017-03-07 12:34:31 +00:00
|
|
|
}
|
2017-03-27 14:17:33 +00:00
|
|
|
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
|
|
|
|
fn parse_shortcode(input: &str) -> (String, HashMap<String, String>) {
|
|
|
|
let mut args = HashMap::new();
|
|
|
|
let caps = SHORTCODE_RE.captures(input).unwrap();
|
|
|
|
// caps[0] is the full match
|
|
|
|
let name = &caps[1];
|
|
|
|
let arg_list = &caps[2];
|
|
|
|
for arg in arg_list.split(',') {
|
|
|
|
let bits = arg.split('=').collect::<Vec<_>>();
|
|
|
|
args.insert(bits[0].trim().to_string(), bits[1].replace("\"", ""));
|
|
|
|
}
|
|
|
|
|
|
|
|
(name.to_string(), args)
|
|
|
|
}
|
2017-03-07 12:34:31 +00:00
|
|
|
|
2017-03-27 14:17:33 +00:00
|
|
|
/// Renders a shortcode or return an error
|
|
|
|
fn render_simple_shortcode(tera: &Tera, name: &str, args: &HashMap<String, String>) -> 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))
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config) -> Result<String> {
|
|
|
|
// 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 config.highlight_code.unwrap() {
|
|
|
|
content.contains("```")
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
};
|
|
|
|
let highlight_theme = config.highlight_theme.clone().unwrap();
|
|
|
|
// Set while parsing
|
|
|
|
let mut error = None;
|
|
|
|
let mut highlighter: Option<HighlightLines> = None;
|
|
|
|
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;
|
2017-04-06 07:07:18 +00:00
|
|
|
// If we get text in header, we need to insert the id and a anchor
|
|
|
|
let mut in_header = false;
|
2017-04-22 02:40:11 +00:00
|
|
|
// pulldown_cmark can send several text events for a title if there are markdown
|
|
|
|
// specific characters like `!` in them. We only want to insert the anchor the first time
|
|
|
|
let mut header_already_inserted = false;
|
2017-03-27 14:17:33 +00:00
|
|
|
// the rendered html
|
|
|
|
let mut html = String::new();
|
2017-04-06 07:07:18 +00:00
|
|
|
let mut anchors: Vec<String> = vec![];
|
|
|
|
|
|
|
|
// 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: &Vec<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)
|
|
|
|
}
|
2017-03-27 14:17:33 +00:00
|
|
|
|
2017-04-19 13:16:21 +00:00
|
|
|
let mut opts = Options::empty();
|
|
|
|
opts.insert(OPTION_ENABLE_TABLES);
|
|
|
|
opts.insert(OPTION_ENABLE_FOOTNOTES);
|
|
|
|
|
2017-03-27 14:17:33 +00:00
|
|
|
{
|
2017-04-19 13:16:21 +00:00
|
|
|
let parser = Parser::new_ext(content, opts).map(|event| match event {
|
2017-03-07 12:34:31 +00:00
|
|
|
Event::Text(text) => {
|
|
|
|
// if we are in the middle of a code block
|
2017-03-27 14:17:33 +00:00
|
|
|
if let Some(ref mut highlighter) = highlighter {
|
2017-03-07 12:34:31 +00:00
|
|
|
let highlighted = &highlighter.highlight(&text);
|
|
|
|
let html = styles_to_coloured_html(highlighted, IncludeBackground::Yes);
|
2017-03-27 14:17:33 +00:00
|
|
|
return Event::Html(Owned(html));
|
|
|
|
}
|
|
|
|
|
|
|
|
if in_code_block {
|
|
|
|
return Event::Text(text);
|
2017-03-07 12:34:31 +00:00
|
|
|
}
|
2017-03-27 14:17:33 +00:00
|
|
|
|
|
|
|
// Shortcode without body
|
|
|
|
if shortcode_block.is_none() && text.starts_with("{{") && text.ends_with("}}") {
|
|
|
|
if SHORTCODE_RE.is_match(&text) {
|
|
|
|
let (name, args) = parse_shortcode(&text);
|
|
|
|
added_shortcode = true;
|
|
|
|
match render_simple_shortcode(tera, &name, &args) {
|
|
|
|
Ok(s) => return Event::Html(Owned(format!("</p>{}", s))),
|
|
|
|
Err(e) => {
|
|
|
|
error = Some(e);
|
|
|
|
return Event::Html(Owned("".to_string()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// non-matching will be returned normally below
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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("".to_string()));
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
match shortcode.render(tera) {
|
|
|
|
Ok(s) => return Event::Html(Owned(format!("</p>{}", s))),
|
|
|
|
Err(e) => {
|
|
|
|
error = Some(e);
|
|
|
|
return Event::Html(Owned("".to_string()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
shortcode.append(&text);
|
|
|
|
return Event::Html(Owned("".to_string()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-06 07:07:18 +00:00
|
|
|
if in_header {
|
2017-04-22 02:40:11 +00:00
|
|
|
if header_already_inserted {
|
|
|
|
return Event::Text(text);
|
|
|
|
}
|
2017-04-06 07:07:18 +00:00
|
|
|
let id = find_anchor(&anchors, slugify(&text), 0);
|
|
|
|
anchors.push(id.clone());
|
2017-04-06 08:07:53 +00:00
|
|
|
let anchor_link = if config.insert_anchor_links.unwrap() {
|
|
|
|
let mut context = Context::new();
|
|
|
|
context.add("id", &id);
|
|
|
|
tera.render("anchor-link.html", &context).unwrap()
|
|
|
|
} else {
|
|
|
|
String::new()
|
|
|
|
};
|
2017-04-22 02:40:11 +00:00
|
|
|
header_already_inserted = true;
|
2017-04-06 08:07:53 +00:00
|
|
|
return Event::Html(Owned(format!(r#"id="{}">{}{}"#, id, anchor_link, text)));
|
2017-04-06 07:07:18 +00:00
|
|
|
}
|
|
|
|
|
2017-03-27 14:17:33 +00:00
|
|
|
// Business as usual
|
|
|
|
Event::Text(text)
|
2017-03-07 12:34:31 +00:00
|
|
|
},
|
|
|
|
Event::Start(Tag::CodeBlock(ref info)) => {
|
2017-03-27 14:17:33 +00:00
|
|
|
in_code_block = true;
|
|
|
|
if !should_highlight {
|
|
|
|
return Event::Html(Owned("<pre><code>".to_owned()));
|
|
|
|
}
|
|
|
|
let theme = &SETUP.theme_set.themes[&highlight_theme];
|
2017-03-07 12:34:31 +00:00
|
|
|
let syntax = info
|
|
|
|
.split(' ')
|
|
|
|
.next()
|
|
|
|
.and_then(|lang| SETUP.syntax_set.find_syntax_by_token(lang))
|
|
|
|
.unwrap_or_else(|| SETUP.syntax_set.find_syntax_plain_text());
|
2017-03-27 14:17:33 +00:00
|
|
|
highlighter = Some(HighlightLines::new(syntax, theme));
|
2017-03-22 11:59:49 +00:00
|
|
|
let snippet = start_coloured_html_snippet(theme);
|
2017-03-27 14:17:33 +00:00
|
|
|
Event::Html(Owned(snippet))
|
2017-03-07 12:34:31 +00:00
|
|
|
},
|
|
|
|
Event::End(Tag::CodeBlock(_)) => {
|
2017-03-27 14:17:33 +00:00
|
|
|
in_code_block = false;
|
|
|
|
if !should_highlight{
|
|
|
|
return Event::Html(Owned("</code></pre>\n".to_owned()))
|
|
|
|
}
|
2017-03-07 12:34:31 +00:00
|
|
|
// reset highlight and close the code block
|
2017-03-27 14:17:33 +00:00
|
|
|
highlighter = None;
|
|
|
|
Event::Html(Owned("</pre>".to_owned()))
|
2017-03-07 12:34:31 +00:00
|
|
|
},
|
2017-03-30 05:48:07 +00:00
|
|
|
// Need to handle relative links
|
|
|
|
Event::Start(Tag::Link(ref link, ref title)) => {
|
|
|
|
if link.starts_with("./") {
|
2017-04-06 07:33:59 +00:00
|
|
|
// First we remove the ./ since that's gutenberg specific
|
|
|
|
let clean_link = link.replacen("./", "", 1);
|
|
|
|
// Then we remove any potential anchor
|
|
|
|
// parts[0] will be the file path and parts[1] the anchor if present
|
|
|
|
let parts = clean_link.split('#').collect::<Vec<_>>();
|
|
|
|
match permalinks.get(parts[0]) {
|
|
|
|
Some(p) => {
|
|
|
|
let url = if parts.len() > 1 {
|
|
|
|
format!("{}#{}", p, parts[1])
|
|
|
|
} else {
|
|
|
|
p.to_string()
|
|
|
|
};
|
|
|
|
return Event::Start(Tag::Link(Owned(url), title.clone()));
|
|
|
|
},
|
2017-03-30 05:48:07 +00:00
|
|
|
None => {
|
|
|
|
error = Some(format!("Relative link {} not found.", link).into());
|
|
|
|
return Event::Html(Owned("".to_string()));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return Event::Start(Tag::Link(link.clone(), title.clone()));
|
|
|
|
},
|
|
|
|
// need to know when we are in a code block to disable shortcodes in them
|
2017-03-27 14:17:33 +00:00
|
|
|
Event::Start(Tag::Code) => {
|
|
|
|
in_code_block = true;
|
|
|
|
event
|
|
|
|
},
|
|
|
|
Event::End(Tag::Code) => {
|
|
|
|
in_code_block = false;
|
|
|
|
event
|
|
|
|
},
|
2017-04-06 07:07:18 +00:00
|
|
|
Event::Start(Tag::Header(num)) => {
|
|
|
|
in_header = true;
|
|
|
|
// ugly eh
|
|
|
|
return Event::Html(Owned(format!("<h{} ", num)));
|
|
|
|
},
|
|
|
|
Event::End(Tag::Header(_)) => {
|
|
|
|
in_header = false;
|
2017-04-22 02:40:11 +00:00
|
|
|
header_already_inserted = false;
|
2017-04-06 07:07:18 +00:00
|
|
|
event
|
|
|
|
},
|
2017-03-30 05:48:07 +00:00
|
|
|
// If we added shortcodes, don't close a paragraph since there's none
|
2017-03-27 14:17:33 +00:00
|
|
|
Event::End(Tag::Paragraph) => {
|
|
|
|
if added_shortcode {
|
|
|
|
added_shortcode = false;
|
|
|
|
return Event::Html(Owned("".to_owned()));
|
|
|
|
}
|
|
|
|
event
|
|
|
|
},
|
2017-03-30 05:48:07 +00:00
|
|
|
// Ignore softbreaks inside shortcodes
|
2017-03-27 14:17:33 +00:00
|
|
|
Event::SoftBreak => {
|
|
|
|
if shortcode_block.is_some() {
|
|
|
|
return Event::Html(Owned("".to_owned()));
|
|
|
|
}
|
|
|
|
event
|
|
|
|
},
|
|
|
|
_ => {
|
|
|
|
// println!("event = {:?}", event);
|
|
|
|
event
|
|
|
|
},
|
|
|
|
});
|
2017-03-07 12:34:31 +00:00
|
|
|
|
2017-03-27 14:17:33 +00:00
|
|
|
cmark::html::push_html(&mut html, parser);
|
2017-03-07 12:34:31 +00:00
|
|
|
}
|
2017-03-23 05:11:24 +00:00
|
|
|
|
2017-03-27 14:17:33 +00:00
|
|
|
match error {
|
|
|
|
Some(e) => Err(e),
|
2017-03-29 14:32:40 +00:00
|
|
|
None => Ok(html.replace("<p></p>", "")),
|
2017-03-27 14:17:33 +00:00
|
|
|
}
|
2017-03-07 12:34:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2017-03-27 14:17:33 +00:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
2017-03-29 14:32:40 +00:00
|
|
|
use site::GUTENBERG_TERA;
|
2017-03-27 14:17:33 +00:00
|
|
|
use tera::Tera;
|
|
|
|
|
|
|
|
use config::Config;
|
|
|
|
use super::{markdown_to_html, parse_shortcode};
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_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 test_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 test_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");
|
|
|
|
}
|
2017-03-07 12:34:31 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_simple() {
|
2017-04-06 07:33:59 +00:00
|
|
|
let res = markdown_to_html("hello", &HashMap::new(), &Tera::default(), &Config::default()).unwrap();
|
|
|
|
assert_eq!(res, "<p>hello</p>\n");
|
2017-03-07 12:34:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_code_block_highlighting_off() {
|
2017-03-27 14:17:33 +00:00
|
|
|
let mut config = Config::default();
|
|
|
|
config.highlight_code = Some(false);
|
|
|
|
let res = markdown_to_html("```\n$ gutenberg server\n```", &HashMap::new(), &Tera::default(), &config).unwrap();
|
2017-03-07 12:34:31 +00:00
|
|
|
assert_eq!(
|
|
|
|
res,
|
|
|
|
"<pre><code>$ gutenberg server\n</code></pre>\n"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_code_block_no_lang() {
|
2017-03-27 14:17:33 +00:00
|
|
|
let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap();
|
2017-03-07 12:34:31 +00:00
|
|
|
assert_eq!(
|
|
|
|
res,
|
|
|
|
"<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>"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_code_block_with_lang() {
|
2017-03-27 14:17:33 +00:00
|
|
|
let res = markdown_to_html("```python\nlist.append(1)\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap();
|
2017-03-07 12:34:31 +00:00
|
|
|
assert_eq!(
|
|
|
|
res,
|
|
|
|
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">.</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;\">)</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">\n</span></pre>"
|
|
|
|
);
|
|
|
|
}
|
2017-03-27 14:17:33 +00:00
|
|
|
|
2017-03-07 12:34:31 +00:00
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_code_block_with_unknown_lang() {
|
2017-03-27 14:17:33 +00:00
|
|
|
let res = markdown_to_html("```yolo\nlist.append(1)\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap();
|
2017-03-07 12:34:31 +00:00
|
|
|
// defaults to plain text
|
|
|
|
assert_eq!(
|
|
|
|
res,
|
|
|
|
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list.append(1)\n</span></pre>"
|
|
|
|
);
|
|
|
|
}
|
2017-03-27 14:17:33 +00:00
|
|
|
|
|
|
|
#[test]
|
2017-03-29 14:32:40 +00:00
|
|
|
fn test_markdown_to_html_with_shortcode() {
|
2017-03-27 14:17:33 +00:00
|
|
|
let res = markdown_to_html(r#"
|
|
|
|
Hello
|
2017-03-29 14:32:40 +00:00
|
|
|
|
|
|
|
{{ youtube(id="ub36ffWAqgQ") }}
|
|
|
|
"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap();
|
|
|
|
assert!(res.contains("<p>Hello</p>\n<div >"));
|
|
|
|
assert!(res.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_with_several_shortcode_in_row() {
|
|
|
|
let res = markdown_to_html(r#"
|
|
|
|
Hello
|
|
|
|
|
|
|
|
{{ youtube(id="ub36ffWAqgQ") }}
|
|
|
|
|
|
|
|
{{ youtube(id="ub36ffWAqgQ", autoplay=true) }}
|
|
|
|
|
|
|
|
{{ vimeo(id="210073083") }}
|
|
|
|
|
|
|
|
{{ gist(url="https://gist.github.com/Keats/32d26f699dcc13ebd41b") }}
|
|
|
|
|
|
|
|
"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap();
|
|
|
|
assert!(res.contains("<p>Hello</p>\n<div >"));
|
|
|
|
assert!(res.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#));
|
|
|
|
assert!(res.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ?autoplay=1""#));
|
|
|
|
assert!(res.contains(r#"//player.vimeo.com/video/210073083""#));
|
2017-03-27 14:17:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_shortcode_in_code_block() {
|
2017-03-29 14:32:40 +00:00
|
|
|
let res = markdown_to_html(r#"```{{ youtube(id="w7Ft2ymGmfc") }}```"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap();
|
2017-03-27 14:17:33 +00:00
|
|
|
assert_eq!(res, "<p><code>{{ youtube(id="w7Ft2ymGmfc") }}</code></p>\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_shortcode_with_body() {
|
2017-03-29 14:32:40 +00:00
|
|
|
let mut tera = Tera::default();
|
|
|
|
tera.extend(&GUTENBERG_TERA).unwrap();
|
|
|
|
tera.add_raw_template("shortcodes/quote.html", "<blockquote>{{ body }} - {{ author}}</blockquote>").unwrap();
|
2017-03-27 14:17:33 +00:00
|
|
|
let res = markdown_to_html(r#"
|
|
|
|
Hello
|
|
|
|
{% quote(author="Keats") %}
|
|
|
|
A quote
|
|
|
|
{% end %}
|
2017-03-29 14:32:40 +00:00
|
|
|
"#, &HashMap::new(), &tera, &Config::default()).unwrap();
|
|
|
|
assert_eq!(res, "<p>Hello\n</p><blockquote>A quote - Keats</blockquote>");
|
2017-03-27 14:17:33 +00:00
|
|
|
}
|
2017-03-30 05:48:07 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_unknown_shortcode() {
|
|
|
|
let res = markdown_to_html("{{ hello(flash=true) }}", &HashMap::new(), &Tera::default(), &Config::default());
|
|
|
|
assert!(res.is_err());
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_relative_link_exists() {
|
|
|
|
let mut permalinks = HashMap::new();
|
|
|
|
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string());
|
|
|
|
let res = markdown_to_html(
|
|
|
|
r#"[rel link](./pages/about.md), [abs link](https://vincent.is/about)"#,
|
|
|
|
&permalinks,
|
|
|
|
&GUTENBERG_TERA,
|
|
|
|
&Config::default()
|
|
|
|
).unwrap();
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
res.contains(r#"<p><a href="https://vincent.is/about">rel link</a>, <a href="https://vincent.is/about">abs link</a></p>"#)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-04-06 07:33:59 +00:00
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_relative_links_with_anchors() {
|
|
|
|
let mut permalinks = HashMap::new();
|
|
|
|
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string());
|
|
|
|
let res = markdown_to_html(
|
|
|
|
r#"[rel link](./pages/about.md#cv)"#,
|
|
|
|
&permalinks,
|
|
|
|
&GUTENBERG_TERA,
|
|
|
|
&Config::default()
|
|
|
|
).unwrap();
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
res.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-03-30 05:48:07 +00:00
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_relative_link_inexistant() {
|
|
|
|
let res = markdown_to_html("[rel link](./pages/about.md)", &HashMap::new(), &Tera::default(), &Config::default());
|
|
|
|
assert!(res.is_err());
|
|
|
|
}
|
|
|
|
|
2017-04-06 07:07:18 +00:00
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_add_id_to_headers() {
|
|
|
|
let res = markdown_to_html(r#"# Hello"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap();
|
|
|
|
assert_eq!(res, "<h1 id=\"hello\">Hello</h1>\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_add_id_to_headers_same_slug() {
|
|
|
|
let res = markdown_to_html("# Hello\n# Hello", &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap();
|
|
|
|
assert_eq!(res, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n");
|
|
|
|
}
|
2017-04-22 02:40:11 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_insert_anchor() {
|
|
|
|
let mut config = Config::default();
|
|
|
|
config.insert_anchor_links = Some(true);
|
|
|
|
let res = markdown_to_html("# Hello", &HashMap::new(), &GUTENBERG_TERA, &config).unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
res,
|
|
|
|
"<h1 id=\"hello\"><a class=\"anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello</h1>\n"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// See https://github.com/Keats/gutenberg/issues/42
|
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_insert_anchor_with_exclamation_mark() {
|
|
|
|
let mut config = Config::default();
|
|
|
|
config.insert_anchor_links = Some(true);
|
|
|
|
let res = markdown_to_html("# Hello!", &HashMap::new(), &GUTENBERG_TERA, &config).unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
res,
|
|
|
|
"<h1 id=\"hello\"><a class=\"anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello!</h1>\n"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_markdown_to_html_insert_anchor_with_other_special_chars() {
|
|
|
|
let mut config = Config::default();
|
|
|
|
config.insert_anchor_links = Some(true);
|
|
|
|
let res = markdown_to_html("# Hello*_()", &HashMap::new(), &GUTENBERG_TERA, &config).unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
res,
|
|
|
|
"<h1 id=\"hello\"><a class=\"anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello*_()</h1>\n"
|
|
|
|
);
|
|
|
|
}
|
2017-03-07 12:34:31 +00:00
|
|
|
}
|