@@ -1,4 +1,3 @@ | |||
target | |||
.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] | |||
name = "gutenberg" | |||
version = "0.1.0" | |||
version = "0.0.1" | |||
authors = ["Vincent Prouillet <vincent@wearewizards.io>"] | |||
license = "MIT" | |||
readme = "README.md" | |||
@@ -9,8 +9,11 @@ homepage = "https://github.com/Keats/gutenberg" | |||
repository = "https://github.com/Keats/gutenberg" | |||
keywords = ["static", "site", "generator", "blog"] | |||
[[bin]] | |||
name = "gutenberg" | |||
[dependencies] | |||
error-chain = "0.9" | |||
error-chain = "0.10" | |||
clap = "2.19" | |||
walkdir = "1" | |||
pulldown-cmark = "0" | |||
@@ -20,12 +23,20 @@ glob = "0.2" | |||
serde = "0.9" | |||
serde_json = "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" | |||
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 | |||
markdown -> HTML for the content | |||
### Themes | |||
Gallery at https://tmtheme-editor.herokuapp.com/#!/editor/theme/Agola%20Dark | |||
# TODO: | |||
- find a way to add tests | |||
- syntax highlighting | |||
- pass a --config arg to the CLI to change from `config.toml` | |||
- 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::path::Path; | |||
use errors::Result; | |||
use utils::create_file; | |||
use gutenberg::errors::Result; | |||
use gutenberg::create_file; | |||
const CONFIG: &'static str = r#" | |||
@@ -1,5 +1,7 @@ | |||
mod init; | |||
mod build; | |||
mod serve; | |||
pub use self::init::create_new_project; | |||
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}; | |||
// TODO: disable tag(s)/category(ies) page generation | |||
// TO ADD: | |||
// highlight code theme | |||
// generate_tags_pages | |||
// generate_categories_pages | |||
#[derive(Debug, PartialEq, Serialize, Deserialize)] | |||
pub struct Config { | |||
/// Title of the site | |||
pub title: String, | |||
/// Base URL of the site | |||
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 | |||
pub description: Option<String>, | |||
/// The language used in the site. Defaults to "en" | |||
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 | |||
pub extra: Option<HashMap<String, Toml>>, | |||
} | |||
@@ -32,10 +41,19 @@ impl Config { | |||
Ok(c) => c, | |||
Err(e) => bail!(e) | |||
}; | |||
if config.language_code.is_none() { | |||
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) | |||
} | |||
@@ -48,6 +66,44 @@ impl Config { | |||
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; | |||
error_chain! { | |||
errors { | |||
} | |||
errors {} | |||
links { | |||
Tera(tera::Error, tera::ErrorKind); | |||
@@ -1,21 +1,32 @@ | |||
use std::collections::HashMap; | |||
use std::path::Path; | |||
use toml; | |||
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 | |||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | |||
pub struct FrontMatter { | |||
// <title> of the page | |||
// Mandatory fields | |||
/// <title> of the page | |||
pub title: String, | |||
/// Description that appears when linked, e.g. on twitter | |||
pub description: String, | |||
// Optional stuff | |||
/// Date if we want to order pages (ie blog post) | |||
pub date: Option<String>, | |||
/// The page slug. Will be used instead of the filename if present | |||
@@ -31,9 +42,9 @@ pub struct FrontMatter { | |||
pub draft: Option<bool>, | |||
/// Only one category allowed | |||
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)] | |||
pub layout: Option<String>, | |||
pub template: Option<String>, | |||
/// Any extra parameter present in the front matter | |||
pub extra: Option<HashMap<String, Value>>, | |||
} | |||
@@ -44,7 +55,7 @@ impl FrontMatter { | |||
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, | |||
Err(e) => bail!(e), | |||
}; | |||
@@ -63,129 +74,40 @@ impl FrontMatter { | |||
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; | |||
#[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 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 => | |||
(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(); | |||
match matches.subcommand() { | |||
@@ -64,7 +56,6 @@ fn main() { | |||
match cmd::create_new_project(matches.value_of("name").unwrap()) { | |||
Ok(()) => { | |||
println!("Project created"); | |||
println!("You will now need to set a theme in `config.toml`"); | |||
}, | |||
Err(e) => { | |||
println!("Error: {}", e); | |||
@@ -73,9 +64,11 @@ fn main() { | |||
}; | |||
}, | |||
("build", Some(_)) => { | |||
match cmd::build(get_config()) { | |||
println!("Building site"); | |||
let start = Instant::now(); | |||
match cmd::build() { | |||
Ok(()) => { | |||
println!("Project built."); | |||
report_elapsed_time(start); | |||
}, | |||
Err(e) => { | |||
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!(), | |||
} | |||
} | |||
@@ -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 | |||
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 pulldown_cmark as cmark; | |||
use regex::Regex; | |||
use tera::{Tera, Context}; | |||
use serde::ser::{SerializeStruct, self}; | |||
use slug::slugify; | |||
use errors::{Result, ResultExt}; | |||
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 { | |||
/// .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 | |||
#[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` | |||
#[serde(skip_serializing)] | |||
pub sections: Vec<String>, | |||
pub components: Vec<String>, | |||
/// The actual content of the page, in markdown | |||
#[serde(skip_serializing)] | |||
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 | |||
pub content: String, | |||
/// The front matter meta-data | |||
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 | |||
pub previous: Option<Box<Page>>, | |||
/// The next page, by date | |||
@@ -50,227 +83,200 @@ pub struct Page { | |||
impl Page { | |||
pub fn new(meta: FrontMatter) -> 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(), | |||
assets: vec![], | |||
content: "".to_string(), | |||
slug: "".to_string(), | |||
url: "".to_string(), | |||
permalink: "".to_string(), | |||
summary: "".to_string(), | |||
meta: meta, | |||
previous: 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 | |||
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 | |||
// 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 { | |||
page.meta.url = Some(format!("/{}", page.get_slug())); | |||
}; | |||
page.url = page.slug.clone(); | |||
} | |||
} | |||
page.permalink = config.make_permalink(&page.url); | |||
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 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(), | |||
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(); | |||
context.add("site", config); | |||
context.add("config", config); | |||
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 { | |||
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("title", &self.meta.title)?; | |||
state.serialize_field("description", &self.meta.description)?; | |||
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("draft", &self.meta.draft)?; | |||
state.serialize_field("category", &self.meta.category)?; | |||
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() | |||
} | |||
} | |||
// 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] | |||
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::fs::{File}; | |||
use std::fs::{File, create_dir}; | |||
use std::path::Path; | |||
use errors::Result; | |||
use errors::{Result, ResultExt}; | |||
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())?; | |||
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>")); | |||
} |