Merge pull request #368 from Keats/next

Next version
This commit is contained in:
Vincent Prouillet 2018-09-03 19:39:10 +02:00 committed by GitHub
commit 014ce878f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1138 additions and 739 deletions

3
.gitmodules vendored
View file

@ -34,3 +34,6 @@
[submodule "sublime_syntaxes/Sublime-CMakeLists"] [submodule "sublime_syntaxes/Sublime-CMakeLists"]
path = sublime_syntaxes/Sublime-CMakeLists path = sublime_syntaxes/Sublime-CMakeLists
url = https://github.com/zyxar/Sublime-CMakeLists url = https://github.com/zyxar/Sublime-CMakeLists
[submodule "sublime_syntaxes/Swift-for-f-ing-sublime"]
path = sublime_syntaxes/Swift-for-f-ing-sublime
url = git@github.com:colinta/Swift-for-f-ing-sublime.git

View file

@ -1,5 +1,12 @@
# Changelog # Changelog
## 0.4.2 (unreleased)
- Add assets to section indexes
- Allow users to add custom highlighting syntaxes
- Add Swift, MiniZinc syntaxes and update others
- Handle post summaries better: no more cutting references
## 0.4.1 (2018-08-06) ## 0.4.1 (2018-08-06)
- Fix live reload of a section content change getting no pages data - Fix live reload of a section content change getting no pages data

959
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "gutenberg" name = "gutenberg"
version = "0.4.1" version = "0.4.2"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"

View file

@ -90,7 +90,7 @@ $ git submodule update --remote --merge
And finally from the root of the components/highlighting crate run the following command: And finally from the root of the components/highlighting crate run the following command:
```bash ```bash
$ cargo run --example generate_sublime synpack ../../sublime_syntaxes ../../sublime_syntaxes/newlines.packdump ../../sublime_syntaxes/nonewlines.packdump $ cargo run --example generate_sublime synpack ../../sublime_syntaxes ../../sublime_syntaxes/newlines.packdump
``` ```
#### Adding a theme #### Adding a theme

View file

@ -3,23 +3,22 @@ extern crate serde_derive;
extern crate toml; extern crate toml;
#[macro_use] #[macro_use]
extern crate errors; extern crate errors;
extern crate highlighting;
extern crate chrono; extern crate chrono;
extern crate globset; extern crate globset;
extern crate highlighting;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::File; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use toml::Value as Toml;
use chrono::Utc; use chrono::Utc;
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
use toml::Value as Toml;
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use highlighting::THEME_SET; use highlighting::THEME_SET;
mod theme; mod theme;
use theme::Theme; use theme::Theme;
@ -27,7 +26,6 @@ use theme::Theme;
// We want a default base url for tests // We want a default base url for tests
static DEFAULT_BASE_URL: &'static str = "http://a-website.com"; static DEFAULT_BASE_URL: &'static str = "http://a-website.com";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct Taxonomy { pub struct Taxonomy {
@ -101,12 +99,15 @@ pub struct Config {
/// Had to remove the PartialEq derive because GlobSet does not implement it. No impact /// Had to remove the PartialEq derive because GlobSet does not implement it. No impact
/// because it's unused anyway (who wants to sort Configs?). /// because it's unused anyway (who wants to sort Configs?).
pub ignored_content: Vec<String>, pub ignored_content: Vec<String>,
#[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are needed #[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are needed
pub ignored_content_globset: Option<GlobSet>, pub ignored_content_globset: Option<GlobSet>,
/// Whether to check all external links for validity /// Whether to check all external links for validity
pub check_external_links: bool, pub check_external_links: bool,
/// A list of directories to search for additional `.sublime-syntax` files in.
pub extra_syntaxes: Vec<String>,
/// All user params set in [extra] in the config /// All user params set in [extra] in the config
pub extra: HashMap<String, Toml>, pub extra: HashMap<String, Toml>,
@ -114,14 +115,13 @@ pub struct Config {
pub build_timestamp: Option<i64>, pub build_timestamp: Option<i64>,
} }
impl Config { impl Config {
/// Parses a string containing TOML to our Config struct /// Parses a string containing TOML to our Config struct
/// Any extra parameter will end up in the extra field /// Any extra parameter will end up in the extra field
pub fn parse(content: &str) -> Result<Config> { pub fn parse(content: &str) -> Result<Config> {
let mut config: Config = match toml::from_str(content) { let mut config: Config = match toml::from_str(content) {
Ok(c) => c, Ok(c) => c,
Err(e) => bail!(e) Err(e) => bail!(e),
}; };
if config.base_url.is_empty() || config.base_url == DEFAULT_BASE_URL { if config.base_url.is_empty() || config.base_url == DEFAULT_BASE_URL {
@ -134,7 +134,6 @@ impl Config {
config.build_timestamp = Some(Utc::now().timestamp()); config.build_timestamp = Some(Utc::now().timestamp());
if !config.ignored_content.is_empty() { if !config.ignored_content.is_empty() {
// Convert the file glob strings into a compiled glob set matcher. We want to do this once, // Convert the file glob strings into a compiled glob set matcher. We want to do this once,
// at program initialization, rather than for every page, for example. We arrange for the // at program initialization, rather than for every page, for example. We arrange for the
@ -145,11 +144,19 @@ impl Config {
for pat in &config.ignored_content { for pat in &config.ignored_content {
let glob = match Glob::new(pat) { let glob = match Glob::new(pat) {
Ok(g) => g, Ok(g) => g,
Err(e) => bail!("Invalid ignored_content glob pattern: {}, error = {}", pat, e) Err(e) => bail!(
"Invalid ignored_content glob pattern: {}, error = {}",
pat,
e
),
}; };
glob_set_builder.add(glob); glob_set_builder.add(glob);
} }
config.ignored_content_globset = Some(glob_set_builder.build().expect("Bad ignored_content in config file.")); config.ignored_content_globset = Some(
glob_set_builder
.build()
.expect("Bad ignored_content in config file."),
);
} }
Ok(config) Ok(config)
@ -161,7 +168,12 @@ impl Config {
let path = path.as_ref(); let path = path.as_ref();
let file_name = path.file_name().unwrap(); let file_name = path.file_name().unwrap();
File::open(path) File::open(path)
.chain_err(|| format!("No `{:?}` file found. Are you in the right directory?", file_name))? .chain_err(|| {
format!(
"No `{:?}` file found. Are you in the right directory?",
file_name
)
})?
.read_to_string(&mut content)?; .read_to_string(&mut content)?;
Config::parse(&content) Config::parse(&content)
@ -169,7 +181,11 @@ impl Config {
/// Makes a url, taking into account that the base url might have a trailing slash /// Makes a url, taking into account that the base url might have a trailing slash
pub fn make_permalink(&self, path: &str) -> String { pub fn make_permalink(&self, path: &str) -> String {
let trailing_bit = if path.ends_with('/') || path.is_empty() { "" } else { "/" }; let trailing_bit = if path.ends_with('/') || path.is_empty() {
""
} else {
"/"
};
// Index section with a base url that has a trailing slash // Index section with a base url that has a trailing slash
if self.base_url.ends_with('/') && path == "/" { if self.base_url.ends_with('/') && path == "/" {
@ -195,12 +211,16 @@ impl Config {
let original = self.extra.clone(); let original = self.extra.clone();
// 2. inject theme extra values // 2. inject theme extra values
for (key, val) in &theme.extra { for (key, val) in &theme.extra {
self.extra.entry(key.to_string()).or_insert_with(|| val.clone()); self.extra
.entry(key.to_string())
.or_insert_with(|| val.clone());
} }
// 3. overwrite with original config // 3. overwrite with original config
for (key, val) in &original { for (key, val) in &original {
self.extra.entry(key.to_string()).or_insert_with(|| val.clone()); self.extra
.entry(key.to_string())
.or_insert_with(|| val.clone());
} }
Ok(()) Ok(())
@ -233,13 +253,13 @@ impl Default for Config {
ignored_content: Vec::new(), ignored_content: Vec::new(),
ignored_content_globset: None, ignored_content_globset: None,
translations: HashMap::new(), translations: HashMap::new(),
extra_syntaxes: Vec::new(),
extra: HashMap::new(), extra: HashMap::new(),
build_timestamp: Some(1), build_timestamp: Some(1),
} }
} }
} }
/// Get and parse the config. /// Get and parse the config.
/// If it doesn't succeed, exit /// If it doesn't succeed, exit
pub fn get_config(path: &Path, filename: &str) -> Config { pub fn get_config(path: &Path, filename: &str) -> Config {
@ -253,7 +273,6 @@ pub fn get_config(path: &Path, filename: &str) -> Config {
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{Config, Theme}; use super::{Config, Theme};
@ -303,7 +322,16 @@ hello = "world"
let config = Config::parse(config); let config = Config::parse(config);
assert!(config.is_ok()); assert!(config.is_ok());
assert_eq!(config.unwrap().extra.get("hello").unwrap().as_str().unwrap(), "world"); assert_eq!(
config
.unwrap()
.extra
.get("hello")
.unwrap()
.as_str()
.unwrap(),
"world"
);
} }
#[test] #[test]
@ -313,7 +341,6 @@ hello = "world"
assert_eq!(config.make_permalink(""), "http://vincent.is/"); assert_eq!(config.make_permalink(""), "http://vincent.is/");
} }
#[test] #[test]
fn can_make_url_index_page_with_railing_slash_url() { fn can_make_url_index_page_with_railing_slash_url() {
let mut config = Config::default(); let mut config = Config::default();
@ -339,7 +366,10 @@ hello = "world"
fn can_make_url_with_localhost() { fn can_make_url_with_localhost() {
let mut config = Config::default(); let mut config = Config::default();
config.base_url = "http://127.0.0.1:1111".to_string(); config.base_url = "http://127.0.0.1:1111".to_string();
assert_eq!(config.make_permalink("/tags/rust"), "http://127.0.0.1:1111/tags/rust/"); assert_eq!(
config.make_permalink("/tags/rust"),
"http://127.0.0.1:1111/tags/rust/"
);
} }
#[test] #[test]

View file

@ -7,6 +7,7 @@ extern crate front_matter;
extern crate config; extern crate config;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path;
use config::Config; use config::Config;
use tera::Tera; use tera::Tera;
@ -14,19 +15,16 @@ use front_matter::{SortBy, InsertAnchor};
use content::{Page, sort_pages, populate_siblings}; use content::{Page, sort_pages, populate_siblings};
fn create_pages(number: usize, sort_by: SortBy) -> Vec<Page> { fn create_pages(number: usize) -> Vec<Page> {
let mut pages = vec![]; let mut pages = vec![];
let config = Config::default(); let config = Config::default();
let tera = Tera::default(); let mut tera = Tera::default();
tera.add_raw_template("shortcodes/youtube.html", "hello");
let permalinks = HashMap::new(); let permalinks = HashMap::new();
for i in 0..number { for i in 0..number {
let mut page = Page::default(); let mut page = Page::default();
match sort_by { page.meta.weight = Some(i);
SortBy::Weight => { page.meta.weight = Some(i); }
SortBy::Order => { page.meta.order = Some(i); }
_ => (),
};
page.raw_content = r#" page.raw_content = r#"
# Modus cognitius profanam ne duae virtutis mundi # Modus cognitius profanam ne duae virtutis mundi
@ -98,7 +96,7 @@ if __name__ == "__main__":
gen_site("basic-blog", [""], 250, paginate=True) gen_site("basic-blog", [""], 250, paginate=True)
``` ```
"#.to_string(); "#.to_string();
page.render_markdown(&permalinks, &tera, &config, InsertAnchor::None).unwrap(); page.render_markdown(&permalinks, &tera, &config, &Path::new(""), InsertAnchor::None).unwrap();
pages.push(page); pages.push(page);
} }
@ -111,34 +109,34 @@ if __name__ == "__main__":
#[bench] #[bench]
fn bench_baseline_cloning(b: &mut test::Bencher) { fn bench_baseline_cloning(b: &mut test::Bencher) {
let pages = create_pages(250, SortBy::Order); let pages = create_pages(250);
b.iter(|| pages.clone()); b.iter(|| pages.clone());
} }
#[bench] #[bench]
fn bench_sorting_none(b: &mut test::Bencher) { fn bench_sorting_none(b: &mut test::Bencher) {
let pages = create_pages(250, SortBy::Order); let pages = create_pages(250);
b.iter(|| sort_pages(pages.clone(), SortBy::None)); b.iter(|| sort_pages(pages.clone(), SortBy::Weight));
} }
#[bench] #[bench]
fn bench_sorting_order(b: &mut test::Bencher) { fn bench_sorting_order(b: &mut test::Bencher) {
let pages = create_pages(250, SortBy::Order); let pages = create_pages(250);
b.iter(|| sort_pages(pages.clone(), SortBy::Order)); b.iter(|| sort_pages(pages.clone(), SortBy::Weight));
} }
#[bench] #[bench]
fn bench_populate_siblings(b: &mut test::Bencher) { fn bench_populate_siblings(b: &mut test::Bencher) {
let pages = create_pages(250, SortBy::Order); let pages = create_pages(250);
let (sorted_pages, _) = sort_pages(pages, SortBy::Order); let (sorted_pages, _) = sort_pages(pages, SortBy::Weight);
b.iter(|| populate_siblings(&sorted_pages.clone())); b.iter(|| populate_siblings(&sorted_pages.clone(), SortBy::Weight));
} }
#[bench] #[bench]
fn bench_page_render_html(b: &mut test::Bencher) { fn bench_page_render_html(b: &mut test::Bencher) {
let pages = create_pages(10, SortBy::Order); let pages = create_pages(10);
let (mut sorted_pages, _) = sort_pages(pages, SortBy::Order); let (mut sorted_pages, _) = sort_pages(pages, SortBy::Weight);
sorted_pages = populate_siblings(&sorted_pages); sorted_pages = populate_siblings(&sorted_pages, SortBy::Weight);
let config = Config::default(); let config = Config::default();
let mut tera = Tera::default(); let mut tera = Tera::default();

View file

@ -166,30 +166,31 @@ impl Page {
/// We need access to all pages url to render links relative to content /// We need access to all pages url to render links relative to content
/// so that can't happen at the same time as parsing /// so that can't happen at the same time as parsing
pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config, anchor_insert: InsertAnchor) -> Result<()> { pub fn render_markdown(
&mut self,
permalinks: &HashMap<String, String>,
tera: &Tera,
config: &Config,
base_path: &Path,
anchor_insert: InsertAnchor,
) -> Result<()> {
let mut context = RenderContext::new( let mut context = RenderContext::new(
tera, tera,
config, config,
&self.permalink, &self.permalink,
permalinks, permalinks,
base_path,
anchor_insert, anchor_insert,
); );
context.tera_context.add("page", self); context.tera_context.add("page", self);
let res = render_content( let res = render_content(&self.raw_content, &context)
&self.raw_content.replacen("<!-- more -->", "<a name=\"continue-reading\"></a>", 1), .chain_err(|| format!("Failed to render content of {}", self.file.path.display()))?;
&context,
).chain_err(|| format!("Failed to render content of {}", self.file.path.display()))?; self.summary = res.summary_len.map(|l| res.body[0..l].to_owned());
self.content = res.0; self.content = res.body;
self.toc = res.1; self.toc = res.toc;
if self.raw_content.contains("<!-- more -->") {
self.summary = Some({
let summary = self.raw_content.splitn(2, "<!-- more -->").collect::<Vec<&str>>()[0];
render_content(summary, &context)
.chain_err(|| format!("Failed to render content of {}", self.file.path.display()))?.0
})
}
Ok(()) Ok(())
} }
@ -310,7 +311,13 @@ Hello world"#;
let res = Page::parse(Path::new("post.md"), content, &Config::default()); let res = Page::parse(Path::new("post.md"), content, &Config::default());
assert!(res.is_ok()); assert!(res.is_ok());
let mut page = res.unwrap(); let mut page = res.unwrap();
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default(), InsertAnchor::None).unwrap(); page.render_markdown(
&HashMap::default(),
&Tera::default(),
&Config::default(),
Path::new("something"),
InsertAnchor::None,
).unwrap();
assert_eq!(page.meta.title.unwrap(), "Hello".to_string()); assert_eq!(page.meta.title.unwrap(), "Hello".to_string());
assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string()); assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string());
@ -416,7 +423,13 @@ Hello world
let res = Page::parse(Path::new("hello.md"), &content, &config); let res = Page::parse(Path::new("hello.md"), &content, &config);
assert!(res.is_ok()); assert!(res.is_ok());
let mut page = res.unwrap(); let mut page = res.unwrap();
page.render_markdown(&HashMap::default(), &Tera::default(), &config, InsertAnchor::None).unwrap(); page.render_markdown(
&HashMap::default(),
&Tera::default(),
&config,
Path::new("something"),
InsertAnchor::None,
).unwrap();
assert_eq!(page.summary, Some("<p>Hello world</p>\n".to_string())); assert_eq!(page.summary, Some("<p>Hello world</p>\n".to_string()));
} }

View file

@ -8,7 +8,7 @@ use serde::ser::{SerializeStruct, self};
use config::Config; use config::Config;
use front_matter::{SectionFrontMatter, split_section_content}; use front_matter::{SectionFrontMatter, split_section_content};
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use utils::fs::read_file; use utils::fs::{read_file, find_related_assets};
use utils::templates::render_template; use utils::templates::render_template;
use utils::site::get_reading_analytics; use utils::site::get_reading_analytics;
use rendering::{RenderContext, Header, render_content}; use rendering::{RenderContext, Header, render_content};
@ -33,6 +33,8 @@ pub struct Section {
pub raw_content: String, pub raw_content: String,
/// The HTML rendered of the page /// The HTML rendered of the page
pub content: String, pub content: String,
/// All the non-md files we found next to the .md file
pub assets: Vec<PathBuf>,
/// All direct pages of that section /// All direct pages of that section
pub pages: Vec<Page>, pub pages: Vec<Page>,
/// All pages that cannot be sorted in this section /// All pages that cannot be sorted in this section
@ -54,6 +56,7 @@ impl Section {
components: vec![], components: vec![],
permalink: "".to_string(), permalink: "".to_string(),
raw_content: "".to_string(), raw_content: "".to_string(),
assets: vec![],
content: "".to_string(), content: "".to_string(),
pages: vec![], pages: vec![],
ignored_pages: vec![], ignored_pages: vec![],
@ -79,8 +82,31 @@ impl Section {
pub fn from_file<P: AsRef<Path>>(path: P, config: &Config) -> Result<Section> { pub fn from_file<P: AsRef<Path>>(path: P, config: &Config) -> Result<Section> {
let path = path.as_ref(); let path = path.as_ref();
let content = read_file(path)?; let content = read_file(path)?;
let mut section = Section::parse(path, &content, config)?;
Section::parse(path, &content, config) let parent_dir = path.parent().unwrap();
let assets = find_related_assets(parent_dir);
if let Some(ref globset) = config.ignored_content_globset {
// `find_related_assets` only scans the immediate directory (it is not recursive) so our
// filtering only needs to work against the file_name component, not the full suffix. If
// `find_related_assets` was changed to also return files in subdirectories, we could
// use `PathBuf.strip_prefix` to remove the parent directory and then glob-filter
// against the remaining path. Note that the current behaviour effectively means that
// the `ignored_content` setting in the config file is limited to single-file glob
// patterns (no "**" patterns).
section.assets = assets.into_iter()
.filter(|path|
match path.file_name() {
None => true,
Some(file) => !globset.is_match(file)
}
).collect();
} else {
section.assets = assets;
}
Ok(section)
} }
pub fn get_template_name(&self) -> String { pub fn get_template_name(&self) -> String {
@ -97,12 +123,13 @@ impl Section {
/// We need access to all pages url to render links relative to content /// We need access to all pages url to render links relative to content
/// so that can't happen at the same time as parsing /// 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<()> { pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config, base_path: &Path) -> Result<()> {
let mut context = RenderContext::new( let mut context = RenderContext::new(
tera, tera,
config, config,
&self.permalink, &self.permalink,
permalinks, permalinks,
base_path,
self.meta.insert_anchor_links, self.meta.insert_anchor_links,
); );
@ -110,8 +137,8 @@ impl Section {
let res = render_content(&self.raw_content, &context) let res = render_content(&self.raw_content, &context)
.chain_err(|| format!("Failed to render content of {}", self.file.path.display()))?; .chain_err(|| format!("Failed to render content of {}", self.file.path.display()))?;
self.content = res.0; self.content = res.body;
self.toc = res.1; self.toc = res.toc;
Ok(()) Ok(())
} }
@ -146,6 +173,15 @@ impl Section {
pub fn is_child_page(&self, path: &PathBuf) -> bool { pub fn is_child_page(&self, path: &PathBuf) -> bool {
self.all_pages_path().contains(path) self.all_pages_path().contains(path)
} }
/// Creates a vectors of asset URLs.
fn serialize_assets(&self) -> Vec<String> {
self.assets.iter()
.filter_map(|asset| asset.file_name())
.filter_map(|filename| filename.to_str())
.map(|filename| self.path.clone() + filename)
.collect()
}
} }
impl ser::Serialize for Section { impl ser::Serialize for Section {
@ -165,6 +201,8 @@ impl ser::Serialize for Section {
state.serialize_field("word_count", &word_count)?; state.serialize_field("word_count", &word_count)?;
state.serialize_field("reading_time", &reading_time)?; state.serialize_field("reading_time", &reading_time)?;
state.serialize_field("toc", &self.toc)?; state.serialize_field("toc", &self.toc)?;
let assets = self.serialize_assets();
state.serialize_field("assets", &assets)?;
state.end() state.end()
} }
} }
@ -179,6 +217,7 @@ impl Default for Section {
components: vec![], components: vec![],
permalink: "".to_string(), permalink: "".to_string(),
raw_content: "".to_string(), raw_content: "".to_string(),
assets: vec![],
content: "".to_string(), content: "".to_string(),
pages: vec![], pages: vec![],
ignored_pages: vec![], ignored_pages: vec![],
@ -187,3 +226,69 @@ impl Default for Section {
} }
} }
} }
#[cfg(test)]
mod tests {
use std::io::Write;
use std::fs::{File, create_dir};
use tempfile::tempdir;
use globset::{Glob, GlobSetBuilder};
use config::Config;
use super::Section;
#[test]
fn section_with_assets_gets_right_info() {
let tmp_dir = tempdir().expect("create temp dir");
let path = tmp_dir.path();
create_dir(&path.join("content")).expect("create content temp dir");
create_dir(&path.join("content").join("posts")).expect("create posts temp dir");
let nested_path = path.join("content").join("posts").join("with-assets");
create_dir(&nested_path).expect("create nested temp dir");
let mut f = File::create(nested_path.join("_index.md")).unwrap();
f.write_all(b"+++\n+++\n").unwrap();
File::create(nested_path.join("example.js")).unwrap();
File::create(nested_path.join("graph.jpg")).unwrap();
File::create(nested_path.join("fail.png")).unwrap();
let res = Section::from_file(
nested_path.join("_index.md").as_path(),
&Config::default(),
);
assert!(res.is_ok());
let section = res.unwrap();
assert_eq!(section.assets.len(), 3);
assert_eq!(section.permalink, "http://a-website.com/posts/with-assets/");
}
#[test]
fn section_with_ignored_assets_filters_out_correct_files() {
let tmp_dir = tempdir().expect("create temp dir");
let path = tmp_dir.path();
create_dir(&path.join("content")).expect("create content temp dir");
create_dir(&path.join("content").join("posts")).expect("create posts temp dir");
let nested_path = path.join("content").join("posts").join("with-assets");
create_dir(&nested_path).expect("create nested temp dir");
let mut f = File::create(nested_path.join("_index.md")).unwrap();
f.write_all(b"+++\nslug=\"hey\"\n+++\n").unwrap();
File::create(nested_path.join("example.js")).unwrap();
File::create(nested_path.join("graph.jpg")).unwrap();
File::create(nested_path.join("fail.png")).unwrap();
let mut gsb = GlobSetBuilder::new();
gsb.add(Glob::new("*.{js,png}").unwrap());
let mut config = Config::default();
config.ignored_content_globset = Some(gsb.build().unwrap());
let res = Section::from_file(
nested_path.join("_index.md").as_path(),
&config,
);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.assets.len(), 1);
assert_eq!(page.assets[0].file_name().unwrap().to_str(), Some("graph.jpg"));
}
}

View file

@ -19,25 +19,20 @@ fn usage_and_exit() -> ! {
// Check README for more details // Check README for more details
fn main() { fn main() {
let mut args = env::args().skip(1); let mut args = env::args().skip(1);
match (args.next(), args.next(), args.next(), args.next()) { match (args.next(), args.next(), args.next()) {
(Some(ref cmd), Some(ref package_dir), Some(ref packpath_newlines), Some(ref packpath_nonewlines)) if cmd == "synpack" => { (Some(ref cmd), Some(ref package_dir), Some(ref packpath_newlines)) if cmd == "synpack" => {
let mut ps = SyntaxSet::new(); let mut ps = SyntaxSet::new();
ps.load_plain_text_syntax(); ps.load_plain_text_syntax();
ps.load_syntaxes(package_dir, true).unwrap(); ps.load_syntaxes(package_dir, true).unwrap();
dump_to_file(&ps, packpath_newlines).unwrap(); dump_to_file(&ps, packpath_newlines).unwrap();
ps = SyntaxSet::new();
ps.load_plain_text_syntax();
ps.load_syntaxes(package_dir, false).unwrap();
dump_to_file(&ps, packpath_nonewlines).unwrap();
for s in ps.syntaxes() { for s in ps.syntaxes() {
if !s.file_extensions.is_empty() { if !s.file_extensions.is_empty() {
println!("- {} -> {:?}", s.name, s.file_extensions); println!("- {} -> {:?}", s.name, s.file_extensions);
} }
} }
}, },
(Some(ref cmd), Some(ref theme_dir), Some(ref packpath), None) if cmd == "themepack" => { (Some(ref cmd), Some(ref theme_dir), Some(ref packpath)) if cmd == "themepack" => {
let ts = ThemeSet::load_from_folder(theme_dir).unwrap(); let ts = ThemeSet::load_from_folder(theme_dir).unwrap();
for path in ts.themes.keys() { for path in ts.themes.keys() {
println!("{:?}", path); println!("{:?}", path);

View file

@ -2,16 +2,20 @@
extern crate lazy_static; extern crate lazy_static;
extern crate syntect; extern crate syntect;
use std::cell::RefCell;
use std::path::Path;
use syntect::LoadingError;
use syntect::dumps::from_binary; use syntect::dumps::from_binary;
use syntect::parsing::SyntaxSet; use syntect::parsing::SyntaxSet;
use syntect::highlighting::{ThemeSet, Theme}; use syntect::highlighting::{ThemeSet, Theme};
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
thread_local! { thread_local! {
pub static SYNTAX_SET: SyntaxSet = { /// A pair of the set and whether extras have been added to it.
let mut ss: SyntaxSet = from_binary(include_bytes!("../../../sublime_syntaxes/newlines.packdump")); pub static SYNTAX_SET: RefCell<(SyntaxSet, bool)> = {
ss.link_syntaxes(); let ss: SyntaxSet = from_binary(include_bytes!("../../../sublime_syntaxes/newlines.packdump"));
ss RefCell::new((ss, false))
}; };
} }
@ -19,14 +23,22 @@ lazy_static! {
pub static ref THEME_SET: ThemeSet = from_binary(include_bytes!("../../../sublime_themes/all.themedump")); pub static ref THEME_SET: ThemeSet = from_binary(include_bytes!("../../../sublime_themes/all.themedump"));
} }
pub fn get_highlighter<'a>(theme: &'a Theme, info: &str, base_path: &Path, extra_syntaxes: &[String]) -> Result<HighlightLines<'a>, LoadingError> {
SYNTAX_SET.with(|rc| {
let (ss, extras_added) = &mut *rc.borrow_mut();
if !*extras_added {
for dir in extra_syntaxes {
ss.load_syntaxes(base_path.join(dir), true)?;
}
ss.link_syntaxes();
*extras_added = true;
}
pub fn get_highlighter<'a>(theme: &'a Theme, info: &str) -> HighlightLines<'a> {
SYNTAX_SET.with(|ss| {
let syntax = info let syntax = info
.split(' ') .split(' ')
.next() .next()
.and_then(|lang| ss.find_syntax_by_token(lang)) .and_then(|lang| ss.find_syntax_by_token(lang))
.unwrap_or_else(|| ss.find_syntax_plain_text()); .unwrap_or_else(|| ss.find_syntax_plain_text());
HighlightLines::new(syntax, theme) Ok(HighlightLines::new(syntax, theme))
}) })
} }

View file

@ -2,11 +2,11 @@ extern crate reqwest;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
use reqwest::header::{qitem, Accept, Headers};
use reqwest::{mime, StatusCode};
use std::collections::HashMap; use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use reqwest::StatusCode;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct LinkResult { pub struct LinkResult {
@ -54,19 +54,30 @@ pub fn check_url(url: &str) -> LinkResult {
} }
} }
let mut headers = Headers::new();
headers.set(Accept(vec![qitem(mime::TEXT_HTML), qitem(mime::STAR_STAR)]));
let client = reqwest::Client::new();
// Need to actually do the link checking // Need to actually do the link checking
let res = match reqwest::get(url) { let res = match client.get(url).headers(headers).send() {
Ok(response) => LinkResult { code: Some(response.status()), error: None }, Ok(response) => LinkResult {
Err(e) => LinkResult { code: None, error: Some(e.description().to_string()) }, code: Some(response.status()),
error: None,
},
Err(e) => LinkResult {
code: None,
error: Some(e.description().to_string()),
},
}; };
LINKS.write().unwrap().insert(url.to_string(), res.clone()); LINKS.write().unwrap().insert(url.to_string(), res.clone());
return res; res
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{LINKS, check_url}; use super::{check_url, LINKS};
#[test] #[test]
fn can_validate_ok_links() { fn can_validate_ok_links() {

View file

@ -7,6 +7,7 @@ extern crate config;
extern crate front_matter; extern crate front_matter;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path;
use tera::Tera; use tera::Tera;
use rendering::{RenderContext, render_content, render_shortcodes}; use rendering::{RenderContext, render_content, render_shortcodes};
@ -91,7 +92,7 @@ fn bench_render_content_with_highlighting(b: &mut test::Bencher) {
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap(); tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None);
b.iter(|| render_content(CONTENT, &context).unwrap()); b.iter(|| render_content(CONTENT, &context).unwrap());
} }
@ -102,7 +103,7 @@ fn bench_render_content_without_highlighting(b: &mut test::Bencher) {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let mut config = Config::default(); let mut config = Config::default();
config.highlight_code = false; config.highlight_code = false;
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None);
b.iter(|| render_content(CONTENT, &context).unwrap()); b.iter(|| render_content(CONTENT, &context).unwrap());
} }
@ -113,7 +114,7 @@ fn bench_render_content_no_shortcode(b: &mut test::Bencher) {
let mut config = Config::default(); let mut config = Config::default();
config.highlight_code = false; config.highlight_code = false;
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None);
b.iter(|| render_content(&content2, &context).unwrap()); b.iter(|| render_content(&content2, &context).unwrap());
} }
@ -124,7 +125,7 @@ fn bench_render_shortcodes_one_present(b: &mut test::Bencher) {
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap(); tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap();
let config = Config::default(); let config = Config::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None);
b.iter(|| render_shortcodes(CONTENT, &context)); b.iter(|| render_shortcodes(CONTENT, &context));
} }

View file

@ -1,4 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path;
use tera::{Tera, Context}; use tera::{Tera, Context};
use front_matter::InsertAnchor; use front_matter::InsertAnchor;
@ -13,6 +14,7 @@ pub struct RenderContext<'a> {
pub tera_context: Context, pub tera_context: Context,
pub current_page_permalink: &'a str, pub current_page_permalink: &'a str,
pub permalinks: &'a HashMap<String, String>, pub permalinks: &'a HashMap<String, String>,
pub base_path: &'a Path,
pub insert_anchor: InsertAnchor, pub insert_anchor: InsertAnchor,
} }
@ -22,6 +24,7 @@ impl<'a> RenderContext<'a> {
config: &'a Config, config: &'a Config,
current_page_permalink: &'a str, current_page_permalink: &'a str,
permalinks: &'a HashMap<String, String>, permalinks: &'a HashMap<String, String>,
base_path: &'a Path,
insert_anchor: InsertAnchor, insert_anchor: InsertAnchor,
) -> RenderContext<'a> { ) -> RenderContext<'a> {
let mut tera_context = Context::new(); let mut tera_context = Context::new();
@ -32,6 +35,7 @@ impl<'a> RenderContext<'a> {
current_page_permalink, current_page_permalink,
permalinks, permalinks,
insert_anchor, insert_anchor,
base_path,
config, config,
} }
} }

View file

@ -32,7 +32,7 @@ pub use table_of_contents::Header;
pub use shortcode::render_shortcodes; pub use shortcode::render_shortcodes;
pub use context::RenderContext; pub use context::RenderContext;
pub fn render_content(content: &str, context: &RenderContext) -> Result<(String, Vec<Header>)> { pub fn render_content(content: &str, context: &RenderContext) -> Result<markdown::Rendered> {
// Don't do anything if there is nothing like a shortcode in the content // Don't do anything if there is nothing like a shortcode in the content
if content.contains("{{") || content.contains("{%") { if content.contains("{{") || content.contains("{%") {
let rendered = render_shortcodes(content, context)?; let rendered = render_shortcodes(content, context)?;

View file

@ -1,4 +1,4 @@
use std::borrow::Cow::Owned; use std::borrow::Cow::{Owned, Borrowed};
use pulldown_cmark as cmark; use pulldown_cmark as cmark;
use self::cmark::{Parser, Event, Tag, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES}; use self::cmark::{Parser, Event, Tag, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
@ -14,6 +14,15 @@ use link_checker::check_url;
use table_of_contents::{TempHeader, Header, make_table_of_contents}; use table_of_contents::{TempHeader, Header, make_table_of_contents};
use context::RenderContext; use context::RenderContext;
const CONTINUE_READING: &str = "<p><a name=\"continue-reading\"></a></p>\n";
#[derive(Debug)]
pub struct Rendered {
pub body: String,
pub summary_len: Option<usize>,
pub toc: Vec<Header>
}
// We might have cases where the slug is already present in our list of anchor // We might have cases where the slug is already present in our list of anchor
// for example an article could have several titles named Example // for example an article could have several titles named Example
// We add a counter after the slug if the slug is already present, which // We add a counter after the slug if the slug is already present, which
@ -36,8 +45,7 @@ fn is_colocated_asset_link(link: &str) -> bool {
&& !link.starts_with("mailto:") && !link.starts_with("mailto:")
} }
pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Rendered> {
pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(String, Vec<Header>)> {
// the rendered html // the rendered html
let mut html = String::with_capacity(content.len()); let mut html = String::with_capacity(content.len());
// Set while parsing // Set while parsing
@ -57,6 +65,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(Strin
let mut temp_header = TempHeader::default(); let mut temp_header = TempHeader::default();
let mut opts = Options::empty(); let mut opts = Options::empty();
let mut has_summary = false;
opts.insert(OPTION_ENABLE_TABLES); opts.insert(OPTION_ENABLE_TABLES);
opts.insert(OPTION_ENABLE_FOOTNOTES); opts.insert(OPTION_ENABLE_FOOTNOTES);
@ -68,7 +77,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(Strin
if in_header { if in_header {
if header_created { if header_created {
temp_header.push(&text); temp_header.push(&text);
return Event::Html(Owned(String::new())); return Event::Html(Borrowed(""));
} }
let id = find_anchor(&anchors, slugify(&text), 0); let id = find_anchor(&anchors, slugify(&text), 0);
anchors.push(id.clone()); anchors.push(id.clone());
@ -78,7 +87,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(Strin
// += as we might have some <code> or other things already there // += as we might have some <code> or other things already there
temp_header.title += &text; temp_header.title += &text;
header_created = true; header_created = true;
return Event::Html(Owned(String::new())); return Event::Html(Borrowed(""));
} }
// if we are in the middle of a code block // if we are in the middle of a code block
@ -93,21 +102,27 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(Strin
} }
Event::Start(Tag::CodeBlock(ref info)) => { Event::Start(Tag::CodeBlock(ref info)) => {
if !context.config.highlight_code { if !context.config.highlight_code {
return Event::Html(Owned("<pre><code>".to_string())); return Event::Html(Borrowed("<pre><code>"));
} }
let theme = &THEME_SET.themes[&context.config.highlight_theme]; let theme = &THEME_SET.themes[&context.config.highlight_theme];
highlighter = Some(get_highlighter(&theme, info)); match get_highlighter(&theme, info, context.base_path, &context.config.extra_syntaxes) {
Ok(h) => highlighter = Some(h),
Err(err) => {
error = Some(format!("Could not load syntax: {}", err).into());
return Event::Html(Borrowed(""));
}
}
let snippet = start_coloured_html_snippet(theme); let snippet = start_coloured_html_snippet(theme);
Event::Html(Owned(snippet)) Event::Html(Owned(snippet))
} }
Event::End(Tag::CodeBlock(_)) => { Event::End(Tag::CodeBlock(_)) => {
if !context.config.highlight_code { if !context.config.highlight_code {
return Event::Html(Owned("</code></pre>\n".to_string())); return Event::Html(Borrowed("</code></pre>\n"));
} }
// reset highlight and close the code block // reset highlight and close the code block
highlighter = None; highlighter = None;
Event::Html(Owned("</pre>".to_string())) Event::Html(Borrowed("</pre>"))
} }
Event::Start(Tag::Image(src, title)) => { Event::Start(Tag::Image(src, title)) => {
if is_colocated_asset_link(&src) { if is_colocated_asset_link(&src) {
@ -133,7 +148,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(Strin
Ok(url) => url, Ok(url) => url,
Err(_) => { Err(_) => {
error = Some(format!("Relative link {} not found.", link).into()); error = Some(format!("Relative link {} not found.", link).into());
return Event::Html(Owned(String::new())); return Event::Html(Borrowed(""));
} }
} }
} else if is_colocated_asset_link(&link) { } else if is_colocated_asset_link(&link) {
@ -161,7 +176,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(Strin
format!("<a href=\"{}\" title=\"{}\">", fixed_link, title) format!("<a href=\"{}\" title=\"{}\">", fixed_link, title)
}; };
temp_header.push(&html); temp_header.push(&html);
return Event::Html(Owned(String::new())); return Event::Html(Borrowed(""));
} }
Event::Start(Tag::Link(Owned(fixed_link), title)) Event::Start(Tag::Link(Owned(fixed_link), title))
@ -169,28 +184,28 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(Strin
Event::End(Tag::Link(_, _)) => { Event::End(Tag::Link(_, _)) => {
if in_header { if in_header {
temp_header.push("</a>"); temp_header.push("</a>");
return Event::Html(Owned(String::new())); return Event::Html(Borrowed(""));
} }
event event
} }
Event::Start(Tag::Code) => { Event::Start(Tag::Code) => {
if in_header { if in_header {
temp_header.push("<code>"); temp_header.push("<code>");
return Event::Html(Owned(String::new())); return Event::Html(Borrowed(""));
} }
event event
} }
Event::End(Tag::Code) => { Event::End(Tag::Code) => {
if in_header { if in_header {
temp_header.push("</code>"); temp_header.push("</code>");
return Event::Html(Owned(String::new())); return Event::Html(Borrowed(""));
} }
event event
} }
Event::Start(Tag::Header(num)) => { Event::Start(Tag::Header(num)) => {
in_header = true; in_header = true;
temp_header = TempHeader::new(num); temp_header = TempHeader::new(num);
Event::Html(Owned(String::new())) Event::Html(Borrowed(""))
} }
Event::End(Tag::Header(_)) => { Event::End(Tag::Header(_)) => {
// End of a header, reset all the things and return the stringified // End of a header, reset all the things and return the stringified
@ -202,6 +217,10 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(Strin
temp_header = TempHeader::default(); temp_header = TempHeader::default();
Event::Html(Owned(val)) Event::Html(Owned(val))
} }
Event::Html(ref markup) if markup.contains("<!-- more -->") => {
has_summary = true;
Event::Html(Borrowed(CONTINUE_READING))
}
_ => event, _ => event,
} }
}); });
@ -209,11 +228,14 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(Strin
cmark::html::push_html(&mut html, parser); cmark::html::push_html(&mut html, parser);
} }
match error { if let Some(e) = error {
Some(e) => Err(e), return Err(e)
None => Ok(( } else {
html.replace("<p></p>", "").replace("</p></p>", "</p>"), html = html.replace("<p></p>", "").replace("</p></p>", "</p>");
make_table_of_contents(&headers) Ok(Rendered {
)), summary_len: if has_summary { html.find(CONTINUE_READING) } else { None },
body: html,
toc: make_table_of_contents(&headers)
})
} }
} }

View file

@ -180,6 +180,8 @@ pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<Strin
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path;
use tera::Tera; use tera::Tera;
use config::Config; use config::Config;
use front_matter::InsertAnchor; use front_matter::InsertAnchor;
@ -202,7 +204,7 @@ mod tests {
fn render_shortcodes(code: &str, tera: &Tera) -> String { fn render_shortcodes(code: &str, tera: &Tera) -> String {
let config = Config::default(); let config = Config::default();
let permalinks = HashMap::new(); let permalinks = HashMap::new();
let context = RenderContext::new(&tera, &config, "", &permalinks, InsertAnchor::None); let context = RenderContext::new(&tera, &config, "", &permalinks, Path::new("something"), InsertAnchor::None);
super::render_shortcodes(code, &context).unwrap() super::render_shortcodes(code, &context).unwrap()
} }

View file

@ -5,6 +5,7 @@ extern crate rendering;
extern crate config; extern crate config;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path;
use tera::Tera; use tera::Tera;
@ -19,9 +20,9 @@ fn can_do_render_content_simple() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("hello", &context).unwrap(); let res = render_content("hello", &context).unwrap();
assert_eq!(res.0, "<p>hello</p>\n"); assert_eq!(res.body, "<p>hello</p>\n");
} }
#[test] #[test]
@ -30,10 +31,10 @@ fn doesnt_highlight_code_block_with_highlighting_off() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let mut config = Config::default(); let mut config = Config::default();
config.highlight_code = false; config.highlight_code = false;
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("```\n$ gutenberg server\n```", &context).unwrap(); let res = render_content("```\n$ gutenberg server\n```", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<pre><code>$ gutenberg server\n</code></pre>\n" "<pre><code>$ gutenberg server\n</code></pre>\n"
); );
} }
@ -43,10 +44,10 @@ fn can_highlight_code_block_no_lang() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("```\n$ gutenberg server\n$ ping\n```", &context).unwrap(); let res = render_content("```\n$ gutenberg server\n$ ping\n```", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<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>" "<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>"
); );
} }
@ -56,10 +57,10 @@ fn can_highlight_code_block_with_lang() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("```python\nlist.append(1)\n```", &context).unwrap(); let res = render_content("```python\nlist.append(1)\n```", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list.</span><span style=\"background-color:#2b303b;color:#bf616a;\">append</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">(</span><span style=\"background-color:#2b303b;color:#d08770;\">1</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">)\n</span></pre>" "<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list.</span><span style=\"background-color:#2b303b;color:#bf616a;\">append</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">(</span><span style=\"background-color:#2b303b;color:#d08770;\">1</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">)\n</span></pre>"
); );
} }
@ -69,11 +70,11 @@ fn can_higlight_code_block_with_unknown_lang() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("```yolo\nlist.append(1)\n```", &context).unwrap(); let res = render_content("```yolo\nlist.append(1)\n```", &context).unwrap();
// defaults to plain text // defaults to plain text
assert_eq!( assert_eq!(
res.0, res.body,
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list.append(1)\n</span></pre>" "<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list.append(1)\n</span></pre>"
); );
} }
@ -82,21 +83,21 @@ fn can_higlight_code_block_with_unknown_lang() {
fn can_render_shortcode() { fn can_render_shortcode() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content(r#" let res = render_content(r#"
Hello Hello
{{ youtube(id="ub36ffWAqgQ") }} {{ youtube(id="ub36ffWAqgQ") }}
"#, &context).unwrap(); "#, &context).unwrap();
assert!(res.0.contains("<p>Hello</p>\n<div >")); assert!(res.body.contains("<p>Hello</p>\n<div >"));
assert!(res.0.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#)); assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#));
} }
#[test] #[test]
fn can_render_shortcode_with_markdown_char_in_args_name() { fn can_render_shortcode_with_markdown_char_in_args_name() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let input = vec![ let input = vec![
"name", "name",
"na_me", "na_me",
@ -105,7 +106,7 @@ fn can_render_shortcode_with_markdown_char_in_args_name() {
]; ];
for i in input { for i in input {
let res = render_content(&format!("{{{{ youtube(id=\"hey\", {}=1) }}}}", i), &context).unwrap(); let res = render_content(&format!("{{{{ youtube(id=\"hey\", {}=1) }}}}", i), &context).unwrap();
assert!(res.0.contains(r#"<iframe src="https://www.youtube.com/embed/hey""#)); assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/hey""#));
} }
} }
@ -113,7 +114,7 @@ fn can_render_shortcode_with_markdown_char_in_args_name() {
fn can_render_shortcode_with_markdown_char_in_args_value() { fn can_render_shortcode_with_markdown_char_in_args_value() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let input = vec![ let input = vec![
"ub36ffWAqgQ-hey", "ub36ffWAqgQ-hey",
"ub36ffWAqgQ_hey", "ub36ffWAqgQ_hey",
@ -123,7 +124,7 @@ fn can_render_shortcode_with_markdown_char_in_args_value() {
]; ];
for i in input { for i in input {
let res = render_content(&format!("{{{{ youtube(id=\"{}\") }}}}", i), &context).unwrap(); let res = render_content(&format!("{{{{ youtube(id=\"{}\") }}}}", i), &context).unwrap();
assert!(res.0.contains(&format!(r#"<iframe src="https://www.youtube.com/embed/{}""#, i))); assert!(res.body.contains(&format!(r#"<iframe src="https://www.youtube.com/embed/{}""#, i)));
} }
} }
@ -140,11 +141,11 @@ fn can_render_body_shortcode_with_markdown_char_in_name() {
for i in input { for i in input {
tera.add_raw_template(&format!("shortcodes/{}.html", i), "<blockquote>{{ body }} - {{ author}}</blockquote>").unwrap(); tera.add_raw_template(&format!("shortcodes/{}.html", i), "<blockquote>{{ body }} - {{ author}}</blockquote>").unwrap();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content(&format!("{{% {}(author=\"Bob\") %}}\nhey\n{{% end %}}", i), &context).unwrap(); let res = render_content(&format!("{{% {}(author=\"Bob\") %}}\nhey\n{{% end %}}", i), &context).unwrap();
println!("{:?}", res); println!("{:?}", res);
assert!(res.0.contains("<blockquote>hey - Bob</blockquote>")); assert!(res.body.contains("<blockquote>hey - Bob</blockquote>"));
} }
} }
@ -169,11 +170,11 @@ Here is another paragraph.
tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap(); tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content(markdown_string, &context).unwrap(); let res = render_content(markdown_string, &context).unwrap();
println!("{:?}", res); println!("{:?}", res);
assert_eq!(res.0, expected); assert_eq!(res.body, expected);
} }
#[test] #[test]
@ -202,18 +203,18 @@ Here is another paragraph.
tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap(); tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content(markdown_string, &context).unwrap(); let res = render_content(markdown_string, &context).unwrap();
println!("{:?}", res); println!("{:?}", res);
assert_eq!(res.0, expected); assert_eq!(res.body, expected);
} }
#[test] #[test]
fn can_render_several_shortcode_in_row() { fn can_render_several_shortcode_in_row() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content(r#" let res = render_content(r#"
Hello Hello
@ -228,11 +229,11 @@ Hello
{{ gist(url="https://gist.github.com/Keats/32d26f699dcc13ebd41b") }} {{ gist(url="https://gist.github.com/Keats/32d26f699dcc13ebd41b") }}
"#, &context).unwrap(); "#, &context).unwrap();
assert!(res.0.contains("<p>Hello</p>\n<div >")); assert!(res.body.contains("<p>Hello</p>\n<div >"));
assert!(res.0.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#)); assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#));
assert!(res.0.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ?autoplay=1""#)); assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ?autoplay=1""#));
assert!(res.0.contains(r#"<iframe src="https://www.streamable.com/e/c0ic""#)); assert!(res.body.contains(r#"<iframe src="https://www.streamable.com/e/c0ic""#));
assert!(res.0.contains(r#"//player.vimeo.com/video/210073083""#)); assert!(res.body.contains(r#"//player.vimeo.com/video/210073083""#));
} }
#[test] #[test]
@ -240,9 +241,9 @@ fn doesnt_render_ignored_shortcodes() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let mut config = Config::default(); let mut config = Config::default();
config.highlight_code = false; config.highlight_code = false;
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content(r#"```{{/* youtube(id="w7Ft2ymGmfc") */}}```"#, &context).unwrap(); let res = render_content(r#"```{{/* youtube(id="w7Ft2ymGmfc") */}}```"#, &context).unwrap();
assert_eq!(res.0, "<p><code>{{ youtube(id=&quot;w7Ft2ymGmfc&quot;) }}</code></p>\n"); assert_eq!(res.body, "<p><code>{{ youtube(id=&quot;w7Ft2ymGmfc&quot;) }}</code></p>\n");
} }
#[test] #[test]
@ -252,7 +253,7 @@ fn can_render_shortcode_with_body() {
tera.add_raw_template("shortcodes/quote.html", "<blockquote>{{ body }} - {{ author }}</blockquote>").unwrap(); tera.add_raw_template("shortcodes/quote.html", "<blockquote>{{ body }} - {{ author }}</blockquote>").unwrap();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content(r#" let res = render_content(r#"
Hello Hello
@ -260,7 +261,7 @@ Hello
A quote A quote
{% end %} {% end %}
"#, &context).unwrap(); "#, &context).unwrap();
assert_eq!(res.0, "<p>Hello</p>\n<blockquote>A quote - Keats</blockquote>\n"); assert_eq!(res.body, "<p>Hello</p>\n<blockquote>A quote - Keats</blockquote>\n");
} }
#[test] #[test]
@ -268,7 +269,7 @@ fn errors_rendering_unknown_shortcode() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("{{ hello(flash=true) }}", &context); let res = render_content("{{ hello(flash=true) }}", &context);
assert!(res.is_err()); assert!(res.is_err());
} }
@ -279,14 +280,14 @@ fn can_make_valid_relative_link() {
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string()); permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string());
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, Path::new("something"), InsertAnchor::None);
let res = render_content( let res = render_content(
r#"[rel link](./pages/about.md), [abs link](https://vincent.is/about)"#, r#"[rel link](./pages/about.md), [abs link](https://vincent.is/about)"#,
&context &context,
).unwrap(); ).unwrap();
assert!( assert!(
res.0.contains(r#"<p><a href="https://vincent.is/about">rel link</a>, <a href="https://vincent.is/about">abs link</a></p>"#) res.body.contains(r#"<p><a href="https://vincent.is/about">rel link</a>, <a href="https://vincent.is/about">abs link</a></p>"#)
); );
} }
@ -296,11 +297,11 @@ fn can_make_relative_links_with_anchors() {
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string()); permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string());
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, Path::new("something"), InsertAnchor::None);
let res = render_content(r#"[rel link](./pages/about.md#cv)"#, &context).unwrap(); let res = render_content(r#"[rel link](./pages/about.md#cv)"#, &context).unwrap();
assert!( assert!(
res.0.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#) res.body.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#)
); );
} }
@ -309,7 +310,7 @@ fn errors_relative_link_inexistant() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("[rel link](./pages/about.md)", &context); let res = render_content("[rel link](./pages/about.md)", &context);
assert!(res.is_err()); assert!(res.is_err());
} }
@ -319,9 +320,9 @@ fn can_add_id_to_headers() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content(r#"# Hello"#, &context).unwrap(); let res = render_content(r#"# Hello"#, &context).unwrap();
assert_eq!(res.0, "<h1 id=\"hello\">Hello</h1>\n"); assert_eq!(res.body, "<h1 id=\"hello\">Hello</h1>\n");
} }
#[test] #[test]
@ -329,19 +330,19 @@ fn can_add_id_to_headers_same_slug() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("# Hello\n# Hello", &context).unwrap(); let res = render_content("# Hello\n# Hello", &context).unwrap();
assert_eq!(res.0, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n"); assert_eq!(res.body, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n");
} }
#[test] #[test]
fn can_insert_anchor_left() { fn can_insert_anchor_left() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::Left);
let res = render_content("# Hello", &context).unwrap(); let res = render_content("# Hello", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello</h1>\n" "<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello</h1>\n"
); );
} }
@ -350,10 +351,10 @@ fn can_insert_anchor_left() {
fn can_insert_anchor_right() { fn can_insert_anchor_right() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Right); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::Right);
let res = render_content("# Hello", &context).unwrap(); let res = render_content("# Hello", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<h1 id=\"hello\">Hello<a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\n</h1>\n" "<h1 id=\"hello\">Hello<a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\n</h1>\n"
); );
} }
@ -363,10 +364,10 @@ fn can_insert_anchor_right() {
fn can_insert_anchor_with_exclamation_mark() { fn can_insert_anchor_with_exclamation_mark() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::Left);
let res = render_content("# Hello!", &context).unwrap(); let res = render_content("# Hello!", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello!</h1>\n" "<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello!</h1>\n"
); );
} }
@ -376,10 +377,10 @@ fn can_insert_anchor_with_exclamation_mark() {
fn can_insert_anchor_with_link() { fn can_insert_anchor_with_link() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::Left);
let res = render_content("## [Rust](https://rust-lang.org)", &context).unwrap(); let res = render_content("## [Rust](https://rust-lang.org)", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<h2 id=\"rust\"><a class=\"gutenberg-anchor\" href=\"#rust\" aria-label=\"Anchor link for: rust\">🔗</a>\n<a href=\"https://rust-lang.org\">Rust</a></h2>\n" "<h2 id=\"rust\"><a class=\"gutenberg-anchor\" href=\"#rust\" aria-label=\"Anchor link for: rust\">🔗</a>\n<a href=\"https://rust-lang.org\">Rust</a></h2>\n"
); );
} }
@ -388,10 +389,10 @@ fn can_insert_anchor_with_link() {
fn can_insert_anchor_with_other_special_chars() { fn can_insert_anchor_with_other_special_chars() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::Left); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::Left);
let res = render_content("# Hello*_()", &context).unwrap(); let res = render_content("# Hello*_()", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello*_()</h1>\n" "<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello*_()</h1>\n"
); );
} }
@ -405,7 +406,8 @@ fn can_make_toc() {
&config, &config,
"https://mysite.com/something", "https://mysite.com/something",
&permalinks_ctx, &permalinks_ctx,
InsertAnchor::Left Path::new("something"),
InsertAnchor::Left,
); );
let res = render_content(r#" let res = render_content(r#"
@ -418,21 +420,20 @@ fn can_make_toc() {
### Last one ### Last one
"#, &context).unwrap(); "#, &context).unwrap();
let toc = res.1; let toc = res.toc;
assert_eq!(toc.len(), 1); assert_eq!(toc.len(), 1);
assert_eq!(toc[0].children.len(), 2); assert_eq!(toc[0].children.len(), 2);
assert_eq!(toc[0].children[1].children.len(), 1); assert_eq!(toc[0].children[1].children.len(), 1);
} }
#[test] #[test]
fn can_understand_backtick_in_titles() { fn can_understand_backtick_in_titles() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("# `Hello`", &context).unwrap(); let res = render_content("# `Hello`", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<h1 id=\"hello\"><code>Hello</code></h1>\n" "<h1 id=\"hello\"><code>Hello</code></h1>\n"
); );
} }
@ -441,10 +442,10 @@ fn can_understand_backtick_in_titles() {
fn can_understand_backtick_in_paragraphs() { fn can_understand_backtick_in_paragraphs() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("Hello `world`", &context).unwrap(); let res = render_content("Hello `world`", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<p>Hello <code>world</code></p>\n" "<p>Hello <code>world</code></p>\n"
); );
} }
@ -454,10 +455,10 @@ fn can_understand_backtick_in_paragraphs() {
fn can_understand_links_in_header() { fn can_understand_links_in_header() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("# [Rust](https://rust-lang.org)", &context).unwrap(); let res = render_content("# [Rust](https://rust-lang.org)", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<h1 id=\"rust\"><a href=\"https://rust-lang.org\">Rust</a></h1>\n" "<h1 id=\"rust\"><a href=\"https://rust-lang.org\">Rust</a></h1>\n"
); );
} }
@ -466,10 +467,10 @@ fn can_understand_links_in_header() {
fn can_understand_link_with_title_in_header() { fn can_understand_link_with_title_in_header() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("# [Rust](https://rust-lang.org \"Rust homepage\")", &context).unwrap(); let res = render_content("# [Rust](https://rust-lang.org \"Rust homepage\")", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<h1 id=\"rust\"><a href=\"https://rust-lang.org\" title=\"Rust homepage\">Rust</a></h1>\n" "<h1 id=\"rust\"><a href=\"https://rust-lang.org\" title=\"Rust homepage\">Rust</a></h1>\n"
); );
} }
@ -480,14 +481,14 @@ fn can_make_valid_relative_link_in_header() {
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about/".to_string()); permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about/".to_string());
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, Path::new("something"), InsertAnchor::None);
let res = render_content( let res = render_content(
r#" # [rel link](./pages/about.md)"#, r#" # [rel link](./pages/about.md)"#,
&context &context,
).unwrap(); ).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<h1 id=\"rel-link\"><a href=\"https://vincent.is/about/\">rel link</a></h1>\n" "<h1 id=\"rel-link\"><a href=\"https://vincent.is/about/\">rel link</a></h1>\n"
); );
} }
@ -496,10 +497,10 @@ fn can_make_valid_relative_link_in_header() {
fn can_make_permalinks_with_colocated_assets_for_link() { fn can_make_permalinks_with_colocated_assets_for_link() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("[an image](image.jpg)", &context).unwrap(); let res = render_content("[an image](image.jpg)", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<p><a href=\"https://vincent.is/about/image.jpg\">an image</a></p>\n" "<p><a href=\"https://vincent.is/about/image.jpg\">an image</a></p>\n"
); );
} }
@ -508,10 +509,10 @@ fn can_make_permalinks_with_colocated_assets_for_link() {
fn can_make_permalinks_with_colocated_assets_for_image() { fn can_make_permalinks_with_colocated_assets_for_image() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("![alt text](image.jpg)", &context).unwrap(); let res = render_content("![alt text](image.jpg)", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<p><img src=\"https://vincent.is/about/image.jpg\" alt=\"alt text\" /></p>\n" "<p><img src=\"https://vincent.is/about/image.jpg\" alt=\"alt text\" /></p>\n"
); );
} }
@ -520,7 +521,7 @@ fn can_make_permalinks_with_colocated_assets_for_image() {
fn markdown_doesnt_wrap_html_in_paragraph() { fn markdown_doesnt_wrap_html_in_paragraph() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&GUTENBERG_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content(r#" let res = render_content(r#"
Some text Some text
@ -533,7 +534,7 @@ Some text
</div> </div>
"#, &context).unwrap(); "#, &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<p>Some text</p>\n<h1>Helo</h1>\n<div>\n<a href=\"mobx-flow.png\">\n <img src=\"mobx-flow.png\" alt=\"MobX flow\">\n </a>\n</div>\n" "<p>Some text</p>\n<h1>Helo</h1>\n<div>\n<a href=\"mobx-flow.png\">\n <img src=\"mobx-flow.png\" alt=\"MobX flow\">\n </a>\n</div>\n"
); );
} }
@ -543,10 +544,10 @@ fn can_validate_valid_external_links() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let mut config = Config::default(); let mut config = Config::default();
config.check_external_links = true; config.check_external_links = true;
let context = RenderContext::new(&GUTENBERG_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("[a link](http://google.com)", &context).unwrap(); let res = render_content("[a link](http://google.com)", &context).unwrap();
assert_eq!( assert_eq!(
res.0, res.body,
"<p><a href=\"http://google.com\">a link</a></p>\n" "<p><a href=\"http://google.com\">a link</a></p>\n"
); );
} }
@ -556,9 +557,26 @@ fn can_show_error_message_for_invalid_external_links() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let mut config = Config::default(); let mut config = Config::default();
config.check_external_links = true; config.check_external_links = true;
let context = RenderContext::new(&GUTENBERG_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&GUTENBERG_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("[a link](http://google.comy)", &context); let res = render_content("[a link](http://google.comy)", &context);
assert!(res.is_err()); assert!(res.is_err());
let err = res.unwrap_err(); let err = res.unwrap_err();
assert!(err.description().contains("Link http://google.comy is not valid")); assert!(err.description().contains("Link http://google.comy is not valid"));
} }
#[test]
fn can_handle_summaries() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, Path::new("something"), InsertAnchor::None);
let res = render_content("Hello [world]\n\n<!-- more -->\n\nBla bla\n\n[world]: https://vincent.is/about/", &context).unwrap();
assert_eq!(
res.body,
"<p>Hello <a href=\"https://vincent.is/about/\">world</a></p>\n<p><a name=\"continue-reading\"></a></p>\n<p>Bla bla</p>\n"
);
assert_eq!(
res.summary_len,
Some("<p>Hello <a href=\"https://vincent.is/about/\">world</a></p>\n".len())
);
}

View file

@ -15,8 +15,10 @@ PAGE = """
+++ +++
title = "Hello" title = "Hello"
date = REPLACE_DATE date = REPLACE_DATE
[taxonomies]
tags = REPLACE_TAG tags = REPLACE_TAG
category = "REPLACE_CATEGORY" categories = ["REPLACE_CATEGORY"]
+++ +++
# Modus cognitius profanam ne duae virtutis mundi # Modus cognitius profanam ne duae virtutis mundi
@ -103,10 +105,13 @@ def gen_skeleton(name, is_blog):
f.write(""" f.write("""
title = "My site" title = "My site"
base_url = "https://replace-this-with-your-url.com" base_url = "https://replace-this-with-your-url.com"
generate_tags_pages = true
generate_categories_pages = true
theme = "sample" theme = "sample"
taxonomies = [
{name = "tags", rss = true},
{name = "categories"}
]
[extra.author] [extra.author]
name = "Vincent Prouillet" name = "Vincent Prouillet"
""") """)
@ -121,8 +126,8 @@ name = "Vincent Prouillet"
""") """)
# Re-use the test templates # Re-use the test templates
shutil.copytree("../test_site/templates", os.path.join(name, "templates")) shutil.copytree("../../../test_site/templates", os.path.join(name, "templates"))
shutil.copytree("../test_site/themes", os.path.join(name, "themes")) shutil.copytree("../../../test_site/themes", os.path.join(name, "themes"))
def gen_section(path, num_pages, is_blog): def gen_section(path, num_pages, is_blog):

View file

@ -25,7 +25,7 @@ fn bench_loading_small_blog_with_syntax_highlighting(b: &mut test::Bencher) {
path.push("benches"); path.push("benches");
path.push("small-blog"); path.push("small-blog");
let mut site = Site::new(&path, "config.toml").unwrap(); let mut site = Site::new(&path, "config.toml").unwrap();
site.config.highlight_code = Some(true); site.config.highlight_code = true;
b.iter(|| site.load().unwrap()); b.iter(|| site.load().unwrap());
} }
@ -46,7 +46,7 @@ fn bench_loading_small_blog_with_syntax_highlighting(b: &mut test::Bencher) {
// path.push("benches"); // path.push("benches");
// path.push("medium-blog"); // path.push("medium-blog");
// let mut site = Site::new(&path, "config.toml").unwrap(); // let mut site = Site::new(&path, "config.toml").unwrap();
// site.config.highlight_code = Some(true); // site.config.highlight_code = true;
// //
// b.iter(|| site.load().unwrap()); // b.iter(|| site.load().unwrap());
//} //}
@ -67,7 +67,7 @@ fn bench_loading_small_blog_with_syntax_highlighting(b: &mut test::Bencher) {
// path.push("benches"); // path.push("benches");
// path.push("big-blog"); // path.push("big-blog");
// let mut site = Site::new(&path, "config.toml").unwrap(); // let mut site = Site::new(&path, "config.toml").unwrap();
// site.config.highlight_code = Some(true); // site.config.highlight_code = true;
// //
// b.iter(|| site.load().unwrap()); // b.iter(|| site.load().unwrap());
//} //}
@ -88,7 +88,7 @@ fn bench_loading_small_blog_with_syntax_highlighting(b: &mut test::Bencher) {
// path.push("benches"); // path.push("benches");
// path.push("huge-blog"); // path.push("huge-blog");
// let mut site = Site::new(&path, "config.toml").unwrap(); // let mut site = Site::new(&path, "config.toml").unwrap();
// site.config.highlight_code = Some(true); // site.config.highlight_code = true;
// //
// b.iter(|| site.load().unwrap()); // b.iter(|| site.load().unwrap());
//} //}
@ -109,7 +109,7 @@ fn bench_loading_small_kb_with_syntax_highlighting(b: &mut test::Bencher) {
path.push("benches"); path.push("benches");
path.push("small-kb"); path.push("small-kb");
let mut site = Site::new(&path, "config.toml").unwrap(); let mut site = Site::new(&path, "config.toml").unwrap();
site.config.highlight_code = Some(true); site.config.highlight_code = true;
b.iter(|| site.load().unwrap()); b.iter(|| site.load().unwrap());
} }

View file

@ -1,12 +1,14 @@
#![feature(test)] #![feature(test)]
extern crate test; extern crate test;
extern crate site; extern crate site;
extern crate pagination;
extern crate tempfile; extern crate tempfile;
use std::env; use std::env;
use tempfile::tempdir; use tempfile::tempdir;
use site::Site; use site::Site;
use pagination::Paginator;
fn setup_site(name: &str) -> Site { fn setup_site(name: &str) -> Site {
@ -20,8 +22,8 @@ fn setup_site(name: &str) -> Site {
#[bench] #[bench]
fn bench_render_aliases(b: &mut test::Bencher) { fn bench_render_aliases(b: &mut test::Bencher) {
let mut site = setup_site("huge-blog"); let mut site = setup_site("small-blog");
let tmp_dir = TempDir::new("benches").expect("create temp dir"); let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public"); let public = &tmp_dir.path().join("public");
site.set_output_path(&public); site.set_output_path(&public);
b.iter(|| site.render_aliases().unwrap()); b.iter(|| site.render_aliases().unwrap());
@ -29,8 +31,8 @@ fn bench_render_aliases(b: &mut test::Bencher) {
#[bench] #[bench]
fn bench_render_sitemap(b: &mut test::Bencher) { fn bench_render_sitemap(b: &mut test::Bencher) {
let mut site = setup_site("huge-blog"); let mut site = setup_site("small-blog");
let tmp_dir = TempDir::new("benches").expect("create temp dir"); let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public"); let public = &tmp_dir.path().join("public");
site.set_output_path(&public); site.set_output_path(&public);
b.iter(|| site.render_sitemap().unwrap()); b.iter(|| site.render_sitemap().unwrap());
@ -38,29 +40,30 @@ fn bench_render_sitemap(b: &mut test::Bencher) {
#[bench] #[bench]
fn bench_render_rss_feed(b: &mut test::Bencher) { fn bench_render_rss_feed(b: &mut test::Bencher) {
let mut site = setup_site("huge-blog"); let mut site = setup_site("small-blog");
let tmp_dir = TempDir::new("benches").expect("create temp dir"); let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public"); let public = &tmp_dir.path().join("public");
site.set_output_path(&public); site.set_output_path(&public);
b.iter(|| site.render_rss_feed().unwrap()); b.iter(|| site.render_rss_feed(None, None).unwrap());
} }
#[bench] #[bench]
fn bench_render_categories(b: &mut test::Bencher) { fn bench_render_taxonomies(b: &mut test::Bencher) {
let mut site = setup_site("huge-blog"); let mut site = setup_site("small-blog");
let tmp_dir = TempDir::new("benches").expect("create temp dir"); let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public"); let public = &tmp_dir.path().join("public");
site.set_output_path(&public); site.set_output_path(&public);
b.iter(|| site.render_categories().unwrap()); b.iter(|| site.render_taxonomies().unwrap());
} }
#[bench] #[bench]
fn bench_render_paginated(b: &mut test::Bencher) { fn bench_render_paginated(b: &mut test::Bencher) {
let mut site = setup_site("medium-blog"); let mut site = setup_site("small-blog");
let tmp_dir = TempDir::new("benches").expect("create temp dir"); let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public"); let public = &tmp_dir.path().join("public");
site.set_output_path(&public); site.set_output_path(&public);
let section = site.sections.values().collect::<Vec<_>>()[0]; let section = site.sections.values().collect::<Vec<_>>()[0];
let paginator = Paginator::from_section(&section.pages, section);
b.iter(|| site.render_paginated(public, section)); b.iter(|| site.render_paginated(public, &paginator));
} }

View file

@ -187,7 +187,6 @@ impl Site {
section_entries section_entries
.into_par_iter() .into_par_iter()
.filter(|entry| entry.as_path().file_name().unwrap() == "_index.md")
.map(|entry| { .map(|entry| {
let path = entry.as_path(); let path = entry.as_path();
Section::from_file(path, config) Section::from_file(path, config)
@ -200,7 +199,6 @@ impl Site {
page_entries page_entries
.into_par_iter() .into_par_iter()
.filter(|entry| entry.as_path().file_name().unwrap() != "_index.md")
.map(|entry| { .map(|entry| {
let path = entry.as_path(); let path = entry.as_path();
Page::from_file(path, config) Page::from_file(path, config)
@ -216,7 +214,7 @@ impl Site {
} }
// Insert a default index section if necessary so we don't need to create // Insert a default index section if necessary so we don't need to create
// a _index.md to render the index page // a _index.md to render the index page at the root of the site
let index_path = self.index_section_path(); let index_path = self.index_section_path();
if let Some(ref index_section) = self.sections.get(&index_path) { if let Some(ref index_section) = self.sections.get(&index_path) {
if self.config.build_search_index && !index_section.meta.in_search_index { if self.config.build_search_index && !index_section.meta.in_search_index {
@ -260,6 +258,7 @@ impl Site {
let permalinks = &self.permalinks; let permalinks = &self.permalinks;
let tera = &self.tera; let tera = &self.tera;
let config = &self.config; let config = &self.config;
let base_path = &self.base_path;
// TODO: avoid the duplication with function above for that part // TODO: avoid the duplication with function above for that part
// This is needed in the first place because of silly borrow checker // This is needed in the first place because of silly borrow checker
@ -271,13 +270,13 @@ impl Site {
self.pages.par_iter_mut() self.pages.par_iter_mut()
.map(|(_, page)| { .map(|(_, page)| {
let insert_anchor = pages_insert_anchors[&page.file.path]; let insert_anchor = pages_insert_anchors[&page.file.path];
page.render_markdown(permalinks, tera, config, insert_anchor) page.render_markdown(permalinks, tera, config, base_path, insert_anchor)
}) })
.fold(|| Ok(()), Result::and) .fold(|| Ok(()), Result::and)
.reduce(|| Ok(()), Result::and)?; .reduce(|| Ok(()), Result::and)?;
self.sections.par_iter_mut() self.sections.par_iter_mut()
.map(|(_, section)| section.render_markdown(permalinks, tera, config)) .map(|(_, section)| section.render_markdown(permalinks, tera, config, base_path))
.fold(|| Ok(()), Result::and) .fold(|| Ok(()), Result::and)
.reduce(|| Ok(()), Result::and)?; .reduce(|| Ok(()), Result::and)?;
@ -320,7 +319,7 @@ impl Site {
if render { if render {
let insert_anchor = self.find_parent_section_insert_anchor(&self.pages[&path].file.parent); let insert_anchor = self.find_parent_section_insert_anchor(&self.pages[&path].file.parent);
let page = self.pages.get_mut(&path).unwrap(); let page = self.pages.get_mut(&path).unwrap();
page.render_markdown(&self.permalinks, &self.tera, &self.config, insert_anchor)?; page.render_markdown(&self.permalinks, &self.tera, &self.config, &self.base_path, insert_anchor)?;
} }
Ok(prev) Ok(prev)
@ -337,7 +336,7 @@ impl Site {
if render { if render {
let section = self.sections.get_mut(&path).unwrap(); let section = self.sections.get_mut(&path).unwrap();
section.render_markdown(&self.permalinks, &self.tera, &self.config)?; section.render_markdown(&self.permalinks, &self.tera, &self.config, &self.base_path)?;
} }
Ok(prev) Ok(prev)
@ -837,6 +836,12 @@ impl Site {
} }
} }
// Copy any asset we found previously into the same directory as the index.html
for asset in &section.assets {
let asset_path = asset.as_path();
copy(&asset_path, &output_path.join(asset_path.file_name().unwrap()))?;
}
if render_pages { if render_pages {
section section
.pages .pages

View file

@ -19,7 +19,7 @@ fn can_parse_site() {
site.load().unwrap(); site.load().unwrap();
// Correct number of pages (sections are pages too) // Correct number of pages (sections are pages too)
assert_eq!(site.pages.len(), 14); assert_eq!(site.pages.len(), 15);
let posts_path = path.join("content").join("posts"); let posts_path = path.join("content").join("posts");
// Make sure we remove all the pwd + content from the sections // Make sure we remove all the pwd + content from the sections
@ -44,7 +44,7 @@ fn can_parse_site() {
let posts_section = &site.sections[&posts_path.join("_index.md")]; let posts_section = &site.sections[&posts_path.join("_index.md")];
assert_eq!(posts_section.subsections.len(), 1); assert_eq!(posts_section.subsections.len(), 1);
assert_eq!(posts_section.pages.len(), 6); assert_eq!(posts_section.pages.len(), 7);
let tutorials_section = &site.sections[&posts_path.join("tutorials").join("_index.md")]; let tutorials_section = &site.sections[&posts_path.join("tutorials").join("_index.md")];
assert_eq!(tutorials_section.subsections.len(), 2); assert_eq!(tutorials_section.subsections.len(), 2);
@ -321,22 +321,41 @@ fn can_build_site_with_pagination_for_section() {
"posts/page/1/index.html", "posts/page/1/index.html",
"http-equiv=\"refresh\" content=\"0;url=https://replace-this-with-your-url.com/posts/\"" "http-equiv=\"refresh\" content=\"0;url=https://replace-this-with-your-url.com/posts/\""
)); ));
assert!(file_contains!(public, "posts/index.html", "Num pagers: 3")); assert!(file_contains!(public, "posts/index.html", "Num pagers: 4"));
assert!(file_contains!(public, "posts/index.html", "Page size: 2")); assert!(file_contains!(public, "posts/index.html", "Page size: 2"));
assert!(file_contains!(public, "posts/index.html", "Current index: 1")); assert!(file_contains!(public, "posts/index.html", "Current index: 1"));
assert!(!file_contains!(public, "posts/index.html", "has_prev"));
assert!(file_contains!(public, "posts/index.html", "has_next")); assert!(file_contains!(public, "posts/index.html", "has_next"));
assert!(file_contains!(public, "posts/index.html", "First: https://replace-this-with-your-url.com/posts/")); assert!(file_contains!(public, "posts/index.html", "First: https://replace-this-with-your-url.com/posts/"));
assert!(file_contains!(public, "posts/index.html", "Last: https://replace-this-with-your-url.com/posts/page/3/")); assert!(file_contains!(public, "posts/index.html", "Last: https://replace-this-with-your-url.com/posts/page/4/"));
assert_eq!(file_contains!(public, "posts/index.html", "has_prev"), false); assert_eq!(file_contains!(public, "posts/index.html", "has_prev"), false);
assert!(file_exists!(public, "posts/page/2/index.html")); assert!(file_exists!(public, "posts/page/2/index.html"));
assert!(file_contains!(public, "posts/page/2/index.html", "Num pagers: 3")); assert!(file_contains!(public, "posts/page/2/index.html", "Num pagers: 4"));
assert!(file_contains!(public, "posts/page/2/index.html", "Page size: 2")); assert!(file_contains!(public, "posts/page/2/index.html", "Page size: 2"));
assert!(file_contains!(public, "posts/page/2/index.html", "Current index: 2")); assert!(file_contains!(public, "posts/page/2/index.html", "Current index: 2"));
assert!(file_contains!(public, "posts/page/2/index.html", "has_prev")); assert!(file_contains!(public, "posts/page/2/index.html", "has_prev"));
assert!(file_contains!(public, "posts/page/2/index.html", "has_next")); assert!(file_contains!(public, "posts/page/2/index.html", "has_next"));
assert!(file_contains!(public, "posts/page/2/index.html", "First: https://replace-this-with-your-url.com/posts/")); assert!(file_contains!(public, "posts/page/2/index.html", "First: https://replace-this-with-your-url.com/posts/"));
assert!(file_contains!(public, "posts/page/2/index.html", "Last: https://replace-this-with-your-url.com/posts/page/3/")); assert!(file_contains!(public, "posts/page/2/index.html", "Last: https://replace-this-with-your-url.com/posts/page/4/"));
assert!(file_exists!(public, "posts/page/3/index.html"));
assert!(file_contains!(public, "posts/page/3/index.html", "Num pagers: 4"));
assert!(file_contains!(public, "posts/page/3/index.html", "Page size: 2"));
assert!(file_contains!(public, "posts/page/3/index.html", "Current index: 3"));
assert!(file_contains!(public, "posts/page/3/index.html", "has_prev"));
assert!(file_contains!(public, "posts/page/3/index.html", "has_next"));
assert!(file_contains!(public, "posts/page/3/index.html", "First: https://replace-this-with-your-url.com/posts/"));
assert!(file_contains!(public, "posts/page/3/index.html", "Last: https://replace-this-with-your-url.com/posts/page/4/"));
assert!(file_exists!(public, "posts/page/4/index.html"));
assert!(file_contains!(public, "posts/page/4/index.html", "Num pagers: 4"));
assert!(file_contains!(public, "posts/page/4/index.html", "Page size: 2"));
assert!(file_contains!(public, "posts/page/4/index.html", "Current index: 4"));
assert!(file_contains!(public, "posts/page/4/index.html", "has_prev"));
assert!(!file_contains!(public, "posts/page/4/index.html", "has_next"));
assert!(file_contains!(public, "posts/page/4/index.html", "First: https://replace-this-with-your-url.com/posts/"));
assert!(file_contains!(public, "posts/page/4/index.html", "Last: https://replace-this-with-your-url.com/posts/page/4/"));
} }
#[test] #[test]
@ -397,10 +416,10 @@ fn can_build_rss_feed() {
assert!(Path::new(&public).exists()); assert!(Path::new(&public).exists());
assert!(file_exists!(public, "rss.xml")); assert!(file_exists!(public, "rss.xml"));
// latest article is posts/simple.md // latest article is posts/extra-syntax.md
assert!(file_contains!(public, "rss.xml", "Extra Syntax"));
// Next is posts/simple.md
assert!(file_contains!(public, "rss.xml", "Simple article with shortcodes")); assert!(file_contains!(public, "rss.xml", "Simple article with shortcodes"));
// Next is posts/python.md
assert!(file_contains!(public, "rss.xml", "Python in posts"));
} }
@ -420,3 +439,20 @@ fn can_build_search_index() {
assert!(file_exists!(public, "elasticlunr.min.js")); assert!(file_exists!(public, "elasticlunr.min.js"));
assert!(file_exists!(public, "search_index.en.js")); assert!(file_exists!(public, "search_index.en.js"));
} }
#[test]
fn can_build_with_extra_syntaxes() {
let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
site.load().unwrap();
let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
site.build().unwrap();
assert!(&public.exists());
assert!(file_exists!(public, "posts/extra-syntax/index.html"));
assert!(file_contains!(public, "posts/extra-syntax/index.html",
r#"<span style="background-color:#2b303b;color:#d08770;">test</span>"#));
}

View file

@ -40,18 +40,34 @@ While not shown in the example, sections can be nested indefinitely.
## Assets colocation ## Assets colocation
The `content` directory is not limited to markup files though: it's natural to want to co-locate a page and some related The `content` directory is not limited to markup files though: it's natural to want to co-locate a page and some related
assets. assets, for instance images or spreadsheets. Gutenberg supports that pattern out of the box for both sections and pages.
Any non-markdown file you add in the page/section folder will be copied alongside the generated page when building the site,
which allows us to use a relative path to access them.
For pages to use assets colocation, they should not be placed directly in their section folder (such as `latest-experiment.md`), but as an `index.md` file
in a dedicated folder (`latest-experiment/index.md`), like so:
Gutenberg supports that pattern out of the box: create a folder, add a `index.md` file and as many non-markdown files as you want.
Those assets will be copied in the same folder when building the site which allows you to use a relative path to access them.
```bash ```bash
└── with-assets └── research
├── index.md ├── latest-experiment
└── yavascript.js │ ├── index.md
│ └── yavascript.js
├── _index.md
└── research.jpg
``` ```
By default, this page will get the folder name (`with-assets` in this case) as its slug. In this setup, you may access `research.jpg` from your 'research' section,
and `yavascript.js` from your 'latest-experiment' directly within the Markdown:
```markdown
Check out the complete program [here](yavascript.js). It's **really cool free-software**!
```
By default, this page will get the folder name as its slug. So its permalink would be in the form of `https://example.com/research/latest-experiment/`
### Excluding files from assets
It is possible to ignore selected asset files using the It is possible to ignore selected asset files using the
[ignored_content](./documentation/getting-started/configuration.md) setting in the config file. [ignored_content](./documentation/getting-started/configuration.md) setting in the config file.

View file

@ -20,7 +20,7 @@ content directory `about.md` would also create a page at `[base_url]/about`.
As you can see, creating an `about.md` file is exactly equivalent to creating an As you can see, creating an `about.md` file is exactly equivalent to creating an
`about/index.md` file. The only difference between the two methods is that creating `about/index.md` file. The only difference between the two methods is that creating
the `about` folder allows you to use asset colocation, as discussed in the the `about` folder allows you to use asset colocation, as discussed in the
[Overview](./documentation/content/overview.md) section of this documentation. [Overview](./documentation/content/overview.md#assets-colocation) section of this documentation.
## Front-matter ## Front-matter

View file

@ -14,6 +14,8 @@ not have any content or metadata. If you would like to add content or metadata,
`_index.md` file at the root of the `content` folder and edit it just as you would edit any other `_index.md` file at the root of the `content` folder and edit it just as you would edit any other
`_index.md` file; your `index.html` template will then have access to that content and metadata. `_index.md` file; your `index.html` template will then have access to that content and metadata.
Any non-Markdown file in the section folder is added to the `assets` collection of the section, as explained in the [Content Overview](./documentation/content/overview.md#assets-colocation). These files are then available from the Markdown using relative links.
## Front-matter ## Front-matter
The `_index.md` file within a folder defines the content and metadata for that section. To set The `_index.md` file within a folder defines the content and metadata for that section. To set

View file

@ -3,7 +3,7 @@ title = "Syntax Highlighting"
weight = 80 weight = 80
+++ +++
Gutenberg comes with built-in syntax highlighting but you first Gutenberg comes with built-in syntax highlighting but you first
need to enable it in the [configuration](./documentation/getting-started/configuration.md). need to enable it in the [configuration](./documentation/getting-started/configuration.md).
Once this is done, Gutenberg will automatically highlight all code blocks Once this is done, Gutenberg will automatically highlight all code blocks
@ -17,7 +17,7 @@ let highlight = true;
```` ````
You can replace the `rust` by the language you want to highlight or not put anything to get it You can replace the `rust` by the language you want to highlight or not put anything to get it
interpreted as plain text. interpreted as plain text.
Here is a full list of the supported languages and the short names you can use: Here is a full list of the supported languages and the short names you can use:
@ -27,12 +27,12 @@ Here is a full list of the supported languages and the short names you can use:
- Assembly x86 (NASM) -> ["asm", "inc", "nasm"] - Assembly x86 (NASM) -> ["asm", "inc", "nasm"]
- Crystal -> ["cr"] - Crystal -> ["cr"]
- Elixir -> ["ex", "exs"] - Elixir -> ["ex", "exs"]
- Elm -> ["elm"]
- Handlebars -> ["handlebars", "handlebars.html", "hbr", "hbrs", "hbs", "hdbs", "hjs", "mu", "mustache", "rac", "stache", "template", "tmpl"] - Handlebars -> ["handlebars", "handlebars.html", "hbr", "hbrs", "hbs", "hdbs", "hjs", "mu", "mustache", "rac", "stache", "template", "tmpl"]
- Jinja2 -> ["j2", "jinja2"] - Jinja2 -> ["j2", "jinja2"]
- Julia -> ["jl"] - Julia -> ["jl"]
- Kotlin -> ["kt", "kts"] - Kotlin -> ["kt", "kts"]
- Less -> ["less", "css.less"] - Less -> ["less", "css.less"]
- MiniZinc (MZN) -> ["mzn", "dzn"]
- Nim -> ["nim", "nims"] - Nim -> ["nim", "nims"]
- ASP -> ["asa"] - ASP -> ["asa"]
- HTML (ASP) -> ["asp"] - HTML (ASP) -> ["asp"]
@ -49,10 +49,17 @@ Here is a full list of the supported languages and the short names you can use:
- Diff -> ["diff", "patch"] - Diff -> ["diff", "patch"]
- Erlang -> ["erl", "hrl", "Emakefile", "emakefile"] - Erlang -> ["erl", "hrl", "Emakefile", "emakefile"]
- HTML (Erlang) -> ["yaws"] - HTML (Erlang) -> ["yaws"]
- Git Attributes -> ["attributes", "gitattributes", ".gitattributes"]
- Git Commit -> ["COMMIT_EDITMSG", "MERGE_MSG", "TAG_EDITMSG"]
- Git Config -> ["gitconfig", ".gitconfig", ".gitmodules"]
- Git Ignore -> ["exclude", "gitignore", ".gitignore"]
- Git Link -> [".git"]
- Git Log -> ["gitlog"]
- Git Rebase Todo -> ["git-rebase-todo"]
- Go -> ["go"] - Go -> ["go"]
- Graphviz (DOT) -> ["dot", "DOT", "gv"] - Graphviz (DOT) -> ["dot", "DOT", "gv"]
- Groovy -> ["groovy", "gvy", "gradle"] - Groovy -> ["groovy", "gvy", "gradle", "Jenkinsfile"]
- HTML -> ["html", "htm", "shtml", "xhtml", "inc", "tmpl", "tpl"] - HTML -> ["html", "htm", "shtml", "xhtml"]
- Haskell -> ["hs"] - Haskell -> ["hs"]
- Literate Haskell -> ["lhs"] - Literate Haskell -> ["lhs"]
- Java Server Page (JSP) -> ["jsp"] - Java Server Page (JSP) -> ["jsp"]
@ -65,7 +72,7 @@ Here is a full list of the supported languages and the short names you can use:
- TeX -> ["sty", "cls"] - TeX -> ["sty", "cls"]
- Lisp -> ["lisp", "cl", "clisp", "l", "mud", "el", "scm", "ss", "lsp", "fasl"] - Lisp -> ["lisp", "cl", "clisp", "l", "mud", "el", "scm", "ss", "lsp", "fasl"]
- Lua -> ["lua"] - Lua -> ["lua"]
- Makefile -> ["make", "GNUmakefile", "makefile", "Makefile", "OCamlMakefile", "mak", "mk"] - Makefile -> ["make", "GNUmakefile", "makefile", "Makefile", "makefile.am", "Makefile.am", "makefile.in", "Makefile.in", "OCamlMakefile", "mak", "mk"]
- Markdown -> ["md", "mdown", "markdown", "markdn"] - Markdown -> ["md", "mdown", "markdown", "markdn"]
- MATLAB -> ["matlab"] - MATLAB -> ["matlab"]
- OCaml -> ["ml", "mli"] - OCaml -> ["ml", "mli"]
@ -90,19 +97,45 @@ Here is a full list of the supported languages and the short names you can use:
- Rust -> ["rs"] - Rust -> ["rs"]
- SQL -> ["sql", "ddl", "dml"] - SQL -> ["sql", "ddl", "dml"]
- Scala -> ["scala", "sbt"] - Scala -> ["scala", "sbt"]
- Bourne Again Shell (bash) -> ["sh", "bash", "zsh", "fish", ".bash_aliases", ".bash_completions", ".bash_functions", ".bash_login", ".bash_logout", ".bash_profile", ".bash_variables", ".bashrc", ".profile", ".textmate_init"] - Bourne Again Shell (bash) -> ["sh", "bash", "zsh", "fish", ".bash_aliases", ".bash_completions", ".bash_functions", ".bash_login", ".bash_logout", ".bash_profile", ".bash_variables", ".bashrc", ".profile", ".textmate_init", ".zshrc"]
- HTML (Tcl) -> ["adp"] - HTML (Tcl) -> ["adp"]
- Tcl -> ["tcl"] - Tcl -> ["tcl"]
- Textile -> ["textile"] - Textile -> ["textile"]
- XML -> ["xml", "xsd", "xslt", "tld", "dtml", "rss", "opml", "svg"] - XML -> ["xml", "xsd", "xslt", "tld", "dtml", "rss", "opml", "svg"]
- YAML -> ["yaml", "yml", "sublime-syntax"] - YAML -> ["yaml", "yml", "sublime-syntax"]
- SWI-Prolog -> ["pro"] - SWI-Prolog -> ["pro"]
- CMake C Header -> ["h.in"]
- CMake C++ Header -> ["hh.in", "hpp.in", "hxx.in", "h++.in"]
- CMake -> ["CMakeLists.txt", "cmake"]
- CMakeCache -> ["CMakeCache.txt"]
- Generic Config -> ["cfg", "conf", "config", "ini", "pro", "mak", "mk", "Doxyfile", "inputrc", ".inputrc", "dircolors", ".dircolors", "gitmodules", ".gitmodules", "gitignore", ".gitignore", "gitattributes", ".gitattributes"] - Generic Config -> ["cfg", "conf", "config", "ini", "pro", "mak", "mk", "Doxyfile", "inputrc", ".inputrc", "dircolors", ".dircolors", "gitmodules", ".gitmodules", "gitignore", ".gitignore", "gitattributes", ".gitattributes"]
- Elm -> ["elm"]
- Linker Script -> ["ld"] - Linker Script -> ["ld"]
- TOML -> ["toml", "tml"] - TOML -> ["toml", "tml"]
- TypeScript -> ["ts"] - TypeScript -> ["ts"]
- TypeScriptReact -> ["tsx"] - TypeScriptReact -> ["tsx"]
- VimL -> ["vim"] - VimL -> ["vim"]
- TOML -> ["toml", "tml", "Cargo.lock", "Gopkg.lock"]
``` ```
If you want to highlight a language not on that list, please open an issue or a pull request on the [Gutenberg repo](https://github.com/Keats/gutenberg). If you want to highlight a language not on that list, please open an issue or a pull request on the [Gutenberg repo](https://github.com/Keats/gutenberg).
Alternatively, the `extra_syntaxes` config option can be used to add additional syntax files.
If your site source is laid out as follows:
```
.
├── config.toml
├── content/
│   └── ...
├── static/
│   └── ...
├── syntaxes/
│   ├── Sublime-Language1/
│   │   └── lang1.sublime-syntax
│   └── lang2.sublime-syntax
└── templates/
└── ...
```
you would set your `extra_syntaxes` to `["syntaxes", "syntaxes/Sublime-Language1"]` in order to load `lang1.sublime-syntax` and `lang2.sublime-syntax`.

View file

@ -70,6 +70,9 @@ check_external_links = false
# ignored_content = ["*.{graphml,xlsx}", "temp.*"] # ignored_content = ["*.{graphml,xlsx}", "temp.*"]
ignored_content = [] ignored_content = []
# A list of directories to search for additional `.sublime-syntax` files in.
extra_syntaxes = []
# Optional translation object. The key if present should be a language code # Optional translation object. The key if present should be a language code
[translations] [translations]

View file

@ -77,6 +77,8 @@ word_count: Number;
reading_time: Number; reading_time: Number;
// See the Table of contents section below for more details // See the Table of contents section below for more details
toc: Array<Header>; toc: Array<Header>;
// Paths of colocated assets, relative to the content directory
assets: Array<String>;
``` ```
## Table of contents ## Table of contents

@ -1 +1 @@
Subproject commit 6f0da0ace6d881648dbfb2c3c1a3799ff7d5c54a Subproject commit d3ddfe7b51e01140db209f57af3bc47d04d9ac0a

View file

@ -0,0 +1,65 @@
%YAML 1.2
---
# http://www.sublimetext.com/docs/3/syntax.html
name: MiniZinc (MZN)
file_extensions:
- mzn
- dzn
scope: source.mzn
contexts:
main:
- match: \%.*
scope: comment.line.percentage.mzn
- match: /\*
push:
- meta_scope: comment.block.mzn
- match: \*/
pop: true
- match: \'.*?\'
scope: string.quoted.single.mzn
- match: \".*?\"
scope: string.quoted.double.mzn
- match: \b(ann|annotation|any|constraint|function|in|include|list|of|op|output|minimize|maximize|par|predicate|record|satisfy|solve|test|type|var)\b
scope: keyword.control.mzn
- match: \b(array|set|bool|enum|float|int|string|tuple)\b
scope: storage.type.mzn
- match: \b(for|forall|if|then|else|endif|where)\b
scope: keyword.control.mzn
- match: \b(abort|abs|acosh|array_intersect|array_union|array1d|array2d|array3d|array4d|array5d|array6d|asin|assert|atan|bool2int|card|ceil|concat|cos|cosh|dom|dom_array|dom_size|fix|exp|floor|index_set|index_set_1of2|index_set_2of2|index_set_1of3|index_set_2of3|index_set_3of3|int2float|is_fixed|join|lb|lb_array|length|ln|log|log2|log10|min|max|pow|product|round|set2array|show|show_int|show_float|sin|sinh|sqrt|sum|tan|tanh|trace|ub|ub_array)\b
scope: entity.name.function.mzn
- match: \b(circuit|disjoint|maximum|maximum_arg|member|minimum|minimum_arg|network_flow|network_flow_cost|partition_set|range|roots|sliding_sum|subcircuit|sum_pred)\b
scope: support.function.mzn
- match: \b(alldifferent|all_different|all_disjoint|all_equal|alldifferent_except_0|nvalue|symmetric_all_different)\b
scope: support.function.mzn
- match: \b(lex2|lex_greater|lex_greatereq|lex_less|lex_lesseq|strict_lex2|value_precede|value_precede_chain)\b
scope: support.function.mzn
- match: \b(arg_sort|decreasing|increasing|sort)\b
scope: support.function.mzn
- match: \b(int_set_channel|inverse|inverse_set|link_set_to_booleans)\b
scope: support.function.mzn
- match: \b(among|at_least|at_most|at_most1|count|count_eq|count_geq|count_gt|count_leq|count_lt|count_neq|distribute|exactly|global_cardinality|global_cardinality_closed|global_cardinality_low_up|global_cardinality_low_up_closed)\b
scope: support.function.mzn
- match: \b(bin_packing|bin_packing_capa|bin_packing_load|diffn|diffn_k|diffn_nonstrict|diffn_nonstrict_k|geost|geost_bb|geost_smallest_bb|knapsack)\b
scope: support.function.mzn
- match: \b(alternative|cumulative|disjunctive|disjunctive_strict|span)\b
scope: support.function.mzn
- match: \b(regular|regular_nfa|table)\b
scope: support.function.mzn
- match: \b(not|\+|-)\b
scope: keyword.operator.math.mzn
- match: \b(<->|->|<-|\\/|xor|/\\)\b
scope: keyword.operator.logical.mzn
- match: \b(<|>|<=|>=|==|=|!=)\b
scope: keyword.operator.math.mzn
- match: \b(\+|-|\*|/|div|mod)\b
scope: keyword.operator.math.mzn
- match: \b(in|subset|superset|union|diff|symdiff|intersect)\b
scope: keyword.operator.sets.mzn
- match: \|\.\.|\+\+
scope: keyword.operator.math.mzn
- match: \b(true|false)\b
scope: constant.language.mzn
- match: '\b([_A-Za-z])(\w*)\b'
scope: variable.other.mzn
- match: '([+-]?)\d+(\.[^\.]\d*)?([eE][-+]?\d+)?'
scope: constant.numeric.mzn

@ -1 +1 @@
Subproject commit 1cb4c3ec368c751d6f7ecfa16fe02ceff23a1f6b Subproject commit 289782ff2e4cb58de171579c7fc86fe00d280619

@ -1 +1 @@
Subproject commit ff9a800a4ca942edd095de553ca05fba03b02275 Subproject commit aba96a0862369e9f960bb63a38e2d7563ea6475e

@ -1 +1 @@
Subproject commit a9055b118c991601c3a0876730c8918beb422c84 Subproject commit 3c90f249ee6e4daa1d25a2dd9cda53071e42a076

Binary file not shown.

Binary file not shown.

@ -1 +1 @@
Subproject commit 1deb0745d7cfd069bdd5652878e321019b1ed229 Subproject commit 36f8239551f09ed354a6872a6cc2fd0168883446

View file

@ -10,5 +10,7 @@ taxonomies = [
{name = "categories", rss = true}, {name = "categories", rss = true},
] ]
extra_syntaxes = ["syntaxes"]
[extra.author] [extra.author]
name = "Vincent Prouillet" name = "Vincent Prouillet"

View file

@ -3,4 +3,5 @@ title = "Posts"
paginate_by = 2 paginate_by = 2
template = "section_paginated.html" template = "section_paginated.html"
insert_anchor_links = "left" insert_anchor_links = "left"
sort_by = "date"
+++ +++

View file

@ -0,0 +1,9 @@
+++
title = "Extra Syntax"
description = ""
date = 2018-08-14
+++
```test-syntax
This is a test code snippet.
```

View file

@ -1,6 +1,7 @@
+++ +++
title = "With assets" title = "With assets"
description = "hey there" description = "hey there"
date = 2015-03-01
+++ +++
Hello world Hello world

View file

@ -0,0 +1,10 @@
%YAML 1.2
---
file_extensions:
- test-syntax
scope: source.test
contexts:
main:
- match: "test"
scope: constant.language.test

View file

@ -3,6 +3,6 @@
{% block content %} {% block content %}
{{ page.content | safe }} {{ page.content | safe }}
{% if page.previous %}Previous article: {{ page.previous.permalink }}{% endif %} {% if page.earlier %}Previous article: {{ page.earlier.permalink }}{% endif %}
{% if page.next %}Next article: {{ page.next.permalink }}{% endif %} {% if page.later %}Next article: {{ page.later.permalink }}{% endif %}
{% endblock content %} {% endblock content %}

View file

@ -1,3 +1,3 @@
{% for tag in tags %} {% for tag in terms %}
{{ tag.name }} {{ tag.slug }} {{ tag.pages | length }} {{ tag.name }} {{ tag.slug }} {{ tag.pages | length }}
{% endfor %} {% endfor %}

View file

@ -1,6 +1,6 @@
Tag: {{ tag.name }} Tag: {{ term.name }}
{% for page in tag.pages %} {% for page in term.pages %}
<article> <article>
<h3 class="post__title"><a href="{{ page.permalink }}">{{ page.title }}</a></h3> <h3 class="post__title"><a href="{{ page.permalink }}">{{ page.title }}</a></h3>
</article> </article>