From a6b8caf6de58f765fd2f8d58bf8e38697bd8f717 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 27 Mar 2017 23:17:33 +0900 Subject: [PATCH] Add shortcodes --- CHANGELOG.md | 2 + Cargo.lock | 40 +++--- README.md | 3 + src/markdown.rs | 323 ++++++++++++++++++++++++++++++++++++++---------- src/page.rs | 32 +++-- src/section.rs | 7 +- src/site.rs | 38 ++++-- tests/page.rs | 46 ++++--- tests/site.rs | 2 +- 9 files changed, 365 insertions(+), 128 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b2f77b..2fd926b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,3 +4,5 @@ - Add some colours in console - Allow using a file other than config.toml for config - Add sections to the index page context +- Fix page rendering not working when containing `+++` +- Add shortcodes (see README for details) diff --git a/Cargo.lock b/Cargo.lock index a03f1e6..fcaa5d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,13 +7,13 @@ dependencies = [ "error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "mount 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "notify 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "pulldown-cmark 0.0.14 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.9.12 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 0.9.12 (registry+https://github.com/rust-lang/crates.io-index)", "slug 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "staticfile 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "syntect 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -325,7 +325,7 @@ dependencies = [ "conduit-mime-types 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "error 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.10.5 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "modifier 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -355,7 +355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "lazy_static" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -555,7 +555,7 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "onig_sys 61.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -588,7 +588,7 @@ dependencies = [ "byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.9.12 (registry+https://github.com/rust-lang/crates.io-index)", "xml-rs 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -688,12 +688,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde_codegen_internals" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "syn 0.11.9 (registry+https://github.com/rust-lang/crates.io-index)", @@ -701,11 +701,11 @@ dependencies = [ [[package]] name = "serde_derive" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_codegen_internals 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_codegen_internals 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 0.11.9 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -717,7 +717,7 @@ dependencies = [ "dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.9.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -786,7 +786,7 @@ dependencies = [ "bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", "flate2 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)", "fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "onig 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "plist 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -812,10 +812,10 @@ dependencies = [ "error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "humansize 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "pest 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.9.12 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 0.9.9 (registry+https://github.com/rust-lang/crates.io-index)", "slug 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -882,7 +882,7 @@ name = "toml" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.9.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1093,7 +1093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "eb2f404fbc66fd9aac13e998248505e7ecb2ad8e44ab6388684c5fb11c6c251c" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" -"checksum lazy_static 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7291b1dd97d331f752620b02dfdbc231df7fc01bf282a00769e1cdb963c460dc" +"checksum lazy_static 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4732c563b9a21a406565c4747daa7b46742f082911ae4753f390dc9ec7ee1a97" "checksum lazycell 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce12306c4739d86ee97c23139f3a34ddf0387bbf181bc7929d287025a8c3ef6b" "checksum libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "88ee81885f9f04bff991e306fea7c1c60a5f0f9e409e99f6b40e3311a3363135" "checksum log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "5141eca02775a762cc6cd564d8d2c50f67c0ea3a372cbf1c51592b3e029e10ad" @@ -1134,9 +1134,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac" "checksum sequence_trie 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c915714ca833b1d4d6b8f6a9d72a3ff632fe45b40a8d184ef79c81bec6327eed" "checksum serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" -"checksum serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)" = "a702319c807c016e51f672e5c77d6f0b46afddd744b5e437d6b8436b888b458f" -"checksum serde_codegen_internals 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d52006899f910528a10631e5b727973fe668f3228109d1707ccf5bad5490b6e" -"checksum serde_derive 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)" = "f15ea24bd037b2d64646b4d934fa99c649be66e3f7b29fb595a5543b212b1452" +"checksum serde 0.9.12 (registry+https://github.com/rust-lang/crates.io-index)" = "f023838e7e1878c679322dc7f66c3648bd33763a215fad752f378a623856898d" +"checksum serde_codegen_internals 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bc888bd283bd2420b16ad0d860e35ad8acb21941180a83a189bb2046f9d00400" +"checksum serde_derive 0.9.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ebb753639f6d55ba1acbcd330ccaf4d9f5862353ac2851e43eac63c2a5343a11" "checksum serde_json 0.9.9 (registry+https://github.com/rust-lang/crates.io-index)" = "dbc45439552eb8fb86907a2c41c1fd0ef97458efb87ff7f878db466eb581824e" "checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c" "checksum slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d807fd58c4181bbabed77cb3b891ba9748241a552bcc5be698faaebefc54f46e" diff --git a/README.md b/README.md index f8e52d0..98984a5 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,9 @@ Gutenberg supports that pattern out of the box: you can create a folder, put a f along with it that are NOT markdown. Those assets will be copied in the same folder when building so you can just use a relative path to use them. +A summary is only defined if you put `` in the content. If present in a page, the summary will be from +the start up to that tag.s + ### Sections Sections represent a group of pages, for example a `tutorials` section of your site. Sections are only created in Gutenberg when a file named `_index.md` is found in the `content` directory. diff --git a/src/markdown.rs b/src/markdown.rs index 0c17a86..5e49acd 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -1,13 +1,18 @@ use std::borrow::Cow::Owned; +use std::collections::HashMap; use pulldown_cmark as cmark; use self::cmark::{Parser, Event, Tag}; - +use regex::Regex; use syntect::dumps::from_binary; use syntect::easy::HighlightLines; use syntect::parsing::SyntaxSet; use syntect::highlighting::ThemeSet; use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, IncludeBackground}; +use tera::{Tera, Context}; + +use config::Config; +use errors::{Result, ResultExt}; // We need to put those in a struct to impl Send and sync @@ -20,117 +25,272 @@ unsafe impl Send for Setup {} unsafe impl Sync for Setup {} lazy_static!{ + static ref SHORTCODE_RE: Regex = Regex::new(r#"\{(?:%|\{)\s+([[:alnum:]]+?)\(([[:alnum:]]+?="?.+?"?)\)\s+(?:%|\})\}"#).unwrap(); pub static ref SETUP: Setup = Setup { syntax_set: SyntaxSet::load_defaults_newlines(), theme_set: from_binary(include_bytes!("../sublime_themes/all.themedump")) }; } - -struct CodeHighlightingParser<'a> { - // The block we're currently highlighting - highlighter: Option>, - parser: Parser<'a>, - theme: &'a str, +/// 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, + body: String, } -impl<'a> CodeHighlightingParser<'a> { - pub fn new(parser: Parser<'a>, theme: &'a str) -> CodeHighlightingParser<'a> { - CodeHighlightingParser { - highlighter: None, - parser: parser, - theme: theme, +impl ShortCode { + pub fn new(name: &str, args: HashMap) -> ShortCode { + ShortCode { + name: name.to_string(), + args: args, + body: String::new(), } } -} -impl<'a> Iterator for CodeHighlightingParser<'a> { - type Item = Event<'a>; + pub fn append(&mut self, text: &str) { + self.body.push_str(text) + } - fn next(&mut self) -> Option> { - // Not using pattern matching to reduce indentation levels - let next_opt = self.parser.next(); - if next_opt.is_none() { - return None; + pub fn render(&self, tera: &Tera) -> Result { + let mut context = Context::new(); + for (key, value) in self.args.iter() { + 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 +fn parse_shortcode(input: &str) -> (String, HashMap) { + 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::>(); + args.insert(bits[0].trim().to_string(), bits[1].replace("\"", "")); + } + + (name.to_string(), args) +} - let item = next_opt.unwrap(); - // Below we just look for the start of a code block and highlight everything - // until we see the end of a code block. - // Everything else happens as normal in pulldown_cmark - match item { +/// Renders a shortcode or return an error +fn render_simple_shortcode(tera: &Tera, name: &str, args: &HashMap) -> Result { + 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, tera: &Tera, config: &Config) -> Result { + // 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 = 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; + // the rendered html + let mut html = String::new(); + + { + let parser = Parser::new(content).map(|event| match event { Event::Text(text) => { // if we are in the middle of a code block - if let Some(ref mut highlighter) = self.highlighter { + if let Some(ref mut highlighter) = highlighter { let highlighted = &highlighter.highlight(&text); let html = styles_to_coloured_html(highlighted, IncludeBackground::Yes); - Some(Event::Html(Owned(html))) - } else { - Some(Event::Text(text)) + return Event::Html(Owned(html)); + } + + if in_code_block { + return Event::Text(text); } + + // 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!("

{}", 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!("

{}", s))), + Err(e) => { + error = Some(e); + return Event::Html(Owned("".to_string())); + } + } + } else { + shortcode.append(&text); + return Event::Html(Owned("".to_string())); + } + } + } + + // Business as usual + Event::Text(text) }, Event::Start(Tag::CodeBlock(ref info)) => { - let theme = &SETUP.theme_set.themes[self.theme]; + in_code_block = true; + if !should_highlight { + return Event::Html(Owned("
".to_owned()));
+                }
+                let theme = &SETUP.theme_set.themes[&highlight_theme];
                 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());
-                self.highlighter = Some(
-                    HighlightLines::new(syntax, theme)
-                );
+                highlighter = Some(HighlightLines::new(syntax, theme));
                 let snippet = start_coloured_html_snippet(theme);
-                Some(Event::Html(Owned(snippet)))
+                Event::Html(Owned(snippet))
             },
             Event::End(Tag::CodeBlock(_)) => {
+                in_code_block = false;
+                if !should_highlight{
+                    return Event::Html(Owned("
\n".to_owned())) + } // reset highlight and close the code block - self.highlighter = None; - Some(Event::Html(Owned("".to_owned()))) + highlighter = None; + Event::Html(Owned("".to_owned())) }, - _ => Some(item) - } + Event::Start(Tag::Code) => { + in_code_block = true; + event + }, + Event::End(Tag::Code) => { + in_code_block = false; + event + }, + Event::End(Tag::Paragraph) => { + if added_shortcode { + added_shortcode = false; + return Event::Html(Owned("".to_owned())); + } + event + }, + 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); } -} - -pub fn markdown_to_html(content: &str, highlight_code: bool, highlight_theme: &str) -> 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 highlight_code { - content.contains("```") - } else { - false - }; - - let mut html = String::new(); - if should_highlight { - let parser = CodeHighlightingParser::new(Parser::new(content), highlight_theme); - cmark::html::push_html(&mut html, parser); - } else { - let parser = Parser::new(content); - cmark::html::push_html(&mut html, parser); - }; - html + match error { + Some(e) => Err(e), + None => Ok(html), + } } #[cfg(test)] mod tests { - use super::{markdown_to_html}; + use std::collections::HashMap; + + use tera::Tera; + + use config::Config; + use super::{markdown_to_html, parse_shortcode}; + + fn create_test_tera() -> Tera { + let mut tera = Tera::default(); + tera.add_raw_template("shortcodes/youtube.html", "Youtube video: {{id}}").unwrap(); + tera.add_raw_template("shortcodes/quote.html", "Quote: {{body}} - {{author}}").unwrap(); + tera + } + + #[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"); + } #[test] fn test_markdown_to_html_simple() { - let res = markdown_to_html("# hello", true, "base16-ocean-dark"); + let res = markdown_to_html("# hello", &HashMap::new(), &Tera::default(), &Config::default()).unwrap(); assert_eq!(res, "

hello

\n"); } #[test] fn test_markdown_to_html_code_block_highlighting_off() { - let res = markdown_to_html("```\n$ gutenberg server\n```", false, "base16-ocean-dark"); + 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(); assert_eq!( res, "
$ gutenberg server\n
\n" @@ -139,7 +299,7 @@ mod tests { #[test] fn test_markdown_to_html_code_block_no_lang() { - let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", true, "base16-ocean-dark"); + let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap(); assert_eq!( res, "
\n$ gutenberg server\n$ ping\n
" @@ -148,19 +308,46 @@ mod tests { #[test] fn test_markdown_to_html_code_block_with_lang() { - let res = markdown_to_html("```python\nlist.append(1)\n```", true, "base16-ocean-dark"); + let res = markdown_to_html("```python\nlist.append(1)\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap(); assert_eq!( res, "
\nlist.append(1)\n
" ); } + #[test] fn test_markdown_to_html_code_block_with_unknown_lang() { - let res = markdown_to_html("```yolo\nlist.append(1)\n```", true, "base16-ocean-dark"); + let res = markdown_to_html("```yolo\nlist.append(1)\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap(); // defaults to plain text assert_eq!( res, "
\nlist.append(1)\n
" ); } + + #[test] + fn test_markdown_to_html_simple_shortcode() { + let res = markdown_to_html(r#" +Hello +{{ youtube(id="w7Ft2ymGmfc") }} + "#, &HashMap::new(), &create_test_tera(), &Config::default()).unwrap(); + assert_eq!(res, "

Hello\n

Youtube video: w7Ft2ymGmfc"); + } + + #[test] + fn test_markdown_to_html_shortcode_in_code_block() { + let res = markdown_to_html(r#"```{{ youtube(id="w7Ft2ymGmfc") }}```"#, &HashMap::new(), &create_test_tera(), &Config::default()).unwrap(); + assert_eq!(res, "

{{ youtube(id="w7Ft2ymGmfc") }}

\n"); + } + + #[test] + fn test_markdown_to_html_shortcode_with_body() { + let res = markdown_to_html(r#" +Hello +{% quote(author="Keats") %} +A quote +{% end %} + "#, &HashMap::new(), &create_test_tera(), &Config::default()).unwrap(); + assert_eq!(res, "

Hello\n

Quote: A quote - Keats"); + } } diff --git a/src/page.rs b/src/page.rs index c80ff8e..761f8d6 100644 --- a/src/page.rs +++ b/src/page.rs @@ -1,5 +1,6 @@ /// A page, can be a blog post or a basic page use std::cmp::Ordering; +use std::collections::HashMap; use std::fs::{read_dir}; use std::path::{Path, PathBuf}; use std::result::Result as StdResult; @@ -43,6 +44,8 @@ fn find_related_assets(path: &Path) -> Vec { pub struct Page { /// The .md path pub file_path: PathBuf, + /// The .md path, starting from the content directory, with / slashes + pub relative_path: String, /// The parent directory of the file. Is actually the grand parent directory /// if it's an asset folder pub parent_path: PathBuf, @@ -88,6 +91,7 @@ impl Page { pub fn new(meta: FrontMatter) -> Page { Page { file_path: PathBuf::new(), + relative_path: String::new(), parent_path: PathBuf::new(), file_name: "".to_string(), components: vec![], @@ -131,16 +135,6 @@ impl Page { page.parent_path = page.file_path.parent().unwrap().to_path_buf(); page.raw_content = content; - let highlight_theme = config.highlight_theme.clone().unwrap(); - page.content = markdown_to_html(&page.raw_content, config.highlight_code.unwrap(), &highlight_theme); - - if page.raw_content.contains("") { - page.summary = { - let summary = page.raw_content.splitn(2, "").collect::>()[0]; - markdown_to_html(summary, config.highlight_code.unwrap(), &highlight_theme) - } - } - let path = Path::new(file_path); page.file_name = path.file_stem().unwrap().to_string_lossy().to_string(); @@ -151,13 +145,14 @@ impl Page { slugify(page.file_name.clone()) } }; + page.components = find_content_components(&page.file_path); + page.relative_path = format!("{}/{}.md", page.components.join("/"), page.file_name); // 4. Find sections // Pages with custom urls exists outside of sections if let Some(ref u) = page.meta.url { page.url = u.trim().to_string(); } else { - page.components = find_content_components(&page.file_path); if !page.components.is_empty() { // If we have a folder with an asset, don't consider it as a component if page.file_name == "index" { @@ -193,6 +188,21 @@ 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, tera: &Tera, config: &Config) -> Result<()> { + self.content = markdown_to_html(&self.raw_content, permalinks, tera, config)?; + + if self.raw_content.contains("") { + self.summary = { + let summary = self.raw_content.splitn(2, "").collect::>()[0]; + markdown_to_html(summary, permalinks, tera, config)? + } + } + + Ok(()) + } + /// Renders the page using the default layout, unless specified in front-matter pub fn render_html(&self, tera: &Tera, config: &Config) -> Result { let tpl_name = match self.meta.template { diff --git a/src/section.rs b/src/section.rs index 3013a44..566c1cd 100644 --- a/src/section.rs +++ b/src/section.rs @@ -15,6 +15,8 @@ use page::Page; pub struct Section { /// The _index.md full path pub file_path: PathBuf, + /// The .md path, starting from the content directory, with / slashes + pub relative_path: String, /// Path of the directory containing the _index.md file pub parent_path: PathBuf, /// The folder names from `content` to this section file @@ -35,6 +37,7 @@ impl Section { pub fn new(file_path: &Path, meta: FrontMatter) -> Section { Section { file_path: file_path.to_path_buf(), + relative_path: "".to_string(), parent_path: file_path.parent().unwrap().to_path_buf(), components: vec![], url: "".to_string(), @@ -50,9 +53,9 @@ impl Section { let mut section = Section::new(file_path, meta); section.components = find_content_components(§ion.file_path); section.url = section.components.join("/"); - section.permalink = section.components.join("/"); - section.permalink = config.make_permalink(§ion.url); + section.relative_path = format!("{}/_index.md", section.components.join("/")); + Ok(section) } diff --git a/src/site.rs b/src/site.rs index 88b592c..c1f25b3 100644 --- a/src/site.rs +++ b/src/site.rs @@ -58,7 +58,7 @@ pub struct Site { pub config: Config, pub pages: HashMap, pub sections: BTreeMap, - pub templates: Tera, + pub tera: Tera, live_reload: bool, output_path: PathBuf, pub tags: HashMap>, @@ -80,7 +80,7 @@ impl Site { config: get_config(path, config_file), pages: HashMap::new(), sections: BTreeMap::new(), - templates: tera, + tera: tera, live_reload: false, output_path: PathBuf::from("public"), tags: HashMap::new(), @@ -119,6 +119,22 @@ impl Site { } } + // A map of all .md files (section and pages) and their permalink + // We need that if there are relative links in the content that need to be resolved + let mut permalinks = HashMap::new(); + + for page in self.pages.values() { + permalinks.insert(page.relative_path.clone(), page.permalink.clone()); + } + + for section in self.sections.values() { + permalinks.insert(section.relative_path.clone(), section.permalink.clone()); + } + + for page in self.pages.values_mut() { + page.render_markdown(&permalinks, &self.tera, &self.config)?; + } + self.populate_sections(); self.populate_tags_and_categories(); @@ -262,7 +278,7 @@ impl Site { } pub fn rebuild_after_template_change(&mut self) -> Result<()> { - self.templates.full_reload()?; + self.tera.full_reload()?; self.build_pages() } @@ -291,7 +307,7 @@ impl Site { create_directory(¤t_path)?; // Finally, create a index.html file there with the page rendered - let output = page.render_html(&self.templates, &self.config)?; + let output = page.render_html(&self.tera, &self.config)?; create_file(current_path.join("index.html"), &self.inject_livereload(output))?; // Copy any asset we found previously into the same directory as the index.html @@ -318,7 +334,7 @@ impl Site { context.add("pages", &populate_previous_and_next_pages(&pages, false)); context.add("sections", &self.sections.values().collect::>()); context.add("config", &self.config); - let index = self.templates.render("index.html", &context)?; + let index = self.tera.render("index.html", &context)?; create_file(public.join("index.html"), &self.inject_livereload(index))?; Ok(()) @@ -343,7 +359,7 @@ impl Site { fn render_robots(&self) -> Result<()> { create_file( self.output_path.join("robots.txt"), - &self.templates.render("robots.txt", &Context::new())? + &self.tera.render("robots.txt", &Context::new())? ) } @@ -382,7 +398,7 @@ impl Site { context.add(name, &sorted_items); context.add("config", &self.config); // And render it immediately - let list_output = self.templates.render(list_tpl_name, &context)?; + let list_output = self.tera.render(list_tpl_name, &context)?; create_file(output_path.join("index.html"), &self.inject_livereload(list_output))?; // Now, each individual item @@ -400,7 +416,7 @@ impl Site { context.add(&format!("{}_slug", var_name), &slug); context.add("pages", &pages); context.add("config", &self.config); - let single_output = self.templates.render(single_tpl_name, &context)?; + let single_output = self.tera.render(single_tpl_name, &context)?; create_directory(&output_path.join(&slug))?; create_file( @@ -439,7 +455,7 @@ impl Site { } context.add("tags", &tags); - let sitemap = self.templates.render("sitemap.xml", &context)?; + let sitemap = self.tera.render("sitemap.xml", &context)?; create_file(self.output_path.join("sitemap.xml"), &sitemap)?; @@ -470,7 +486,7 @@ impl Site { }; context.add("feed_url", &rss_feed_url); - let sitemap = self.templates.render("rss.xml", &context)?; + let sitemap = self.tera.render("rss.xml", &context)?; create_file(self.output_path.join("rss.xml"), &sitemap)?; @@ -490,7 +506,7 @@ impl Site { } } - let output = section.render_html(&self.templates, &self.config)?; + let output = section.render_html(&self.tera, &self.config)?; create_file(output_path.join("index.html"), &self.inject_livereload(output))?; } diff --git a/tests/page.rs b/tests/page.rs index fc8dacc..8f11527 100644 --- a/tests/page.rs +++ b/tests/page.rs @@ -1,11 +1,14 @@ extern crate gutenberg; +extern crate tera; extern crate tempdir; -use tempdir::TempDir; - +use std::collections::HashMap; use std::fs::File; use std::path::Path; +use tempdir::TempDir; +use tera::Tera; + use gutenberg::{Page, Config}; @@ -20,7 +23,8 @@ slug = "hello-world" Hello world"#; let res = Page::parse(Path::new("post.md"), content, &Config::default()); assert!(res.is_ok()); - let page = res.unwrap(); + let mut page = res.unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); assert_eq!(page.meta.title, "Hello".to_string()); assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string()); @@ -39,7 +43,8 @@ slug = "hello-world" Hello world"#; let res = Page::parse(Path::new("content/posts/intro.md"), content, &Config::default()); assert!(res.is_ok()); - let page = res.unwrap(); + let mut page = res.unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); assert_eq!(page.components, vec!["posts".to_string()]); } @@ -54,7 +59,8 @@ slug = "hello-world" Hello world"#; let res = Page::parse(Path::new("content/posts/intro/start.md"), content, &Config::default()); assert!(res.is_ok()); - let page = res.unwrap(); + let mut page = res.unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); assert_eq!(page.components, vec!["posts".to_string(), "intro".to_string()]); } @@ -71,7 +77,8 @@ Hello world"#; conf.base_url = "http://hello.com/".to_string(); let res = Page::parse(Path::new("content/posts/intro/start.md"), content, &conf); assert!(res.is_ok()); - let page = res.unwrap(); + let mut page = res.unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); assert_eq!(page.url, "posts/intro/hello-world"); assert_eq!(page.permalink, "http://hello.com/posts/intro/hello-world"); } @@ -89,7 +96,8 @@ Hello world"#; conf.base_url = "http://hello.com".to_string(); let res = Page::parse(Path::new("content/posts/intro/hello-world.md"), content, &conf); assert!(res.is_ok()); - let page = res.unwrap(); + let mut page = res.unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); assert_eq!(page.url, "posts/intro/hello-world"); assert_eq!(page.permalink, format!("{}{}", conf.base_url, "/posts/intro/hello-world")); } @@ -105,7 +113,8 @@ slug = "hello-world" Hello world"#; let res = Page::parse(Path::new("start.md"), content, &Config::default()); assert!(res.is_ok()); - let page = res.unwrap(); + let mut page = res.unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); assert_eq!(page.url, "hello-world"); assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "hello-world")); } @@ -132,7 +141,8 @@ description = "hey there" Hello world"#; let res = Page::parse(Path::new("file with space.md"), content, &Config::default()); assert!(res.is_ok()); - let page = res.unwrap(); + let mut page = res.unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); assert_eq!(page.slug, "file-with-space"); assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "file-with-space")); } @@ -147,7 +157,8 @@ description = "hey there" Hello world"#; let res = Page::parse(Path::new(" file with space.md"), content, &Config::default()); assert!(res.is_ok()); - let page = res.unwrap(); + let mut page = res.unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); assert_eq!(page.slug, "file-with-space"); assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "file-with-space")); } @@ -162,7 +173,8 @@ description = "hey there" Hello world"#; let res = Page::parse(Path::new("hello.md"), content, &Config::default()); assert!(res.is_ok()); - let page = res.unwrap(); + let mut page = res.unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); let (word_count, reading_time) = page.get_reading_analytics(); assert_eq!(word_count, 2); assert_eq!(reading_time, 0); @@ -181,7 +193,8 @@ Hello world"#.to_string(); } let res = Page::parse(Path::new("hello.md"), &content, &Config::default()); assert!(res.is_ok()); - let page = res.unwrap(); + let mut page = res.unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); let (word_count, reading_time) = page.get_reading_analytics(); assert_eq!(word_count, 2002); assert_eq!(reading_time, 10); @@ -197,7 +210,8 @@ description = "hey there" Hello world"#.to_string(); let res = Page::parse(Path::new("hello.md"), &content, &Config::default()); assert!(res.is_ok()); - let page = res.unwrap(); + let mut page = res.unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); assert_eq!(page.summary, ""); } @@ -213,7 +227,8 @@ Hello world "#.to_string(); let res = Page::parse(Path::new("hello.md"), &content, &Config::default()); assert!(res.is_ok()); - let page = res.unwrap(); + let mut page = res.unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); assert_eq!(page.summary, "

Hello world

\n"); } @@ -232,7 +247,8 @@ Hey there config.highlight_code = Some(true); let res = Page::parse(Path::new("hello.md"), &content, &config); assert!(res.is_ok()); - let page = res.unwrap(); + let mut page = res.unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); assert!(page.content.starts_with("