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"]
path = sublime_syntaxes/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
## 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)
- 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]
name = "gutenberg"
version = "0.4.1"
version = "0.4.2"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
license = "MIT"
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:
```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

View file

@ -3,23 +3,22 @@ extern crate serde_derive;
extern crate toml;
#[macro_use]
extern crate errors;
extern crate highlighting;
extern crate chrono;
extern crate globset;
extern crate highlighting;
use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use toml::Value as Toml;
use chrono::Utc;
use globset::{Glob, GlobSet, GlobSetBuilder};
use toml::Value as Toml;
use errors::{Result, ResultExt};
use highlighting::THEME_SET;
mod theme;
use theme::Theme;
@ -27,7 +26,6 @@ use theme::Theme;
// We want a default base url for tests
static DEFAULT_BASE_URL: &'static str = "http://a-website.com";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Taxonomy {
@ -101,12 +99,15 @@ pub struct Config {
/// Had to remove the PartialEq derive because GlobSet does not implement it. No impact
/// because it's unused anyway (who wants to sort Configs?).
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>,
/// Whether to check all external links for validity
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
pub extra: HashMap<String, Toml>,
@ -114,14 +115,13 @@ pub struct Config {
pub build_timestamp: Option<i64>,
}
impl Config {
/// Parses a string containing TOML to our Config struct
/// Any extra parameter will end up in the extra field
pub fn parse(content: &str) -> Result<Config> {
let mut config: Config = match toml::from_str(content) {
Ok(c) => c,
Err(e) => bail!(e)
Err(e) => bail!(e),
};
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());
if !config.ignored_content.is_empty() {
// 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
@ -145,11 +144,19 @@ impl Config {
for pat in &config.ignored_content {
let glob = match Glob::new(pat) {
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);
}
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)
@ -161,7 +168,12 @@ impl Config {
let path = path.as_ref();
let file_name = path.file_name().unwrap();
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)?;
Config::parse(&content)
@ -169,7 +181,11 @@ impl Config {
/// Makes a url, taking into account that the base url might have a trailing slash
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
if self.base_url.ends_with('/') && path == "/" {
@ -195,12 +211,16 @@ impl Config {
let original = self.extra.clone();
// 2. inject theme extra values
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
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(())
@ -233,13 +253,13 @@ impl Default for Config {
ignored_content: Vec::new(),
ignored_content_globset: None,
translations: HashMap::new(),
extra_syntaxes: Vec::new(),
extra: HashMap::new(),
build_timestamp: Some(1),
}
}
}
/// Get and parse the config.
/// If it doesn't succeed, exit
pub fn get_config(path: &Path, filename: &str) -> Config {
@ -253,7 +273,6 @@ pub fn get_config(path: &Path, filename: &str) -> Config {
}
}
#[cfg(test)]
mod tests {
use super::{Config, Theme};
@ -303,7 +322,16 @@ hello = "world"
let config = Config::parse(config);
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]
@ -313,7 +341,6 @@ hello = "world"
assert_eq!(config.make_permalink(""), "http://vincent.is/");
}
#[test]
fn can_make_url_index_page_with_railing_slash_url() {
let mut config = Config::default();
@ -339,7 +366,10 @@ hello = "world"
fn can_make_url_with_localhost() {
let mut config = Config::default();
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]

View file

@ -7,6 +7,7 @@ extern crate front_matter;
extern crate config;
use std::collections::HashMap;
use std::path::Path;
use config::Config;
use tera::Tera;
@ -14,19 +15,16 @@ use front_matter::{SortBy, InsertAnchor};
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 config = Config::default();
let tera = Tera::default();
let mut tera = Tera::default();
tera.add_raw_template("shortcodes/youtube.html", "hello");
let permalinks = HashMap::new();
for i in 0..number {
let mut page = Page::default();
match sort_by {
SortBy::Weight => { page.meta.weight = Some(i); }
SortBy::Order => { page.meta.order = Some(i); }
_ => (),
};
page.meta.weight = Some(i);
page.raw_content = r#"
# Modus cognitius profanam ne duae virtutis mundi
@ -98,7 +96,7 @@ if __name__ == "__main__":
gen_site("basic-blog", [""], 250, paginate=True)
```
"#.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);
}
@ -111,34 +109,34 @@ if __name__ == "__main__":
#[bench]
fn bench_baseline_cloning(b: &mut test::Bencher) {
let pages = create_pages(250, SortBy::Order);
let pages = create_pages(250);
b.iter(|| pages.clone());
}
#[bench]
fn bench_sorting_none(b: &mut test::Bencher) {
let pages = create_pages(250, SortBy::Order);
b.iter(|| sort_pages(pages.clone(), SortBy::None));
let pages = create_pages(250);
b.iter(|| sort_pages(pages.clone(), SortBy::Weight));
}
#[bench]
fn bench_sorting_order(b: &mut test::Bencher) {
let pages = create_pages(250, SortBy::Order);
b.iter(|| sort_pages(pages.clone(), SortBy::Order));
let pages = create_pages(250);
b.iter(|| sort_pages(pages.clone(), SortBy::Weight));
}
#[bench]
fn bench_populate_siblings(b: &mut test::Bencher) {
let pages = create_pages(250, SortBy::Order);
let (sorted_pages, _) = sort_pages(pages, SortBy::Order);
b.iter(|| populate_siblings(&sorted_pages.clone()));
let pages = create_pages(250);
let (sorted_pages, _) = sort_pages(pages, SortBy::Weight);
b.iter(|| populate_siblings(&sorted_pages.clone(), SortBy::Weight));
}
#[bench]
fn bench_page_render_html(b: &mut test::Bencher) {
let pages = create_pages(10, SortBy::Order);
let (mut sorted_pages, _) = sort_pages(pages, SortBy::Order);
sorted_pages = populate_siblings(&sorted_pages);
let pages = create_pages(10);
let (mut sorted_pages, _) = sort_pages(pages, SortBy::Weight);
sorted_pages = populate_siblings(&sorted_pages, SortBy::Weight);
let config = Config::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
/// 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(
tera,
config,
&self.permalink,
permalinks,
base_path,
anchor_insert,
);
context.tera_context.add("page", self);
let res = render_content(
&self.raw_content.replacen("<!-- more -->", "<a name=\"continue-reading\"></a>", 1),
&context,
).chain_err(|| format!("Failed to render content of {}", self.file.path.display()))?;
self.content = res.0;
self.toc = res.1;
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
})
}
let res = render_content(&self.raw_content, &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.body;
self.toc = res.toc;
Ok(())
}
@ -310,7 +311,13 @@ Hello world"#;
let res = Page::parse(Path::new("post.md"), content, &Config::default());
assert!(res.is_ok());
let mut page = res.unwrap();
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default(), 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.slug.unwrap(), "hello-world".to_string());
@ -416,7 +423,13 @@ Hello world
let res = Page::parse(Path::new("hello.md"), &content, &config);
assert!(res.is_ok());
let mut page = res.unwrap();
page.render_markdown(&HashMap::default(), &Tera::default(), &config, 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()));
}

View file

@ -8,7 +8,7 @@ use serde::ser::{SerializeStruct, self};
use config::Config;
use front_matter::{SectionFrontMatter, split_section_content};
use errors::{Result, ResultExt};
use utils::fs::read_file;
use utils::fs::{read_file, find_related_assets};
use utils::templates::render_template;
use utils::site::get_reading_analytics;
use rendering::{RenderContext, Header, render_content};
@ -33,6 +33,8 @@ pub struct Section {
pub raw_content: String,
/// The HTML rendered of the page
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
pub pages: Vec<Page>,
/// All pages that cannot be sorted in this section
@ -54,6 +56,7 @@ impl Section {
components: vec![],
permalink: "".to_string(),
raw_content: "".to_string(),
assets: vec![],
content: "".to_string(),
pages: vec![],
ignored_pages: vec![],
@ -79,8 +82,31 @@ impl Section {
pub fn from_file<P: AsRef<Path>>(path: P, config: &Config) -> Result<Section> {
let path = path.as_ref();
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 {
@ -97,12 +123,13 @@ impl Section {
/// We need access to all pages url to render links relative to content
/// so that can't happen at the same time as parsing
pub fn render_markdown(&mut self, permalinks: &HashMap<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(
tera,
config,
&self.permalink,
permalinks,
base_path,
self.meta.insert_anchor_links,
);
@ -110,8 +137,8 @@ impl Section {
let res = render_content(&self.raw_content, &context)
.chain_err(|| format!("Failed to render content of {}", self.file.path.display()))?;
self.content = res.0;
self.toc = res.1;
self.content = res.body;
self.toc = res.toc;
Ok(())
}
@ -146,6 +173,15 @@ impl Section {
pub fn is_child_page(&self, path: &PathBuf) -> bool {
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 {
@ -165,6 +201,8 @@ impl ser::Serialize for Section {
state.serialize_field("word_count", &word_count)?;
state.serialize_field("reading_time", &reading_time)?;
state.serialize_field("toc", &self.toc)?;
let assets = self.serialize_assets();
state.serialize_field("assets", &assets)?;
state.end()
}
}
@ -179,6 +217,7 @@ impl Default for Section {
components: vec![],
permalink: "".to_string(),
raw_content: "".to_string(),
assets: vec![],
content: "".to_string(),
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
fn main() {
let mut args = env::args().skip(1);
match (args.next(), args.next(), args.next(), args.next()) {
(Some(ref cmd), Some(ref package_dir), Some(ref packpath_newlines), Some(ref packpath_nonewlines)) if cmd == "synpack" => {
match (args.next(), args.next(), args.next()) {
(Some(ref cmd), Some(ref package_dir), Some(ref packpath_newlines)) if cmd == "synpack" => {
let mut ps = SyntaxSet::new();
ps.load_plain_text_syntax();
ps.load_syntaxes(package_dir, true).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() {
if !s.file_extensions.is_empty() {
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();
for path in ts.themes.keys() {
println!("{:?}", path);

View file

@ -2,16 +2,20 @@
extern crate lazy_static;
extern crate syntect;
use std::cell::RefCell;
use std::path::Path;
use syntect::LoadingError;
use syntect::dumps::from_binary;
use syntect::parsing::SyntaxSet;
use syntect::highlighting::{ThemeSet, Theme};
use syntect::easy::HighlightLines;
thread_local! {
pub static SYNTAX_SET: SyntaxSet = {
let mut ss: SyntaxSet = from_binary(include_bytes!("../../../sublime_syntaxes/newlines.packdump"));
ss.link_syntaxes();
ss
/// A pair of the set and whether extras have been added to it.
pub static SYNTAX_SET: RefCell<(SyntaxSet, bool)> = {
let ss: SyntaxSet = from_binary(include_bytes!("../../../sublime_syntaxes/newlines.packdump"));
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 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
.split(' ')
.next()
.and_then(|lang| ss.find_syntax_by_token(lang))
.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]
extern crate lazy_static;
use reqwest::header::{qitem, Accept, Headers};
use reqwest::{mime, StatusCode};
use std::collections::HashMap;
use std::error::Error;
use std::sync::{Arc, RwLock};
use reqwest::StatusCode;
#[derive(Clone, Debug, PartialEq)]
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
let res = match reqwest::get(url) {
Ok(response) => LinkResult { code: Some(response.status()), error: None },
Err(e) => LinkResult { code: None, error: Some(e.description().to_string()) },
let res = match client.get(url).headers(headers).send() {
Ok(response) => LinkResult {
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());
return res;
res
}
#[cfg(test)]
mod tests {
use super::{LINKS, check_url};
use super::{check_url, LINKS};
#[test]
fn can_validate_ok_links() {

View file

@ -7,6 +7,7 @@ extern crate config;
extern crate front_matter;
use std::collections::HashMap;
use std::path::Path;
use tera::Tera;
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();
let permalinks_ctx = HashMap::new();
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());
}
@ -102,7 +103,7 @@ fn bench_render_content_without_highlighting(b: &mut test::Bencher) {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
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());
}
@ -113,7 +114,7 @@ fn bench_render_content_no_shortcode(b: &mut test::Bencher) {
let mut config = Config::default();
config.highlight_code = false;
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());
}
@ -124,7 +125,7 @@ fn bench_render_shortcodes_one_present(b: &mut test::Bencher) {
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap();
let config = Config::default();
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));
}

View file

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

View file

@ -32,7 +32,7 @@ pub use table_of_contents::Header;
pub use shortcode::render_shortcodes;
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
if content.contains("{{") || content.contains("{%") {
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 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 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
// for example an article could have several titles named Example
// 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:")
}
pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(String, Vec<Header>)> {
pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Rendered> {
// the rendered html
let mut html = String::with_capacity(content.len());
// 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 opts = Options::empty();
let mut has_summary = false;
opts.insert(OPTION_ENABLE_TABLES);
opts.insert(OPTION_ENABLE_FOOTNOTES);
@ -68,7 +77,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(Strin
if in_header {
if header_created {
temp_header.push(&text);
return Event::Html(Owned(String::new()));
return Event::Html(Borrowed(""));
}
let id = find_anchor(&anchors, slugify(&text), 0);
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
temp_header.title += &text;
header_created = true;
return Event::Html(Owned(String::new()));
return Event::Html(Borrowed(""));
}
// 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)) => {
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];
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);
Event::Html(Owned(snippet))
}
Event::End(Tag::CodeBlock(_)) => {
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
highlighter = None;
Event::Html(Owned("</pre>".to_string()))
Event::Html(Borrowed("</pre>"))
}
Event::Start(Tag::Image(src, title)) => {
if is_colocated_asset_link(&src) {
@ -133,7 +148,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(Strin
Ok(url) => url,
Err(_) => {
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) {
@ -161,7 +176,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(Strin
format!("<a href=\"{}\" title=\"{}\">", fixed_link, title)
};
temp_header.push(&html);
return Event::Html(Owned(String::new()));
return Event::Html(Borrowed(""));
}
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(_, _)) => {
if in_header {
temp_header.push("</a>");
return Event::Html(Owned(String::new()));
return Event::Html(Borrowed(""));
}
event
}
Event::Start(Tag::Code) => {
if in_header {
temp_header.push("<code>");
return Event::Html(Owned(String::new()));
return Event::Html(Borrowed(""));
}
event
}
Event::End(Tag::Code) => {
if in_header {
temp_header.push("</code>");
return Event::Html(Owned(String::new()));
return Event::Html(Borrowed(""));
}
event
}
Event::Start(Tag::Header(num)) => {
in_header = true;
temp_header = TempHeader::new(num);
Event::Html(Owned(String::new()))
Event::Html(Borrowed(""))
}
Event::End(Tag::Header(_)) => {
// 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();
Event::Html(Owned(val))
}
Event::Html(ref markup) if markup.contains("<!-- more -->") => {
has_summary = true;
Event::Html(Borrowed(CONTINUE_READING))
}
_ => event,
}
});
@ -209,11 +228,14 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<(Strin
cmark::html::push_html(&mut html, parser);
}
match error {
Some(e) => Err(e),
None => Ok((
html.replace("<p></p>", "").replace("</p></p>", "</p>"),
make_table_of_contents(&headers)
)),
if let Some(e) = error {
return Err(e)
} else {
html = html.replace("<p></p>", "").replace("</p></p>", "</p>");
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)]
mod tests {
use std::collections::HashMap;
use std::path::Path;
use tera::Tera;
use config::Config;
use front_matter::InsertAnchor;
@ -202,7 +204,7 @@ mod tests {
fn render_shortcodes(code: &str, tera: &Tera) -> String {
let config = Config::default();
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()
}

View file

@ -5,6 +5,7 @@ extern crate rendering;
extern crate config;
use std::collections::HashMap;
use std::path::Path;
use tera::Tera;
@ -19,9 +20,9 @@ fn can_do_render_content_simple() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
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();
assert_eq!(res.0, "<p>hello</p>\n");
assert_eq!(res.body, "<p>hello</p>\n");
}
#[test]
@ -30,10 +31,10 @@ fn doesnt_highlight_code_block_with_highlighting_off() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
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();
assert_eq!(
res.0,
res.body,
"<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 permalinks_ctx = HashMap::new();
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();
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>"
);
}
@ -56,10 +57,10 @@ fn can_highlight_code_block_with_lang() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
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();
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>"
);
}
@ -69,11 +70,11 @@ fn can_higlight_code_block_with_unknown_lang() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
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();
// defaults to plain text
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>"
);
}
@ -82,21 +83,21 @@ fn can_higlight_code_block_with_unknown_lang() {
fn can_render_shortcode() {
let permalinks_ctx = HashMap::new();
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#"
Hello
{{ youtube(id="ub36ffWAqgQ") }}
"#, &context).unwrap();
assert!(res.0.contains("<p>Hello</p>\n<div >"));
assert!(res.0.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#));
assert!(res.body.contains("<p>Hello</p>\n<div >"));
assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#));
}
#[test]
fn can_render_shortcode_with_markdown_char_in_args_name() {
let permalinks_ctx = HashMap::new();
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![
"name",
"na_me",
@ -105,7 +106,7 @@ fn can_render_shortcode_with_markdown_char_in_args_name() {
];
for i in input {
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() {
let permalinks_ctx = HashMap::new();
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![
"ub36ffWAqgQ-hey",
"ub36ffWAqgQ_hey",
@ -123,7 +124,7 @@ fn can_render_shortcode_with_markdown_char_in_args_value() {
];
for i in input {
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 {
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();
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();
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();
println!("{:?}", res);
assert_eq!(res.0, expected);
assert_eq!(res.body, expected);
}
#[test]
@ -202,18 +203,18 @@ Here is another paragraph.
tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap();
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();
println!("{:?}", res);
assert_eq!(res.0, expected);
assert_eq!(res.body, expected);
}
#[test]
fn can_render_several_shortcode_in_row() {
let permalinks_ctx = HashMap::new();
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#"
Hello
@ -228,11 +229,11 @@ Hello
{{ gist(url="https://gist.github.com/Keats/32d26f699dcc13ebd41b") }}
"#, &context).unwrap();
assert!(res.0.contains("<p>Hello</p>\n<div >"));
assert!(res.0.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.0.contains(r#"<iframe src="https://www.streamable.com/e/c0ic""#));
assert!(res.0.contains(r#"//player.vimeo.com/video/210073083""#));
assert!(res.body.contains("<p>Hello</p>\n<div >"));
assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#));
assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ?autoplay=1""#));
assert!(res.body.contains(r#"<iframe src="https://www.streamable.com/e/c0ic""#));
assert!(res.body.contains(r#"//player.vimeo.com/video/210073083""#));
}
#[test]
@ -240,9 +241,9 @@ fn doesnt_render_ignored_shortcodes() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
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();
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]
@ -252,7 +253,7 @@ fn can_render_shortcode_with_body() {
tera.add_raw_template("shortcodes/quote.html", "<blockquote>{{ body }} - {{ author }}</blockquote>").unwrap();
let permalinks_ctx = HashMap::new();
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#"
Hello
@ -260,7 +261,7 @@ Hello
A quote
{% end %}
"#, &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]
@ -268,7 +269,7 @@ fn errors_rendering_unknown_shortcode() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
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);
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());
let tera_ctx = Tera::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), [abs link](https://vincent.is/about)"#,
&context
&context,
).unwrap();
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());
let tera_ctx = Tera::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();
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 permalinks_ctx = HashMap::new();
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);
assert!(res.is_err());
}
@ -319,9 +320,9 @@ fn can_add_id_to_headers() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
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();
assert_eq!(res.0, "<h1 id=\"hello\">Hello</h1>\n");
assert_eq!(res.body, "<h1 id=\"hello\">Hello</h1>\n");
}
#[test]
@ -329,19 +330,19 @@ fn can_add_id_to_headers_same_slug() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
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();
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]
fn can_insert_anchor_left() {
let permalinks_ctx = HashMap::new();
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();
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"
);
}
@ -350,10 +351,10 @@ fn can_insert_anchor_left() {
fn can_insert_anchor_right() {
let permalinks_ctx = HashMap::new();
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();
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"
);
}
@ -363,10 +364,10 @@ fn can_insert_anchor_right() {
fn can_insert_anchor_with_exclamation_mark() {
let permalinks_ctx = HashMap::new();
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();
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"
);
}
@ -376,10 +377,10 @@ fn can_insert_anchor_with_exclamation_mark() {
fn can_insert_anchor_with_link() {
let permalinks_ctx = HashMap::new();
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();
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"
);
}
@ -388,10 +389,10 @@ fn can_insert_anchor_with_link() {
fn can_insert_anchor_with_other_special_chars() {
let permalinks_ctx = HashMap::new();
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();
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"
);
}
@ -405,7 +406,8 @@ fn can_make_toc() {
&config,
"https://mysite.com/something",
&permalinks_ctx,
InsertAnchor::Left
Path::new("something"),
InsertAnchor::Left,
);
let res = render_content(r#"
@ -418,21 +420,20 @@ fn can_make_toc() {
### Last one
"#, &context).unwrap();
let toc = res.1;
let toc = res.toc;
assert_eq!(toc.len(), 1);
assert_eq!(toc[0].children.len(), 2);
assert_eq!(toc[0].children[1].children.len(), 1);
}
#[test]
fn can_understand_backtick_in_titles() {
let permalinks_ctx = HashMap::new();
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();
assert_eq!(
res.0,
res.body,
"<h1 id=\"hello\"><code>Hello</code></h1>\n"
);
}
@ -441,10 +442,10 @@ fn can_understand_backtick_in_titles() {
fn can_understand_backtick_in_paragraphs() {
let permalinks_ctx = HashMap::new();
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();
assert_eq!(
res.0,
res.body,
"<p>Hello <code>world</code></p>\n"
);
}
@ -454,10 +455,10 @@ fn can_understand_backtick_in_paragraphs() {
fn can_understand_links_in_header() {
let permalinks_ctx = HashMap::new();
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();
assert_eq!(
res.0,
res.body,
"<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() {
let permalinks_ctx = HashMap::new();
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();
assert_eq!(
res.0,
res.body,
"<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());
let tera_ctx = Tera::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)"#,
&context
&context,
).unwrap();
assert_eq!(
res.0,
res.body,
"<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() {
let permalinks_ctx = HashMap::new();
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();
assert_eq!(
res.0,
res.body,
"<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() {
let permalinks_ctx = HashMap::new();
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();
assert_eq!(
res.0,
res.body,
"<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() {
let permalinks_ctx = HashMap::new();
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#"
Some text
@ -533,7 +534,7 @@ Some text
</div>
"#, &context).unwrap();
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"
);
}
@ -543,10 +544,10 @@ fn can_validate_valid_external_links() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
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();
assert_eq!(
res.0,
res.body,
"<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 mut config = Config::default();
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);
assert!(res.is_err());
let err = res.unwrap_err();
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"
date = REPLACE_DATE
[taxonomies]
tags = REPLACE_TAG
category = "REPLACE_CATEGORY"
categories = ["REPLACE_CATEGORY"]
+++
# Modus cognitius profanam ne duae virtutis mundi
@ -103,10 +105,13 @@ def gen_skeleton(name, is_blog):
f.write("""
title = "My site"
base_url = "https://replace-this-with-your-url.com"
generate_tags_pages = true
generate_categories_pages = true
theme = "sample"
taxonomies = [
{name = "tags", rss = true},
{name = "categories"}
]
[extra.author]
name = "Vincent Prouillet"
""")
@ -121,8 +126,8 @@ name = "Vincent Prouillet"
""")
# Re-use the test 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/templates", os.path.join(name, "templates"))
shutil.copytree("../../../test_site/themes", os.path.join(name, "themes"))
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("small-blog");
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());
}
@ -46,7 +46,7 @@ fn bench_loading_small_blog_with_syntax_highlighting(b: &mut test::Bencher) {
// path.push("benches");
// path.push("medium-blog");
// 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());
//}
@ -67,7 +67,7 @@ fn bench_loading_small_blog_with_syntax_highlighting(b: &mut test::Bencher) {
// path.push("benches");
// path.push("big-blog");
// 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());
//}
@ -88,7 +88,7 @@ fn bench_loading_small_blog_with_syntax_highlighting(b: &mut test::Bencher) {
// path.push("benches");
// path.push("huge-blog");
// 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());
//}
@ -109,7 +109,7 @@ fn bench_loading_small_kb_with_syntax_highlighting(b: &mut test::Bencher) {
path.push("benches");
path.push("small-kb");
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());
}

View file

@ -1,12 +1,14 @@
#![feature(test)]
extern crate test;
extern crate site;
extern crate pagination;
extern crate tempfile;
use std::env;
use tempfile::tempdir;
use site::Site;
use pagination::Paginator;
fn setup_site(name: &str) -> Site {
@ -20,8 +22,8 @@ fn setup_site(name: &str) -> Site {
#[bench]
fn bench_render_aliases(b: &mut test::Bencher) {
let mut site = setup_site("huge-blog");
let tmp_dir = TempDir::new("benches").expect("create temp dir");
let mut site = setup_site("small-blog");
let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
b.iter(|| site.render_aliases().unwrap());
@ -29,8 +31,8 @@ fn bench_render_aliases(b: &mut test::Bencher) {
#[bench]
fn bench_render_sitemap(b: &mut test::Bencher) {
let mut site = setup_site("huge-blog");
let tmp_dir = TempDir::new("benches").expect("create temp dir");
let mut site = setup_site("small-blog");
let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
b.iter(|| site.render_sitemap().unwrap());
@ -38,29 +40,30 @@ fn bench_render_sitemap(b: &mut test::Bencher) {
#[bench]
fn bench_render_rss_feed(b: &mut test::Bencher) {
let mut site = setup_site("huge-blog");
let tmp_dir = TempDir::new("benches").expect("create temp dir");
let mut site = setup_site("small-blog");
let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
b.iter(|| site.render_rss_feed().unwrap());
b.iter(|| site.render_rss_feed(None, None).unwrap());
}
#[bench]
fn bench_render_categories(b: &mut test::Bencher) {
let mut site = setup_site("huge-blog");
let tmp_dir = TempDir::new("benches").expect("create temp dir");
fn bench_render_taxonomies(b: &mut test::Bencher) {
let mut site = setup_site("small-blog");
let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
b.iter(|| site.render_categories().unwrap());
b.iter(|| site.render_taxonomies().unwrap());
}
#[bench]
fn bench_render_paginated(b: &mut test::Bencher) {
let mut site = setup_site("medium-blog");
let tmp_dir = TempDir::new("benches").expect("create temp dir");
let mut site = setup_site("small-blog");
let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
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
.into_par_iter()
.filter(|entry| entry.as_path().file_name().unwrap() == "_index.md")
.map(|entry| {
let path = entry.as_path();
Section::from_file(path, config)
@ -200,7 +199,6 @@ impl Site {
page_entries
.into_par_iter()
.filter(|entry| entry.as_path().file_name().unwrap() != "_index.md")
.map(|entry| {
let path = entry.as_path();
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
// 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();
if let Some(ref index_section) = self.sections.get(&index_path) {
if self.config.build_search_index && !index_section.meta.in_search_index {
@ -260,6 +258,7 @@ impl Site {
let permalinks = &self.permalinks;
let tera = &self.tera;
let config = &self.config;
let base_path = &self.base_path;
// TODO: avoid the duplication with function above for that part
// This is needed in the first place because of silly borrow checker
@ -271,13 +270,13 @@ impl Site {
self.pages.par_iter_mut()
.map(|(_, page)| {
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)
.reduce(|| Ok(()), Result::and)?;
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)
.reduce(|| Ok(()), Result::and)?;
@ -320,7 +319,7 @@ impl Site {
if render {
let insert_anchor = self.find_parent_section_insert_anchor(&self.pages[&path].file.parent);
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)
@ -337,7 +336,7 @@ impl Site {
if render {
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)
@ -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 {
section
.pages

View file

@ -19,7 +19,7 @@ fn can_parse_site() {
site.load().unwrap();
// 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");
// 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")];
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")];
assert_eq!(tutorials_section.subsections.len(), 2);
@ -321,22 +321,41 @@ fn can_build_site_with_pagination_for_section() {
"posts/page/1/index.html",
"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", "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", "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!(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", "Current index: 2"));
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", "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]
@ -397,10 +416,10 @@ fn can_build_rss_feed() {
assert!(Path::new(&public).exists());
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"));
// 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, "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
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
└── with-assets
├── index.md
└── yavascript.js
└── research
├── latest-experiment
│ ├── 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
[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
`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
[Overview](./documentation/content/overview.md) section of this documentation.
[Overview](./documentation/content/overview.md#assets-colocation) section of this documentation.
## 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; 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
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
+++
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).
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.
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"]
- Crystal -> ["cr"]
- Elixir -> ["ex", "exs"]
- Elm -> ["elm"]
- Handlebars -> ["handlebars", "handlebars.html", "hbr", "hbrs", "hbs", "hdbs", "hjs", "mu", "mustache", "rac", "stache", "template", "tmpl"]
- Jinja2 -> ["j2", "jinja2"]
- Julia -> ["jl"]
- Kotlin -> ["kt", "kts"]
- Less -> ["less", "css.less"]
- MiniZinc (MZN) -> ["mzn", "dzn"]
- Nim -> ["nim", "nims"]
- ASP -> ["asa"]
- 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"]
- Erlang -> ["erl", "hrl", "Emakefile", "emakefile"]
- 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"]
- Graphviz (DOT) -> ["dot", "DOT", "gv"]
- Groovy -> ["groovy", "gvy", "gradle"]
- HTML -> ["html", "htm", "shtml", "xhtml", "inc", "tmpl", "tpl"]
- Groovy -> ["groovy", "gvy", "gradle", "Jenkinsfile"]
- HTML -> ["html", "htm", "shtml", "xhtml"]
- Haskell -> ["hs"]
- Literate Haskell -> ["lhs"]
- 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"]
- Lisp -> ["lisp", "cl", "clisp", "l", "mud", "el", "scm", "ss", "lsp", "fasl"]
- 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"]
- MATLAB -> ["matlab"]
- 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"]
- SQL -> ["sql", "ddl", "dml"]
- 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"]
- Tcl -> ["tcl"]
- Textile -> ["textile"]
- XML -> ["xml", "xsd", "xslt", "tld", "dtml", "rss", "opml", "svg"]
- YAML -> ["yaml", "yml", "sublime-syntax"]
- 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"]
- Elm -> ["elm"]
- Linker Script -> ["ld"]
- TOML -> ["toml", "tml"]
- TypeScript -> ["ts"]
- TypeScriptReact -> ["tsx"]
- 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).
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 = []
# 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
[translations]

View file

@ -77,6 +77,8 @@ word_count: Number;
reading_time: Number;
// See the Table of contents section below for more details
toc: Array<Header>;
// Paths of colocated assets, relative to the content directory
assets: Array<String>;
```
## 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},
]
extra_syntaxes = ["syntaxes"]
[extra.author]
name = "Vincent Prouillet"

View file

@ -3,4 +3,5 @@ title = "Posts"
paginate_by = 2
template = "section_paginated.html"
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"
description = "hey there"
date = 2015-03-01
+++
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 %}
{{ page.content | safe }}
{% if page.previous %}Previous article: {{ page.previous.permalink }}{% endif %}
{% if page.next %}Next article: {{ page.next.permalink }}{% endif %}
{% if page.earlier %}Previous article: {{ page.earlier.permalink }}{% endif %}
{% if page.later %}Next article: {{ page.later.permalink }}{% endif %}
{% endblock content %}

View file

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

View file

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