diff --git a/CHANGELOG.md b/CHANGELOG.md index 99bdad82..a113313f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Change the single item template context for categories/tags - Add a `get_url` global Tera function - Add a config option to control how many articles to show in RSS feed +- Move `insert_anchor_links` from config to being a section option ## 0.0.5 (2017-05-15) diff --git a/Cargo.lock b/Cargo.lock index eab7fd13..2250fef8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,7 +56,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "backtrace-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cpp_demangle 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cpp_demangle 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", @@ -187,10 +187,12 @@ dependencies = [ [[package]] name = "cpp_demangle" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "clap 2.24.2 (registry+https://github.com/rust-lang/crates.io-index)", "fixedbitset 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1100,7 +1102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum clap 2.24.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b8f69e518f967224e628896b54e41ff6acfb4dcfefc5076325c36525dac900f" "checksum cmake 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)" = "92278eb79412c8f75cfc89e707a1bb3a6490b68f7f2e78d15c774f30fe701122" "checksum conduit-mime-types 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "95ca30253581af809925ef68c2641cc140d6183f43e12e0af4992d53768bd7b8" -"checksum cpp_demangle 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae1040e8145a72a251c1ccdd1d3d4b4ad175acc363da8a9c21a21cbb5b1f9056" +"checksum cpp_demangle 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2692e985e8b489736612f206c8b2d071767c05ac9343a654dd5ebd791a9035d5" "checksum dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850" "checksum dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80c8b71fd71146990a9742fc06dcbbde19161a267e0ad4e572c35162f4578c90" "checksum error 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "a6e606f14042bb87cc02ef6a14db6c90ab92ed6f62d87e69377bc759fd7987cc" diff --git a/src/bin/rebuild.rs b/src/bin/rebuild.rs index 4f75bb97..6ea7944d 100644 --- a/src/bin/rebuild.rs +++ b/src/bin/rebuild.rs @@ -7,7 +7,7 @@ use gutenberg::errors::Result; /// Finds the section that contains the page given if there is one pub fn find_parent_section<'a>(site: &'a Site, page: &Page) -> Option<&'a Section> { for section in site.sections.values() { - if section.is_child_page(page) { + if section.is_child_page(&page.file.path) { return Some(section) } } diff --git a/src/content/page.rs b/src/content/page.rs index caa9fdb5..667b88cf 100644 --- a/src/content/page.rs +++ b/src/content/page.rs @@ -4,14 +4,15 @@ use std::path::{Path, PathBuf}; use std::result::Result as StdResult; -use tera::{Tera, Context}; +use tera::{Tera, Context as TeraContext}; use serde::ser::{SerializeStruct, self}; use slug::slugify; use errors::{Result, ResultExt}; use config::Config; -use front_matter::{PageFrontMatter, split_page_content}; +use front_matter::{PageFrontMatter, InsertAnchor, split_page_content}; use rendering::markdown::markdown_to_html; +use rendering::context::Context; use fs::{read_file}; use content::utils::{find_related_assets, get_reading_analytics}; use content::file_info::FileInfo; @@ -112,13 +113,13 @@ 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)?; - + pub fn render_markdown(&mut self, permalinks: &HashMap, tera: &Tera, config: &Config, anchor_insert: InsertAnchor) -> Result<()> { + let context = Context::new(tera, config, permalinks, anchor_insert); + self.content = markdown_to_html(&self.raw_content, &context)?; if self.raw_content.contains("") { self.summary = Some({ let summary = self.raw_content.splitn(2, "").collect::>()[0]; - markdown_to_html(summary, permalinks, tera, config)? + markdown_to_html(summary, &context)? }) } @@ -132,7 +133,7 @@ impl Page { None => "page.html".to_string() }; - let mut context = Context::new(); + let mut context = TeraContext::new(); context.add("config", config); context.add("page", self); context.add("current_url", &self.permalink); @@ -195,6 +196,7 @@ mod tests { use config::Config; use super::Page; + use front_matter::InsertAnchor; #[test] @@ -209,7 +211,7 @@ Hello world"#; let res = Page::parse(Path::new("post.md"), content, &Config::default()); assert!(res.is_ok()); let mut page = res.unwrap(); - page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default(), InsertAnchor::None).unwrap(); assert_eq!(page.meta.title.unwrap(), "Hello".to_string()); assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string()); @@ -228,8 +230,7 @@ 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 mut page = res.unwrap(); - page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); + let page = res.unwrap(); assert_eq!(page.path, "posts/intro/hello-world"); assert_eq!(page.permalink, "http://hello.com/posts/intro/hello-world"); } @@ -244,8 +245,7 @@ Hello world"#; let config = Config::default(); let res = Page::parse(Path::new("start.md"), content, &config); assert!(res.is_ok()); - let mut page = res.unwrap(); - page.render_markdown(&HashMap::default(), &Tera::default(), &config).unwrap(); + let page = res.unwrap(); assert_eq!(page.path, "hello-world"); assert_eq!(page.permalink, config.make_permalink("hello-world")); } @@ -268,8 +268,7 @@ Hello world"#; let config = Config::default(); let res = Page::parse(Path::new(" file with space.md"), "+++\n+++", &config); assert!(res.is_ok()); - let mut page = res.unwrap(); - page.render_markdown(&HashMap::default(), &Tera::default(), &config).unwrap(); + let page = res.unwrap(); assert_eq!(page.slug, "file-with-space"); assert_eq!(page.permalink, config.make_permalink(&page.slug)); } @@ -285,7 +284,7 @@ Hello world let res = Page::parse(Path::new("hello.md"), &content, &config); assert!(res.is_ok()); let mut page = res.unwrap(); - page.render_markdown(&HashMap::default(), &Tera::default(), &config).unwrap(); + page.render_markdown(&HashMap::default(), &Tera::default(), &config, InsertAnchor::None).unwrap(); assert_eq!(page.summary, Some("

Hello world

\n".to_string())); } diff --git a/src/content/section.rs b/src/content/section.rs index a8e41ef7..4468bceb 100644 --- a/src/content/section.rs +++ b/src/content/section.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::result::Result as StdResult; -use tera::{Tera, Context}; +use tera::{Tera, Context as TeraContext}; use serde::ser::{SerializeStruct, self}; use config::Config; @@ -10,6 +10,7 @@ use front_matter::{SectionFrontMatter, split_section_content}; use errors::{Result, ResultExt}; use fs::{read_file}; use rendering::markdown::markdown_to_html; +use rendering::context::Context; use content::Page; use content::file_info::FileInfo; @@ -85,7 +86,8 @@ impl Section { /// We need access to all pages url to render links relative to content /// so that can't happen at the same time as parsing pub fn render_markdown(&mut self, permalinks: &HashMap, tera: &Tera, config: &Config) -> Result<()> { - self.content = markdown_to_html(&self.raw_content, permalinks, tera, config)?; + let context = Context::new(tera, config, permalinks, self.meta.insert_anchor.unwrap()); + self.content = markdown_to_html(&self.raw_content, &context)?; Ok(()) } @@ -93,7 +95,7 @@ impl Section { pub fn render_html(&self, sections: HashMap, tera: &Tera, config: &Config) -> Result { let tpl_name = self.get_template_name(); - let mut context = Context::new(); + let mut context = TeraContext::new(); context.add("config", config); context.add("section", self); context.add("current_url", &self.permalink); @@ -120,8 +122,8 @@ impl Section { } /// Whether the page given belongs to that section - pub fn is_child_page(&self, page: &Page) -> bool { - self.all_pages_path().contains(&page.file.path) + pub fn is_child_page(&self, path: &PathBuf) -> bool { + self.all_pages_path().contains(path) } } diff --git a/src/front_matter/mod.rs b/src/front_matter/mod.rs index 6bc075b8..b46fadf1 100644 --- a/src/front_matter/mod.rs +++ b/src/front_matter/mod.rs @@ -8,7 +8,7 @@ mod page; mod section; pub use self::page::PageFrontMatter; -pub use self::section::{SectionFrontMatter}; +pub use self::section::{SectionFrontMatter, InsertAnchor}; lazy_static! { static ref PAGE_RE: Regex = Regex::new(r"^[[:space:]]*\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap(); diff --git a/src/front_matter/section.rs b/src/front_matter/section.rs index 8d7c7d27..2f216659 100644 --- a/src/front_matter/section.rs +++ b/src/front_matter/section.rs @@ -9,6 +9,14 @@ use content::SortBy; static DEFAULT_PAGINATE_PATH: &'static str = "page"; +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum InsertAnchor { + Left, + Right, + None, +} + /// The front matter of every section #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SectionFrontMatter { @@ -28,6 +36,10 @@ pub struct SectionFrontMatter { /// Path to be used by pagination: the page number will be appended after it. Defaults to `page`. #[serde(skip_serializing)] pub paginate_path: Option, + /// Whether to insert a link for each header like in Github READMEs. Defaults to false + /// The default template can be overridden by creating a `anchor-link.html` template and CSS will need to be + /// written if you turn that on. + pub insert_anchor: Option, /// Whether to render that section or not. Defaults to `true`. /// Useful when the section is only there to organize things but is not meant /// to be used directly, like a posts section in a personal site @@ -56,6 +68,10 @@ impl SectionFrontMatter { f.sort_by = Some(SortBy::None); } + if f.insert_anchor.is_none() { + f.insert_anchor = Some(InsertAnchor::None); + } + Ok(f) } @@ -87,6 +103,7 @@ impl Default for SectionFrontMatter { paginate_by: None, paginate_path: Some(DEFAULT_PAGINATE_PATH.to_string()), render: Some(true), + insert_anchor: Some(InsertAnchor::None), extra: None, } } diff --git a/src/lib.rs b/src/lib.rs index b72af67a..01f809d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,6 @@ mod templates; pub use site::{Site}; pub use config::{Config, get_config}; -pub use front_matter::{PageFrontMatter, SectionFrontMatter, split_page_content, split_section_content}; +pub use front_matter::{PageFrontMatter, SectionFrontMatter, InsertAnchor, split_page_content, split_section_content}; pub use content::{Page, Section, SortBy, sort_pages, populate_previous_and_next_pages}; pub use fs::{create_file}; diff --git a/src/rendering/context.rs b/src/rendering/context.rs new file mode 100644 index 00000000..d7f77c43 --- /dev/null +++ b/src/rendering/context.rs @@ -0,0 +1,33 @@ +use std::collections::HashMap; + +use tera::Tera; + +use config::Config; +use front_matter::InsertAnchor; + + +/// All the information from the gutenberg site that is needed to render HTML from markdown +#[derive(Debug)] +pub struct Context<'a> { + pub tera: &'a Tera, + pub highlight_code: bool, + pub highlight_theme: String, + pub permalinks: &'a HashMap, + pub insert_anchor: InsertAnchor, +} + +impl<'a> Context<'a> { + pub fn new(tera: &'a Tera, config: &'a Config, permalinks: &'a HashMap, insert_anchor: InsertAnchor) -> Context<'a> { + Context { + tera, + permalinks, + insert_anchor, + highlight_code: config.highlight_code.unwrap(), + highlight_theme: config.highlight_theme.clone().unwrap(), + } + } + + pub fn should_insert_anchor(&self) -> bool { + self.insert_anchor != InsertAnchor::None + } +} diff --git a/src/rendering/markdown.rs b/src/rendering/markdown.rs index bc93cdf3..5e18a4b7 100644 --- a/src/rendering/markdown.rs +++ b/src/rendering/markdown.rs @@ -1,5 +1,4 @@ use std::borrow::Cow::Owned; -use std::collections::HashMap; use pulldown_cmark as cmark; use self::cmark::{Parser, Event, Tag, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES}; @@ -9,11 +8,12 @@ use syntect::dumps::from_binary; use syntect::easy::HighlightLines; use syntect::parsing::SyntaxSet; use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, IncludeBackground}; -use tera::{Tera, Context}; +use tera::{Context as TeraContext}; -use config::Config; use errors::{Result}; use site::resolve_internal_link; +use front_matter::InsertAnchor; +use rendering::context::Context; use rendering::highlighting::THEME_SET; use rendering::short_code::{ShortCode, parse_shortcode, render_simple_shortcode}; @@ -36,18 +36,18 @@ lazy_static!{ }; } -pub fn markdown_to_html(content: &str, permalinks: &HashMap, tera: &Tera, config: &Config) -> Result { + +pub fn markdown_to_html(content: &str, context: &Context) -> 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() { + let should_highlight = if context.highlight_code { content.contains("```") } else { false }; - let highlight_theme = config.highlight_theme.clone().unwrap(); // Set while parsing let mut error = None; let mut highlighter: Option = None; @@ -105,7 +105,7 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap, ter if shortcode_block.is_none() && text.starts_with("{{") && text.ends_with("}}") && SHORTCODE_RE.is_match(&text) { let (name, args) = parse_shortcode(&text); added_shortcode = true; - match render_simple_shortcode(tera, &name, &args) { + match render_simple_shortcode(context.tera, &name, &args) { Ok(s) => return Event::Html(Owned(format!("

{}", s))), Err(e) => { error = Some(e); @@ -131,7 +131,7 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap, ter if let Some(ref mut shortcode) = shortcode_block { if text.trim() == "{% end %}" { added_shortcode = true; - match shortcode.render(tera) { + match shortcode.render(context.tera) { Ok(s) => return Event::Html(Owned(format!("

{}", s))), Err(e) => { error = Some(e); @@ -151,15 +151,20 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap, ter } let id = find_anchor(&anchors, slugify(&text), 0); anchors.push(id.clone()); - let anchor_link = if config.insert_anchor_links.unwrap() { - let mut context = Context::new(); - context.add("id", &id); - tera.render("anchor-link.html", &context).unwrap() + let anchor_link = if context.should_insert_anchor() { + let mut c = TeraContext::new(); + c.add("id", &id); + context.tera.render("anchor-link.html", &c).unwrap() } else { String::new() }; header_already_inserted = true; - return Event::Html(Owned(format!(r#"id="{}">{}{}"#, id, anchor_link, text))); + let event = match context.insert_anchor { + InsertAnchor::Left => Event::Html(Owned(format!(r#"id="{}">{}{}"#, id, anchor_link, text))), + InsertAnchor::Right => Event::Html(Owned(format!(r#"id="{}">{}{}"#, id, text, anchor_link))), + InsertAnchor::None => Event::Html(Owned(format!(r#"id="{}">{}"#, id, text))) + }; + return event; } // Business as usual @@ -170,7 +175,7 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap, ter if !should_highlight { return Event::Html(Owned("
".to_owned()));
                 }
-                let theme = &THEME_SET.themes[&highlight_theme];
+                let theme = &THEME_SET.themes[&context.highlight_theme];
                 let syntax = info
                     .split(' ')
                     .next()
@@ -195,7 +200,7 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap, ter
                     return Event::Html(Owned("".to_owned()));
                 }
                 if link.starts_with("./") {
-                    match resolve_internal_link(link, permalinks) {
+                    match resolve_internal_link(link, context.permalinks) {
                         Ok(url) => {
                             return Event::Start(Tag::Link(Owned(url), title.clone()));
                         },
@@ -268,46 +273,33 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap, ter
 mod tests {
     use std::collections::HashMap;
 
-    use templates::GUTENBERG_TERA;
     use tera::Tera;
 
     use config::Config;
-    use super::{markdown_to_html, parse_shortcode};
+    use front_matter::InsertAnchor;
+    use templates::GUTENBERG_TERA;
+    use rendering::context::Context;
 
-    #[test]
-    fn can_parse_simple_shortcode_one_arg() {
-        let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc") }}"#);
-        assert_eq!(name, "youtube");
-        assert_eq!(args["id"], "w7Ft2ymGmfc");
-    }
-
-    #[test]
-    fn can_parse_simple_shortcode_several_arg() {
-        let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc", autoplay=true) }}"#);
-        assert_eq!(name, "youtube");
-        assert_eq!(args["id"], "w7Ft2ymGmfc");
-        assert_eq!(args["autoplay"], "true");
-    }
-
-    #[test]
-    fn can_parse_block_shortcode_several_arg() {
-        let (name, args) = parse_shortcode(r#"{% youtube(id="w7Ft2ymGmfc", autoplay=true) %}"#);
-        assert_eq!(name, "youtube");
-        assert_eq!(args["id"], "w7Ft2ymGmfc");
-        assert_eq!(args["autoplay"], "true");
-    }
+    use super::markdown_to_html;
 
     #[test]
     fn can_do_markdown_to_html_simple() {
-        let res = markdown_to_html("hello", &HashMap::new(), &Tera::default(), &Config::default()).unwrap();
+        let tera_ctx = Tera::default();
+        let permalinks_ctx = HashMap::new();
+        let config_ctx = Config::default();
+        let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None);
+        let res = markdown_to_html("hello", &context).unwrap();
         assert_eq!(res, "

hello

\n"); } #[test] fn doesnt_highlight_code_block_with_highlighting_off() { - 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(); + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let config_ctx = Config::default(); + let mut context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None); + context.highlight_code = false; + let res = markdown_to_html("```\n$ gutenberg server\n```", &context).unwrap(); assert_eq!( res, "
$ gutenberg server\n
\n" @@ -316,7 +308,11 @@ mod tests { #[test] fn can_highlight_code_block_no_lang() { - let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap(); + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let config_ctx = Config::default(); + let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None); + let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", &context).unwrap(); assert_eq!( res, "
\n$ gutenberg server\n$ ping\n
" @@ -325,7 +321,11 @@ mod tests { #[test] fn can_highlight_code_block_with_lang() { - let res = markdown_to_html("```python\nlist.append(1)\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap(); + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let config_ctx = Config::default(); + let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None); + let res = markdown_to_html("```python\nlist.append(1)\n```", &context).unwrap(); assert_eq!( res, "
\nlist.append(1)\n
" @@ -334,7 +334,11 @@ mod tests { #[test] fn can_higlight_code_block_with_unknown_lang() { - let res = markdown_to_html("```yolo\nlist.append(1)\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap(); + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let config_ctx = Config::default(); + let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None); + let res = markdown_to_html("```yolo\nlist.append(1)\n```", &context).unwrap(); // defaults to plain text assert_eq!( res, @@ -344,17 +348,23 @@ mod tests { #[test] fn can_render_shortcode() { + let permalinks_ctx = HashMap::new(); + let config_ctx = Config::default(); + let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::None); let res = markdown_to_html(r#" Hello {{ youtube(id="ub36ffWAqgQ") }} - "#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap(); + "#, &context).unwrap(); assert!(res.contains("

Hello

\n
")); assert!(res.contains(r#"