@@ -1,4 +1,3 @@ | |||||
target | target | ||||
.idea/ | .idea/ | ||||
site | |||||
theme | |||||
test_site/public |
@@ -0,0 +1,10 @@ | |||||
language: rust | |||||
cache: cargo | |||||
rust: | |||||
- nightly | |||||
- beta | |||||
- stable | |||||
notifications: | |||||
email: false |
@@ -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] | |||||
version = "0.3" | |||||
default-features = false | |||||
features = ["serde"] | |||||
# Below is for the serve cmd | |||||
staticfile = "0.4" | |||||
iron = "0.5" | |||||
mount = "0.3" | |||||
notify = "4" | |||||
ws = "0.6" | |||||
[dev-dependencies] | |||||
tempdir = "0.3" |
@@ -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 |
@@ -1,87 +1,9 @@ | |||||
use std::collections::HashMap; | |||||
use std::fs::{create_dir, remove_dir_all}; | |||||
use std::path::Path; | |||||
use std::env; | |||||
use glob::glob; | |||||
use tera::{Tera, Context}; | |||||
use gutenberg::errors::Result; | |||||
use gutenberg::Site; | |||||
use config:: Config; | |||||
use errors::{Result, ResultExt}; | |||||
use page::{Page, order_pages}; | |||||
use utils::create_file; | |||||
pub fn build(config: Config) -> 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")?; | |||||
} | |||||
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(¤t_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(¤t_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(§ion); | |||||
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)?) | |||||
pub fn build() -> Result<()> { | |||||
Site::new(env::current_dir().unwrap())?.build() | |||||
} | } |
@@ -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 utils::create_file; | |||||
use gutenberg::errors::Result; | |||||
use gutenberg::create_file; | |||||
const CONFIG: &'static str = r#" | const CONFIG: &'static str = r#" | ||||
@@ -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; |
@@ -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)); | |||||
} | |||||
} | |||||
} |
@@ -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) | |||||
pub disable_rss: Option<bool>, | |||||
/// Whether to generate RSS, defaults to false | |||||
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); | |||||
} | |||||
} | |||||
} | } | ||||
@@ -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); | ||||
@@ -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, ResultExt}; | |||||
use errors::{Result}; | |||||
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) | ||||
} | } | ||||
} | |||||
#[cfg(test)] | |||||
mod tests { | |||||
use super::{FrontMatter}; | |||||
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()]); | |||||
/// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime | |||||
pub fn parse_date(&self) -> Option<NaiveDateTime> { | |||||
match self.date { | |||||
Some(ref d) => { | |||||
if d.contains('T') { | |||||
DateTime::parse_from_rfc3339(d).ok().and_then(|s| Some(s.naive_local())) | |||||
} else { | |||||
NaiveDate::parse_from_str(d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0,0,0))) | |||||
} | |||||
}, | |||||
None => None, | |||||
} | |||||
} | } | ||||
} | |||||
#[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()); | |||||
/// 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()); | |||||
} | } | ||||
#[test] | |||||
fn test_errors_with_empty_front_matter() { | |||||
let content = r#" "#; | |||||
let res = FrontMatter::parse(content); | |||||
assert!(res.is_err()); | |||||
} | |||||
// 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]; | |||||
#[test] | |||||
fn test_errors_with_invalid_front_matter() { | |||||
let content = r#"title = 1\n"#; | |||||
let res = FrontMatter::parse(content); | |||||
assert!(res.is_err()); | |||||
} | |||||
// 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()))?; | |||||
#[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()); | |||||
} | |||||
Ok((meta, content.to_string())) | |||||
} | } |
@@ -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; |
@@ -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 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 gutenberg; | |||||
extern crate chrono; | |||||
extern crate staticfile; | |||||
extern crate iron; | |||||
extern crate mount; | |||||
extern crate notify; | |||||
extern crate ws; | |||||
mod utils; | |||||
mod config; | |||||
mod errors; | |||||
mod cmd; | |||||
mod page; | |||||
mod front_matter; | |||||
use std::time::Instant; | |||||
use chrono::Duration; | |||||
mod cmd; | |||||
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; | |||||
// Get and parse the config. | |||||
// If it doesn't succeed, exit | |||||
fn get_config() -> Config { | |||||
match Config::from_file("config.toml") { | |||||
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); | |||||
} | |||||
if duration_ms < 1000.0 { | |||||
println!("Done in {}ms.\n", duration_ms); | |||||
} else { | |||||
let duration_sec = duration_ms / 1000.0; | |||||
println!("Done in {:.1}s.\n", ((duration_sec * 10.0).round() / 10.0)); | |||||
} | } | ||||
} | } | ||||
@@ -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!(), | ||||
} | } | ||||
} | } | ||||
@@ -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>" | |||||
); | |||||
} | |||||
} |
@@ -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::io::prelude::*; | |||||
use std::path::Path; | |||||
use std::cmp::Ordering; | |||||
use std::fs::{read_dir}; | |||||
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}; | |||||
/// 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, | |||||
} | |||||
} | |||||
} | |||||
lazy_static! { | |||||
static ref PAGE_RE: Regex = Regex::new(r"^\n?\+\+\+\n((?s).*(?-s))\+\+\+\n((?s).*(?-s))$").unwrap(); | |||||
assets | |||||
} | } | ||||
#[derive(Clone, Debug, PartialEq, Deserialize)] | |||||
#[derive(Clone, Debug, PartialEq)] | |||||
pub struct Page { | pub struct Page { | ||||
/// .md filepath, excluding the content/ bit | |||||
#[serde(skip_serializing)] | |||||
pub filepath: String, | |||||
/// The .md path | |||||
pub file_path: PathBuf, | |||||
/// 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 filename: String, | |||||
/// The directories above our .md file are called sections | |||||
/// for example a file at content/kb/solutions/blabla.md will have 2 sections: | |||||
pub file_name: String, | |||||
/// The directories above our .md file | |||||
/// for example a file at content/kb/solutions/blabla.md will have 2 components: | |||||
/// `kb` and `solutions` | /// `kb` and `solutions` | ||||
#[serde(skip_serializing)] | |||||
pub sections: Vec<String>, | |||||
pub components: 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(), | |||||
filename: "".to_string(), | |||||
sections: vec![], | |||||
file_path: PathBuf::new(), | |||||
parent_path: PathBuf::new(), | |||||
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. | |||||
/// First tries to find the slug in the meta and defaults to filename otherwise | |||||
pub fn get_slug(&self) -> String { | |||||
if let Some(ref slug) = self.meta.slug { | |||||
slug.to_string() | |||||
} else { | |||||
self.filename.clone() | |||||
} | |||||
/// Get word count and estimated reading time | |||||
pub fn get_reading_analytics(&self) -> (usize, usize) { | |||||
// Only works for latin language but good enough for a start | |||||
let word_count: usize = self.raw_content.split_whitespace().count(); | |||||
// https://help.medium.com/hc/en-us/articles/214991667-Read-time | |||||
// 275 seems a bit too high though | |||||
(word_count, (word_count / 200)) | |||||
} | } | ||||
// Parse a page given the content of the .md file | |||||
// Files without front matter or with invalid front matter are considered | |||||
// erroneous | |||||
pub fn parse(filepath: &str, content: &str) -> Result<Page> { | |||||
/// Parse a page given the content of the .md file | |||||
/// Files without front matter or with invalid front matter are considered | |||||
/// erroneous | |||||
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) { | |||||
bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", filepath); | |||||
} | |||||
let (meta, content) = split_content(file_path, content)?; | |||||
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 | |||||
}; | |||||
// 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]; | |||||
page.content = markdown_to_html(&page.raw_content, should_highlight); | |||||
// 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 `{}`", filepath))?; | |||||
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) | |||||
} | |||||
} | |||||
let mut page = Page::new(meta); | |||||
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 | |||||
let path = Path::new(file_path); | |||||
page.file_name = path.file_stem().unwrap().to_string_lossy().to_string(); | |||||
page.slug = { | |||||
if let Some(ref slug) = page.meta.slug { | |||||
slug.trim().to_string() | |||||
} else { | |||||
slugify(page.file_name.clone()) | |||||
} | |||||
}; | }; | ||||
// 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() { | |||||
let path = Path::new(filepath); | |||||
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())); | |||||
if let Some(ref u) = page.meta.url { | |||||
page.url = u.trim().to_string(); | |||||
} else { | |||||
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 { | } else { | ||||
page.meta.url = Some(format!("/{}", page.get_slug())); | |||||
}; | |||||
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(); | |||||
File::open(path) | |||||
.chain_err(|| format!("Failed to open '{:?}'", path.display()))? | |||||
.read_to_string(&mut content)?; | |||||
if !page.assets.is_empty() && page.file_name != "index" { | |||||
bail!("Page `{}` has assets but is not named index.md", path.display()); | |||||
} | |||||
Ok(page) | |||||
// 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 { | |||||
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() | ||||
} | |||||
} | |||||
pub fn render_html(&mut self, tera: &Tera, config: &Config) -> Result<String> { | |||||
let tpl = self.get_layout_name(); | |||||
}; | |||||
// TODO: create a helper to create context to ensure all contexts | |||||
// have the same names | |||||
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) | |||||
.chain_err(|| "Error while rendering template") | |||||
tera.render(&tpl_name, &context) | |||||
.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("url", &self.meta.url)?; | |||||
state.serialize_field("slug", &self.slug)?; | |||||
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 | |||||
// TODO: impl PartialOrd on Vec<Page> so we can use sort()? | |||||
pub fn order_pages(pages: Vec<Page>) -> Vec<Page> { | |||||
pages | |||||
} | |||||
impl PartialOrd for Page { | |||||
fn partial_cmp(&self, other: &Page) -> Option<Ordering> { | |||||
if self.meta.date.is_none() { | |||||
return Some(Ordering::Less); | |||||
} | |||||
if other.meta.date.is_none() { | |||||
return Some(Ordering::Greater); | |||||
} | |||||
#[cfg(test)] | |||||
mod tests { | |||||
use super::{Page}; | |||||
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); | |||||
} | |||||
#[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("post.md", content); | |||||
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()); | |||||
Some(Ordering::Equal) | |||||
} | } | ||||
} | |||||
#[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()]); | |||||
} | |||||
#[cfg(test)] | |||||
mod tests { | |||||
use tempdir::TempDir; | |||||
#[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"); | |||||
} | |||||
use std::fs::File; | |||||
#[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"); | |||||
} | |||||
use super::{find_related_assets}; | |||||
#[test] | #[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()); | |||||
fn test_find_related_assets() { | |||||
let tmp_dir = TempDir::new("example").expect("create temp dir"); | |||||
File::create(tmp_dir.path().join("index.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 assets = find_related_assets(tmp_dir.path()); | |||||
assert_eq!(assets.len(), 3); | |||||
assert_eq!(assets.iter().filter(|p| p.extension().unwrap() != "md").count(), 3); | |||||
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); | |||||
} | } | ||||
} | } |
@@ -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(§ion.file_path); | |||||
section.url = section.components.join("/"); | |||||
section.permalink = section.components.join("/"); | |||||
section.permalink = config.make_permalink(§ion.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() | |||||
} | |||||
} |
@@ -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(¤t_path)?; | |||||
} | |||||
} | |||||
// Make sure the folder exists | |||||
create_directory(¤t_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, ¤t_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 §ion.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(()) | |||||
} | |||||
} |
@@ -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> |
@@ -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> |
@@ -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()]); | |||||
} | |||||
} |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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/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> |
@@ -0,0 +1,7 @@ | |||||
title = "My site" | |||||
base_url = "https://replace-this-with-your-url.com" | |||||
highlight_code = true | |||||
[extra.author] | |||||
name = "Vincent Prouillet" |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Posts" | |||||
description = "" | |||||
+++ |
@@ -0,0 +1,7 @@ | |||||
+++ | |||||
title = "Fixed slug" | |||||
description = "" | |||||
slug = "something-else" | |||||
+++ | |||||
A simple page with a slug defined |
@@ -0,0 +1,7 @@ | |||||
+++ | |||||
title = "Fixed URL" | |||||
description = "" | |||||
url = "a-fixed-url" | |||||
+++ | |||||
A simple page with fixed url |
@@ -0,0 +1,6 @@ | |||||
+++ | |||||
title = "Simple" | |||||
description = "" | |||||
+++ | |||||
A simple page |
@@ -0,0 +1,6 @@ | |||||
+++ | |||||
title = "Python in posts" | |||||
description = "" | |||||
+++ | |||||
Same filename but different path |
@@ -0,0 +1,6 @@ | |||||
+++ | |||||
title = "Simple" | |||||
description = "" | |||||
+++ | |||||
A simple page |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Tutorials" | |||||
description = "" | |||||
+++ |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "DevOps" | |||||
description = "" | |||||
+++ |
@@ -0,0 +1,6 @@ | |||||
+++ | |||||
title = "Docker" | |||||
description = "" | |||||
+++ | |||||
A simple page |
@@ -0,0 +1,6 @@ | |||||
+++ | |||||
title = "Nix" | |||||
description = "" | |||||
+++ | |||||
A simple page |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Programming" | |||||
description = "" | |||||
+++ |
@@ -0,0 +1,6 @@ | |||||
+++ | |||||
title = "Python tutorial" | |||||
description = "" | |||||
+++ | |||||
A simple page |
@@ -0,0 +1,6 @@ | |||||
+++ | |||||
title = "Rust" | |||||
description = "" | |||||
+++ | |||||
A simple page |
@@ -0,0 +1,7 @@ | |||||
+++ | |||||
title = "With assets" | |||||
description = "hey there" | |||||
slug = "with-assets" | |||||
+++ | |||||
Hello world |
@@ -0,0 +1,3 @@ | |||||
body { | |||||
color: red; | |||||
} |
@@ -0,0 +1,3 @@ | |||||
{% for category in categories %} | |||||
{{ category.name }} {{ category.slug }} {{ category.count }} | |||||
{% endfor %} |
@@ -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 %} |
@@ -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> |
@@ -0,0 +1,5 @@ | |||||
{% extends "index.html" %} | |||||
{% block content %} | |||||
{{ page.content | safe }} | |||||
{% endblock content %} |
@@ -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 %} |
@@ -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 %} |
@@ -0,0 +1,3 @@ | |||||
{% for tag in tags %} | |||||
{{ tag.name }} {{ tag.slug }} {{ tag.count }} | |||||
{% endfor %} |
@@ -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()); | |||||
} |
@@ -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()); | |||||
} |
@@ -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>")); | |||||
} |