Use the new shortcode parser
This commit is contained in:
parent
204f514c0e
commit
85e13483eb
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -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,19 +86,36 @@ 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_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]
|
||||
|
@ -109,11 +126,3 @@ fn bench_render_shortcodes_one_present(b: &mut test::Bencher) {
|
|||
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()));
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
Event::Start(Tag::Link(link.clone(), title.clone()))
|
||||
},
|
||||
Event::End(Tag::Link(_, _)) => {
|
||||
if in_header {
|
||||
return Event::Html(Owned("".to_owned()));
|
||||
}
|
||||
event
|
||||
}
|
||||
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
|
||||
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"
|
||||
|
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in a new issue