Merge pull request #4 from Keats/0.0.1

0.0.1
This commit is contained in:
Vincent Prouillet 2017-03-20 13:11:47 +09:00 committed by GitHub
commit 1b237dc466
61 changed files with 15045 additions and 568 deletions

3
.gitignore vendored
View file

@ -1,4 +1,3 @@
target target
.idea/ .idea/
site test_site/public
theme

10
.travis.yml Normal file
View file

@ -0,0 +1,10 @@
language: rust
cache: cargo
rust:
- nightly
- beta
- stable
notifications:
email: false

685
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.1.0" version = "0.0.1"
authors = ["Vincent Prouillet <vincent@wearewizards.io>"] authors = ["Vincent Prouillet <vincent@wearewizards.io>"]
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"
@ -9,8 +9,11 @@ homepage = "https://github.com/Keats/gutenberg"
repository = "https://github.com/Keats/gutenberg" repository = "https://github.com/Keats/gutenberg"
keywords = ["static", "site", "generator", "blog"] keywords = ["static", "site", "generator", "blog"]
[[bin]]
name = "gutenberg"
[dependencies] [dependencies]
error-chain = "0.9" error-chain = "0.10"
clap = "2.19" clap = "2.19"
walkdir = "1" walkdir = "1"
pulldown-cmark = "0" pulldown-cmark = "0"
@ -20,12 +23,20 @@ glob = "0.2"
serde = "0.9" serde = "0.9"
serde_json = "0.9" serde_json = "0.9"
serde_derive = "0.9" serde_derive = "0.9"
tera = { git = "https://github.com/Keats/tera", branch = "next" } # tera = { path = "../tera" }
# tera = { git = "https://github.com/Keats/tera", branch = "reload" }
tera = "0.8"
slug = "0.1"
syntect = "1" syntect = "1"
chrono = "0.3"
toml = { version = "0.3", default-features = false, features = ["serde"]}
[dependencies.toml] # Below is for the serve cmd
version = "0.3" staticfile = "0.4"
default-features = false iron = "0.5"
features = ["serde"] mount = "0.3"
notify = "4"
ws = "0.6"
[dev-dependencies]
tempdir = "0.3"

View file

@ -32,8 +32,12 @@ Split the file between front matter and content
Parse the front matter Parse the front matter
markdown -> HTML for the content markdown -> HTML for the content
### Themes
Gallery at https://tmtheme-editor.herokuapp.com/#!/editor/theme/Agola%20Dark
# TODO: # TODO:
- find a way to add tests
- syntax highlighting - syntax highlighting
- pass a --config arg to the CLI to change from `config.toml` - pass a --config arg to the CLI to change from `config.toml`
- have verbosity levels with a `verbosity` config variable with a default - have verbosity levels with a `verbosity` config variable with a default

View file

@ -1,87 +1,9 @@
use std::collections::HashMap; use std::env;
use std::fs::{create_dir, remove_dir_all};
use std::path::Path;
use glob::glob; use gutenberg::errors::Result;
use tera::{Tera, Context}; use gutenberg::Site;
use config:: Config;
use errors::{Result, ResultExt};
use page::{Page, order_pages};
use utils::create_file;
pub fn build() -> Result<()> {
pub fn build(config: Config) -> Result<()> { Site::new(env::current_dir().unwrap())?.build()
if Path::new("public").exists() {
// Delete current `public` directory so we can start fresh
remove_dir_all("public").chain_err(|| "Couldn't delete `public` directory")?;
}
let tera = Tera::new("templates/**/*").chain_err(|| "Error parsing templates")?;
// ok we got all the pages HTML, time to write them down to disk
create_dir("public")?;
let public = Path::new("public");
let mut pages: Vec<Page> = vec![];
let mut sections: HashMap<String, Vec<Page>> = HashMap::new();
// First step: do all the articles and group article by sections
// hardcoded pattern so can't error
for entry in glob("content/**/*.md").unwrap().filter_map(|e| e.ok()) {
let path = entry.as_path();
let mut page = Page::from_file(&path)?;
let mut current_path = public.to_path_buf();
for section in &page.sections {
current_path.push(section);
if !current_path.exists() {
create_dir(&current_path)?;
}
let str_path = current_path.as_path().to_string_lossy().to_string();
sections.entry(str_path).or_insert_with(|| vec![page.clone()]);
}
if let Some(ref url) = page.meta.url {
println!("URL: {:?}", url);
current_path.push(url);
} else {
println!("REMOVE ME IF YOU DONT SEE ME");
current_path.push(&page.get_slug());
}
create_dir(&current_path)?;
create_file(current_path.join("index.html"), &page.render_html(&tera, &config)?)?;
pages.push(page);
}
for (section, pages) in sections {
render_section_index(section, pages, &tera, &config)?;
}
// and now the index page
let mut context = Context::new();
context.add("pages", &order_pages(pages));
context.add("config", &config);
create_file(public.join("index.html"), &tera.render("index.html", &context)?)?;
Ok(())
}
fn render_section_index(section: String, pages: Vec<Page>, tera: &Tera, config: &Config) -> Result<()> {
let path = Path::new(&section);
let mut context = Context::new();
context.add("pages", &order_pages(pages));
context.add("config", &config);
let section_name = match path.components().into_iter().last() {
Some(s) => s.as_ref().to_string_lossy().to_string(),
None => bail!("Couldn't find a section name in {:?}", path.display())
};
create_file(path.join("index.html"), &tera.render(&format!("{}.html", section_name), &context)?)
} }

View file

@ -2,8 +2,8 @@
use std::fs::{create_dir}; use std::fs::{create_dir};
use std::path::Path; use std::path::Path;
use errors::Result; use gutenberg::errors::Result;
use utils::create_file; use gutenberg::create_file;
const CONFIG: &'static str = r#" const CONFIG: &'static str = r#"

1175
src/cmd/livereload.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,7 @@
mod init; mod init;
mod build; mod build;
mod serve;
pub use self::init::create_new_project; pub use self::init::create_new_project;
pub use self::build::build; pub use self::build::build;
pub use self::serve::serve;

245
src/cmd/serve.rs Normal file
View file

@ -0,0 +1,245 @@
use std::env;
use std::path::Path;
use std::sync::mpsc::channel;
use std::time::{Instant, Duration};
use std::thread;
use chrono::prelude::*;
use iron::{Iron, Request, IronResult, Response, status};
use mount::Mount;
use staticfile::Static;
use notify::{Watcher, RecursiveMode, watcher};
use ws::{WebSocket, Sender};
use gutenberg::Site;
use gutenberg::errors::{Result};
use ::report_elapsed_time;
#[derive(Debug, PartialEq)]
enum ChangeKind {
Content,
Templates,
StaticFiles,
}
const LIVE_RELOAD: &'static str = include_str!("livereload.js");
fn livereload_handler(_: &mut Request) -> IronResult<Response> {
Ok(Response::with((status::Ok, LIVE_RELOAD.to_string())))
}
fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &str) {
match res {
Ok(_) => {
broadcaster.send(format!(r#"
{{
"command": "reload",
"path": "{}",
"originalPath": "",
"liveCSS": true,
"liveImg": true,
"protocol": ["http://livereload.com/protocols/official-7"]
}}"#, reload_path)
).unwrap();
},
Err(e) => {
println!("Failed to build the site");
println!("Error: {}", e);
for e in e.iter().skip(1) {
println!("Reason: {}", e)
}
}
}
}
// Most of it taken from mdbook
pub fn serve(interface: &str, port: &str) -> Result<()> {
println!("Building site...");
let start = Instant::now();
let mut site = Site::new(env::current_dir().unwrap())?;
site.enable_live_reload();
site.build()?;
report_elapsed_time(start);
let address = format!("{}:{}", interface, port);
let ws_address = format!("{}:{}", interface, "1112");
// Start a webserver that serves the `public` directory
let mut mount = Mount::new();
mount.mount("/", Static::new(Path::new("public/")));
mount.mount("/livereload.js", livereload_handler);
// Starts with a _ to not trigger the unused lint
// we need to assign to a variable otherwise it will block
let _iron = Iron::new(mount).http(address.as_str()).unwrap();
println!("Web server is available at http://{}", address);
// The websocket for livereload
let ws_server = WebSocket::new(|_| {
|_| {
Ok(())
}
}).unwrap();
let broadcaster = ws_server.broadcaster();
thread::spawn(move || {
ws_server.listen(&*ws_address).unwrap();
});
// And finally watching/reacting on file changes
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap();
watcher.watch("content/", RecursiveMode::Recursive).unwrap();
watcher.watch("static/", RecursiveMode::Recursive).unwrap();
watcher.watch("templates/", RecursiveMode::Recursive).unwrap();
let pwd = format!("{}", env::current_dir().unwrap().display());
println!("Listening for changes in {}/{{content, static, templates}}", pwd);
println!("Press CTRL+C to stop\n");
use notify::DebouncedEvent::*;
loop {
// See https://github.com/spf13/hugo/blob/master/commands/hugo.go
// for a more complete version of that
match rx.recv() {
Ok(event) => {
match event {
Create(path) |
Write(path) |
Remove(path) |
Rename(_, path) => {
if is_temp_file(&path) {
continue;
}
println!("Change detected @ {}", Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
let start = Instant::now();
match detect_change_kind(&pwd, &path) {
(ChangeKind::Content, _) => {
println!("-> Content changed {}", path.display());
// Force refresh
rebuild_done_handling(&broadcaster, site.rebuild_after_content_change(), "/x.js");
},
(ChangeKind::Templates, _) => {
println!("-> Template changed {}", path.display());
// Force refresh
rebuild_done_handling(&broadcaster, site.rebuild_after_template_change(), "/x.js");
},
(ChangeKind::StaticFiles, p) => {
println!("-> Static file changes detected {}", path.display());
rebuild_done_handling(&broadcaster, site.copy_static_directory(), &p);
},
};
report_elapsed_time(start);
}
_ => {}
}
},
Err(e) => println!("Watch error: {:?}", e),
};
}
}
/// Returns whether the path we received corresponds to a temp file create
/// by an editor
fn is_temp_file(path: &Path) -> bool {
let ext = path.extension();
match ext {
Some(ex) => match ex.to_str().unwrap() {
"swp" | "swx" | "tmp" | ".DS_STORE" => true,
// jetbrains IDE
x if x.ends_with("jb_old___") => true,
x if x.ends_with("jb_tmp___") => true,
x if x.ends_with("jb_bak___") => true,
// vim
x if x.ends_with('~') => true,
_ => {
if let Some(filename) = path.file_stem() {
// emacs
filename.to_str().unwrap().starts_with('#')
} else {
false
}
}
},
None => {
path.ends_with(".DS_STORE")
},
}
}
/// Detect what changed from the given path so we have an idea what needs
/// to be reloaded
fn detect_change_kind(pwd: &str, path: &Path) -> (ChangeKind, String) {
let path_str = format!("{}", path.display())
.replace(pwd, "")
.replace("\\", "/");
let change_kind = if path_str.starts_with("/templates") {
ChangeKind::Templates
} else if path_str.starts_with("/content") {
ChangeKind::Content
} else if path_str.starts_with("/static") {
ChangeKind::StaticFiles
} else {
panic!("Got a change in an unexpected path: {}", path_str);
};
(change_kind, path_str)
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::{is_temp_file, detect_change_kind, ChangeKind};
#[test]
fn test_can_recognize_temp_files() {
let testcases = vec![
Path::new("hello.swp"),
Path::new("hello.swx"),
Path::new(".DS_STORE"),
Path::new("hello.tmp"),
Path::new("hello.html.__jb_old___"),
Path::new("hello.html.__jb_tmp___"),
Path::new("hello.html.__jb_bak___"),
Path::new("hello.html~"),
Path::new("#hello.html"),
];
for t in testcases {
println!("{:?}", t.display());
assert!(is_temp_file(&t));
}
}
#[test]
fn test_can_detect_kind_of_changes() {
let testcases = vec![
(
(ChangeKind::Templates, "/templates/hello.html".to_string()),
"/home/vincent/site", Path::new("/home/vincent/site/templates/hello.html")
),
(
(ChangeKind::StaticFiles, "/static/site.css".to_string()),
"/home/vincent/site", Path::new("/home/vincent/site/static/site.css")
),
(
(ChangeKind::Content, "/content/posts/hello.md".to_string()),
"/home/vincent/site", Path::new("/home/vincent/site/content/posts/hello.md")
),
];
for (expected, pwd, path) in testcases {
println!("{:?}", path.display());
assert_eq!(expected, detect_change_kind(&pwd, &path));
}
}
}

View file

@ -7,19 +7,28 @@ use toml::{Value as Toml, self};
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
// TODO: disable tag(s)/category(ies) page generation
// TO ADD:
// highlight code theme
// generate_tags_pages
// generate_categories_pages
#[derive(Debug, PartialEq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Config { pub struct Config {
/// Title of the site /// Title of the site
pub title: String, pub title: String,
/// Base URL of the site /// Base URL of the site
pub base_url: String, pub base_url: String,
/// Whether to highlight all code blocks found in markdown files. Defaults to false
pub highlight_code: Option<bool>,
/// Description of the site /// Description of the site
pub description: Option<String>, pub description: Option<String>,
/// The language used in the site. Defaults to "en" /// The language used in the site. Defaults to "en"
pub language_code: Option<String>, pub language_code: Option<String>,
/// Whether to disable RSS generation, defaults to None (== generate RSS) /// Whether to generate RSS, defaults to false
pub disable_rss: Option<bool>, pub generate_rss: Option<bool>,
/// All user params set in [extra] in the config /// All user params set in [extra] in the config
pub extra: Option<HashMap<String, Toml>>, pub extra: Option<HashMap<String, Toml>>,
} }
@ -32,10 +41,19 @@ impl Config {
Ok(c) => c, Ok(c) => c,
Err(e) => bail!(e) Err(e) => bail!(e)
}; };
if config.language_code.is_none() { if config.language_code.is_none() {
config.language_code = Some("en".to_string()); config.language_code = Some("en".to_string());
} }
if config.highlight_code.is_none() {
config.highlight_code = Some(false);
}
if config.generate_rss.is_none() {
config.generate_rss = Some(false);
}
Ok(config) Ok(config)
} }
@ -48,6 +66,44 @@ impl Config {
Config::parse(&content) Config::parse(&content)
} }
/// Makes a url, taking into account that the base url might have a trailing slash
pub fn make_permalink(&self, path: &str) -> String {
if self.base_url.ends_with('/') {
format!("{}{}", self.base_url, path)
} else {
format!("{}/{}", self.base_url, path)
}
}
}
impl Default for Config {
/// Exists for testing purposes
fn default() -> Config {
Config {
title: "".to_string(),
base_url: "http://a-website.com/".to_string(),
highlight_code: Some(true),
description: None,
language_code: Some("en".to_string()),
generate_rss: Some(false),
extra: None,
}
}
}
/// Get and parse the config.
/// If it doesn't succeed, exit
pub fn get_config(path: &Path) -> Config {
match Config::from_file(path.join("config.toml")) {
Ok(c) => c,
Err(e) => {
println!("Failed to load config.toml");
println!("Error: {}", e);
::std::process::exit(1);
}
}
} }

View file

@ -2,8 +2,7 @@ use tera;
use toml; use toml;
error_chain! { error_chain! {
errors { errors {}
}
links { links {
Tera(tera::Error, tera::ErrorKind); Tera(tera::Error, tera::ErrorKind);

View file

@ -1,21 +1,32 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path;
use toml; use toml;
use tera::Value; use tera::Value;
use chrono::prelude::*;
use regex::Regex;
use errors::{Result}; use errors::{Result, ResultExt};
lazy_static! {
static ref PAGE_RE: Regex = Regex::new(r"^\n?\+\+\+\n((?s).*(?-s))\+\+\+\n?((?s).*(?-s))$").unwrap();
}
/// The front matter of every page /// The front matter of every page
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FrontMatter { pub struct FrontMatter {
// <title> of the page // Mandatory fields
/// <title> of the page
pub title: String, pub title: String,
/// Description that appears when linked, e.g. on twitter /// Description that appears when linked, e.g. on twitter
pub description: String, pub description: String,
// Optional stuff
/// Date if we want to order pages (ie blog post) /// Date if we want to order pages (ie blog post)
pub date: Option<String>, pub date: Option<String>,
/// The page slug. Will be used instead of the filename if present /// The page slug. Will be used instead of the filename if present
@ -31,9 +42,9 @@ pub struct FrontMatter {
pub draft: Option<bool>, pub draft: Option<bool>,
/// Only one category allowed /// Only one category allowed
pub category: Option<String>, pub category: Option<String>,
/// Optional layout, if we want to specify which tpl to render for that page /// Optional template, if we want to specify which template to render for that page
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub layout: Option<String>, pub template: Option<String>,
/// Any extra parameter present in the front matter /// Any extra parameter present in the front matter
pub extra: Option<HashMap<String, Value>>, pub extra: Option<HashMap<String, Value>>,
} }
@ -44,7 +55,7 @@ impl FrontMatter {
bail!("Front matter of file is missing"); bail!("Front matter of file is missing");
} }
let mut f: FrontMatter = match toml::from_str(toml) { let f: FrontMatter = match toml::from_str(toml) {
Ok(d) => d, Ok(d) => d,
Err(e) => bail!(e), Err(e) => bail!(e),
}; };
@ -63,129 +74,40 @@ impl FrontMatter {
Ok(f) Ok(f)
} }
}
/// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime
#[cfg(test)] pub fn parse_date(&self) -> Option<NaiveDateTime> {
mod tests { match self.date {
use super::{FrontMatter}; Some(ref d) => {
use tera::to_value; if d.contains('T') {
DateTime::parse_from_rfc3339(d).ok().and_then(|s| Some(s.naive_local()))
} else {
#[test] NaiveDate::parse_from_str(d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0,0,0)))
fn test_can_parse_a_valid_front_matter() {
let content = r#"
title = "Hello"
description = "hey there""#;
let res = FrontMatter::parse(content);
println!("{:?}", res);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(res.title, "Hello".to_string());
assert_eq!(res.description, "hey there".to_string());
} }
},
#[test] None => None,
fn test_can_parse_tags() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
tags = ["rust", "html"]"#;
let res = FrontMatter::parse(content);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(res.title, "Hello".to_string());
assert_eq!(res.slug.unwrap(), "hello-world".to_string());
assert_eq!(res.tags.unwrap(), ["rust".to_string(), "html".to_string()]);
} }
#[test]
fn test_can_parse_extra_attributes_in_frontmatter() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
[extra]
language = "en"
authors = ["Bob", "Alice"]"#;
let res = FrontMatter::parse(content);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(res.title, "Hello".to_string());
assert_eq!(res.slug.unwrap(), "hello-world".to_string());
let extra = res.extra.unwrap();
assert_eq!(extra.get("language").unwrap(), &to_value("en").unwrap());
assert_eq!(
extra.get("authors").unwrap(),
&to_value(["Bob".to_string(), "Alice".to_string()]).unwrap()
);
}
#[test]
fn test_is_ok_with_url_instead_of_slug() {
let content = r#"
title = "Hello"
description = "hey there"
url = "hello-world""#;
let res = FrontMatter::parse(content);
assert!(res.is_ok());
let res = res.unwrap();
assert!(res.slug.is_none());
assert_eq!(res.url.unwrap(), "hello-world".to_string());
}
#[test]
fn test_errors_with_empty_front_matter() {
let content = r#" "#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn test_errors_with_invalid_front_matter() {
let content = r#"title = 1\n"#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn test_errors_with_missing_required_value_front_matter() {
let content = r#"title = """#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn test_errors_on_non_string_tag() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
tags = ["rust", 1]"#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn test_errors_on_present_but_empty_slug() {
let content = r#"
title = "Hello"
description = "hey there"
slug = """#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn test_errors_on_present_but_empty_url() {
let content = r#"
title = "Hello"
description = "hey there"
url = """#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
} }
} }
/// Split a file between the front matter and its content
/// It will parse the front matter as well and returns any error encountered
/// TODO: add tests
pub fn split_content(file_path: &Path, content: &str) -> Result<(FrontMatter, String)> {
if !PAGE_RE.is_match(content) {
bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", file_path.to_string_lossy());
}
// 2. extract the front matter and the content
let caps = PAGE_RE.captures(content).unwrap();
// caps[0] is the full match
let front_matter = &caps[1];
let content = &caps[2];
// 3. create our page, parse front matter and assign all of that
let meta = FrontMatter::parse(front_matter)
.chain_err(|| format!("Error when parsing front matter of file `{}`", file_path.to_string_lossy()))?;
Ok((meta, content.to_string()))
}

34
src/lib.rs Normal file
View file

@ -0,0 +1,34 @@
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate toml;
extern crate walkdir;
extern crate pulldown_cmark;
extern crate regex;
extern crate tera;
extern crate glob;
extern crate syntect;
extern crate slug;
extern crate chrono;
#[cfg(test)]
extern crate tempdir;
mod utils;
mod config;
pub mod errors;
mod page;
mod front_matter;
mod site;
mod markdown;
mod section;
pub use site::Site;
pub use config::Config;
pub use front_matter::{FrontMatter, split_content};
pub use page::{Page};
pub use section::{Section};
pub use utils::create_file;

View file

@ -2,44 +2,31 @@
extern crate clap; extern crate clap;
#[macro_use] #[macro_use]
extern crate error_chain; extern crate error_chain;
#[macro_use] extern crate gutenberg;
extern crate lazy_static; extern crate chrono;
#[macro_use]
extern crate serde_derive; extern crate staticfile;
extern crate serde; extern crate iron;
extern crate toml; extern crate mount;
extern crate walkdir; extern crate notify;
extern crate pulldown_cmark; extern crate ws;
extern crate regex;
extern crate tera;
extern crate glob;
extern crate syntect;
mod utils; use std::time::Instant;
mod config; use chrono::Duration;
mod errors;
mod cmd; mod cmd;
mod page;
mod front_matter;
use config::Config; // Print the time elapsed rounded to 1 decimal
fn report_elapsed_time(instant: Instant) {
let duration_ms = Duration::from_std(instant.elapsed()).unwrap().num_milliseconds() as f64;
if duration_ms < 1000.0 {
// Get and parse the config. println!("Done in {}ms.\n", duration_ms);
// If it doesn't succeed, exit } else {
fn get_config() -> Config { let duration_sec = duration_ms / 1000.0;
match Config::from_file("config.toml") { println!("Done in {:.1}s.\n", ((duration_sec * 10.0).round() / 10.0));
Ok(c) => c,
Err(e) => {
println!("Failed to load config.toml");
println!("Error: {}", e);
for e in e.iter().skip(1) {
println!("Reason: {}", e)
}
::std::process::exit(1);
}
} }
} }
@ -57,6 +44,11 @@ fn main() {
(@subcommand build => (@subcommand build =>
(about: "Builds the site") (about: "Builds the site")
) )
(@subcommand serve =>
(about: "Serve the site. Rebuild and reload on change automatically")
(@arg interface: "Interface to bind on (default to 127.0.0.1)")
(@arg port: "Which port to use (default to 1111)")
)
).get_matches(); ).get_matches();
match matches.subcommand() { match matches.subcommand() {
@ -64,7 +56,6 @@ fn main() {
match cmd::create_new_project(matches.value_of("name").unwrap()) { match cmd::create_new_project(matches.value_of("name").unwrap()) {
Ok(()) => { Ok(()) => {
println!("Project created"); println!("Project created");
println!("You will now need to set a theme in `config.toml`");
}, },
Err(e) => { Err(e) => {
println!("Error: {}", e); println!("Error: {}", e);
@ -73,9 +64,11 @@ fn main() {
}; };
}, },
("build", Some(_)) => { ("build", Some(_)) => {
match cmd::build(get_config()) { println!("Building site");
let start = Instant::now();
match cmd::build() {
Ok(()) => { Ok(()) => {
println!("Project built."); report_elapsed_time(start);
}, },
Err(e) => { Err(e) => {
println!("Failed to build the site"); println!("Failed to build the site");
@ -87,6 +80,20 @@ fn main() {
}, },
}; };
}, },
("serve", Some(matches)) => {
let interface = matches.value_of("interface").unwrap_or("127.0.0.1");
let port = matches.value_of("port").unwrap_or("1111");
match cmd::serve(interface, port) {
Ok(()) => (),
Err(e) => {
println!("Error: {}", e);
for e in e.iter().skip(1) {
println!("Reason: {}", e)
}
::std::process::exit(1);
},
};
},
_ => unreachable!(), _ => unreachable!(),
} }
} }

150
src/markdown.rs Normal file
View file

@ -0,0 +1,150 @@
use std::borrow::Cow::Owned;
use pulldown_cmark as cmark;
use self::cmark::{Parser, Event, Tag};
use syntect::easy::HighlightLines;
use syntect::parsing::SyntaxSet;
use syntect::highlighting::ThemeSet;
use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, IncludeBackground};
// We need to put those in a struct to impl Send and sync
struct Setup {
syntax_set: SyntaxSet,
theme_set: ThemeSet,
}
unsafe impl Send for Setup {}
unsafe impl Sync for Setup {}
lazy_static!{
static ref SETUP: Setup = Setup {
syntax_set: SyntaxSet::load_defaults_newlines(),
theme_set: ThemeSet::load_defaults()
};
}
struct CodeHighlightingParser<'a> {
// The block we're currently highlighting
highlighter: Option<HighlightLines<'a>>,
parser: Parser<'a>,
}
impl<'a> CodeHighlightingParser<'a> {
pub fn new(parser: Parser<'a>) -> CodeHighlightingParser<'a> {
CodeHighlightingParser {
highlighter: None,
parser: parser,
}
}
}
impl<'a> Iterator for CodeHighlightingParser<'a> {
type Item = Event<'a>;
fn next(&mut self) -> Option<Event<'a>> {
// Not using pattern matching to reduce indentation levels
let next_opt = self.parser.next();
if next_opt.is_none() {
return None;
}
let item = next_opt.unwrap();
// Below we just look for the start of a code block and highlight everything
// until we see the end of a code block.
// Everything else happens as normal in pulldown_cmark
match item {
Event::Text(text) => {
// if we are in the middle of a code block
if let Some(ref mut highlighter) = self.highlighter {
let highlighted = &highlighter.highlight(&text);
let html = styles_to_coloured_html(highlighted, IncludeBackground::Yes);
Some(Event::Html(Owned(html)))
} else {
Some(Event::Text(text))
}
},
Event::Start(Tag::CodeBlock(ref info)) => {
let syntax = info
.split(' ')
.next()
.and_then(|lang| SETUP.syntax_set.find_syntax_by_token(lang))
.unwrap_or_else(|| SETUP.syntax_set.find_syntax_plain_text());
self.highlighter = Some(
HighlightLines::new(syntax, &SETUP.theme_set.themes["base16-ocean.dark"])
);
let snippet = start_coloured_html_snippet(&SETUP.theme_set.themes["base16-ocean.dark"]);
Some(Event::Html(Owned(snippet)))
},
Event::End(Tag::CodeBlock(_)) => {
// reset highlight and close the code block
self.highlighter = None;
Some(Event::Html(Owned("</pre>".to_owned())))
},
_ => Some(item)
}
}
}
pub fn markdown_to_html(content: &str, highlight_code: bool) -> String {
let mut html = String::new();
if highlight_code {
let parser = CodeHighlightingParser::new(Parser::new(content));
cmark::html::push_html(&mut html, parser);
} else {
let parser = Parser::new(content);
cmark::html::push_html(&mut html, parser);
};
html
}
#[cfg(test)]
mod tests {
use super::{markdown_to_html};
#[test]
fn test_markdown_to_html_simple() {
let res = markdown_to_html("# hello", true);
assert_eq!(res, "<h1>hello</h1>\n");
}
#[test]
fn test_markdown_to_html_code_block_highlighting_off() {
let res = markdown_to_html("```\n$ gutenberg server\n```", false);
assert_eq!(
res,
"<pre><code>$ gutenberg server\n</code></pre>\n"
);
}
#[test]
fn test_markdown_to_html_code_block_no_lang() {
let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", true);
assert_eq!(
res,
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">$ gutenberg server\n</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">$ ping\n</span></pre>"
);
}
#[test]
fn test_markdown_to_html_code_block_with_lang() {
let res = markdown_to_html("```python\nlist.append(1)\n```", true);
assert_eq!(
res,
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">.</span><span style=\"background-color:#2b303b;color:#bf616a;\">append</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">(</span><span style=\"background-color:#2b303b;color:#d08770;\">1</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">)</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">\n</span></pre>"
);
}
#[test]
fn test_markdown_to_html_code_block_with_unknown_lang() {
let res = markdown_to_html("```yolo\nlist.append(1)\n```", true);
// defaults to plain text
assert_eq!(
res,
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list.append(1)\n</span></pre>"
);
}
}

View file

@ -1,45 +1,78 @@
/// A page, can be a blog post or a basic page /// A page, can be a blog post or a basic page
use std::fs::File; use std::cmp::Ordering;
use std::io::prelude::*; use std::fs::{read_dir};
use std::path::Path; use std::path::{Path, PathBuf};
use std::result::Result as StdResult; use std::result::Result as StdResult;
use pulldown_cmark as cmark;
use regex::Regex;
use tera::{Tera, Context}; use tera::{Tera, Context};
use serde::ser::{SerializeStruct, self}; use serde::ser::{SerializeStruct, self};
use slug::slugify;
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use config::Config; use config::Config;
use front_matter::{FrontMatter}; use front_matter::{FrontMatter, split_content};
use markdown::markdown_to_html;
use utils::{read_file, find_content_components};
lazy_static! {
static ref PAGE_RE: Regex = Regex::new(r"^\n?\+\+\+\n((?s).*(?-s))\+\+\+\n((?s).*(?-s))$").unwrap(); /// Looks into the current folder for the path and see if there's anything that is not a .md
/// file. Those will be copied next to the rendered .html file
fn find_related_assets(path: &Path) -> Vec<PathBuf> {
let mut assets = vec![];
for entry in read_dir(path).unwrap().filter_map(|e| e.ok()) {
let entry_path = entry.path();
if entry_path.is_file() {
match entry_path.extension() {
Some(e) => match e.to_str() {
Some("md") => continue,
_ => assets.push(entry_path.to_path_buf()),
},
None => continue,
}
}
}
assets
} }
#[derive(Clone, Debug, PartialEq, Deserialize)] #[derive(Clone, Debug, PartialEq)]
pub struct Page { pub struct Page {
/// .md filepath, excluding the content/ bit /// The .md path
#[serde(skip_serializing)] pub file_path: PathBuf,
pub filepath: String, /// The parent directory of the file. Is actually the grand parent directory
/// if it's an asset folder
pub parent_path: PathBuf,
/// The name of the .md file /// The name of the .md file
#[serde(skip_serializing)] pub file_name: String,
pub filename: String, /// The directories above our .md file
/// The directories above our .md file are called sections /// for example a file at content/kb/solutions/blabla.md will have 2 components:
/// for example a file at content/kb/solutions/blabla.md will have 2 sections:
/// `kb` and `solutions` /// `kb` and `solutions`
#[serde(skip_serializing)] pub components: Vec<String>,
pub sections: Vec<String>,
/// The actual content of the page, in markdown /// The actual content of the page, in markdown
#[serde(skip_serializing)]
pub raw_content: String, pub raw_content: String,
/// All the non-md files we found next to the .md file
pub assets: Vec<PathBuf>,
/// The HTML rendered of the page /// The HTML rendered of the page
pub content: String, pub content: String,
/// The front matter meta-data /// The front matter meta-data
pub meta: FrontMatter, pub meta: FrontMatter,
/// The slug of that page.
/// First tries to find the slug in the meta and defaults to filename otherwise
pub slug: String,
/// The relative URL of the page
pub url: String,
/// The full URL for that page
pub permalink: String,
/// The summary for the article, defaults to empty string
/// When <!-- more --> is found in the text, will take the content up to that part
/// as summary
pub summary: String,
/// The previous page, by date /// The previous page, by date
pub previous: Option<Box<Page>>, pub previous: Option<Box<Page>>,
/// The next page, by date /// The next page, by date
@ -50,227 +83,200 @@ pub struct Page {
impl Page { impl Page {
pub fn new(meta: FrontMatter) -> Page { pub fn new(meta: FrontMatter) -> Page {
Page { Page {
filepath: "".to_string(), file_path: PathBuf::new(),
filename: "".to_string(), parent_path: PathBuf::new(),
sections: vec![], file_name: "".to_string(),
components: vec![],
raw_content: "".to_string(), raw_content: "".to_string(),
assets: vec![],
content: "".to_string(), content: "".to_string(),
slug: "".to_string(),
url: "".to_string(),
permalink: "".to_string(),
summary: "".to_string(),
meta: meta, meta: meta,
previous: None, previous: None,
next: None, next: None,
} }
} }
/// Get the slug for the page. /// Get word count and estimated reading time
/// First tries to find the slug in the meta and defaults to filename otherwise pub fn get_reading_analytics(&self) -> (usize, usize) {
pub fn get_slug(&self) -> String { // Only works for latin language but good enough for a start
if let Some(ref slug) = self.meta.slug { let word_count: usize = self.raw_content.split_whitespace().count();
slug.to_string()
} else { // https://help.medium.com/hc/en-us/articles/214991667-Read-time
self.filename.clone() // 275 seems a bit too high though
} (word_count, (word_count / 200))
} }
// Parse a page given the content of the .md file /// Parse a page given the content of the .md file
// Files without front matter or with invalid front matter are considered /// Files without front matter or with invalid front matter are considered
// erroneous /// erroneous
pub fn parse(filepath: &str, content: &str) -> Result<Page> { pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Page> {
// 1. separate front matter from content // 1. separate front matter from content
if !PAGE_RE.is_match(content) { let (meta, content) = split_content(file_path, content)?;
bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", filepath); let mut page = Page::new(meta);
page.file_path = file_path.to_path_buf();
page.parent_path = page.file_path.parent().unwrap().to_path_buf();
page.raw_content = content;
// We try to be smart about highlighting code as it can be time-consuming
// If the global config disables it, then we do nothing. However,
// if we see a code block in the content, we assume that this page needs
// to be highlighted. It could potentially have false positive if the content
// has ``` in it but that seems kind of unlikely
let should_highlight = if config.highlight_code.unwrap() {
page.raw_content.contains("```")
} else {
false
};
page.content = markdown_to_html(&page.raw_content, should_highlight);
if page.raw_content.contains("<!-- more -->") {
page.summary = {
let summary = page.raw_content.splitn(2, "<!-- more -->").collect::<Vec<&str>>()[0];
markdown_to_html(summary, should_highlight)
}
} }
// 2. extract the front matter and the content let path = Path::new(file_path);
let caps = PAGE_RE.captures(content).unwrap(); page.file_name = path.file_stem().unwrap().to_string_lossy().to_string();
// caps[0] is the full match
let front_matter = &caps[1];
let content = &caps[2];
// 3. create our page, parse front matter and assign all of that page.slug = {
let meta = FrontMatter::parse(&front_matter) if let Some(ref slug) = page.meta.slug {
.chain_err(|| format!("Error when parsing front matter of file `{}`", filepath))?; slug.trim().to_string()
} else {
let mut page = Page::new(meta); slugify(page.file_name.clone())
page.filepath = filepath.to_string(); }
page.raw_content = content.to_string();
page.content = {
let mut html = String::new();
let parser = cmark::Parser::new(&page.raw_content);
cmark::html::push_html(&mut html, parser);
html
}; };
// 4. Find sections // 4. Find sections
// Pages with custom urls exists outside of sections // Pages with custom urls exists outside of sections
if page.meta.url.is_none() { if let Some(ref u) = page.meta.url {
let path = Path::new(filepath); page.url = u.trim().to_string();
page.filename = path.file_stem().expect("Couldn't get filename").to_string_lossy().to_string();
// find out if we have sections
for section in path.parent().unwrap().components() {
page.sections.push(section.as_ref().to_string_lossy().to_string());
}
// now the url
// We get it from a combination of sections + slug
if !page.sections.is_empty() {
page.meta.url = Some(format!("/{}/{}", page.sections.join("/"), page.get_slug()));
} else { } else {
page.meta.url = Some(format!("/{}", page.get_slug())); page.components = find_content_components(&page.file_path);
}; if !page.components.is_empty() {
// If we have a folder with an asset, don't consider it as a component
if page.file_name == "index" {
page.components.pop();
// also set parent_path to grandparent instead
page.parent_path = page.parent_path.parent().unwrap().to_path_buf();
} }
// Don't add a trailing slash to sections
page.url = format!("{}/{}", page.components.join("/"), page.slug);
} else {
page.url = page.slug.clone();
}
}
page.permalink = config.make_permalink(&page.url);
Ok(page) Ok(page)
} }
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Page> { /// Read and parse a .md file into a Page struct
pub fn from_file<P: AsRef<Path>>(path: P, config: &Config) -> Result<Page> {
let path = path.as_ref(); let path = path.as_ref();
let content = read_file(path)?;
let mut page = Page::parse(path, &content, config)?;
page.assets = find_related_assets(path.parent().unwrap());
let mut content = String::new(); if !page.assets.is_empty() && page.file_name != "index" {
File::open(path) bail!("Page `{}` has assets but is not named index.md", path.display());
.chain_err(|| format!("Failed to open '{:?}'", path.display()))?
.read_to_string(&mut content)?;
// Remove the content string from name
// Maybe get a path as an arg instead and use strip_prefix?
Page::parse(&path.strip_prefix("content").unwrap().to_string_lossy(), &content)
} }
fn get_layout_name(&self) -> String { Ok(page)
match self.meta.layout {
}
/// Renders the page using the default layout, unless specified in front-matter
pub fn render_html(&self, tera: &Tera, config: &Config) -> Result<String> {
let tpl_name = match self.meta.template {
Some(ref l) => l.to_string(), Some(ref l) => l.to_string(),
None => "page.html".to_string() None => "page.html".to_string()
} };
} // TODO: create a helper to create context to ensure all contexts
// have the same names
pub fn render_html(&mut self, tera: &Tera, config: &Config) -> Result<String> {
let tpl = self.get_layout_name();
let mut context = Context::new(); let mut context = Context::new();
context.add("site", config); context.add("config", config);
context.add("page", self); context.add("page", self);
tera.render(&tpl, &context) tera.render(&tpl_name, &context)
.chain_err(|| "Error while rendering template") .chain_err(|| format!("Failed to render page '{}'", self.file_name))
} }
} }
impl ser::Serialize for Page { impl ser::Serialize for Page {
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer { fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer {
let mut state = serializer.serialize_struct("page", 10)?; let mut state = serializer.serialize_struct("page", 13)?;
state.serialize_field("content", &self.content)?; state.serialize_field("content", &self.content)?;
state.serialize_field("title", &self.meta.title)?; state.serialize_field("title", &self.meta.title)?;
state.serialize_field("description", &self.meta.description)?; state.serialize_field("description", &self.meta.description)?;
state.serialize_field("date", &self.meta.date)?; state.serialize_field("date", &self.meta.date)?;
state.serialize_field("slug", &self.meta.slug)?; state.serialize_field("slug", &self.slug)?;
state.serialize_field("url", &self.meta.url)?; state.serialize_field("url", &format!("/{}", self.url))?;
state.serialize_field("permalink", &self.permalink)?;
state.serialize_field("tags", &self.meta.tags)?; state.serialize_field("tags", &self.meta.tags)?;
state.serialize_field("draft", &self.meta.draft)?; state.serialize_field("draft", &self.meta.draft)?;
state.serialize_field("category", &self.meta.category)?; state.serialize_field("category", &self.meta.category)?;
state.serialize_field("extra", &self.meta.extra)?; state.serialize_field("extra", &self.meta.extra)?;
let (word_count, reading_time) = self.get_reading_analytics();
state.serialize_field("word_count", &word_count)?;
state.serialize_field("reading_time", &reading_time)?;
state.end() state.end()
} }
} }
// Order pages by date, no-op for now impl PartialOrd for Page {
// TODO: impl PartialOrd on Vec<Page> so we can use sort()? fn partial_cmp(&self, other: &Page) -> Option<Ordering> {
pub fn order_pages(pages: Vec<Page>) -> Vec<Page> { if self.meta.date.is_none() {
pages return Some(Ordering::Less);
}
if other.meta.date.is_none() {
return Some(Ordering::Greater);
}
let this_date = self.meta.parse_date().unwrap();
let other_date = other.meta.parse_date().unwrap();
if this_date > other_date {
return Some(Ordering::Less);
}
if this_date < other_date {
return Some(Ordering::Greater);
}
Some(Ordering::Equal)
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{Page}; use tempdir::TempDir;
use std::fs::File;
use super::{find_related_assets};
#[test] #[test]
fn test_can_parse_a_valid_page() { fn test_find_related_assets() {
let content = r#" let tmp_dir = TempDir::new("example").expect("create temp dir");
+++ File::create(tmp_dir.path().join("index.md")).unwrap();
title = "Hello" File::create(tmp_dir.path().join("example.js")).unwrap();
description = "hey there" File::create(tmp_dir.path().join("graph.jpg")).unwrap();
slug = "hello-world" File::create(tmp_dir.path().join("fail.png")).unwrap();
+++
Hello world"#;
let res = Page::parse("post.md", content);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.meta.title, "Hello".to_string()); let assets = find_related_assets(tmp_dir.path());
assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string()); assert_eq!(assets.len(), 3);
assert_eq!(page.raw_content, "Hello world".to_string()); assert_eq!(assets.iter().filter(|p| p.extension().unwrap() != "md").count(), 3);
assert_eq!(page.content, "<p>Hello world</p>\n".to_string()); assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "example.js").count(), 1);
} assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "graph.jpg").count(), 1);
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "fail.png").count(), 1);
#[test]
fn test_can_find_one_parent_directory() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse("posts/intro.md", content);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.sections, vec!["posts".to_string()]);
}
#[test]
fn test_can_find_multiple_parent_directories() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse("posts/intro/start.md", content);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.sections, vec!["posts".to_string(), "intro".to_string()]);
}
#[test]
fn test_can_make_url_from_sections_and_slug() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse("posts/intro/start.md", content);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.meta.url.unwrap(), "/posts/intro/hello-world");
}
#[test]
fn test_can_make_url_from_sections_and_slug_root() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse("start.md", content);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.meta.url.unwrap(), "/hello-world");
}
#[test]
fn test_errors_on_invalid_front_matter_format() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse("start.md", content);
assert!(res.is_err());
} }
} }

97
src/section.rs Normal file
View file

@ -0,0 +1,97 @@
use std::path::{Path, PathBuf};
use std::result::Result as StdResult;
use tera::{Tera, Context};
use serde::ser::{SerializeStruct, self};
use config::Config;
use front_matter::{FrontMatter, split_content};
use errors::{Result, ResultExt};
use utils::{read_file, find_content_components};
use page::Page;
#[derive(Clone, Debug, PartialEq)]
pub struct Section {
/// The _index.md full path
pub file_path: PathBuf,
/// Path of the directory containing the _index.md file
pub parent_path: PathBuf,
/// The folder names from `content` to this section file
pub components: Vec<String>,
/// The relative URL of the page
pub url: String,
/// The full URL for that page
pub permalink: String,
/// The front matter meta-data
pub meta: FrontMatter,
/// All direct pages of that section
pub pages: Vec<Page>,
/// All direct subsections
pub subsections: Vec<Section>,
}
impl Section {
pub fn new(file_path: &Path, meta: FrontMatter) -> Section {
Section {
file_path: file_path.to_path_buf(),
parent_path: file_path.parent().unwrap().to_path_buf(),
components: vec![],
url: "".to_string(),
permalink: "".to_string(),
meta: meta,
pages: vec![],
subsections: vec![],
}
}
pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Section> {
let (meta, _) = split_content(file_path, content)?;
let mut section = Section::new(file_path, meta);
section.components = find_content_components(&section.file_path);
section.url = section.components.join("/");
section.permalink = section.components.join("/");
section.permalink = config.make_permalink(&section.url);
Ok(section)
}
/// Read and parse a .md file into a Page struct
pub fn from_file<P: AsRef<Path>>(path: P, config: &Config) -> Result<Section> {
let path = path.as_ref();
let content = read_file(path)?;
Section::parse(path, &content, config)
}
/// Renders the page using the default layout, unless specified in front-matter
pub fn render_html(&self, tera: &Tera, config: &Config) -> Result<String> {
let tpl_name = match self.meta.template {
Some(ref l) => l.to_string(),
None => "section.html".to_string()
};
// TODO: create a helper to create context to ensure all contexts
// have the same names
let mut context = Context::new();
context.add("config", config);
context.add("section", self);
tera.render(&tpl_name, &context)
.chain_err(|| format!("Failed to render section '{}'", self.file_path.display()))
}
}
impl ser::Serialize for Section {
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer {
let mut state = serializer.serialize_struct("section", 6)?;
state.serialize_field("title", &self.meta.title)?;
state.serialize_field("description", &self.meta.description)?;
state.serialize_field("url", &format!("/{}", self.url))?;
state.serialize_field("permalink", &self.permalink)?;
state.serialize_field("pages", &self.pages)?;
state.serialize_field("subsections", &self.subsections)?;
state.end()
}
}

449
src/site.rs Normal file
View file

@ -0,0 +1,449 @@
use std::collections::{BTreeMap, HashMap};
use std::iter::FromIterator;
use std::fs::{remove_dir_all, copy, remove_file};
use std::path::{Path, PathBuf};
use glob::glob;
use tera::{Tera, Context};
use slug::slugify;
use walkdir::WalkDir;
use errors::{Result, ResultExt};
use config::{Config, get_config};
use page::{Page};
use utils::{create_file, create_directory};
use section::{Section};
lazy_static! {
static ref GUTENBERG_TERA: Tera = {
let mut tera = Tera::default();
tera.add_raw_templates(vec![
("rss.xml", include_str!("templates/rss.xml")),
("sitemap.xml", include_str!("templates/sitemap.xml")),
]).unwrap();
tera
};
}
#[derive(Debug, PartialEq)]
enum RenderList {
Tags,
Categories,
}
/// A tag or category
#[derive(Debug, Serialize, PartialEq)]
struct ListItem {
name: String,
slug: String,
count: usize,
}
impl ListItem {
pub fn new(name: &str, count: usize) -> ListItem {
ListItem {
name: name.to_string(),
slug: slugify(name),
count: count,
}
}
}
#[derive(Debug)]
pub struct Site {
pub base_path: PathBuf,
pub config: Config,
pub pages: HashMap<PathBuf, Page>,
pub sections: BTreeMap<PathBuf, Section>,
pub templates: Tera,
live_reload: bool,
output_path: PathBuf,
pub tags: HashMap<String, Vec<PathBuf>>,
pub categories: HashMap<String, Vec<PathBuf>>,
}
impl Site {
/// Parse a site at the given path. Defaults to the current dir
/// Passing in a path is only used in tests
pub fn new<P: AsRef<Path>>(path: P) -> Result<Site> {
let path = path.as_ref();
let tpl_glob = format!("{}/{}", path.to_string_lossy().replace("\\", "/"), "templates/**/*");
let mut tera = Tera::new(&tpl_glob).chain_err(|| "Error parsing templates")?;
tera.extend(&GUTENBERG_TERA)?;
let mut site = Site {
base_path: path.to_path_buf(),
config: get_config(path),
pages: HashMap::new(),
sections: BTreeMap::new(),
templates: tera,
live_reload: false,
output_path: PathBuf::from("public"),
tags: HashMap::new(),
categories: HashMap::new(),
};
site.parse()?;
Ok(site)
}
/// What the function name says
pub fn enable_live_reload(&mut self) {
self.live_reload = true;
}
/// Used by tests to change the output path to a tmp dir
#[doc(hidden)]
pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) {
self.output_path = path.as_ref().to_path_buf();
}
/// Reads all .md files in the `content` directory and create pages
/// out of them
pub fn parse(&mut self) -> Result<()> {
let path = self.base_path.to_string_lossy().replace("\\", "/");
let content_glob = format!("{}/{}", path, "content/**/*.md");
// parent_dir -> Section
let mut sections = BTreeMap::new();
// Glob is giving us the result order so _index will show up first
// for each directory
for entry in glob(&content_glob).unwrap().filter_map(|e| e.ok()) {
let path = entry.as_path();
if path.file_name().unwrap() == "_index.md" {
let section = Section::from_file(&path, &self.config)?;
sections.insert(section.parent_path.clone(), section);
} else {
let page = Page::from_file(&path, &self.config)?;
if sections.contains_key(&page.parent_path) {
sections.get_mut(&page.parent_path).unwrap().pages.push(page.clone());
}
self.pages.insert(page.file_path.clone(), page);
}
}
// Find out the direct subsections of each subsection if there are some
let mut grandparent_paths = HashMap::new();
for section in sections.values() {
let grand_parent = section.parent_path.parent().unwrap().to_path_buf();
grandparent_paths.entry(grand_parent).or_insert_with(|| vec![]).push(section.clone());
}
for (parent_path, section) in &mut sections {
section.pages.sort_by(|a, b| a.partial_cmp(b).unwrap());
match grandparent_paths.get(parent_path) {
Some(paths) => section.subsections.extend(paths.clone()),
None => continue,
};
}
self.sections = sections;
self.parse_tags_and_categories();
Ok(())
}
/// Separated from `parse` for easier testing
pub fn parse_tags_and_categories(&mut self) {
for page in self.pages.values() {
if let Some(ref category) = page.meta.category {
self.categories
.entry(category.to_string())
.or_insert_with(|| vec![])
.push(page.file_path.clone());
}
if let Some(ref tags) = page.meta.tags {
for tag in tags {
self.tags
.entry(tag.to_string())
.or_insert_with(|| vec![])
.push(page.file_path.clone());
}
}
}
}
/// Inject live reload script tag if in live reload mode
fn inject_livereload(&self, html: String) -> String {
if self.live_reload {
return html.replace(
"</body>",
r#"<script src="/livereload.js?port=1112&mindelay=10"></script></body>"#
);
}
html
}
/// Copy the content of the `static` folder into the `public` folder
///
/// TODO: only copy one file if possible because that would be a waste
/// to do re-copy the whole thing. Benchmark first to see if it's a big difference
pub fn copy_static_directory(&self) -> Result<()> {
let from = Path::new("static");
let target = Path::new("public");
for entry in WalkDir::new(from).into_iter().filter_map(|e| e.ok()) {
let relative_path = entry.path().strip_prefix(&from).unwrap();
let target_path = {
let mut target_path = target.to_path_buf();
target_path.push(relative_path);
target_path
};
if entry.path().is_dir() {
if !target_path.exists() {
create_directory(&target_path)?;
}
} else {
if target_path.exists() {
remove_file(&target_path)?;
}
copy(entry.path(), &target_path)?;
}
}
Ok(())
}
/// Deletes the `public` directory if it exists
pub fn clean(&self) -> Result<()> {
if Path::new("public").exists() {
// Delete current `public` directory so we can start fresh
remove_dir_all("public").chain_err(|| "Couldn't delete `public` directory")?;
}
Ok(())
}
pub fn rebuild_after_content_change(&mut self) -> Result<()> {
self.parse()?;
self.build()
}
pub fn rebuild_after_template_change(&mut self) -> Result<()> {
self.templates.full_reload()?;
self.build_pages()
}
pub fn build_pages(&self) -> Result<()> {
let public = self.output_path.clone();
if !public.exists() {
create_directory(&public)?;
}
let mut pages = vec![];
// First we render the pages themselves
for page in self.pages.values() {
// Copy the nesting of the content directory if we have sections for that page
let mut current_path = public.to_path_buf();
for component in page.url.split('/') {
current_path.push(component);
if !current_path.exists() {
create_directory(&current_path)?;
}
}
// Make sure the folder exists
create_directory(&current_path)?;
// Finally, create a index.html file there with the page rendered
let output = page.render_html(&self.templates, &self.config)?;
create_file(current_path.join("index.html"), &self.inject_livereload(output))?;
// Copy any asset we found previously into the same directory as the index.html
for asset in &page.assets {
let asset_path = asset.as_path();
copy(&asset_path, &current_path.join(asset_path.file_name().unwrap()))?;
}
pages.push(page);
}
// Outputting categories and pages
self.render_categories_and_tags(RenderList::Categories)?;
self.render_categories_and_tags(RenderList::Tags)?;
// And finally the index page
let mut context = Context::new();
pages.sort_by(|a, b| a.partial_cmp(b).unwrap());
context.add("pages", &pages);
context.add("config", &self.config);
let index = self.templates.render("index.html", &context)?;
create_file(public.join("index.html"), &self.inject_livereload(index))?;
Ok(())
}
/// Builds the site to the `public` directory after deleting it
pub fn build(&self) -> Result<()> {
self.clean()?;
self.build_pages()?;
self.render_sitemap()?;
if self.config.generate_rss.unwrap() {
self.render_rss_feed()?;
}
self.render_sections()?;
self.copy_static_directory()
}
/// Render the /{categories, list} pages and each individual category/tag page
/// They are the same thing fundamentally, a list of pages with something in common
fn render_categories_and_tags(&self, kind: RenderList) -> Result<()> {
let items = match kind {
RenderList::Categories => &self.categories,
RenderList::Tags => &self.tags,
};
if items.is_empty() {
return Ok(());
}
let (list_tpl_name, single_tpl_name, name, var_name) = if kind == RenderList::Categories {
("categories.html", "category.html", "categories", "category")
} else {
("tags.html", "tag.html", "tags", "tag")
};
// Create the categories/tags directory first
let public = self.output_path.clone();
let mut output_path = public.to_path_buf();
output_path.push(name);
create_directory(&output_path)?;
// Then render the index page for that kind.
// We sort by number of page in that category/tag
let mut sorted_items = vec![];
for (item, count) in Vec::from_iter(items).into_iter().map(|(a, b)| (a, b.len())) {
sorted_items.push(ListItem::new(&item, count));
}
sorted_items.sort_by(|a, b| b.count.cmp(&a.count));
let mut context = Context::new();
context.add(name, &sorted_items);
context.add("config", &self.config);
// And render it immediately
let list_output = self.templates.render(list_tpl_name, &context)?;
create_file(output_path.join("index.html"), &self.inject_livereload(list_output))?;
// Now, each individual item
for (item_name, pages_paths) in items.iter() {
let mut pages: Vec<&Page> = self.pages
.iter()
.filter(|&(path, _)| pages_paths.contains(&path))
.map(|(_, page)| page)
.collect();
pages.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mut context = Context::new();
let slug = slugify(&item_name);
context.add(var_name, &item_name);
context.add(&format!("{}_slug", var_name), &slug);
context.add("pages", &pages);
context.add("config", &self.config);
let single_output = self.templates.render(single_tpl_name, &context)?;
create_directory(&output_path.join(&slug))?;
create_file(
output_path.join(&slug).join("index.html"),
&self.inject_livereload(single_output)
)?;
}
Ok(())
}
fn render_sitemap(&self) -> Result<()> {
let mut context = Context::new();
context.add("pages", &self.pages.values().collect::<Vec<&Page>>());
context.add("sections", &self.sections.values().collect::<Vec<&Section>>());
let mut categories = vec![];
if !self.categories.is_empty() {
categories.push(self.config.make_permalink("categories"));
for category in self.categories.keys() {
categories.push(
self.config.make_permalink(&format!("categories/{}", slugify(category)))
);
}
}
context.add("categories", &categories);
let mut tags = vec![];
if !self.tags.is_empty() {
tags.push(self.config.make_permalink("tags"));
for tag in self.tags.keys() {
tags.push(
self.config.make_permalink(&format!("tags/{}", slugify(tag)))
);
}
}
context.add("tags", &tags);
let sitemap = self.templates.render("sitemap.xml", &context)?;
create_file(self.output_path.join("sitemap.xml"), &sitemap)?;
Ok(())
}
fn render_rss_feed(&self) -> Result<()> {
let mut context = Context::new();
let mut pages = self.pages.values()
.filter(|p| p.meta.date.is_some())
.take(15) // limit to the last 15 elements
.collect::<Vec<&Page>>();
// Don't generate a RSS feed if none of the pages has a date
if pages.is_empty() {
return Ok(());
}
pages.sort_by(|a, b| a.partial_cmp(b).unwrap());
context.add("pages", &pages);
context.add("last_build_date", &pages[0].meta.date);
context.add("config", &self.config);
let rss_feed_url = if self.config.base_url.ends_with('/') {
format!("{}{}", self.config.base_url, "feed.xml")
} else {
format!("{}/{}", self.config.base_url, "feed.xml")
};
context.add("feed_url", &rss_feed_url);
let sitemap = self.templates.render("rss.xml", &context)?;
create_file(self.output_path.join("rss.xml"), &sitemap)?;
Ok(())
}
fn render_sections(&self) -> Result<()> {
let public = self.output_path.clone();
for section in self.sections.values() {
let mut output_path = public.to_path_buf();
for component in &section.components {
output_path.push(component);
if !output_path.exists() {
create_directory(&output_path)?;
}
}
let output = section.render_html(&self.templates, &self.config)?;
create_file(output_path.join("index.html"), &self.inject_livereload(output))?;
}
Ok(())
}
}

20
src/templates/rss.xml Normal file
View file

@ -0,0 +1,20 @@
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>{{ config.title }}</title>
<link>{{ config.base_url }}</link>
<description>{{ config.description }}</description>
<generator>Gutenberg</generator>
<language>{{ config.language_code }}</language>
<atom:link href="{{ feed_url }}" rel="self" type="application/rss+xml"/>
<lastBuildDate>{{ last_build_date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate>
{% for page in pages %}
<item>
<title>{{ page.title }}</title>
<pubDate>{{ page.date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</pubDate>
<link>{{ page.permalink }}</link>
<guid>{{ page.permalink }}</guid>
<description>"{{ page.content | escape }}"</description>
</item>
{% endfor %}
</channel>
</rss>

25
src/templates/sitemap.xml Normal file
View file

@ -0,0 +1,25 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for page in pages %}
<url>
<loc>{{ page.permalink | safe }}</loc>
{% if page.date %}
<lastmod>{{ page.date }}</lastmod>
{% endif %}
</url>
{% endfor %}
{% for section in sections %}
<url>
<loc>{{ section.permalink | safe }}</loc>
</url>
{% endfor %}
{% for category in categories %}
<url>
<loc>{{ category | safe }}</loc>
</url>
{% endfor %}
{% for tag in tags %}
<url>
<loc>{{ tag | safe }}</loc>
</url>
{% endfor %}
</urlset>

View file

@ -1,8 +1,8 @@
use std::io::prelude::*; use std::io::prelude::*;
use std::fs::{File}; use std::fs::{File, create_dir};
use std::path::Path; use std::path::Path;
use errors::Result; use errors::{Result, ResultExt};
pub fn create_file<P: AsRef<Path>>(path: P, content: &str) -> Result<()> { pub fn create_file<P: AsRef<Path>>(path: P, content: &str) -> Result<()> {
@ -10,3 +10,63 @@ pub fn create_file<P: AsRef<Path>>(path: P, content: &str) -> Result<()> {
file.write_all(content.as_bytes())?; file.write_all(content.as_bytes())?;
Ok(()) Ok(())
} }
/// Very similar to `create_dir` from the std except it checks if the folder
/// exists before creating it
pub fn create_directory<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
if !path.exists() {
create_dir(path)
.chain_err(|| format!("Was not able to create folder {}", path.display()))?;
}
Ok(())
}
/// Return the content of a file, with error handling added
pub fn read_file<P: AsRef<Path>>(path: P) -> Result<String> {
let path = path.as_ref();
let mut content = String::new();
File::open(path)
.chain_err(|| format!("Failed to open '{:?}'", path.display()))?
.read_to_string(&mut content)?;
Ok(content)
}
/// Takes a full path to a .md and returns only the components after the `content` directory
/// Will not return the filename as last component
pub fn find_content_components<P: AsRef<Path>>(path: P) -> Vec<String> {
let path = path.as_ref();
let mut is_in_content = false;
let mut components = vec![];
for section in path.parent().unwrap().components() {
let component = section.as_ref().to_string_lossy();
if is_in_content {
components.push(component.to_string());
continue;
}
if component == "content" {
is_in_content = true;
}
}
components
}
#[cfg(test)]
mod tests {
use super::{find_content_components};
#[test]
fn test_find_content_components() {
let res = find_content_components("/home/vincent/code/site/content/posts/tutorials/python.md");
assert_eq!(res, ["posts".to_string(), "tutorials".to_string()]);
}
}

View file

@ -0,0 +1,593 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>author</key>
<string>Chris Kempson (http://chriskempson.com)</string>
<key>name</key>
<string>Base16 Ocean Dark</string>
<key>semanticClass</key>
<string>base16.ocean.dark</string>
<key>colorSpaceName</key>
<string>sRGB</string>
<key>gutterSettings</key>
<dict>
<key>background</key>
<string>#343d46</string>
<key>divider</key>
<string>#343d46</string>
<key>foreground</key>
<string>#65737e</string>
<key>selectionBackground</key>
<string>#4f5b66</string>
<key>selectionForeground</key>
<string>#a7adba</string>
</dict>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>background</key>
<string>#2b303b</string>
<key>caret</key>
<string>#c0c5ce</string>
<key>foreground</key>
<string>#c0c5ce</string>
<key>invisibles</key>
<string>#65737e</string>
<key>lineHighlight</key>
<string>#65737e30</string>
<key>selection</key>
<string>#4f5b66</string>
<key>guide</key>
<string>#3b5364</string>
<key>activeGuide</key>
<string>#96b5b4</string>
<key>stackGuide</key>
<string>#343d46</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Text</string>
<key>scope</key>
<string>variable.parameter.function</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c0c5ce</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comments</string>
<key>scope</key>
<string>comment, punctuation.definition.comment</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#65737e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation</string>
<key>scope</key>
<string>punctuation.definition.string, punctuation.definition.variable, punctuation.definition.string, punctuation.definition.parameters, punctuation.definition.string, punctuation.definition.array</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c0c5ce</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Delimiters</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c0c5ce</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Operators</string>
<key>scope</key>
<string>keyword.operator</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c0c5ce</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keywords</string>
<key>scope</key>
<string>keyword</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b48ead</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variables</string>
<key>scope</key>
<string>variable</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#bf616a</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Functions</string>
<key>scope</key>
<string>entity.name.function, meta.require, support.function.any-method</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8fa1b3</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Classes</string>
<key>scope</key>
<string>support.class, entity.name.class, entity.name.type.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#ebcb8b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Classes</string>
<key>scope</key>
<string>meta.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#eff1f5</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Methods</string>
<key>scope</key>
<string>keyword.other.special-method</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8fa1b3</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage</string>
<key>scope</key>
<string>storage</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b48ead</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support</string>
<key>scope</key>
<string>support.function</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#96b5b4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Strings, Inherited Class</string>
<key>scope</key>
<string>string, constant.other.symbol, entity.other.inherited-class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#a3be8c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Integers</string>
<key>scope</key>
<string>constant.numeric</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Floats</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Boolean</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constants</string>
<key>scope</key>
<string>constant</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Tags</string>
<key>scope</key>
<string>entity.name.tag</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#bf616a</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Attributes</string>
<key>scope</key>
<string>entity.other.attribute-name</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Attribute IDs</string>
<key>scope</key>
<string>entity.other.attribute-name.id, punctuation.definition.entity</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8fa1b3</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Selector</string>
<key>scope</key>
<string>meta.selector</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b48ead</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Values</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Headings</string>
<key>scope</key>
<string>markup.heading punctuation.definition.heading, entity.name.section</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#8fa1b3</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Units</string>
<key>scope</key>
<string>keyword.other.unit</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Bold</string>
<key>scope</key>
<string>markup.bold, punctuation.definition.bold</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#ebcb8b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Italic</string>
<key>scope</key>
<string>markup.italic, punctuation.definition.italic</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#b48ead</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Code</string>
<key>scope</key>
<string>markup.raw.inline</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#a3be8c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Link Text</string>
<key>scope</key>
<string>string.other.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#bf616a</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Link Url</string>
<key>scope</key>
<string>meta.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Lists</string>
<key>scope</key>
<string>markup.list</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#bf616a</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Quotes</string>
<key>scope</key>
<string>markup.quote</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Separator</string>
<key>scope</key>
<string>meta.separator</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#4f5b66</string>
<key>foreground</key>
<string>#c0c5ce</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Inserted</string>
<key>scope</key>
<string>markup.inserted, markup.inserted.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#a3be8c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Deleted</string>
<key>scope</key>
<string>markup.deleted, markup.deleted.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#bf616a</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Changed</string>
<key>scope</key>
<string>markup.changed, markup.changed.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b48ead</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Ignored</string>
<key>scope</key>
<string>markup.ignored, markup.ignored.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#4f5b66</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Untracked</string>
<key>scope</key>
<string>markup.untracked, markup.untracked.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#4f5b66</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Colors</string>
<key>scope</key>
<string>constant.other.color</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#96b5b4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Regular Expressions</string>
<key>scope</key>
<string>string.regexp</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#96b5b4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Escape Characters</string>
<key>scope</key>
<string>constant.character.escape</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#96b5b4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Embedded</string>
<key>scope</key>
<string>punctuation.section.embedded, variable.interpolation</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#ab7967</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Invalid</string>
<key>scope</key>
<string>invalid.illegal</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#bf616a</string>
<key>foreground</key>
<string>#2b303b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>GitGutter deleted</string>
<key>scope</key>
<string>markup.deleted.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#F92672</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>GitGutter inserted</string>
<key>scope</key>
<string>markup.inserted.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#A6E22E</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>GitGutter changed</string>
<key>scope</key>
<string>markup.changed.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#967EFB</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>GitGutter ignored</string>
<key>scope</key>
<string>markup.ignored.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#565656</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>GitGutter untracked</string>
<key>scope</key>
<string>markup.untracked.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#565656</string>
</dict>
</dict>
</array>
<key>uuid</key>
<string>59c1e2f2-7b41-46f9-91f2-1b4c6f5866f7</string>
</dict>
</plist>

View file

@ -0,0 +1,589 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>author</key>
<string>Chris Kempson (http://chriskempson.com)</string>
<key>name</key>
<string>Base16 Ocean Light</string>
<key>semanticClass</key>
<string>base16.ocean.light</string>
<key>colorSpaceName</key>
<string>sRGB</string>
<key>gutterSettings</key>
<dict>
<key>background</key>
<string>#eff1f5</string>
<key>divider</key>
<string>#eff1f5</string>
<key>foreground</key>
<string>#4f5b66</string>
<key>selectionBackground</key>
<string>#eff1f5</string>
<key>selectionForeground</key>
<string>#c0c5ce</string>
</dict>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>background</key>
<string>#eff1f5</string>
<key>caret</key>
<string>#4f5b66</string>
<key>foreground</key>
<string>#4f5b66</string>
<key>invisibles</key>
<string>#dfe1e8</string>
<key>lineHighlight</key>
<string>#a7adba30</string>
<key>selection</key>
<string>#dfe1e8</string>
<key>shadow</key>
<string>#dfe1e8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Text</string>
<key>scope</key>
<string>variable.parameter.function</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#4f5b66</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comments</string>
<key>scope</key>
<string>comment, punctuation.definition.comment</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#a7adba</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation</string>
<key>scope</key>
<string>punctuation.definition.string, punctuation.definition.variable, punctuation.definition.string, punctuation.definition.parameters, punctuation.definition.string, punctuation.definition.array</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#4f5b66</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Delimiters</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#4f5b66</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Operators</string>
<key>scope</key>
<string>keyword.operator</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#4f5b66</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keywords</string>
<key>scope</key>
<string>keyword</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b48ead</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variables</string>
<key>scope</key>
<string>variable</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#bf616a</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Functions</string>
<key>scope</key>
<string>entity.name.function, meta.require, support.function.any-method</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8fa1b3</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Classes</string>
<key>scope</key>
<string>support.class, entity.name.class, entity.name.type.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Classes</string>
<key>scope</key>
<string>meta.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#343d46</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Methods</string>
<key>scope</key>
<string>keyword.other.special-method</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8fa1b3</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage</string>
<key>scope</key>
<string>storage</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b48ead</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support</string>
<key>scope</key>
<string>support.function</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#96b5b4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Strings, Inherited Class</string>
<key>scope</key>
<string>string, constant.other.symbol, entity.other.inherited-class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#a3be8c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Integers</string>
<key>scope</key>
<string>constant.numeric</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Floats</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Boolean</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constants</string>
<key>scope</key>
<string>constant</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Tags</string>
<key>scope</key>
<string>entity.name.tag</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#bf616a</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Attributes</string>
<key>scope</key>
<string>entity.other.attribute-name</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Attribute IDs</string>
<key>scope</key>
<string>entity.other.attribute-name.id, punctuation.definition.entity</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8fa1b3</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Selector</string>
<key>scope</key>
<string>meta.selector</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b48ead</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Values</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Headings</string>
<key>scope</key>
<string>markup.heading punctuation.definition.heading, entity.name.section</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#8fa1b3</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Units</string>
<key>scope</key>
<string>keyword.other.unit</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Bold</string>
<key>scope</key>
<string>markup.bold, punctuation.definition.bold</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Italic</string>
<key>scope</key>
<string>markup.italic, punctuation.definition.italic</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#b48ead</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Code</string>
<key>scope</key>
<string>markup.raw.inline</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#a3be8c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Link Text</string>
<key>scope</key>
<string>string.other.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#bf616a</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Link Url</string>
<key>scope</key>
<string>meta.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Lists</string>
<key>scope</key>
<string>markup.list</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#bf616a</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Quotes</string>
<key>scope</key>
<string>markup.quote</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d08770</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Separator</string>
<key>scope</key>
<string>meta.separator</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#dfe1e8</string>
<key>foreground</key>
<string>#4f5b66</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Inserted</string>
<key>scope</key>
<string>markup.inserted, markup.inserted.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#a3be8c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Deleted</string>
<key>scope</key>
<string>markup.deleted, markup.deleted.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#bf616a</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Changed</string>
<key>scope</key>
<string>markup.changed, markup.changed.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b48ead</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Ignored</string>
<key>scope</key>
<string>markup.ignored, markup.ignored.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c0c5ce</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Untracked</string>
<key>scope</key>
<string>markup.untracked, markup.untracked.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c0c5ce</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Colors</string>
<key>scope</key>
<string>constant.other.color</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#96b5b4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Regular Expressions</string>
<key>scope</key>
<string>string.regexp</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#96b5b4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Escape Characters</string>
<key>scope</key>
<string>constant.character.escape</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#96b5b4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Embedded</string>
<key>scope</key>
<string>punctuation.section.embedded, variable.interpolation</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#ab7967</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Invalid</string>
<key>scope</key>
<string>invalid.illegal</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#bf616a</string>
<key>foreground</key>
<string>#eff1f5</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>GitGutter deleted</string>
<key>scope</key>
<string>markup.deleted.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#F92672</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>GitGutter inserted</string>
<key>scope</key>
<string>markup.inserted.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#A6E22E</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>GitGutter changed</string>
<key>scope</key>
<string>markup.changed.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#967EFB</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>GitGutter ignored</string>
<key>scope</key>
<string>markup.ignored.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#565656</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>GitGutter untracked</string>
<key>scope</key>
<string>markup.untracked.git_gutter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#565656</string>
</dict>
</dict>
</array>
<key>uuid</key>
<string>52997033-52ea-4534-af9f-7572613947d8</string>
</dict>
</plist>

View file

@ -0,0 +1,766 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>comment</key>
<string>Based on original gruvbox color scheme.</string>
<key>author</key>
<string>peaceant</string>
<key>name</key>
<string>gruvbox</string>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>background</key>
<string>#282828</string>
<key>caret</key>
<string>#fcf9e3</string>
<key>foreground</key>
<string>#fdf4c1aa</string>
<key>invisibles</key>
<string>#fabd2f</string>
<key>lineHighlight</key>
<string>#3c3836</string>
<key>selection</key>
<string>#504945</string>
<key>bracketContentsForeground</key>
<string>#928374</string>
<key>bracketsForeground</key>
<string>#d5c4a1</string>
<key>guide</key>
<string>#3c3836</string>
<key>activeGuide</key>
<string>#a89984</string>
<key>stackGuide</key>
<string>#665c54</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation</string>
<key>scope</key>
<string>punctuation.definition.tag</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#83a598</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation</string>
<key>scope</key>
<string>punctuation.definition.entity</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#d3869b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant</string>
<key>scope</key>
<string>constant</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#d3869b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant escape</string>
<key>scope</key>
<string>constant.character.escape</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant other</string>
<key>scope</key>
<string>constant.other</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Entity</string>
<key>scope</key>
<string>entity</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keyword</string>
<key>scope</key>
<string>keyword.operator.comparison, keyword.operator, keyword.operator.symbolic, keyword.operator.string, keyword.operator.assignment, keyword.operator.arithmetic, keyword.operator.class, keyword.operator.key, keyword.operator.logical</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fe8019</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keyword</string>
<key>scope</key>
<string>keyword, keyword.operator.new, keyword.other, keyword.control</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fa5c4b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage</string>
<key>scope</key>
<string>storage</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fa5c4b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String</string>
<key>scope</key>
<string>string -string.unquoted.old-plist -string.unquoted.heredoc, string.unquoted.heredoc string</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comment</string>
<key>scope</key>
<string>comment</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#928374</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Regexp</string>
<key>scope</key>
<string>string.regexp constant.character.escape</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support</string>
<key>scope</key>
<string>support</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variable</string>
<key>scope</key>
<string>variable</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Lang Variable</string>
<key>scope</key>
<string>variable.language</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Function Call</string>
<key>scope</key>
<string>meta.function-call</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Invalid</string>
<key>scope</key>
<string>invalid</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#932b1e</string>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Embedded Source</string>
<key>scope</key>
<string>text source, string.unquoted.heredoc, source source</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String embedded-source</string>
<key>scope</key>
<string>string.quoted source</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String constant</string>
<key>scope</key>
<string>string</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support.constant</string>
<key>scope</key>
<string>support.constant</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support.class</string>
<key>scope</key>
<string>support.class</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Meta.tag.A</string>
<key>scope</key>
<string>entity.name.tag</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Inner tag</string>
<key>scope</key>
<string>meta.tag, meta.tag entity</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css colors</string>
<key>scope</key>
<string>constant.other.color.rgb-value</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#83a598</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css tag-name</string>
<key>scope</key>
<string>meta.selector.css entity.name.tag</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fa5c4b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css#id</string>
<key>scope</key>
<string>meta.selector.css, entity.other.attribute-name.id</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css.class</string>
<key>scope</key>
<string>meta.selector.css entity.other.attribute-name.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css property-name:</string>
<key>scope</key>
<string>support.type.property-name.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css @at-rule</string>
<key>scope</key>
<string>meta.preprocessor.at-rule keyword.control.at-rule</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css additional-constants</string>
<key>scope</key>
<string>meta.property-value constant</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css additional-constants</string>
<key>scope</key>
<string>meta.property-value support.constant.named-color.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fe8019</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css constructor.argument</string>
<key>scope</key>
<string>meta.constructor.argument.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<!-- diff -->
<dict>
<key>name</key>
<string>diff.header</string>
<key>scope</key>
<string>meta.diff, meta.diff.header</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#83a598</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.deleted</string>
<key>scope</key>
<string>markup.deleted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fa5c4b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.changed</string>
<key>scope</key>
<string>markup.changed</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.inserted</string>
<key>scope</key>
<string>markup.inserted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<!-- markup -->
<dict>
<key>name</key>
<string>Bold Markup</string>
<key>scope</key>
<string>markup.bold</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Italic Markup</string>
<key>scope</key>
<string>markup.italic</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Heading Markup</string>
<key>scope</key>
<string>markup.heading</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<!-- Language Specific -->
<!-- PHP -->
<dict>
<key>name</key>
<string>PHP: class name</string>
<key>scope</key>
<string>entity.name.type.class.php</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>PHP: Comment</string>
<key>scope</key>
<string>keyword.other.phpdoc</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#928374</string>
</dict>
</dict>
<!-- CSS -->
<dict>
<key>name</key>
<string>CSS: numbers</string>
<key>scope</key>
<string>constant.numeric.css, keyword.other.unit.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d3869b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>CSS: entity dot, hash, comma, etc.</string>
<key>scope</key>
<string>punctuation.definition.entity.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<!-- JS -->
<dict>
<key>name</key>
<string>JS: variable</string>
<key>scope</key>
<string>variable.language.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>JS: unquoted labe</string>
<key>scope</key>
<string>string.unquoted.label.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<!-- SQL -->
<dict>
<key>name</key>
<string>Constant other sql</string>
<key>scope</key>
<string>constant.other.table-name.sql</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant other sql</string>
<key>scope</key>
<string>constant.other.database-name.sql</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<!-- Plugins -->
<!-- dired plugin -->
<dict>
<key>name</key>
<string>dired directory</string>
<key>scope</key>
<string>storage.type.dired.item.directory, dired.item.directory</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<!-- orgmode plugin -->
<dict>
<key>name</key>
<string>orgmode link</string>
<key>scope</key>
<string>orgmode.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
<key>fontStyle</key>
<string>underline</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode page</string>
<key>scope</key>
<string>orgmode.page</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode break</string>
<key>scope</key>
<string>orgmode.break</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d3869b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode headline</string>
<key>scope</key>
<string>orgmode.headline</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode tack</string>
<key>scope</key>
<string>orgmode.tack</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode follow up</string>
<key>scope</key>
<string>orgmode.follow_up</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode checkbox</string>
<key>scope</key>
<string>orgmode.checkbox</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode checkbox summary</string>
<key>scope</key>
<string>orgmode.checkbox.summary</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode tags</string>
<key>scope</key>
<string>orgmode.tags</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fa5c4b</string>
</dict>
</dict>
</array>
<key>uuid</key>
<string>06CD1FB2-A00A-4F8C-97B2-60E131980454</string>
</dict>
</plist>

View file

@ -0,0 +1,774 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>comment</key>
<string>Based on original gruvbox color scheme.</string>
<key>author</key>
<string>Martin Radimec</string>
<key>name</key>
<string>gruvbox</string>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>background</key>
<string>#FCF0CA</string>
<key>caret</key>
<string>#3C3836</string>
<key>foreground</key>
<string>#282828aa</string>
<key>invisibles</key>
<string>#b57614</string>
<key>lineHighlight</key>
<string>#EDDAB5</string>
<key>selection</key>
<string>#D6C3A3</string>
<key>bracketContentsForeground</key>
<string>#928374</string>
<key>bracketsForeground</key>
<string>#d5c4a1</string>
<key>guide</key>
<string>#EDDAB5</string>
<key>activeGuide</key>
<string>#7c6f64</string>
<key>stackGuide</key>
<string>#BEAD95</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation</string>
<key>scope</key>
<string>punctuation.definition.tag</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#076678</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation</string>
<key>scope</key>
<string>punctuation.definition.entity</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#8f3f71</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant</string>
<key>scope</key>
<string>constant</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#8f3f71</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant escape</string>
<key>scope</key>
<string>constant.character.escape</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant other</string>
<key>scope</key>
<string>constant.other</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Entity</string>
<key>scope</key>
<string>entity</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keyword</string>
<key>scope</key>
<string>keyword.operator.comparison, keyword.operator, keyword.operator.symbolic, keyword.operator.string, keyword.operator.assignment, keyword.operator.arithmetic, keyword.operator.class, keyword.operator.key, keyword.operator.logical</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#B23C15</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keyword</string>
<key>scope</key>
<string>keyword, keyword.operator.new, keyword.other, keyword.control</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#9d0006</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage</string>
<key>scope</key>
<string>storage</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#9d0006</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String</string>
<key>scope</key>
<string>string -string.unquoted.old-plist -string.unquoted.heredoc, string.unquoted.heredoc string</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comment</string>
<key>scope</key>
<string>comment</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#928374</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Regexp</string>
<key>scope</key>
<string>string.regexp constant.character.escape</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support</string>
<key>scope</key>
<string>support</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variable</string>
<key>scope</key>
<string>variable</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Lang Variable</string>
<key>scope</key>
<string>variable.language</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Function Call</string>
<key>scope</key>
<string>meta.function-call</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Invalid</string>
<key>scope</key>
<string>invalid</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#932b1e</string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Embedded Source</string>
<key>scope</key>
<string>text source, string.unquoted.heredoc, source source</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String embedded-source</string>
<key>scope</key>
<string>string.quoted source</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String constant</string>
<key>scope</key>
<string>string</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support.constant</string>
<key>scope</key>
<string>support.constant</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support.class</string>
<key>scope</key>
<string>support.class</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Meta.tag.A</string>
<key>scope</key>
<string>entity.name.tag</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Inner tag</string>
<key>scope</key>
<string>meta.tag, meta.tag entity</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css colors</string>
<key>scope</key>
<string>constant.other.color.rgb-value</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#076678</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css tag-name</string>
<key>scope</key>
<string>meta.selector.css entity.name.tag</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#9d0006</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css#id</string>
<key>scope</key>
<string>meta.selector.css, entity.other.attribute-name.id</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css.class</string>
<key>scope</key>
<string>meta.selector.css entity.other.attribute-name.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css property-name:</string>
<key>scope</key>
<string>support.type.property-name.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css @at-rule</string>
<key>scope</key>
<string>meta.preprocessor.at-rule keyword.control.at-rule</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css additional-constants</string>
<key>scope</key>
<string>meta.property-value constant</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css additional-constants</string>
<key>scope</key>
<string>meta.property-value support.constant.named-color.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#B23C15</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css constructor.argument</string>
<key>scope</key>
<string>meta.constructor.argument.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<!-- diff -->
<dict>
<key>name</key>
<string>diff.header</string>
<key>scope</key>
<string>meta.diff, meta.diff.header</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#076678</string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.deleted</string>
<key>scope</key>
<string>markup.deleted</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#9d0006</string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.changed</string>
<key>scope</key>
<string>markup.changed</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#b57614</string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.inserted</string>
<key>scope</key>
<string>markup.inserted</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#407959</string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<!-- markup -->
<dict>
<key>name</key>
<string>Bold Markup</string>
<key>scope</key>
<string>markup.bold</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Italic Markup</string>
<key>scope</key>
<string>markup.italic</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Heading Markup</string>
<key>scope</key>
<string>markup.heading</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<!-- Language Specific -->
<!-- PHP -->
<dict>
<key>name</key>
<string>PHP: class name</string>
<key>scope</key>
<string>entity.name.type.class.php</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>PHP: Comment</string>
<key>scope</key>
<string>keyword.other.phpdoc</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#928374</string>
</dict>
</dict>
<!-- CSS -->
<dict>
<key>name</key>
<string>CSS: numbers</string>
<key>scope</key>
<string>constant.numeric.css, keyword.other.unit.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8f3f71</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>CSS: entity dot, hash, comma, etc.</string>
<key>scope</key>
<string>punctuation.definition.entity.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<!-- JS -->
<dict>
<key>name</key>
<string>JS: variable</string>
<key>scope</key>
<string>variable.language.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>JS: unquoted labe</string>
<key>scope</key>
<string>string.unquoted.label.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<!-- SQL -->
<dict>
<key>name</key>
<string>Constant other sql</string>
<key>scope</key>
<string>constant.other.table-name.sql</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant other sql</string>
<key>scope</key>
<string>constant.other.database-name.sql</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<!-- Plugins -->
<!-- dired plugin -->
<dict>
<key>name</key>
<string>dired directory</string>
<key>scope</key>
<string>storage.type.dired.item.directory, dired.item.directory</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<!-- orgmode plugin -->
<dict>
<key>name</key>
<string>orgmode link</string>
<key>scope</key>
<string>orgmode.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
<key>fontStyle</key>
<string>underline</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode page</string>
<key>scope</key>
<string>orgmode.page</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode break</string>
<key>scope</key>
<string>orgmode.break</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8f3f71</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode headline</string>
<key>scope</key>
<string>orgmode.headline</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode tack</string>
<key>scope</key>
<string>orgmode.tack</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode follow up</string>
<key>scope</key>
<string>orgmode.follow_up</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode checkbox</string>
<key>scope</key>
<string>orgmode.checkbox</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode checkbox summary</string>
<key>scope</key>
<string>orgmode.checkbox.summary</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode tags</string>
<key>scope</key>
<string>orgmode.tags</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#9d0006</string>
</dict>
</dict>
</array>
<key>uuid</key>
<string>06CD1FB2-A00A-4F8C-97B2-60E131980454</string>
</dict>
</plist>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,297 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!-- Generated by: TmTheme-Editor -->
<!-- ============================================ -->
<!-- app: http://tmtheme-editor.herokuapp.com -->
<!-- code: https://github.com/aziz/tmTheme-Editor -->
<plist version="1.0">
<dict>
<key>name</key>
<string>Monokai</string>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>background</key>
<string>#272822</string>
<key>caret</key>
<string>#F8F8F0</string>
<key>foreground</key>
<string>#F8F8F2</string>
<key>invisibles</key>
<string>#3B3A32</string>
<key>lineHighlight</key>
<string>#3E3D32</string>
<key>selection</key>
<string>#49483E</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comment</string>
<key>scope</key>
<string>comment</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#75715E</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String</string>
<key>scope</key>
<string>string</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#E6DB74</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Number</string>
<key>scope</key>
<string>constant.numeric</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#AE81FF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Built-in constant</string>
<key>scope</key>
<string>constant.language</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#AE81FF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>User-defined constant</string>
<key>scope</key>
<string>constant.character, constant.other</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#AE81FF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variable</string>
<key>scope</key>
<string>variable</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keyword</string>
<key>scope</key>
<string>keyword</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#F92672</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage</string>
<key>scope</key>
<string>storage</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#F92672</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage type</string>
<key>scope</key>
<string>storage.type</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#66D9EF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Class name</string>
<key>scope</key>
<string>entity.name.class</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>underline</string>
<key>foreground</key>
<string>#A6E22E</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Inherited class</string>
<key>scope</key>
<string>entity.other.inherited-class</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic underline</string>
<key>foreground</key>
<string>#A6E22E</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Function name</string>
<key>scope</key>
<string>entity.name.function</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#A6E22E</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Function argument</string>
<key>scope</key>
<string>variable.parameter</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#FD971F</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Tag name</string>
<key>scope</key>
<string>entity.name.tag</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#F92672</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Tag attribute</string>
<key>scope</key>
<string>entity.other.attribute-name</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#A6E22E</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Library function</string>
<key>scope</key>
<string>support.function</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#66D9EF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Library constant</string>
<key>scope</key>
<string>support.constant</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#66D9EF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Library class&#x2f;type</string>
<key>scope</key>
<string>support.type, support.class</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#66D9EF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Library variable</string>
<key>scope</key>
<string>support.other.variable</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Invalid</string>
<key>scope</key>
<string>invalid</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#F92672</string>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#F8F8F0</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Invalid deprecated</string>
<key>scope</key>
<string>invalid.deprecated</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#AE81FF</string>
<key>foreground</key>
<string>#F8F8F0</string>
</dict>
</dict>
</array>
<key>uuid</key>
<string>D8D5E82E-3D5B-46B5-B38E-8C841C21347D</string>
<key>colorSpaceName</key>
<string>sRGB</string>
<key>semanticClass</key>
<string>theme.dark.monokai</string>
</dict>
</plist>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

7
test_site/config.toml Normal file
View file

@ -0,0 +1,7 @@
title = "My site"
base_url = "https://replace-this-with-your-url.com"
highlight_code = true
[extra.author]
name = "Vincent Prouillet"

View file

@ -0,0 +1,4 @@
+++
title = "Posts"
description = ""
+++

View file

@ -0,0 +1,7 @@
+++
title = "Fixed slug"
description = ""
slug = "something-else"
+++
A simple page with a slug defined

View file

@ -0,0 +1,7 @@
+++
title = "Fixed URL"
description = ""
url = "a-fixed-url"
+++
A simple page with fixed url

View file

@ -0,0 +1,6 @@
+++
title = "Simple"
description = ""
+++
A simple page

View file

@ -0,0 +1,6 @@
+++
title = "Python in posts"
description = ""
+++
Same filename but different path

View file

@ -0,0 +1,6 @@
+++
title = "Simple"
description = ""
+++
A simple page

View file

@ -0,0 +1,4 @@
+++
title = "Tutorials"
description = ""
+++

View file

@ -0,0 +1,4 @@
+++
title = "DevOps"
description = ""
+++

View file

@ -0,0 +1,6 @@
+++
title = "Docker"
description = ""
+++
A simple page

View file

@ -0,0 +1,6 @@
+++
title = "Nix"
description = ""
+++
A simple page

View file

@ -0,0 +1,4 @@
+++
title = "Programming"
description = ""
+++

View file

@ -0,0 +1,6 @@
+++
title = "Python tutorial"
description = ""
+++
A simple page

View file

@ -0,0 +1,6 @@
+++
title = "Rust"
description = ""
+++
A simple page

View file

@ -0,0 +1,7 @@
+++
title = "With assets"
description = "hey there"
slug = "with-assets"
+++
Hello world

View file

View file

@ -0,0 +1,3 @@
body {
color: red;
}

View file

@ -0,0 +1,3 @@
{% for category in categories %}
{{ category.name }} {{ category.slug }} {{ category.count }}
{% endfor %}

View file

@ -0,0 +1,8 @@
Category: {{ category }}
{% for page in pages %}
<article>
<h3 class="post__title"><a href="{{ page.url }}">{{ page.title }}</a></h3>
</article>
{% endfor %}

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="{{ config.language_code }}">
<head>
<meta charset="UTF-8">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{ config.description }}">
<meta name="author" content="{{ config.extra.author.name }}">
<link href="https://fonts.googleapis.com/css?family=Fira+Mono|Fira+Sans|Merriweather" rel="stylesheet">
<link href="site.css" rel="stylesheet">
<title>{{ config.title }}</title>
</head>
<body>
<div class="content">
{% block content %}
<div class="list-posts">
{% for page in pages %}
<article>
<h3 class="post__title"><a href="{{ page.url }}">{{ page.title }}</a></h3>
</article>
{% endfor %}
</div>
{% endblock content %}
</div>
</body>
</html>

View file

@ -0,0 +1,5 @@
{% extends "index.html" %}
{% block content %}
{{ page.content | safe }}
{% endblock content %}

View file

@ -0,0 +1,10 @@
{% extends "index.html" %}
{% block content %}
{% for page in section.pages %}
{{page.title}}
{% endfor %}
{% for subsection in section.subsections %}
{{subsection.title}}
{% endfor %}
{% endblock content %}

View file

@ -0,0 +1,7 @@
Tag: {{ tag }}
{% for page in pages %}
<article>
<h3 class="post__title"><a href="{{ page.url }}">{{ page.title }}</a></h3>
</article>
{% endfor %}

View file

@ -0,0 +1,3 @@
{% for tag in tags %}
{{ tag.name }} {{ tag.slug }} {{ tag.count }}
{% endfor %}

197
tests/front_matter.rs Normal file
View file

@ -0,0 +1,197 @@
extern crate gutenberg;
extern crate tera;
use std::path::Path;
use gutenberg::{FrontMatter, split_content};
use tera::to_value;
#[test]
fn test_can_parse_a_valid_front_matter() {
let content = r#"
title = "Hello"
description = "hey there""#;
let res = FrontMatter::parse(content);
println!("{:?}", res);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(res.title, "Hello".to_string());
assert_eq!(res.description, "hey there".to_string());
}
#[test]
fn test_can_parse_tags() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
tags = ["rust", "html"]"#;
let res = FrontMatter::parse(content);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(res.title, "Hello".to_string());
assert_eq!(res.slug.unwrap(), "hello-world".to_string());
assert_eq!(res.tags.unwrap(), ["rust".to_string(), "html".to_string()]);
}
#[test]
fn test_can_parse_extra_attributes_in_frontmatter() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
[extra]
language = "en"
authors = ["Bob", "Alice"]"#;
let res = FrontMatter::parse(content);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(res.title, "Hello".to_string());
assert_eq!(res.slug.unwrap(), "hello-world".to_string());
let extra = res.extra.unwrap();
assert_eq!(extra.get("language").unwrap(), &to_value("en").unwrap());
assert_eq!(
extra.get("authors").unwrap(),
&to_value(["Bob".to_string(), "Alice".to_string()]).unwrap()
);
}
#[test]
fn test_is_ok_with_url_instead_of_slug() {
let content = r#"
title = "Hello"
description = "hey there"
url = "hello-world""#;
let res = FrontMatter::parse(content);
assert!(res.is_ok());
let res = res.unwrap();
assert!(res.slug.is_none());
assert_eq!(res.url.unwrap(), "hello-world".to_string());
}
#[test]
fn test_errors_with_empty_front_matter() {
let content = r#" "#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn test_errors_with_invalid_front_matter() {
let content = r#"title = 1\n"#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn test_errors_with_missing_required_value_front_matter() {
let content = r#"title = """#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn test_errors_on_non_string_tag() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
tags = ["rust", 1]"#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn test_errors_on_present_but_empty_slug() {
let content = r#"
title = "Hello"
description = "hey there"
slug = """#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn test_errors_on_present_but_empty_url() {
let content = r#"
title = "Hello"
description = "hey there"
url = """#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn test_parse_date_yyyy_mm_dd() {
let content = r#"
title = "Hello"
description = "hey there"
date = "2016-10-10""#;
let res = FrontMatter::parse(content).unwrap();
assert!(res.parse_date().is_some());
}
#[test]
fn test_parse_date_rfc3339() {
let content = r#"
title = "Hello"
description = "hey there"
date = "2002-10-02T15:00:00Z""#;
let res = FrontMatter::parse(content).unwrap();
assert!(res.parse_date().is_some());
}
#[test]
fn test_cant_parse_random_date_format() {
let content = r#"
title = "Hello"
description = "hey there"
date = "2002/10/12""#;
let res = FrontMatter::parse(content).unwrap();
assert!(res.parse_date().is_none());
}
#[test]
fn test_can_split_content_valid() {
let content = r#"
+++
title = "Title"
description = "hey there"
date = "2002/10/12"
+++
Hello
"#;
let (front_matter, content) = split_content(Path::new(""), content).unwrap();
assert_eq!(content, "Hello\n");
assert_eq!(front_matter.title, "Title");
}
#[test]
fn test_can_split_content_with_only_frontmatter_valid() {
let content = r#"
+++
title = "Title"
description = "hey there"
date = "2002/10/12"
+++"#;
let (front_matter, content) = split_content(Path::new(""), content).unwrap();
assert_eq!(content, "");
assert_eq!(front_matter.title, "Title");
}
#[test]
fn test_error_if_cannot_locate_frontmatter() {
let content = r#"
+++
title = "Title"
description = "hey there"
date = "2002/10/12"
"#;
let res = split_content(Path::new(""), content);
assert!(res.is_err());
}

249
tests/page.rs Normal file
View file

@ -0,0 +1,249 @@
extern crate gutenberg;
extern crate tempdir;
use tempdir::TempDir;
use std::fs::File;
use std::path::Path;
use gutenberg::{Page, Config};
#[test]
fn test_can_parse_a_valid_page() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse(Path::new("post.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.meta.title, "Hello".to_string());
assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string());
assert_eq!(page.raw_content, "Hello world".to_string());
assert_eq!(page.content, "<p>Hello world</p>\n".to_string());
}
#[test]
fn test_can_find_one_parent_directory() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse(Path::new("content/posts/intro.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.components, vec!["posts".to_string()]);
}
#[test]
fn test_can_find_multiple_parent_directories() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse(Path::new("content/posts/intro/start.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.components, vec!["posts".to_string(), "intro".to_string()]);
}
#[test]
fn test_can_make_url_from_sections_and_slug() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let mut conf = Config::default();
conf.base_url = "http://hello.com/".to_string();
let res = Page::parse(Path::new("content/posts/intro/start.md"), content, &conf);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.url, "posts/intro/hello-world");
assert_eq!(page.permalink, "http://hello.com/posts/intro/hello-world");
}
#[test]
fn test_can_make_permalink_with_non_trailing_slash_base_url() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let mut conf = Config::default();
conf.base_url = "http://hello.com".to_string();
let res = Page::parse(Path::new("content/posts/intro/hello-world.md"), content, &conf);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.url, "posts/intro/hello-world");
assert_eq!(page.permalink, format!("{}{}", conf.base_url, "/posts/intro/hello-world"));
}
#[test]
fn test_can_make_url_from_slug_only() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse(Path::new("start.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.url, "hello-world");
assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "hello-world"));
}
#[test]
fn test_errors_on_invalid_front_matter_format() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse(Path::new("start.md"), content, &Config::default());
assert!(res.is_err());
}
#[test]
fn test_can_make_slug_from_non_slug_filename() {
let content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world"#;
let res = Page::parse(Path::new("file with space.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.slug, "file-with-space");
assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "file-with-space"));
}
#[test]
fn test_trim_slug_if_needed() {
let content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world"#;
let res = Page::parse(Path::new(" file with space.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.slug, "file-with-space");
assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "file-with-space"));
}
#[test]
fn test_reading_analytics_short() {
let content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world"#;
let res = Page::parse(Path::new("hello.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
let (word_count, reading_time) = page.get_reading_analytics();
assert_eq!(word_count, 2);
assert_eq!(reading_time, 0);
}
#[test]
fn test_reading_analytics_long() {
let mut content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world"#.to_string();
for _ in 0..1000 {
content.push_str(" Hello world");
}
let res = Page::parse(Path::new("hello.md"), &content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
let (word_count, reading_time) = page.get_reading_analytics();
assert_eq!(word_count, 2002);
assert_eq!(reading_time, 10);
}
#[test]
fn test_automatic_summary_is_empty_string() {
let content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world"#.to_string();
let res = Page::parse(Path::new("hello.md"), &content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.summary, "");
}
#[test]
fn test_can_specify_summary() {
let content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world
<!-- more -->
"#.to_string();
let res = Page::parse(Path::new("hello.md"), &content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.summary, "<p>Hello world</p>\n");
}
#[test]
fn test_can_auto_detect_when_highlighting_needed() {
let content = r#"
+++
title = "Hello"
description = "hey there"
+++
```
Hey there
```
"#.to_string();
let mut config = Config::default();
config.highlight_code = Some(true);
let res = Page::parse(Path::new("hello.md"), &content, &config);
assert!(res.is_ok());
let page = res.unwrap();
assert!(page.content.starts_with("<pre"));
}
#[test]
fn test_file_not_named_index_with_assets() {
let tmp_dir = TempDir::new("example").expect("create temp dir");
File::create(tmp_dir.path().join("something.md")).unwrap();
File::create(tmp_dir.path().join("example.js")).unwrap();
File::create(tmp_dir.path().join("graph.jpg")).unwrap();
File::create(tmp_dir.path().join("fail.png")).unwrap();
let page = Page::from_file(tmp_dir.path().join("something.md"), &Config::default());
assert!(page.is_err());
}

258
tests/site.rs Normal file
View file

@ -0,0 +1,258 @@
extern crate gutenberg;
extern crate tempdir;
extern crate glob;
use std::env;
use std::path::Path;
use std::fs::File;
use std::io::prelude::*;
// use glob::glob;
use tempdir::TempDir;
use gutenberg::{Site};
#[test]
fn test_can_parse_site() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let site = Site::new(&path).unwrap();
// Correct number of pages (sections are pages too)
assert_eq!(site.pages.len(), 10);
let posts_path = path.join("content").join("posts");
// Make sure we remove all the pwd + content from the sections
let basic = &site.pages[&posts_path.join("simple.md")];
assert_eq!(basic.components, vec!["posts".to_string()]);
// Make sure the page with a url doesn't have any sections
let url_post = &site.pages[&posts_path.join("fixed-url.md")];
assert!(url_post.components.is_empty());
// Make sure the article in a folder with only asset doesn't get counted as a section
let asset_folder_post = &site.pages[&posts_path.join("with-assets").join("index.md")];
assert_eq!(asset_folder_post.components, vec!["posts".to_string()]);
// That we have the right number of sections
assert_eq!(site.sections.len(), 4);
// And that the sections are correct
let posts_section = &site.sections[&posts_path];
assert_eq!(posts_section.subsections.len(), 1);
assert_eq!(posts_section.pages.len(), 5);
let tutorials_section = &site.sections[&posts_path.join("tutorials")];
assert_eq!(tutorials_section.subsections.len(), 2);
assert_eq!(tutorials_section.pages.len(), 0);
let devops_section = &site.sections[&posts_path.join("tutorials").join("devops")];
assert_eq!(devops_section.subsections.len(), 0);
assert_eq!(devops_section.pages.len(), 2);
let prog_section = &site.sections[&posts_path.join("tutorials").join("programming")];
assert_eq!(prog_section.subsections.len(), 0);
assert_eq!(prog_section.pages.len(), 2);
}
// 2 helper macros to make all the build testing more bearable
macro_rules! file_exists {
($root: expr, $path: expr) => {
{
let mut path = $root.clone();
for component in $path.split("/") {
path = path.join(component);
}
Path::new(&path).exists()
}
}
}
macro_rules! file_contains {
($root: expr, $path: expr, $text: expr) => {
{
let mut path = $root.clone();
for component in $path.split("/") {
path = path.join(component);
}
let mut file = File::open(&path).unwrap();
let mut s = String::new();
file.read_to_string(&mut s).unwrap();
s.contains($text)
}
}
}
#[test]
fn test_can_build_site_without_live_reload() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path).unwrap();
let tmp_dir = TempDir::new("example").expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
site.build().unwrap();
assert!(Path::new(&public).exists());
assert!(file_exists!(public, "index.html"));
assert!(file_exists!(public, "sitemap.xml"));
assert!(file_exists!(public, "a-fixed-url/index.html"));
assert!(file_exists!(public, "posts/python/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/nix/index.html"));
assert!(file_exists!(public, "posts/with-assets/index.html"));
// Sections
assert!(file_exists!(public, "posts/index.html"));
assert!(file_exists!(public, "posts/tutorials/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/index.html"));
assert!(file_exists!(public, "posts/tutorials/programming/index.html"));
// TODO: add assertion for syntax highlighting
// No tags or categories
assert_eq!(file_exists!(public, "categories/index.html"), false);
assert_eq!(file_exists!(public, "tags/index.html"), false);
// no live reload code
assert_eq!(file_contains!(public, "index.html", "/livereload.js?port=1112&mindelay=10"), false);
// Both pages and sections are in the sitemap
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/posts/simple</loc>"));
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/posts</loc>"));
}
#[test]
fn test_can_build_site_with_live_reload() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path).unwrap();
let tmp_dir = TempDir::new("example").expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
site.enable_live_reload();
site.build().unwrap();
assert!(Path::new(&public).exists());
assert!(file_exists!(public, "index.html"));
assert!(file_exists!(public, "sitemap.xml"));
assert!(file_exists!(public, "a-fixed-url/index.html"));
assert!(file_exists!(public, "posts/python/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/nix/index.html"));
assert!(file_exists!(public, "posts/with-assets/index.html"));
// Sections
assert!(file_exists!(public, "posts/index.html"));
assert!(file_exists!(public, "posts/tutorials/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/index.html"));
assert!(file_exists!(public, "posts/tutorials/programming/index.html"));
// TODO: add assertion for syntax highlighting
// No tags or categories
assert_eq!(file_exists!(public, "categories/index.html"), false);
assert_eq!(file_exists!(public, "tags/index.html"), false);
// no live reload code
assert!(file_contains!(public, "index.html", "/livereload.js?port=1112&mindelay=10"));
}
#[test]
fn test_can_build_site_with_categories() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path).unwrap();
for (i, page) in site.pages.values_mut().enumerate() {
page.meta.category = if i % 2 == 0 {
Some("A".to_string())
} else {
Some("B".to_string())
};
}
site.parse_tags_and_categories();
let tmp_dir = TempDir::new("example").expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
site.build().unwrap();
assert!(Path::new(&public).exists());
assert_eq!(site.categories.len(), 2);
assert!(file_exists!(public, "index.html"));
assert!(file_exists!(public, "sitemap.xml"));
assert!(file_exists!(public, "a-fixed-url/index.html"));
assert!(file_exists!(public, "posts/python/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/nix/index.html"));
assert!(file_exists!(public, "posts/with-assets/index.html"));
// Sections
assert!(file_exists!(public, "posts/index.html"));
assert!(file_exists!(public, "posts/tutorials/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/index.html"));
assert!(file_exists!(public, "posts/tutorials/programming/index.html"));
// TODO: add assertion for syntax highlighting
// Categories are there
assert!(file_exists!(public, "categories/index.html"));
assert!(file_exists!(public, "categories/a/index.html"));
assert!(file_exists!(public, "categories/b/index.html"));
// Tags aren't
assert_eq!(file_exists!(public, "tags/index.html"), false);
// Categories are in the sitemap
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/categories</loc>"));
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/categories/a</loc>"));
}
#[test]
fn test_can_build_site_with_tags() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path).unwrap();
for (i, page) in site.pages.values_mut().enumerate() {
page.meta.tags = if i % 2 == 0 {
Some(vec!["tag1".to_string(), "tag2".to_string()])
} else {
Some(vec!["tag with space".to_string()])
};
}
site.parse_tags_and_categories();
let tmp_dir = TempDir::new("example").expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
site.build().unwrap();
assert!(Path::new(&public).exists());
assert_eq!(site.tags.len(), 3);
assert!(file_exists!(public, "index.html"));
assert!(file_exists!(public, "sitemap.xml"));
assert!(file_exists!(public, "a-fixed-url/index.html"));
assert!(file_exists!(public, "posts/python/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/nix/index.html"));
assert!(file_exists!(public, "posts/with-assets/index.html"));
// Sections
assert!(file_exists!(public, "posts/index.html"));
assert!(file_exists!(public, "posts/tutorials/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/index.html"));
assert!(file_exists!(public, "posts/tutorials/programming/index.html"));
// TODO: add assertion for syntax highlighting
// Tags are there
assert!(file_exists!(public, "tags/index.html"));
assert!(file_exists!(public, "tags/tag1/index.html"));
assert!(file_exists!(public, "tags/tag2/index.html"));
assert!(file_exists!(public, "tags/tag-with-space/index.html"));
// Categories aren't
assert_eq!(file_exists!(public, "categories/index.html"), false);
// Tags are in the sitemap
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/tags</loc>"));
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/tags/tag-with-space</loc>"));
}