@@ -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) |
@@ -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" | |||
@@ -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 `<!-- more -->` 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. | |||
@@ -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<HighlightLines<'a>>, | |||
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<String, String>, | |||
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<String, String>) -> 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<Event<'a>> { | |||
// 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<String> { | |||
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<String, String>) { | |||
let mut args = HashMap::new(); | |||
let caps = SHORTCODE_RE.captures(input).unwrap(); | |||
// caps[0] is the full match | |||
let name = &caps[1]; | |||
let arg_list = &caps[2]; | |||
for arg in arg_list.split(',') { | |||
let bits = arg.split('=').collect::<Vec<_>>(); | |||
args.insert(bits[0].trim().to_string(), bits[1].replace("\"", "")); | |||
} | |||
(name.to_string(), args) | |||
} | |||
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<String, String>) -> Result<String> { | |||
let mut context = Context::new(); | |||
for (key, value) in args.iter() { | |||
context.add(key, value); | |||
} | |||
let tpl_name = format!("shortcodes/{}.html", name); | |||
tera.render(&tpl_name, &context).chain_err(|| format!("Failed to render {} shortcode", name)) | |||
} | |||
pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config) -> Result<String> { | |||
// We try to be smart about highlighting code as it can be time-consuming | |||
// If the global config disables it, then we do nothing. However, | |||
// if we see a code block in the content, we assume that this page needs | |||
// to be highlighted. It could potentially have false positive if the content | |||
// has ``` in it but that seems kind of unlikely | |||
let should_highlight = if config.highlight_code.unwrap() { | |||
content.contains("```") | |||
} else { | |||
false | |||
}; | |||
let highlight_theme = config.highlight_theme.clone().unwrap(); | |||
// Set while parsing | |||
let mut error = None; | |||
let mut highlighter: Option<HighlightLines> = None; | |||
let mut shortcode_block = None; | |||
// shortcodes live outside of paragraph so we need to ensure we don't close | |||
// a paragraph that has already been closed | |||
let mut added_shortcode = false; | |||
// Don't transform things that look like shortcodes in code blocks | |||
let mut in_code_block = false; | |||
// 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!("</p>{}", s))), | |||
Err(e) => { | |||
error = Some(e); | |||
return Event::Html(Owned("".to_string())); | |||
} | |||
} | |||
} | |||
// non-matching will be returned normally below | |||
} | |||
// Shortcode with a body | |||
if shortcode_block.is_none() && text.starts_with("{%") && text.ends_with("%}") { | |||
if SHORTCODE_RE.is_match(&text) { | |||
let (name, args) = parse_shortcode(&text); | |||
shortcode_block = Some(ShortCode::new(&name, args)); | |||
} | |||
// Don't return anything | |||
return Event::Text(Owned("".to_string())); | |||
} | |||
// If we have some text while in a shortcode, it's either the body | |||
// or the end tag | |||
if shortcode_block.is_some() { | |||
if let Some(ref mut shortcode) = shortcode_block { | |||
if text.trim() == "{% end %}" { | |||
added_shortcode = true; | |||
match shortcode.render(tera) { | |||
Ok(s) => return Event::Html(Owned(format!("</p>{}", s))), | |||
Err(e) => { | |||
error = Some(e); | |||
return Event::Html(Owned("".to_string())); | |||
} | |||
} | |||
} else { | |||
shortcode.append(&text); | |||
return Event::Html(Owned("".to_string())); | |||
} | |||
} | |||
} | |||
// 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("<pre><code>".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("</code></pre>\n".to_owned())) | |||
} | |||
// reset highlight and close the code block | |||
self.highlighter = None; | |||
Some(Event::Html(Owned("</pre>".to_owned()))) | |||
highlighter = None; | |||
Event::Html(Owned("</pre>".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, "<h1>hello</h1>\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, | |||
"<pre><code>$ gutenberg server\n</code></pre>\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, | |||
"<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>" | |||
@@ -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, | |||
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">.</span><span style=\"background-color:#2b303b;color:#bf616a;\">append</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">(</span><span style=\"background-color:#2b303b;color:#d08770;\">1</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">)</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">\n</span></pre>" | |||
); | |||
} | |||
#[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, | |||
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list.append(1)\n</span></pre>" | |||
); | |||
} | |||
#[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, "<p>Hello\n</p>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, "<p><code>{{ youtube(id="w7Ft2ymGmfc") }}</code></p>\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, "<p>Hello\n</p>Quote: A quote - Keats"); | |||
} | |||
} |
@@ -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<PathBuf> { | |||
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("<!-- more -->") { | |||
page.summary = { | |||
let summary = page.raw_content.splitn(2, "<!-- more -->").collect::<Vec<&str>>()[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<String, String>, tera: &Tera, config: &Config) -> Result<()> { | |||
self.content = markdown_to_html(&self.raw_content, permalinks, tera, config)?; | |||
if self.raw_content.contains("<!-- more -->") { | |||
self.summary = { | |||
let summary = self.raw_content.splitn(2, "<!-- more -->").collect::<Vec<&str>>()[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<String> { | |||
let tpl_name = match self.meta.template { | |||
@@ -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) | |||
} | |||
@@ -58,7 +58,7 @@ pub struct Site { | |||
pub config: Config, | |||
pub pages: HashMap<PathBuf, Page>, | |||
pub sections: BTreeMap<PathBuf, Section>, | |||
pub templates: Tera, | |||
pub tera: Tera, | |||
live_reload: bool, | |||
output_path: PathBuf, | |||
pub tags: HashMap<String, Vec<PathBuf>>, | |||
@@ -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::<Vec<&Section>>()); | |||
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))?; | |||
} | |||
@@ -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, "<p>Hello world</p>\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("<pre")); | |||
} | |||
@@ -28,7 +28,7 @@ fn test_can_parse_site() { | |||
// Make sure the page with a url doesn't have any sections | |||
let url_post = &site.pages[&posts_path.join("fixed-url.md")]; | |||
assert!(url_post.components.is_empty()); | |||
assert_eq!(url_post.url, "a-fixed-url"); | |||
// Make sure the article in a folder with only asset doesn't get counted as a section | |||
let asset_folder_post = &site.pages[&posts_path.join("with-assets").join("index.md")]; | |||