diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b2f77b3..2fd926be 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 a03f1e60..fcaa5d1b 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 7b1fc1ae..791226df 100644
--- a/README.md
+++ b/README.md
@@ -101,6 +101,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.
@@ -147,6 +150,46 @@ built-in:
A gallery containing lots of themes at https://tmtheme-editor.herokuapp.com/#!/editor/theme/Agola%20Dark.
More themes can be easily added to gutenberg, just make a PR with the wanted theme.
+### Shortcodes
+Gutenberg uses markdown for content but sometimes you want to insert some HTML, for example for a YouTube video.
+Rather than copy/pasting the HTML around, Gutenberg supports shortcodes, allowing you to define templates using Tera and call those templates inside markdown.
+
+#### Using a shortcode
+There are 2 kinds of shortcodes: simple ones and those that take some content as body. All shortcodes need to be preceded by a blank line or they
+will be contained in a paragraph.
+
+Simple shortcodes are called the following way:
+
+```markdown
+{{ youtube(id="my_youtube_id") }}
+```
+
+Shortcodes with a body are called like so:
+
+```markdown
+{% quote(author="Me", link="https://google.com") %}
+My quote
+{% end %}
+```
+
+The shortcodes names are taken from the files they are defined in, for example a shortcode with the name youtube will try to render
+the template at `templates/shortcodes/youtube.html`.
+
+#### Built-in shortcodes
+Gutenberg comes with a few built-in shortcodes:
+
+- YouTube: embeds a YouTube player for the given YouTube `id`. Also takes an optional `autoplay` argument that can be set to `true`
+if wanted
+- Vimeo: embeds a Vimeo player for the given Vimeo `id`
+- Gist: embeds a Github gist from the `url` given. Also takes an optional `file` argument if you only want to show one of the files.
+
+#### Defining a shortcode
+All shortcodes need to be in the `templates/shortcodes` folder and their files to end with `.html`.
+Shortcodes templates are simple Tera templates, with all the args being directly accessible in the template.
+
+In case of shortcodes with a body, the body will be passed as the `body` variable.
+
+
## Example sites
- [vincent.is](https://vincent.is): https://gitlab.com/Keats/vincent.is
diff --git a/src/lib.rs b/src/lib.rs
index 22173992..6701114f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -26,7 +26,7 @@ mod site;
mod markdown;
mod section;
-pub use site::Site;
+pub use site::{Site, GUTENBERG_TERA};
pub use config::{Config, get_config};
pub use front_matter::{FrontMatter, split_content};
pub use page::{Page, populate_previous_and_next_pages};
diff --git a/src/markdown.rs b/src/markdown.rs
index 0c17a866..c11e4703 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,266 @@ 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(),
}
}
+
+ pub fn append(&mut self, text: &str) {
+ self.body.push_str(text)
+ }
+
+ 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))
+ }
}
-impl<'a> Iterator for CodeHighlightingParser<'a> {
- type Item = Event<'a>;
-
- 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;
- }
-
- 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 {
- Event::Text(text) => {
- // if we are in the middle of a code block
- if let Some(ref mut highlighter) = self.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))
- }
- },
- Event::Start(Tag::CodeBlock(ref info)) => {
- let theme = &SETUP.theme_set.themes[self.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)
- );
- let snippet = start_coloured_html_snippet(theme);
- Some(Event::Html(Owned(snippet)))
- },
- Event::End(Tag::CodeBlock(_)) => {
- // reset highlight and close the code block
- self.highlighter = None;
- Some(Event::Html(Owned("".to_owned())))
- },
- _ => Some(item)
- }
-
+/// 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)
}
-pub fn markdown_to_html(content: &str, highlight_code: bool, highlight_theme: &str) -> String {
+/// 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 highlight_code {
+ 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();
- if should_highlight {
- let parser = CodeHighlightingParser::new(Parser::new(content), highlight_theme);
+
+ {
+ 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) = 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);
+ }
+
+ // 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)) => {
+ in_code_block = true;
+ if !should_highlight {
+ return Event::Html(Owned("