Browse Source

Merge pull request #4 from Keats/0.0.1

0.0.1
index-subcmd
Vincent Prouillet GitHub 7 years ago
parent
commit
1b237dc466
61 changed files with 15038 additions and 561 deletions
  1. +1
    -2
      .gitignore
  2. +10
    -0
      .travis.yml
  3. +562
    -121
      Cargo.lock
  4. +19
    -8
      Cargo.toml
  5. +4
    -0
      README.md
  6. +5
    -83
      src/cmd/build.rs
  7. +2
    -2
      src/cmd/init.rs
  8. +1175
    -0
      src/cmd/livereload.js
  9. +2
    -0
      src/cmd/mod.rs
  10. +245
    -0
      src/cmd/serve.rs
  11. +59
    -3
      src/config.rs
  12. +1
    -2
      src/errors.rs
  13. +45
    -123
      src/front_matter.rs
  14. +34
    -0
      src/lib.rs
  15. +42
    -35
      src/main.rs
  16. +150
    -0
      src/markdown.rs
  17. +186
    -180
      src/page.rs
  18. +97
    -0
      src/section.rs
  19. +449
    -0
      src/site.rs
  20. +20
    -0
      src/templates/rss.xml
  21. +25
    -0
      src/templates/sitemap.xml
  22. +62
    -2
      src/utils.rs
  23. +593
    -0
      sublime_themes/base16-ocean-dark.tmTheme
  24. +589
    -0
      sublime_themes/base16-ocean-light.tmTheme
  25. +766
    -0
      sublime_themes/gruvbox-dark.tmTheme
  26. +774
    -0
      sublime_themes/gruvbox-light.tmTheme
  27. +1725
    -0
      sublime_themes/inspired-github.tmTheme
  28. +1843
    -0
      sublime_themes/kronuz.tmTheme
  29. +1011
    -0
      sublime_themes/material-dark.tmTheme
  30. +1011
    -0
      sublime_themes/material-light.tmTheme
  31. +297
    -0
      sublime_themes/monokai.tmTheme
  32. +1189
    -0
      sublime_themes/solarized-dark.tmTheme
  33. +1189
    -0
      sublime_themes/solarized-light.tmTheme
  34. +7
    -0
      test_site/config.toml
  35. +4
    -0
      test_site/content/posts/_index.md
  36. +7
    -0
      test_site/content/posts/fixed-slug.md
  37. +7
    -0
      test_site/content/posts/fixed-url.md
  38. +6
    -0
      test_site/content/posts/no-section/simple.md
  39. +6
    -0
      test_site/content/posts/python.md
  40. +6
    -0
      test_site/content/posts/simple.md
  41. +4
    -0
      test_site/content/posts/tutorials/_index.md
  42. +4
    -0
      test_site/content/posts/tutorials/devops/_index.md
  43. +6
    -0
      test_site/content/posts/tutorials/devops/docker.md
  44. +6
    -0
      test_site/content/posts/tutorials/devops/nix.md
  45. +4
    -0
      test_site/content/posts/tutorials/programming/_index.md
  46. +6
    -0
      test_site/content/posts/tutorials/programming/python.md
  47. +6
    -0
      test_site/content/posts/tutorials/programming/rust.md
  48. +7
    -0
      test_site/content/posts/with-assets/index.md
  49. +0
    -0
      test_site/content/posts/with-assets/with.js
  50. +0
    -0
      test_site/static/scripts/hello.js
  51. +3
    -0
      test_site/static/site.css
  52. +3
    -0
      test_site/templates/categories.html
  53. +8
    -0
      test_site/templates/category.html
  54. +27
    -0
      test_site/templates/index.html
  55. +5
    -0
      test_site/templates/page.html
  56. +10
    -0
      test_site/templates/section.html
  57. +7
    -0
      test_site/templates/tag.html
  58. +3
    -0
      test_site/templates/tags.html
  59. +197
    -0
      tests/front_matter.rs
  60. +249
    -0
      tests/page.rs
  61. +258
    -0
      tests/site.rs

+ 1
- 2
.gitignore View File

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

+ 10
- 0
.travis.yml View File

@@ -0,0 +1,10 @@
language: rust
cache: cargo

rust:
- nightly
- beta
- stable

notifications:
email: false

+ 562
- 121
Cargo.lock
File diff suppressed because it is too large
View File


+ 19
- 8
Cargo.toml View File

@@ -1,6 +1,6 @@
[package] [package]
name = "gutenberg" name = "gutenberg"
version = "0.1.0"
version = "0.0.1"
authors = ["Vincent Prouillet <vincent@wearewizards.io>"] authors = ["Vincent Prouillet <vincent@wearewizards.io>"]
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"
@@ -9,8 +9,11 @@ homepage = "https://github.com/Keats/gutenberg"
repository = "https://github.com/Keats/gutenberg" repository = "https://github.com/Keats/gutenberg"
keywords = ["static", "site", "generator", "blog"] keywords = ["static", "site", "generator", "blog"]


[[bin]]
name = "gutenberg"

[dependencies] [dependencies]
error-chain = "0.9"
error-chain = "0.10"
clap = "2.19" clap = "2.19"
walkdir = "1" walkdir = "1"
pulldown-cmark = "0" pulldown-cmark = "0"
@@ -20,12 +23,20 @@ glob = "0.2"
serde = "0.9" serde = "0.9"
serde_json = "0.9" serde_json = "0.9"
serde_derive = "0.9" serde_derive = "0.9"
tera = { git = "https://github.com/Keats/tera", branch = "next" }
# tera = { path = "../tera" }
# tera = { git = "https://github.com/Keats/tera", branch = "reload" }
tera = "0.8"
slug = "0.1"
syntect = "1" syntect = "1"
chrono = "0.3"
toml = { version = "0.3", default-features = false, features = ["serde"]}


[dependencies.toml]
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"

+ 4
- 0
README.md View File

@@ -32,8 +32,12 @@ Split the file between front matter and content
Parse the front matter Parse the front matter
markdown -> HTML for the content markdown -> HTML for the content


### Themes
Gallery at https://tmtheme-editor.herokuapp.com/#!/editor/theme/Agola%20Dark

# TODO: # TODO:


- find a way to add tests
- syntax highlighting - syntax highlighting
- pass a --config arg to the CLI to change from `config.toml` - pass a --config arg to the CLI to change from `config.toml`
- have verbosity levels with a `verbosity` config variable with a default - have verbosity levels with a `verbosity` config variable with a default

+ 5
- 83
src/cmd/build.rs View File

@@ -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(&current_path)?;
}

let str_path = current_path.as_path().to_string_lossy().to_string();
sections.entry(str_path).or_insert_with(|| vec![page.clone()]);
}

if let Some(ref url) = page.meta.url {
println!("URL: {:?}", url);
current_path.push(url);
} else {
println!("REMOVE ME IF YOU DONT SEE ME");
current_path.push(&page.get_slug());
}

create_dir(&current_path)?;
create_file(current_path.join("index.html"), &page.render_html(&tera, &config)?)?;
pages.push(page);
}

for (section, pages) in sections {
render_section_index(section, pages, &tera, &config)?;
}

// and now the index page
let mut context = Context::new();
context.add("pages", &order_pages(pages));
context.add("config", &config);
create_file(public.join("index.html"), &tera.render("index.html", &context)?)?;

Ok(())
}


fn render_section_index(section: String, pages: Vec<Page>, tera: &Tera, config: &Config) -> Result<()> {
let path = Path::new(&section);
let mut context = Context::new();
context.add("pages", &order_pages(pages));
context.add("config", &config);

let section_name = match path.components().into_iter().last() {
Some(s) => s.as_ref().to_string_lossy().to_string(),
None => bail!("Couldn't find a section name in {:?}", path.display())
};

create_file(path.join("index.html"), &tera.render(&format!("{}.html", section_name), &context)?)
pub fn build() -> Result<()> {
Site::new(env::current_dir().unwrap())?.build()
} }

+ 2
- 2
src/cmd/init.rs View File

@@ -2,8 +2,8 @@
use std::fs::{create_dir}; use std::fs::{create_dir};
use std::path::Path; use std::path::Path;


use errors::Result;
use utils::create_file;
use gutenberg::errors::Result;
use gutenberg::create_file;




const CONFIG: &'static str = r#" const CONFIG: &'static str = r#"


+ 1175
- 0
src/cmd/livereload.js
File diff suppressed because it is too large
View File


+ 2
- 0
src/cmd/mod.rs View File

@@ -1,5 +1,7 @@
mod init; mod init;
mod build; mod build;
mod serve;


pub use self::init::create_new_project; pub use self::init::create_new_project;
pub use self::build::build; pub use self::build::build;
pub use self::serve::serve;

+ 245
- 0
src/cmd/serve.rs View File

@@ -0,0 +1,245 @@
use std::env;
use std::path::Path;
use std::sync::mpsc::channel;
use std::time::{Instant, Duration};
use std::thread;

use chrono::prelude::*;
use iron::{Iron, Request, IronResult, Response, status};
use mount::Mount;
use staticfile::Static;
use notify::{Watcher, RecursiveMode, watcher};
use ws::{WebSocket, Sender};
use gutenberg::Site;
use gutenberg::errors::{Result};


use ::report_elapsed_time;


#[derive(Debug, PartialEq)]
enum ChangeKind {
Content,
Templates,
StaticFiles,
}

const LIVE_RELOAD: &'static str = include_str!("livereload.js");


fn livereload_handler(_: &mut Request) -> IronResult<Response> {
Ok(Response::with((status::Ok, LIVE_RELOAD.to_string())))
}


fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &str) {
match res {
Ok(_) => {
broadcaster.send(format!(r#"
{{
"command": "reload",
"path": "{}",
"originalPath": "",
"liveCSS": true,
"liveImg": true,
"protocol": ["http://livereload.com/protocols/official-7"]
}}"#, reload_path)
).unwrap();
},
Err(e) => {
println!("Failed to build the site");
println!("Error: {}", e);
for e in e.iter().skip(1) {
println!("Reason: {}", e)
}
}
}
}


// Most of it taken from mdbook
pub fn serve(interface: &str, port: &str) -> Result<()> {
println!("Building site...");
let start = Instant::now();
let mut site = Site::new(env::current_dir().unwrap())?;
site.enable_live_reload();
site.build()?;
report_elapsed_time(start);

let address = format!("{}:{}", interface, port);
let ws_address = format!("{}:{}", interface, "1112");

// Start a webserver that serves the `public` directory
let mut mount = Mount::new();
mount.mount("/", Static::new(Path::new("public/")));
mount.mount("/livereload.js", livereload_handler);
// Starts with a _ to not trigger the unused lint
// we need to assign to a variable otherwise it will block
let _iron = Iron::new(mount).http(address.as_str()).unwrap();
println!("Web server is available at http://{}", address);

// The websocket for livereload
let ws_server = WebSocket::new(|_| {
|_| {
Ok(())
}
}).unwrap();
let broadcaster = ws_server.broadcaster();
thread::spawn(move || {
ws_server.listen(&*ws_address).unwrap();
});

// And finally watching/reacting on file changes
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap();
watcher.watch("content/", RecursiveMode::Recursive).unwrap();
watcher.watch("static/", RecursiveMode::Recursive).unwrap();
watcher.watch("templates/", RecursiveMode::Recursive).unwrap();
let pwd = format!("{}", env::current_dir().unwrap().display());
println!("Listening for changes in {}/{{content, static, templates}}", pwd);
println!("Press CTRL+C to stop\n");

use notify::DebouncedEvent::*;

loop {
// See https://github.com/spf13/hugo/blob/master/commands/hugo.go
// for a more complete version of that
match rx.recv() {
Ok(event) => {
match event {
Create(path) |
Write(path) |
Remove(path) |
Rename(_, path) => {
if is_temp_file(&path) {
continue;
}

println!("Change detected @ {}", Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
let start = Instant::now();
match detect_change_kind(&pwd, &path) {
(ChangeKind::Content, _) => {
println!("-> Content changed {}", path.display());
// Force refresh
rebuild_done_handling(&broadcaster, site.rebuild_after_content_change(), "/x.js");
},
(ChangeKind::Templates, _) => {
println!("-> Template changed {}", path.display());
// Force refresh
rebuild_done_handling(&broadcaster, site.rebuild_after_template_change(), "/x.js");
},
(ChangeKind::StaticFiles, p) => {
println!("-> Static file changes detected {}", path.display());
rebuild_done_handling(&broadcaster, site.copy_static_directory(), &p);
},
};
report_elapsed_time(start);
}
_ => {}
}
},
Err(e) => println!("Watch error: {:?}", e),
};
}
}


/// Returns whether the path we received corresponds to a temp file create
/// by an editor
fn is_temp_file(path: &Path) -> bool {
let ext = path.extension();
match ext {
Some(ex) => match ex.to_str().unwrap() {
"swp" | "swx" | "tmp" | ".DS_STORE" => true,
// jetbrains IDE
x if x.ends_with("jb_old___") => true,
x if x.ends_with("jb_tmp___") => true,
x if x.ends_with("jb_bak___") => true,
// vim
x if x.ends_with('~') => true,
_ => {
if let Some(filename) = path.file_stem() {
// emacs
filename.to_str().unwrap().starts_with('#')
} else {
false
}
}
},
None => {
path.ends_with(".DS_STORE")
},
}
}


/// Detect what changed from the given path so we have an idea what needs
/// to be reloaded
fn detect_change_kind(pwd: &str, path: &Path) -> (ChangeKind, String) {
let path_str = format!("{}", path.display())
.replace(pwd, "")
.replace("\\", "/");
let change_kind = if path_str.starts_with("/templates") {
ChangeKind::Templates
} else if path_str.starts_with("/content") {
ChangeKind::Content
} else if path_str.starts_with("/static") {
ChangeKind::StaticFiles
} else {
panic!("Got a change in an unexpected path: {}", path_str);
};

(change_kind, path_str)
}

#[cfg(test)]
mod tests {
use std::path::Path;

use super::{is_temp_file, detect_change_kind, ChangeKind};

#[test]
fn test_can_recognize_temp_files() {
let testcases = vec![
Path::new("hello.swp"),
Path::new("hello.swx"),
Path::new(".DS_STORE"),
Path::new("hello.tmp"),
Path::new("hello.html.__jb_old___"),
Path::new("hello.html.__jb_tmp___"),
Path::new("hello.html.__jb_bak___"),
Path::new("hello.html~"),
Path::new("#hello.html"),
];

for t in testcases {
println!("{:?}", t.display());
assert!(is_temp_file(&t));
}
}

#[test]
fn test_can_detect_kind_of_changes() {
let testcases = vec![
(
(ChangeKind::Templates, "/templates/hello.html".to_string()),
"/home/vincent/site", Path::new("/home/vincent/site/templates/hello.html")
),
(
(ChangeKind::StaticFiles, "/static/site.css".to_string()),
"/home/vincent/site", Path::new("/home/vincent/site/static/site.css")
),
(
(ChangeKind::Content, "/content/posts/hello.md".to_string()),
"/home/vincent/site", Path::new("/home/vincent/site/content/posts/hello.md")
),
];

for (expected, pwd, path) in testcases {
println!("{:?}", path.display());
assert_eq!(expected, detect_change_kind(&pwd, &path));
}
}


}

+ 59
- 3
src/config.rs View File

@@ -7,19 +7,28 @@ use toml::{Value as Toml, self};


use errors::{Result, ResultExt}; use errors::{Result, ResultExt};


// TODO: disable tag(s)/category(ies) page generation

// TO ADD:
// highlight code theme
// generate_tags_pages
// generate_categories_pages

#[derive(Debug, PartialEq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Config { pub struct Config {
/// Title of the site /// Title of the site
pub title: String, pub title: String,
/// Base URL of the site /// Base URL of the site
pub base_url: String, pub base_url: String,

/// Whether to highlight all code blocks found in markdown files. Defaults to false
pub highlight_code: Option<bool>,
/// Description of the site /// Description of the site
pub description: Option<String>, pub description: Option<String>,
/// The language used in the site. Defaults to "en" /// The language used in the site. Defaults to "en"
pub language_code: Option<String>, pub language_code: Option<String>,
/// Whether to disable RSS generation, defaults to None (== generate RSS)
pub disable_rss: Option<bool>,
/// Whether to generate RSS, defaults to false
pub generate_rss: Option<bool>,

/// All user params set in [extra] in the config /// All user params set in [extra] in the config
pub extra: Option<HashMap<String, Toml>>, pub extra: Option<HashMap<String, Toml>>,
} }
@@ -32,10 +41,19 @@ impl Config {
Ok(c) => c, Ok(c) => c,
Err(e) => bail!(e) Err(e) => bail!(e)
}; };

if config.language_code.is_none() { if config.language_code.is_none() {
config.language_code = Some("en".to_string()); config.language_code = Some("en".to_string());
} }


if config.highlight_code.is_none() {
config.highlight_code = Some(false);
}

if config.generate_rss.is_none() {
config.generate_rss = Some(false);
}

Ok(config) Ok(config)
} }


@@ -48,6 +66,44 @@ impl Config {


Config::parse(&content) Config::parse(&content)
} }

/// Makes a url, taking into account that the base url might have a trailing slash
pub fn make_permalink(&self, path: &str) -> String {
if self.base_url.ends_with('/') {
format!("{}{}", self.base_url, path)
} else {
format!("{}/{}", self.base_url, path)
}
}
}

impl Default for Config {
/// Exists for testing purposes
fn default() -> Config {
Config {
title: "".to_string(),
base_url: "http://a-website.com/".to_string(),
highlight_code: Some(true),
description: None,
language_code: Some("en".to_string()),
generate_rss: Some(false),
extra: None,
}
}
}


/// Get and parse the config.
/// If it doesn't succeed, exit
pub fn get_config(path: &Path) -> Config {
match Config::from_file(path.join("config.toml")) {
Ok(c) => c,
Err(e) => {
println!("Failed to load config.toml");
println!("Error: {}", e);
::std::process::exit(1);
}
}
} }






+ 1
- 2
src/errors.rs View File

@@ -2,8 +2,7 @@ use tera;
use toml; use toml;


error_chain! { error_chain! {
errors {
}
errors {}


links { links {
Tera(tera::Error, tera::ErrorKind); Tera(tera::Error, tera::ErrorKind);


+ 45
- 123
src/front_matter.rs View File

@@ -1,21 +1,32 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path;


use toml; use toml;
use tera::Value; use tera::Value;
use chrono::prelude::*;
use regex::Regex;


use errors::{Result, ResultExt};




use errors::{Result};
lazy_static! {
static ref PAGE_RE: Regex = Regex::new(r"^\n?\+\+\+\n((?s).*(?-s))\+\+\+\n?((?s).*(?-s))$").unwrap();
}




/// The front matter of every page /// The front matter of every page
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FrontMatter { pub struct FrontMatter {
// <title> of the page
// Mandatory fields

/// <title> of the page
pub title: String, pub title: String,
/// Description that appears when linked, e.g. on twitter /// Description that appears when linked, e.g. on twitter
pub description: String, pub description: String,


// Optional stuff

/// Date if we want to order pages (ie blog post) /// Date if we want to order pages (ie blog post)
pub date: Option<String>, pub date: Option<String>,
/// The page slug. Will be used instead of the filename if present /// The page slug. Will be used instead of the filename if present
@@ -31,9 +42,9 @@ pub struct FrontMatter {
pub draft: Option<bool>, pub draft: Option<bool>,
/// Only one category allowed /// Only one category allowed
pub category: Option<String>, pub category: Option<String>,
/// Optional layout, if we want to specify which tpl to render for that page
/// Optional template, if we want to specify which template to render for that page
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub layout: Option<String>,
pub template: Option<String>,
/// Any extra parameter present in the front matter /// Any extra parameter present in the front matter
pub extra: Option<HashMap<String, Value>>, pub extra: Option<HashMap<String, Value>>,
} }
@@ -44,7 +55,7 @@ impl FrontMatter {
bail!("Front matter of file is missing"); bail!("Front matter of file is missing");
} }


let mut f: FrontMatter = match toml::from_str(toml) {
let f: FrontMatter = match toml::from_str(toml) {
Ok(d) => d, Ok(d) => d,
Err(e) => bail!(e), Err(e) => bail!(e),
}; };
@@ -63,129 +74,40 @@ impl FrontMatter {


Ok(f) Ok(f)
} }
}


#[cfg(test)]
mod tests {
use super::{FrontMatter};
use tera::to_value;


#[test]
fn test_can_parse_a_valid_front_matter() {
let content = r#"
title = "Hello"
description = "hey there""#;
let res = FrontMatter::parse(content);
println!("{:?}", res);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(res.title, "Hello".to_string());
assert_eq!(res.description, "hey there".to_string());
}


#[test]
fn test_can_parse_tags() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
tags = ["rust", "html"]"#;
let res = FrontMatter::parse(content);
assert!(res.is_ok());
let res = res.unwrap();

assert_eq!(res.title, "Hello".to_string());
assert_eq!(res.slug.unwrap(), "hello-world".to_string());
assert_eq!(res.tags.unwrap(), ["rust".to_string(), "html".to_string()]);
/// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime
pub fn parse_date(&self) -> Option<NaiveDateTime> {
match self.date {
Some(ref d) => {
if d.contains('T') {
DateTime::parse_from_rfc3339(d).ok().and_then(|s| Some(s.naive_local()))
} else {
NaiveDate::parse_from_str(d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0,0,0)))
}
},
None => None,
}
} }
}


#[test]
fn test_can_parse_extra_attributes_in_frontmatter() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"

[extra]
language = "en"
authors = ["Bob", "Alice"]"#;
let res = FrontMatter::parse(content);
assert!(res.is_ok());
let res = res.unwrap();

assert_eq!(res.title, "Hello".to_string());
assert_eq!(res.slug.unwrap(), "hello-world".to_string());
let extra = res.extra.unwrap();
assert_eq!(extra.get("language").unwrap(), &to_value("en").unwrap());
assert_eq!(
extra.get("authors").unwrap(),
&to_value(["Bob".to_string(), "Alice".to_string()]).unwrap()
);
}


#[test]
fn test_is_ok_with_url_instead_of_slug() {
let content = r#"
title = "Hello"
description = "hey there"
url = "hello-world""#;
let res = FrontMatter::parse(content);
assert!(res.is_ok());
let res = res.unwrap();
assert!(res.slug.is_none());
assert_eq!(res.url.unwrap(), "hello-world".to_string());
/// Split a file between the front matter and its content
/// It will parse the front matter as well and returns any error encountered
/// TODO: add tests
pub fn split_content(file_path: &Path, content: &str) -> Result<(FrontMatter, String)> {
if !PAGE_RE.is_match(content) {
bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", file_path.to_string_lossy());
} }


#[test]
fn test_errors_with_empty_front_matter() {
let content = r#" "#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
// 2. extract the front matter and the content
let caps = PAGE_RE.captures(content).unwrap();
// caps[0] is the full match
let front_matter = &caps[1];
let content = &caps[2];


#[test]
fn test_errors_with_invalid_front_matter() {
let content = r#"title = 1\n"#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
// 3. create our page, parse front matter and assign all of that
let meta = FrontMatter::parse(front_matter)
.chain_err(|| format!("Error when parsing front matter of file `{}`", file_path.to_string_lossy()))?;


#[test]
fn test_errors_with_missing_required_value_front_matter() {
let content = r#"title = """#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}

#[test]
fn test_errors_on_non_string_tag() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
tags = ["rust", 1]"#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}

#[test]
fn test_errors_on_present_but_empty_slug() {
let content = r#"
title = "Hello"
description = "hey there"
slug = """#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn test_errors_on_present_but_empty_url() {
let content = r#"
title = "Hello"
description = "hey there"
url = """#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}
Ok((meta, content.to_string()))
} }

+ 34
- 0
src/lib.rs View File

@@ -0,0 +1,34 @@
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate toml;
extern crate walkdir;
extern crate pulldown_cmark;
extern crate regex;
extern crate tera;
extern crate glob;
extern crate syntect;
extern crate slug;
extern crate chrono;
#[cfg(test)]
extern crate tempdir;

mod utils;
mod config;
pub mod errors;
mod page;
mod front_matter;
mod site;
mod markdown;
mod section;

pub use site::Site;
pub use config::Config;
pub use front_matter::{FrontMatter, split_content};
pub use page::{Page};
pub use section::{Section};
pub use utils::create_file;

+ 42
- 35
src/main.rs View File

@@ -2,44 +2,31 @@
extern crate clap; extern crate clap;
#[macro_use] #[macro_use]
extern crate error_chain; extern crate error_chain;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate toml;
extern crate walkdir;
extern crate pulldown_cmark;
extern crate regex;
extern crate tera;
extern crate glob;
extern crate syntect;
extern crate gutenberg;
extern crate chrono;


extern crate staticfile;
extern crate iron;
extern crate mount;
extern crate notify;
extern crate ws;


mod utils;
mod config;
mod errors;
mod cmd;
mod page;
mod front_matter;


use std::time::Instant;
use chrono::Duration;

mod cmd;


use config::Config;


// Print the time elapsed rounded to 1 decimal
fn report_elapsed_time(instant: Instant) {
let duration_ms = Duration::from_std(instant.elapsed()).unwrap().num_milliseconds() as f64;


// Get and parse the config.
// If it doesn't succeed, exit
fn get_config() -> Config {
match Config::from_file("config.toml") {
Ok(c) => c,
Err(e) => {
println!("Failed to load config.toml");
println!("Error: {}", e);
for e in e.iter().skip(1) {
println!("Reason: {}", e)
}
::std::process::exit(1);
}
if duration_ms < 1000.0 {
println!("Done in {}ms.\n", duration_ms);
} else {
let duration_sec = duration_ms / 1000.0;
println!("Done in {:.1}s.\n", ((duration_sec * 10.0).round() / 10.0));
} }
} }


@@ -57,6 +44,11 @@ fn main() {
(@subcommand build => (@subcommand build =>
(about: "Builds the site") (about: "Builds the site")
) )
(@subcommand serve =>
(about: "Serve the site. Rebuild and reload on change automatically")
(@arg interface: "Interface to bind on (default to 127.0.0.1)")
(@arg port: "Which port to use (default to 1111)")
)
).get_matches(); ).get_matches();


match matches.subcommand() { match matches.subcommand() {
@@ -64,7 +56,6 @@ fn main() {
match cmd::create_new_project(matches.value_of("name").unwrap()) { match cmd::create_new_project(matches.value_of("name").unwrap()) {
Ok(()) => { Ok(()) => {
println!("Project created"); println!("Project created");
println!("You will now need to set a theme in `config.toml`");
}, },
Err(e) => { Err(e) => {
println!("Error: {}", e); println!("Error: {}", e);
@@ -73,9 +64,11 @@ fn main() {
}; };
}, },
("build", Some(_)) => { ("build", Some(_)) => {
match cmd::build(get_config()) {
println!("Building site");
let start = Instant::now();
match cmd::build() {
Ok(()) => { Ok(()) => {
println!("Project built.");
report_elapsed_time(start);
}, },
Err(e) => { Err(e) => {
println!("Failed to build the site"); println!("Failed to build the site");
@@ -87,6 +80,20 @@ fn main() {
}, },
}; };
}, },
("serve", Some(matches)) => {
let interface = matches.value_of("interface").unwrap_or("127.0.0.1");
let port = matches.value_of("port").unwrap_or("1111");
match cmd::serve(interface, port) {
Ok(()) => (),
Err(e) => {
println!("Error: {}", e);
for e in e.iter().skip(1) {
println!("Reason: {}", e)
}
::std::process::exit(1);
},
};
},
_ => unreachable!(), _ => unreachable!(),
} }
} }


+ 150
- 0
src/markdown.rs View File

@@ -0,0 +1,150 @@
use std::borrow::Cow::Owned;

use pulldown_cmark as cmark;
use self::cmark::{Parser, Event, Tag};

use syntect::easy::HighlightLines;
use syntect::parsing::SyntaxSet;
use syntect::highlighting::ThemeSet;
use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, IncludeBackground};


// We need to put those in a struct to impl Send and sync
struct Setup {
syntax_set: SyntaxSet,
theme_set: ThemeSet,
}

unsafe impl Send for Setup {}
unsafe impl Sync for Setup {}

lazy_static!{
static ref SETUP: Setup = Setup {
syntax_set: SyntaxSet::load_defaults_newlines(),
theme_set: ThemeSet::load_defaults()
};
}


struct CodeHighlightingParser<'a> {
// The block we're currently highlighting
highlighter: Option<HighlightLines<'a>>,
parser: Parser<'a>,
}

impl<'a> CodeHighlightingParser<'a> {
pub fn new(parser: Parser<'a>) -> CodeHighlightingParser<'a> {
CodeHighlightingParser {
highlighter: None,
parser: parser,
}
}
}

impl<'a> Iterator for CodeHighlightingParser<'a> {
type Item = Event<'a>;

fn next(&mut self) -> Option<Event<'a>> {
// Not using pattern matching to reduce indentation levels
let next_opt = self.parser.next();
if next_opt.is_none() {
return None;
}

let item = next_opt.unwrap();
// Below we just look for the start of a code block and highlight everything
// until we see the end of a code block.
// Everything else happens as normal in pulldown_cmark
match item {
Event::Text(text) => {
// if we are in the middle of a code block
if let Some(ref mut highlighter) = self.highlighter {
let highlighted = &highlighter.highlight(&text);
let html = styles_to_coloured_html(highlighted, IncludeBackground::Yes);
Some(Event::Html(Owned(html)))
} else {
Some(Event::Text(text))
}
},
Event::Start(Tag::CodeBlock(ref info)) => {
let syntax = info
.split(' ')
.next()
.and_then(|lang| SETUP.syntax_set.find_syntax_by_token(lang))
.unwrap_or_else(|| SETUP.syntax_set.find_syntax_plain_text());
self.highlighter = Some(
HighlightLines::new(syntax, &SETUP.theme_set.themes["base16-ocean.dark"])
);
let snippet = start_coloured_html_snippet(&SETUP.theme_set.themes["base16-ocean.dark"]);
Some(Event::Html(Owned(snippet)))
},
Event::End(Tag::CodeBlock(_)) => {
// reset highlight and close the code block
self.highlighter = None;
Some(Event::Html(Owned("</pre>".to_owned())))
},
_ => Some(item)
}

}
}

pub fn markdown_to_html(content: &str, highlight_code: bool) -> String {
let mut html = String::new();
if highlight_code {
let parser = CodeHighlightingParser::new(Parser::new(content));
cmark::html::push_html(&mut html, parser);
} else {
let parser = Parser::new(content);
cmark::html::push_html(&mut html, parser);
};
html
}


#[cfg(test)]
mod tests {
use super::{markdown_to_html};

#[test]
fn test_markdown_to_html_simple() {
let res = markdown_to_html("# hello", true);
assert_eq!(res, "<h1>hello</h1>\n");
}

#[test]
fn test_markdown_to_html_code_block_highlighting_off() {
let res = markdown_to_html("```\n$ gutenberg server\n```", false);
assert_eq!(
res,
"<pre><code>$ gutenberg server\n</code></pre>\n"
);
}

#[test]
fn test_markdown_to_html_code_block_no_lang() {
let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", true);
assert_eq!(
res,
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">$ gutenberg server\n</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">$ ping\n</span></pre>"
);
}

#[test]
fn test_markdown_to_html_code_block_with_lang() {
let res = markdown_to_html("```python\nlist.append(1)\n```", true);
assert_eq!(
res,
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">.</span><span style=\"background-color:#2b303b;color:#bf616a;\">append</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">(</span><span style=\"background-color:#2b303b;color:#d08770;\">1</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">)</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">\n</span></pre>"
);
}
#[test]
fn test_markdown_to_html_code_block_with_unknown_lang() {
let res = markdown_to_html("```yolo\nlist.append(1)\n```", true);
// defaults to plain text
assert_eq!(
res,
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list.append(1)\n</span></pre>"
);
}
}

+ 186
- 180
src/page.rs View File

@@ -1,45 +1,78 @@
/// A page, can be a blog post or a basic page /// A page, can be a blog post or a basic page
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use std::cmp::Ordering;
use std::fs::{read_dir};
use std::path::{Path, PathBuf};
use std::result::Result as StdResult; use std::result::Result as StdResult;




use pulldown_cmark as cmark;
use regex::Regex;
use tera::{Tera, Context}; use tera::{Tera, Context};
use serde::ser::{SerializeStruct, self}; use serde::ser::{SerializeStruct, self};
use slug::slugify;


use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use config::Config; use config::Config;
use front_matter::{FrontMatter};

use front_matter::{FrontMatter, split_content};
use markdown::markdown_to_html;
use utils::{read_file, find_content_components};



/// Looks into the current folder for the path and see if there's anything that is not a .md
/// file. Those will be copied next to the rendered .html file
fn find_related_assets(path: &Path) -> Vec<PathBuf> {
let mut assets = vec![];

for entry in read_dir(path).unwrap().filter_map(|e| e.ok()) {
let entry_path = entry.path();
if entry_path.is_file() {
match entry_path.extension() {
Some(e) => match e.to_str() {
Some("md") => continue,
_ => assets.push(entry_path.to_path_buf()),
},
None => continue,
}
}
}


lazy_static! {
static ref PAGE_RE: Regex = Regex::new(r"^\n?\+\+\+\n((?s).*(?-s))\+\+\+\n((?s).*(?-s))$").unwrap();
assets
} }




#[derive(Clone, Debug, PartialEq, Deserialize)]
#[derive(Clone, Debug, PartialEq)]
pub struct Page { pub struct Page {
/// .md filepath, excluding the content/ bit
#[serde(skip_serializing)]
pub filepath: String,
/// The .md path
pub file_path: PathBuf,
/// The parent directory of the file. Is actually the grand parent directory
/// if it's an asset folder
pub parent_path: PathBuf,
/// The name of the .md file /// The name of the .md file
#[serde(skip_serializing)]
pub filename: String,
/// The directories above our .md file are called sections
/// for example a file at content/kb/solutions/blabla.md will have 2 sections:
pub file_name: String,
/// The directories above our .md file
/// for example a file at content/kb/solutions/blabla.md will have 2 components:
/// `kb` and `solutions` /// `kb` and `solutions`
#[serde(skip_serializing)]
pub sections: Vec<String>,
pub components: Vec<String>,
/// The actual content of the page, in markdown /// The actual content of the page, in markdown
#[serde(skip_serializing)]
pub raw_content: String, pub raw_content: String,
/// All the non-md files we found next to the .md file
pub assets: Vec<PathBuf>,
/// The HTML rendered of the page /// The HTML rendered of the page
pub content: String, pub content: String,
/// The front matter meta-data /// The front matter meta-data
pub meta: FrontMatter, pub meta: FrontMatter,

/// The slug of that page.
/// First tries to find the slug in the meta and defaults to filename otherwise
pub slug: String,
/// The relative URL of the page
pub url: String,
/// The full URL for that page
pub permalink: String,
/// The summary for the article, defaults to empty string
/// When <!-- more --> is found in the text, will take the content up to that part
/// as summary
pub summary: String,

/// The previous page, by date /// The previous page, by date
pub previous: Option<Box<Page>>, pub previous: Option<Box<Page>>,
/// The next page, by date /// The next page, by date
@@ -50,227 +83,200 @@ pub struct Page {
impl Page { impl Page {
pub fn new(meta: FrontMatter) -> Page { pub fn new(meta: FrontMatter) -> Page {
Page { Page {
filepath: "".to_string(),
filename: "".to_string(),
sections: vec![],
file_path: PathBuf::new(),
parent_path: PathBuf::new(),
file_name: "".to_string(),
components: vec![],
raw_content: "".to_string(), raw_content: "".to_string(),
assets: vec![],
content: "".to_string(), content: "".to_string(),
slug: "".to_string(),
url: "".to_string(),
permalink: "".to_string(),
summary: "".to_string(),
meta: meta, meta: meta,
previous: None, previous: None,
next: None, next: None,
} }
} }


/// Get the slug for the page.
/// First tries to find the slug in the meta and defaults to filename otherwise
pub fn get_slug(&self) -> String {
if let Some(ref slug) = self.meta.slug {
slug.to_string()
} else {
self.filename.clone()
}
/// Get word count and estimated reading time
pub fn get_reading_analytics(&self) -> (usize, usize) {
// Only works for latin language but good enough for a start
let word_count: usize = self.raw_content.split_whitespace().count();
// https://help.medium.com/hc/en-us/articles/214991667-Read-time
// 275 seems a bit too high though
(word_count, (word_count / 200))
} }


// Parse a page given the content of the .md file
// Files without front matter or with invalid front matter are considered
// erroneous
pub fn parse(filepath: &str, content: &str) -> Result<Page> {
/// Parse a page given the content of the .md file
/// Files without front matter or with invalid front matter are considered
/// erroneous
pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Page> {
// 1. separate front matter from content // 1. separate front matter from content
if !PAGE_RE.is_match(content) {
bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", filepath);
}
let (meta, content) = split_content(file_path, content)?;
let mut page = Page::new(meta);
page.file_path = file_path.to_path_buf();
page.parent_path = page.file_path.parent().unwrap().to_path_buf();
page.raw_content = content;

// We try to be smart about highlighting code as it can be time-consuming
// If the global config disables it, then we do nothing. However,
// if we see a code block in the content, we assume that this page needs
// to be highlighted. It could potentially have false positive if the content
// has ``` in it but that seems kind of unlikely
let should_highlight = if config.highlight_code.unwrap() {
page.raw_content.contains("```")
} else {
false
};


// 2. extract the front matter and the content
let caps = PAGE_RE.captures(content).unwrap();
// caps[0] is the full match
let front_matter = &caps[1];
let content = &caps[2];
page.content = markdown_to_html(&page.raw_content, should_highlight);


// 3. create our page, parse front matter and assign all of that
let meta = FrontMatter::parse(&front_matter)
.chain_err(|| format!("Error when parsing front matter of file `{}`", filepath))?;
if page.raw_content.contains("<!-- more -->") {
page.summary = {
let summary = page.raw_content.splitn(2, "<!-- more -->").collect::<Vec<&str>>()[0];
markdown_to_html(summary, should_highlight)
}
}


let mut page = Page::new(meta);
page.filepath = filepath.to_string();
page.raw_content = content.to_string();
page.content = {
let mut html = String::new();
let parser = cmark::Parser::new(&page.raw_content);
cmark::html::push_html(&mut html, parser);
html
let path = Path::new(file_path);
page.file_name = path.file_stem().unwrap().to_string_lossy().to_string();

page.slug = {
if let Some(ref slug) = page.meta.slug {
slug.trim().to_string()
} else {
slugify(page.file_name.clone())
}
}; };


// 4. Find sections // 4. Find sections
// Pages with custom urls exists outside of sections // Pages with custom urls exists outside of sections
if page.meta.url.is_none() {
let path = Path::new(filepath);
page.filename = path.file_stem().expect("Couldn't get filename").to_string_lossy().to_string();

// find out if we have sections
for section in path.parent().unwrap().components() {
page.sections.push(section.as_ref().to_string_lossy().to_string());
}

// now the url
// We get it from a combination of sections + slug
if !page.sections.is_empty() {
page.meta.url = Some(format!("/{}/{}", page.sections.join("/"), page.get_slug()));
if let Some(ref u) = page.meta.url {
page.url = u.trim().to_string();
} else {
page.components = find_content_components(&page.file_path);
if !page.components.is_empty() {
// If we have a folder with an asset, don't consider it as a component
if page.file_name == "index" {
page.components.pop();
// also set parent_path to grandparent instead
page.parent_path = page.parent_path.parent().unwrap().to_path_buf();
}

// Don't add a trailing slash to sections
page.url = format!("{}/{}", page.components.join("/"), page.slug);
} else { } else {
page.meta.url = Some(format!("/{}", page.get_slug()));
};
page.url = page.slug.clone();
}
} }


page.permalink = config.make_permalink(&page.url);

Ok(page) Ok(page)
} }


pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Page> {
/// Read and parse a .md file into a Page struct
pub fn from_file<P: AsRef<Path>>(path: P, config: &Config) -> Result<Page> {
let path = path.as_ref(); let path = path.as_ref();
let content = read_file(path)?;
let mut page = Page::parse(path, &content, config)?;
page.assets = find_related_assets(path.parent().unwrap());


let mut content = String::new();
File::open(path)
.chain_err(|| format!("Failed to open '{:?}'", path.display()))?
.read_to_string(&mut content)?;
if !page.assets.is_empty() && page.file_name != "index" {
bail!("Page `{}` has assets but is not named index.md", path.display());
}

Ok(page)


// Remove the content string from name
// Maybe get a path as an arg instead and use strip_prefix?
Page::parse(&path.strip_prefix("content").unwrap().to_string_lossy(), &content)
} }


fn get_layout_name(&self) -> String {
match self.meta.layout {
/// Renders the page using the default layout, unless specified in front-matter
pub fn render_html(&self, tera: &Tera, config: &Config) -> Result<String> {
let tpl_name = match self.meta.template {
Some(ref l) => l.to_string(), Some(ref l) => l.to_string(),
None => "page.html".to_string() None => "page.html".to_string()
}
}

pub fn render_html(&mut self, tera: &Tera, config: &Config) -> Result<String> {
let tpl = self.get_layout_name();
};
// TODO: create a helper to create context to ensure all contexts
// have the same names
let mut context = Context::new(); let mut context = Context::new();
context.add("site", config);
context.add("config", config);
context.add("page", self); context.add("page", self);


tera.render(&tpl, &context)
.chain_err(|| "Error while rendering template")
tera.render(&tpl_name, &context)
.chain_err(|| format!("Failed to render page '{}'", self.file_name))
} }
} }


impl ser::Serialize for Page { impl ser::Serialize for Page {
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer { fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer {
let mut state = serializer.serialize_struct("page", 10)?;
let mut state = serializer.serialize_struct("page", 13)?;
state.serialize_field("content", &self.content)?; state.serialize_field("content", &self.content)?;
state.serialize_field("title", &self.meta.title)?; state.serialize_field("title", &self.meta.title)?;
state.serialize_field("description", &self.meta.description)?; state.serialize_field("description", &self.meta.description)?;
state.serialize_field("date", &self.meta.date)?; state.serialize_field("date", &self.meta.date)?;
state.serialize_field("slug", &self.meta.slug)?;
state.serialize_field("url", &self.meta.url)?;
state.serialize_field("slug", &self.slug)?;
state.serialize_field("url", &format!("/{}", self.url))?;
state.serialize_field("permalink", &self.permalink)?;
state.serialize_field("tags", &self.meta.tags)?; state.serialize_field("tags", &self.meta.tags)?;
state.serialize_field("draft", &self.meta.draft)?; state.serialize_field("draft", &self.meta.draft)?;
state.serialize_field("category", &self.meta.category)?; state.serialize_field("category", &self.meta.category)?;
state.serialize_field("extra", &self.meta.extra)?; state.serialize_field("extra", &self.meta.extra)?;
let (word_count, reading_time) = self.get_reading_analytics();
state.serialize_field("word_count", &word_count)?;
state.serialize_field("reading_time", &reading_time)?;
state.end() state.end()
} }
} }


// Order pages by date, no-op for now
// TODO: impl PartialOrd on Vec<Page> so we can use sort()?
pub fn order_pages(pages: Vec<Page>) -> Vec<Page> {
pages
}
impl PartialOrd for Page {
fn partial_cmp(&self, other: &Page) -> Option<Ordering> {
if self.meta.date.is_none() {
return Some(Ordering::Less);
}


if other.meta.date.is_none() {
return Some(Ordering::Greater);
}


#[cfg(test)]
mod tests {
use super::{Page};
let this_date = self.meta.parse_date().unwrap();
let other_date = other.meta.parse_date().unwrap();


if this_date > other_date {
return Some(Ordering::Less);
}
if this_date < other_date {
return Some(Ordering::Greater);
}


#[test]
fn test_can_parse_a_valid_page() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse("post.md", content);
assert!(res.is_ok());
let page = res.unwrap();

assert_eq!(page.meta.title, "Hello".to_string());
assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string());
assert_eq!(page.raw_content, "Hello world".to_string());
assert_eq!(page.content, "<p>Hello world</p>\n".to_string());
Some(Ordering::Equal)
} }
}


#[test]
fn test_can_find_one_parent_directory() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse("posts/intro.md", content);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.sections, vec!["posts".to_string()]);
}


#[test]
fn test_can_find_multiple_parent_directories() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse("posts/intro/start.md", content);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.sections, vec!["posts".to_string(), "intro".to_string()]);
}
#[cfg(test)]
mod tests {
use tempdir::TempDir;


#[test]
fn test_can_make_url_from_sections_and_slug() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse("posts/intro/start.md", content);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.meta.url.unwrap(), "/posts/intro/hello-world");
}
use std::fs::File;


#[test]
fn test_can_make_url_from_sections_and_slug_root() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse("start.md", content);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.meta.url.unwrap(), "/hello-world");
}
use super::{find_related_assets};


#[test] #[test]
fn test_errors_on_invalid_front_matter_format() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse("start.md", content);
assert!(res.is_err());
fn test_find_related_assets() {
let tmp_dir = TempDir::new("example").expect("create temp dir");
File::create(tmp_dir.path().join("index.md")).unwrap();
File::create(tmp_dir.path().join("example.js")).unwrap();
File::create(tmp_dir.path().join("graph.jpg")).unwrap();
File::create(tmp_dir.path().join("fail.png")).unwrap();

let assets = find_related_assets(tmp_dir.path());
assert_eq!(assets.len(), 3);
assert_eq!(assets.iter().filter(|p| p.extension().unwrap() != "md").count(), 3);
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "example.js").count(), 1);
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "graph.jpg").count(), 1);
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "fail.png").count(), 1);
} }
} }

+ 97
- 0
src/section.rs View File

@@ -0,0 +1,97 @@
use std::path::{Path, PathBuf};
use std::result::Result as StdResult;

use tera::{Tera, Context};
use serde::ser::{SerializeStruct, self};

use config::Config;
use front_matter::{FrontMatter, split_content};
use errors::{Result, ResultExt};
use utils::{read_file, find_content_components};
use page::Page;


#[derive(Clone, Debug, PartialEq)]
pub struct Section {
/// The _index.md full path
pub file_path: PathBuf,
/// Path of the directory containing the _index.md file
pub parent_path: PathBuf,
/// The folder names from `content` to this section file
pub components: Vec<String>,
/// The relative URL of the page
pub url: String,
/// The full URL for that page
pub permalink: String,
/// The front matter meta-data
pub meta: FrontMatter,
/// All direct pages of that section
pub pages: Vec<Page>,
/// All direct subsections
pub subsections: Vec<Section>,
}

impl Section {
pub fn new(file_path: &Path, meta: FrontMatter) -> Section {
Section {
file_path: file_path.to_path_buf(),
parent_path: file_path.parent().unwrap().to_path_buf(),
components: vec![],
url: "".to_string(),
permalink: "".to_string(),
meta: meta,
pages: vec![],
subsections: vec![],
}
}

pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Section> {
let (meta, _) = split_content(file_path, content)?;
let mut section = Section::new(file_path, meta);
section.components = find_content_components(&section.file_path);
section.url = section.components.join("/");
section.permalink = section.components.join("/");

section.permalink = config.make_permalink(&section.url);

Ok(section)
}

/// Read and parse a .md file into a Page struct
pub fn from_file<P: AsRef<Path>>(path: P, config: &Config) -> Result<Section> {
let path = path.as_ref();
let content = read_file(path)?;

Section::parse(path, &content, config)
}

/// Renders the page using the default layout, unless specified in front-matter
pub fn render_html(&self, tera: &Tera, config: &Config) -> Result<String> {
let tpl_name = match self.meta.template {
Some(ref l) => l.to_string(),
None => "section.html".to_string()
};

// TODO: create a helper to create context to ensure all contexts
// have the same names
let mut context = Context::new();
context.add("config", config);
context.add("section", self);

tera.render(&tpl_name, &context)
.chain_err(|| format!("Failed to render section '{}'", self.file_path.display()))
}
}

impl ser::Serialize for Section {
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer {
let mut state = serializer.serialize_struct("section", 6)?;
state.serialize_field("title", &self.meta.title)?;
state.serialize_field("description", &self.meta.description)?;
state.serialize_field("url", &format!("/{}", self.url))?;
state.serialize_field("permalink", &self.permalink)?;
state.serialize_field("pages", &self.pages)?;
state.serialize_field("subsections", &self.subsections)?;
state.end()
}
}

+ 449
- 0
src/site.rs View File

@@ -0,0 +1,449 @@
use std::collections::{BTreeMap, HashMap};
use std::iter::FromIterator;
use std::fs::{remove_dir_all, copy, remove_file};
use std::path::{Path, PathBuf};

use glob::glob;
use tera::{Tera, Context};
use slug::slugify;
use walkdir::WalkDir;

use errors::{Result, ResultExt};
use config::{Config, get_config};
use page::{Page};
use utils::{create_file, create_directory};
use section::{Section};


lazy_static! {
static ref GUTENBERG_TERA: Tera = {
let mut tera = Tera::default();
tera.add_raw_templates(vec![
("rss.xml", include_str!("templates/rss.xml")),
("sitemap.xml", include_str!("templates/sitemap.xml")),
]).unwrap();
tera
};
}


#[derive(Debug, PartialEq)]
enum RenderList {
Tags,
Categories,
}

/// A tag or category
#[derive(Debug, Serialize, PartialEq)]
struct ListItem {
name: String,
slug: String,
count: usize,
}

impl ListItem {
pub fn new(name: &str, count: usize) -> ListItem {
ListItem {
name: name.to_string(),
slug: slugify(name),
count: count,
}
}
}

#[derive(Debug)]
pub struct Site {
pub base_path: PathBuf,
pub config: Config,
pub pages: HashMap<PathBuf, Page>,
pub sections: BTreeMap<PathBuf, Section>,
pub templates: Tera,
live_reload: bool,
output_path: PathBuf,
pub tags: HashMap<String, Vec<PathBuf>>,
pub categories: HashMap<String, Vec<PathBuf>>,
}

impl Site {
/// Parse a site at the given path. Defaults to the current dir
/// Passing in a path is only used in tests
pub fn new<P: AsRef<Path>>(path: P) -> Result<Site> {
let path = path.as_ref();

let tpl_glob = format!("{}/{}", path.to_string_lossy().replace("\\", "/"), "templates/**/*");
let mut tera = Tera::new(&tpl_glob).chain_err(|| "Error parsing templates")?;
tera.extend(&GUTENBERG_TERA)?;

let mut site = Site {
base_path: path.to_path_buf(),
config: get_config(path),
pages: HashMap::new(),
sections: BTreeMap::new(),
templates: tera,
live_reload: false,
output_path: PathBuf::from("public"),
tags: HashMap::new(),
categories: HashMap::new(),
};
site.parse()?;

Ok(site)
}

/// What the function name says
pub fn enable_live_reload(&mut self) {
self.live_reload = true;
}

/// Used by tests to change the output path to a tmp dir
#[doc(hidden)]
pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) {
self.output_path = path.as_ref().to_path_buf();
}

/// Reads all .md files in the `content` directory and create pages
/// out of them
pub fn parse(&mut self) -> Result<()> {
let path = self.base_path.to_string_lossy().replace("\\", "/");
let content_glob = format!("{}/{}", path, "content/**/*.md");

// parent_dir -> Section
let mut sections = BTreeMap::new();

// Glob is giving us the result order so _index will show up first
// for each directory
for entry in glob(&content_glob).unwrap().filter_map(|e| e.ok()) {
let path = entry.as_path();

if path.file_name().unwrap() == "_index.md" {
let section = Section::from_file(&path, &self.config)?;
sections.insert(section.parent_path.clone(), section);
} else {
let page = Page::from_file(&path, &self.config)?;
if sections.contains_key(&page.parent_path) {
sections.get_mut(&page.parent_path).unwrap().pages.push(page.clone());
}
self.pages.insert(page.file_path.clone(), page);
}
}
// Find out the direct subsections of each subsection if there are some
let mut grandparent_paths = HashMap::new();
for section in sections.values() {
let grand_parent = section.parent_path.parent().unwrap().to_path_buf();
grandparent_paths.entry(grand_parent).or_insert_with(|| vec![]).push(section.clone());
}

for (parent_path, section) in &mut sections {
section.pages.sort_by(|a, b| a.partial_cmp(b).unwrap());

match grandparent_paths.get(parent_path) {
Some(paths) => section.subsections.extend(paths.clone()),
None => continue,
};
}

self.sections = sections;
self.parse_tags_and_categories();

Ok(())
}

/// Separated from `parse` for easier testing
pub fn parse_tags_and_categories(&mut self) {
for page in self.pages.values() {
if let Some(ref category) = page.meta.category {
self.categories
.entry(category.to_string())
.or_insert_with(|| vec![])
.push(page.file_path.clone());
}

if let Some(ref tags) = page.meta.tags {
for tag in tags {
self.tags
.entry(tag.to_string())
.or_insert_with(|| vec![])
.push(page.file_path.clone());
}
}
}
}

/// Inject live reload script tag if in live reload mode
fn inject_livereload(&self, html: String) -> String {
if self.live_reload {
return html.replace(
"</body>",
r#"<script src="/livereload.js?port=1112&mindelay=10"></script></body>"#
);
}

html
}

/// Copy the content of the `static` folder into the `public` folder
///
/// TODO: only copy one file if possible because that would be a waste
/// to do re-copy the whole thing. Benchmark first to see if it's a big difference
pub fn copy_static_directory(&self) -> Result<()> {
let from = Path::new("static");
let target = Path::new("public");

for entry in WalkDir::new(from).into_iter().filter_map(|e| e.ok()) {
let relative_path = entry.path().strip_prefix(&from).unwrap();
let target_path = {
let mut target_path = target.to_path_buf();
target_path.push(relative_path);
target_path
};

if entry.path().is_dir() {
if !target_path.exists() {
create_directory(&target_path)?;
}
} else {
if target_path.exists() {
remove_file(&target_path)?;
}
copy(entry.path(), &target_path)?;
}
}
Ok(())
}

/// Deletes the `public` directory if it exists
pub fn clean(&self) -> Result<()> {
if Path::new("public").exists() {
// Delete current `public` directory so we can start fresh
remove_dir_all("public").chain_err(|| "Couldn't delete `public` directory")?;
}

Ok(())
}

pub fn rebuild_after_content_change(&mut self) -> Result<()> {
self.parse()?;
self.build()
}

pub fn rebuild_after_template_change(&mut self) -> Result<()> {
self.templates.full_reload()?;
self.build_pages()
}

pub fn build_pages(&self) -> Result<()> {
let public = self.output_path.clone();
if !public.exists() {
create_directory(&public)?;
}

let mut pages = vec![];

// First we render the pages themselves
for page in self.pages.values() {
// Copy the nesting of the content directory if we have sections for that page
let mut current_path = public.to_path_buf();

for component in page.url.split('/') {
current_path.push(component);

if !current_path.exists() {
create_directory(&current_path)?;
}
}

// Make sure the folder exists
create_directory(&current_path)?;

// Finally, create a index.html file there with the page rendered
let output = page.render_html(&self.templates, &self.config)?;
create_file(current_path.join("index.html"), &self.inject_livereload(output))?;

// Copy any asset we found previously into the same directory as the index.html
for asset in &page.assets {
let asset_path = asset.as_path();
copy(&asset_path, &current_path.join(asset_path.file_name().unwrap()))?;
}

pages.push(page);
}

// Outputting categories and pages
self.render_categories_and_tags(RenderList::Categories)?;
self.render_categories_and_tags(RenderList::Tags)?;

// And finally the index page
let mut context = Context::new();
pages.sort_by(|a, b| a.partial_cmp(b).unwrap());
context.add("pages", &pages);
context.add("config", &self.config);
let index = self.templates.render("index.html", &context)?;
create_file(public.join("index.html"), &self.inject_livereload(index))?;

Ok(())
}

/// Builds the site to the `public` directory after deleting it
pub fn build(&self) -> Result<()> {
self.clean()?;
self.build_pages()?;
self.render_sitemap()?;

if self.config.generate_rss.unwrap() {
self.render_rss_feed()?;
}

self.render_sections()?;
self.copy_static_directory()
}

/// Render the /{categories, list} pages and each individual category/tag page
/// They are the same thing fundamentally, a list of pages with something in common
fn render_categories_and_tags(&self, kind: RenderList) -> Result<()> {
let items = match kind {
RenderList::Categories => &self.categories,
RenderList::Tags => &self.tags,
};

if items.is_empty() {
return Ok(());
}

let (list_tpl_name, single_tpl_name, name, var_name) = if kind == RenderList::Categories {
("categories.html", "category.html", "categories", "category")
} else {
("tags.html", "tag.html", "tags", "tag")
};

// Create the categories/tags directory first
let public = self.output_path.clone();
let mut output_path = public.to_path_buf();
output_path.push(name);
create_directory(&output_path)?;

// Then render the index page for that kind.
// We sort by number of page in that category/tag
let mut sorted_items = vec![];
for (item, count) in Vec::from_iter(items).into_iter().map(|(a, b)| (a, b.len())) {
sorted_items.push(ListItem::new(&item, count));
}
sorted_items.sort_by(|a, b| b.count.cmp(&a.count));
let mut context = Context::new();
context.add(name, &sorted_items);
context.add("config", &self.config);
// And render it immediately
let list_output = self.templates.render(list_tpl_name, &context)?;
create_file(output_path.join("index.html"), &self.inject_livereload(list_output))?;

// Now, each individual item
for (item_name, pages_paths) in items.iter() {
let mut pages: Vec<&Page> = self.pages
.iter()
.filter(|&(path, _)| pages_paths.contains(&path))
.map(|(_, page)| page)
.collect();
pages.sort_by(|a, b| a.partial_cmp(b).unwrap());

let mut context = Context::new();
let slug = slugify(&item_name);
context.add(var_name, &item_name);
context.add(&format!("{}_slug", var_name), &slug);
context.add("pages", &pages);
context.add("config", &self.config);
let single_output = self.templates.render(single_tpl_name, &context)?;

create_directory(&output_path.join(&slug))?;
create_file(
output_path.join(&slug).join("index.html"),
&self.inject_livereload(single_output)
)?;
}

Ok(())
}

fn render_sitemap(&self) -> Result<()> {
let mut context = Context::new();
context.add("pages", &self.pages.values().collect::<Vec<&Page>>());
context.add("sections", &self.sections.values().collect::<Vec<&Section>>());

let mut categories = vec![];
if !self.categories.is_empty() {
categories.push(self.config.make_permalink("categories"));
for category in self.categories.keys() {
categories.push(
self.config.make_permalink(&format!("categories/{}", slugify(category)))
);
}
}
context.add("categories", &categories);

let mut tags = vec![];
if !self.tags.is_empty() {
tags.push(self.config.make_permalink("tags"));
for tag in self.tags.keys() {
tags.push(
self.config.make_permalink(&format!("tags/{}", slugify(tag)))
);
}
}
context.add("tags", &tags);

let sitemap = self.templates.render("sitemap.xml", &context)?;

create_file(self.output_path.join("sitemap.xml"), &sitemap)?;

Ok(())
}

fn render_rss_feed(&self) -> Result<()> {
let mut context = Context::new();
let mut pages = self.pages.values()
.filter(|p| p.meta.date.is_some())
.take(15) // limit to the last 15 elements
.collect::<Vec<&Page>>();

// Don't generate a RSS feed if none of the pages has a date
if pages.is_empty() {
return Ok(());
}

pages.sort_by(|a, b| a.partial_cmp(b).unwrap());
context.add("pages", &pages);
context.add("last_build_date", &pages[0].meta.date);
context.add("config", &self.config);

let rss_feed_url = if self.config.base_url.ends_with('/') {
format!("{}{}", self.config.base_url, "feed.xml")
} else {
format!("{}/{}", self.config.base_url, "feed.xml")
};
context.add("feed_url", &rss_feed_url);

let sitemap = self.templates.render("rss.xml", &context)?;

create_file(self.output_path.join("rss.xml"), &sitemap)?;

Ok(())
}

fn render_sections(&self) -> Result<()> {
let public = self.output_path.clone();

for section in self.sections.values() {
let mut output_path = public.to_path_buf();
for component in &section.components {
output_path.push(component);

if !output_path.exists() {
create_directory(&output_path)?;
}
}

let output = section.render_html(&self.templates, &self.config)?;
create_file(output_path.join("index.html"), &self.inject_livereload(output))?;
}

Ok(())
}
}

+ 20
- 0
src/templates/rss.xml View File

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

+ 25
- 0
src/templates/sitemap.xml View File

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

+ 62
- 2
src/utils.rs View File

@@ -1,8 +1,8 @@
use std::io::prelude::*; use std::io::prelude::*;
use std::fs::{File};
use std::fs::{File, create_dir};
use std::path::Path; use std::path::Path;


use errors::Result;
use errors::{Result, ResultExt};




pub fn create_file<P: AsRef<Path>>(path: P, content: &str) -> Result<()> { pub fn create_file<P: AsRef<Path>>(path: P, content: &str) -> Result<()> {
@@ -10,3 +10,63 @@ pub fn create_file<P: AsRef<Path>>(path: P, content: &str) -> Result<()> {
file.write_all(content.as_bytes())?; file.write_all(content.as_bytes())?;
Ok(()) Ok(())
} }

/// Very similar to `create_dir` from the std except it checks if the folder
/// exists before creating it
pub fn create_directory<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
if !path.exists() {
create_dir(path)
.chain_err(|| format!("Was not able to create folder {}", path.display()))?;
}
Ok(())
}


/// Return the content of a file, with error handling added
pub fn read_file<P: AsRef<Path>>(path: P) -> Result<String> {
let path = path.as_ref();

let mut content = String::new();
File::open(path)
.chain_err(|| format!("Failed to open '{:?}'", path.display()))?
.read_to_string(&mut content)?;

Ok(content)
}


/// Takes a full path to a .md and returns only the components after the `content` directory
/// Will not return the filename as last component
pub fn find_content_components<P: AsRef<Path>>(path: P) -> Vec<String> {
let path = path.as_ref();
let mut is_in_content = false;
let mut components = vec![];

for section in path.parent().unwrap().components() {
let component = section.as_ref().to_string_lossy();

if is_in_content {
components.push(component.to_string());
continue;
}

if component == "content" {
is_in_content = true;
}
}

components
}


#[cfg(test)]
mod tests {
use super::{find_content_components};

#[test]
fn test_find_content_components() {
let res = find_content_components("/home/vincent/code/site/content/posts/tutorials/python.md");
assert_eq!(res, ["posts".to_string(), "tutorials".to_string()]);
}
}

+ 593
- 0
sublime_themes/base16-ocean-dark.tmTheme View File

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

+ 589
- 0
sublime_themes/base16-ocean-light.tmTheme View File

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

+ 766
- 0
sublime_themes/gruvbox-dark.tmTheme View File

@@ -0,0 +1,766 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>comment</key>
<string>Based on original gruvbox color scheme.</string>
<key>author</key>
<string>peaceant</string>
<key>name</key>
<string>gruvbox</string>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>background</key>
<string>#282828</string>

<key>caret</key>
<string>#fcf9e3</string>

<key>foreground</key>
<string>#fdf4c1aa</string>

<key>invisibles</key>
<string>#fabd2f</string>

<key>lineHighlight</key>
<string>#3c3836</string>

<key>selection</key>
<string>#504945</string>

<key>bracketContentsForeground</key>
<string>#928374</string>

<key>bracketsForeground</key>
<string>#d5c4a1</string>

<key>guide</key>
<string>#3c3836</string>

<key>activeGuide</key>
<string>#a89984</string>

<key>stackGuide</key>
<string>#665c54</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation</string>
<key>scope</key>
<string>punctuation.definition.tag</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#83a598</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation</string>
<key>scope</key>
<string>punctuation.definition.entity</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#d3869b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant</string>
<key>scope</key>
<string>constant</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#d3869b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant escape</string>
<key>scope</key>
<string>constant.character.escape</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant other</string>
<key>scope</key>
<string>constant.other</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Entity</string>
<key>scope</key>
<string>entity</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keyword</string>
<key>scope</key>
<string>keyword.operator.comparison, keyword.operator, keyword.operator.symbolic, keyword.operator.string, keyword.operator.assignment, keyword.operator.arithmetic, keyword.operator.class, keyword.operator.key, keyword.operator.logical</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fe8019</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keyword</string>
<key>scope</key>
<string>keyword, keyword.operator.new, keyword.other, keyword.control</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fa5c4b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage</string>
<key>scope</key>
<string>storage</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fa5c4b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String</string>
<key>scope</key>
<string>string -string.unquoted.old-plist -string.unquoted.heredoc, string.unquoted.heredoc string</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comment</string>
<key>scope</key>
<string>comment</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#928374</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Regexp</string>
<key>scope</key>
<string>string.regexp constant.character.escape</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support</string>
<key>scope</key>
<string>support</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variable</string>
<key>scope</key>
<string>variable</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Lang Variable</string>
<key>scope</key>
<string>variable.language</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Function Call</string>
<key>scope</key>
<string>meta.function-call</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Invalid</string>
<key>scope</key>
<string>invalid</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#932b1e</string>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Embedded Source</string>
<key>scope</key>
<string>text source, string.unquoted.heredoc, source source</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String embedded-source</string>
<key>scope</key>
<string>string.quoted source</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String constant</string>
<key>scope</key>
<string>string</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>

<dict>
<key>name</key>
<string>Support.constant</string>
<key>scope</key>
<string>support.constant</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support.class</string>
<key>scope</key>
<string>support.class</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Meta.tag.A</string>
<key>scope</key>
<string>entity.name.tag</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Inner tag</string>
<key>scope</key>
<string>meta.tag, meta.tag entity</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css colors</string>
<key>scope</key>
<string>constant.other.color.rgb-value</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#83a598</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css tag-name</string>
<key>scope</key>
<string>meta.selector.css entity.name.tag</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fa5c4b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css#id</string>
<key>scope</key>
<string>meta.selector.css, entity.other.attribute-name.id</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css.class</string>
<key>scope</key>
<string>meta.selector.css entity.other.attribute-name.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css property-name:</string>
<key>scope</key>
<string>support.type.property-name.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css @at-rule</string>
<key>scope</key>
<string>meta.preprocessor.at-rule keyword.control.at-rule</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css additional-constants</string>
<key>scope</key>
<string>meta.property-value constant</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css additional-constants</string>
<key>scope</key>
<string>meta.property-value support.constant.named-color.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fe8019</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css constructor.argument</string>
<key>scope</key>
<string>meta.constructor.argument.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>


<!-- diff -->
<dict>
<key>name</key>
<string>diff.header</string>
<key>scope</key>
<string>meta.diff, meta.diff.header</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#83a598</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.deleted</string>
<key>scope</key>
<string>markup.deleted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fa5c4b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.changed</string>
<key>scope</key>
<string>markup.changed</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.inserted</string>
<key>scope</key>
<string>markup.inserted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>


<!-- markup -->
<dict>
<key>name</key>
<string>Bold Markup</string>
<key>scope</key>
<string>markup.bold</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Italic Markup</string>
<key>scope</key>
<string>markup.italic</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Heading Markup</string>
<key>scope</key>
<string>markup.heading</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>


<!-- Language Specific -->

<!-- PHP -->
<dict>
<key>name</key>
<string>PHP: class name</string>
<key>scope</key>
<string>entity.name.type.class.php</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>PHP: Comment</string>
<key>scope</key>
<string>keyword.other.phpdoc</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#928374</string>
</dict>
</dict>

<!-- CSS -->
<dict>
<key>name</key>
<string>CSS: numbers</string>
<key>scope</key>
<string>constant.numeric.css, keyword.other.unit.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d3869b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>CSS: entity dot, hash, comma, etc.</string>
<key>scope</key>
<string>punctuation.definition.entity.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<!-- JS -->
<dict>
<key>name</key>
<string>JS: variable</string>
<key>scope</key>
<string>variable.language.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>JS: unquoted labe</string>
<key>scope</key>
<string>string.unquoted.label.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fdf4c1</string>
</dict>
</dict>
<!-- SQL -->
<dict>
<key>name</key>
<string>Constant other sql</string>
<key>scope</key>
<string>constant.other.table-name.sql</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant other sql</string>
<key>scope</key>
<string>constant.other.database-name.sql</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>

<!-- Plugins -->

<!-- dired plugin -->
<dict>
<key>name</key>
<string>dired directory</string>
<key>scope</key>
<string>storage.type.dired.item.directory, dired.item.directory</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>

<!-- orgmode plugin -->
<dict>
<key>name</key>
<string>orgmode link</string>
<key>scope</key>
<string>orgmode.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
<key>fontStyle</key>
<string>underline</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode page</string>
<key>scope</key>
<string>orgmode.page</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b8bb26</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode break</string>
<key>scope</key>
<string>orgmode.break</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d3869b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode headline</string>
<key>scope</key>
<string>orgmode.headline</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8ec07c</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode tack</string>
<key>scope</key>
<string>orgmode.tack</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode follow up</string>
<key>scope</key>
<string>orgmode.follow_up</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode checkbox</string>
<key>scope</key>
<string>orgmode.checkbox</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode checkbox summary</string>
<key>scope</key>
<string>orgmode.checkbox.summary</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fabd2f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode tags</string>
<key>scope</key>
<string>orgmode.tags</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fa5c4b</string>
</dict>
</dict>

</array>
<key>uuid</key>
<string>06CD1FB2-A00A-4F8C-97B2-60E131980454</string>
</dict>

</plist>

+ 774
- 0
sublime_themes/gruvbox-light.tmTheme View File

@@ -0,0 +1,774 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>comment</key>
<string>Based on original gruvbox color scheme.</string>
<key>author</key>
<string>Martin Radimec</string>
<key>name</key>
<string>gruvbox</string>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>background</key>
<string>#FCF0CA</string>

<key>caret</key>
<string>#3C3836</string>

<key>foreground</key>
<string>#282828aa</string>

<key>invisibles</key>
<string>#b57614</string>

<key>lineHighlight</key>
<string>#EDDAB5</string>

<key>selection</key>
<string>#D6C3A3</string>

<key>bracketContentsForeground</key>
<string>#928374</string>

<key>bracketsForeground</key>
<string>#d5c4a1</string>

<key>guide</key>
<string>#EDDAB5</string>

<key>activeGuide</key>
<string>#7c6f64</string>

<key>stackGuide</key>
<string>#BEAD95</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation</string>
<key>scope</key>
<string>punctuation.definition.tag</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#076678</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation</string>
<key>scope</key>
<string>punctuation.definition.entity</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#8f3f71</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant</string>
<key>scope</key>
<string>constant</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#8f3f71</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant escape</string>
<key>scope</key>
<string>constant.character.escape</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant other</string>
<key>scope</key>
<string>constant.other</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Entity</string>
<key>scope</key>
<string>entity</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keyword</string>
<key>scope</key>
<string>keyword.operator.comparison, keyword.operator, keyword.operator.symbolic, keyword.operator.string, keyword.operator.assignment, keyword.operator.arithmetic, keyword.operator.class, keyword.operator.key, keyword.operator.logical</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#B23C15</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keyword</string>
<key>scope</key>
<string>keyword, keyword.operator.new, keyword.other, keyword.control</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#9d0006</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage</string>
<key>scope</key>
<string>storage</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#9d0006</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String</string>
<key>scope</key>
<string>string -string.unquoted.old-plist -string.unquoted.heredoc, string.unquoted.heredoc string</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comment</string>
<key>scope</key>
<string>comment</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#928374</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Regexp</string>
<key>scope</key>
<string>string.regexp constant.character.escape</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support</string>
<key>scope</key>
<string>support</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variable</string>
<key>scope</key>
<string>variable</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Lang Variable</string>
<key>scope</key>
<string>variable.language</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Function Call</string>
<key>scope</key>
<string>meta.function-call</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Invalid</string>
<key>scope</key>
<string>invalid</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#932b1e</string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Embedded Source</string>
<key>scope</key>
<string>text source, string.unquoted.heredoc, source source</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String embedded-source</string>
<key>scope</key>
<string>string.quoted source</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String constant</string>
<key>scope</key>
<string>string</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>

<dict>
<key>name</key>
<string>Support.constant</string>
<key>scope</key>
<string>support.constant</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support.class</string>
<key>scope</key>
<string>support.class</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Meta.tag.A</string>
<key>scope</key>
<string>entity.name.tag</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Inner tag</string>
<key>scope</key>
<string>meta.tag, meta.tag entity</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css colors</string>
<key>scope</key>
<string>constant.other.color.rgb-value</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#076678</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css tag-name</string>
<key>scope</key>
<string>meta.selector.css entity.name.tag</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#9d0006</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css#id</string>
<key>scope</key>
<string>meta.selector.css, entity.other.attribute-name.id</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css.class</string>
<key>scope</key>
<string>meta.selector.css entity.other.attribute-name.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css property-name:</string>
<key>scope</key>
<string>support.type.property-name.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css @at-rule</string>
<key>scope</key>
<string>meta.preprocessor.at-rule keyword.control.at-rule</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css additional-constants</string>
<key>scope</key>
<string>meta.property-value constant</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css additional-constants</string>
<key>scope</key>
<string>meta.property-value support.constant.named-color.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#B23C15</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>css constructor.argument</string>
<key>scope</key>
<string>meta.constructor.argument.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>


<!-- diff -->
<dict>
<key>name</key>
<string>diff.header</string>
<key>scope</key>
<string>meta.diff, meta.diff.header</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#076678</string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.deleted</string>
<key>scope</key>
<string>markup.deleted</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#9d0006</string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.changed</string>
<key>scope</key>
<string>markup.changed</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#b57614</string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.inserted</string>
<key>scope</key>
<string>markup.inserted</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#407959</string>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>


<!-- markup -->
<dict>
<key>name</key>
<string>Bold Markup</string>
<key>scope</key>
<string>markup.bold</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Italic Markup</string>
<key>scope</key>
<string>markup.italic</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Heading Markup</string>
<key>scope</key>
<string>markup.heading</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>


<!-- Language Specific -->

<!-- PHP -->
<dict>
<key>name</key>
<string>PHP: class name</string>
<key>scope</key>
<string>entity.name.type.class.php</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>PHP: Comment</string>
<key>scope</key>
<string>keyword.other.phpdoc</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#928374</string>
</dict>
</dict>

<!-- CSS -->
<dict>
<key>name</key>
<string>CSS: numbers</string>
<key>scope</key>
<string>constant.numeric.css, keyword.other.unit.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8f3f71</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>CSS: entity dot, hash, comma, etc.</string>
<key>scope</key>
<string>punctuation.definition.entity.css</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<!-- JS -->
<dict>
<key>name</key>
<string>JS: variable</string>
<key>scope</key>
<string>variable.language.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>JS: unquoted labe</string>
<key>scope</key>
<string>string.unquoted.label.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#282828</string>
</dict>
</dict>
<!-- SQL -->
<dict>
<key>name</key>
<string>Constant other sql</string>
<key>scope</key>
<string>constant.other.table-name.sql</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant other sql</string>
<key>scope</key>
<string>constant.other.database-name.sql</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>

<!-- Plugins -->

<!-- dired plugin -->
<dict>
<key>name</key>
<string>dired directory</string>
<key>scope</key>
<string>storage.type.dired.item.directory, dired.item.directory</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>

<!-- orgmode plugin -->
<dict>
<key>name</key>
<string>orgmode link</string>
<key>scope</key>
<string>orgmode.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
<key>fontStyle</key>
<string>underline</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode page</string>
<key>scope</key>
<string>orgmode.page</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#79740e</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode break</string>
<key>scope</key>
<string>orgmode.break</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#8f3f71</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode headline</string>
<key>scope</key>
<string>orgmode.headline</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#407959</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode tack</string>
<key>scope</key>
<string>orgmode.tack</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode follow up</string>
<key>scope</key>
<string>orgmode.follow_up</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode checkbox</string>
<key>scope</key>
<string>orgmode.checkbox</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode checkbox summary</string>
<key>scope</key>
<string>orgmode.checkbox.summary</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b57614</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>orgmode tags</string>
<key>scope</key>
<string>orgmode.tags</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#9d0006</string>
</dict>
</dict>

</array>
<key>uuid</key>
<string>06CD1FB2-A00A-4F8C-97B2-60E131980454</string>
</dict>

</plist>

+ 1725
- 0
sublime_themes/inspired-github.tmTheme
File diff suppressed because it is too large
View File


+ 1843
- 0
sublime_themes/kronuz.tmTheme
File diff suppressed because it is too large
View File


+ 1011
- 0
sublime_themes/material-dark.tmTheme
File diff suppressed because it is too large
View File


+ 1011
- 0
sublime_themes/material-light.tmTheme
File diff suppressed because it is too large
View File


+ 297
- 0
sublime_themes/monokai.tmTheme View File

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

+ 1189
- 0
sublime_themes/solarized-dark.tmTheme
File diff suppressed because it is too large
View File


+ 1189
- 0
sublime_themes/solarized-light.tmTheme
File diff suppressed because it is too large
View File


+ 7
- 0
test_site/config.toml View File

@@ -0,0 +1,7 @@
title = "My site"
base_url = "https://replace-this-with-your-url.com"
highlight_code = true


[extra.author]
name = "Vincent Prouillet"

+ 4
- 0
test_site/content/posts/_index.md View File

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

+ 7
- 0
test_site/content/posts/fixed-slug.md View File

@@ -0,0 +1,7 @@
+++
title = "Fixed slug"
description = ""
slug = "something-else"
+++

A simple page with a slug defined

+ 7
- 0
test_site/content/posts/fixed-url.md View File

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

A simple page with fixed url

+ 6
- 0
test_site/content/posts/no-section/simple.md View File

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

A simple page

+ 6
- 0
test_site/content/posts/python.md View File

@@ -0,0 +1,6 @@
+++
title = "Python in posts"
description = ""
+++

Same filename but different path

+ 6
- 0
test_site/content/posts/simple.md View File

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

A simple page

+ 4
- 0
test_site/content/posts/tutorials/_index.md View File

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

+ 4
- 0
test_site/content/posts/tutorials/devops/_index.md View File

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

+ 6
- 0
test_site/content/posts/tutorials/devops/docker.md View File

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

A simple page

+ 6
- 0
test_site/content/posts/tutorials/devops/nix.md View File

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

A simple page

+ 4
- 0
test_site/content/posts/tutorials/programming/_index.md View File

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

+ 6
- 0
test_site/content/posts/tutorials/programming/python.md View File

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

A simple page

+ 6
- 0
test_site/content/posts/tutorials/programming/rust.md View File

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

A simple page

+ 7
- 0
test_site/content/posts/with-assets/index.md View File

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

Hello world

+ 0
- 0
test_site/content/posts/with-assets/with.js View File


+ 0
- 0
test_site/static/scripts/hello.js View File


+ 3
- 0
test_site/static/site.css View File

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

+ 3
- 0
test_site/templates/categories.html View File

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

+ 8
- 0
test_site/templates/category.html View File

@@ -0,0 +1,8 @@
Category: {{ category }}


{% for page in pages %}
<article>
<h3 class="post__title"><a href="{{ page.url }}">{{ page.title }}</a></h3>
</article>
{% endfor %}

+ 27
- 0
test_site/templates/index.html View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="{{ config.language_code }}">
<head>
<meta charset="UTF-8">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{ config.description }}">
<meta name="author" content="{{ config.extra.author.name }}">
<link href="https://fonts.googleapis.com/css?family=Fira+Mono|Fira+Sans|Merriweather" rel="stylesheet">
<link href="site.css" rel="stylesheet">
<title>{{ config.title }}</title>
</head>

<body>
<div class="content">
{% block content %}
<div class="list-posts">
{% for page in pages %}
<article>
<h3 class="post__title"><a href="{{ page.url }}">{{ page.title }}</a></h3>
</article>
{% endfor %}
</div>
{% endblock content %}
</div>
</body>
</html>

+ 5
- 0
test_site/templates/page.html View File

@@ -0,0 +1,5 @@
{% extends "index.html" %}

{% block content %}
{{ page.content | safe }}
{% endblock content %}

+ 10
- 0
test_site/templates/section.html View File

@@ -0,0 +1,10 @@
{% extends "index.html" %}

{% block content %}
{% for page in section.pages %}
{{page.title}}
{% endfor %}
{% for subsection in section.subsections %}
{{subsection.title}}
{% endfor %}
{% endblock content %}

+ 7
- 0
test_site/templates/tag.html View File

@@ -0,0 +1,7 @@
Tag: {{ tag }}

{% for page in pages %}
<article>
<h3 class="post__title"><a href="{{ page.url }}">{{ page.title }}</a></h3>
</article>
{% endfor %}

+ 3
- 0
test_site/templates/tags.html View File

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

+ 197
- 0
tests/front_matter.rs View File

@@ -0,0 +1,197 @@
extern crate gutenberg;
extern crate tera;

use std::path::Path;

use gutenberg::{FrontMatter, split_content};
use tera::to_value;


#[test]
fn test_can_parse_a_valid_front_matter() {
let content = r#"
title = "Hello"
description = "hey there""#;
let res = FrontMatter::parse(content);
println!("{:?}", res);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(res.title, "Hello".to_string());
assert_eq!(res.description, "hey there".to_string());
}

#[test]
fn test_can_parse_tags() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
tags = ["rust", "html"]"#;
let res = FrontMatter::parse(content);
assert!(res.is_ok());
let res = res.unwrap();

assert_eq!(res.title, "Hello".to_string());
assert_eq!(res.slug.unwrap(), "hello-world".to_string());
assert_eq!(res.tags.unwrap(), ["rust".to_string(), "html".to_string()]);
}

#[test]
fn test_can_parse_extra_attributes_in_frontmatter() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"

[extra]
language = "en"
authors = ["Bob", "Alice"]"#;
let res = FrontMatter::parse(content);
assert!(res.is_ok());
let res = res.unwrap();

assert_eq!(res.title, "Hello".to_string());
assert_eq!(res.slug.unwrap(), "hello-world".to_string());
let extra = res.extra.unwrap();
assert_eq!(extra.get("language").unwrap(), &to_value("en").unwrap());
assert_eq!(
extra.get("authors").unwrap(),
&to_value(["Bob".to_string(), "Alice".to_string()]).unwrap()
);
}

#[test]
fn test_is_ok_with_url_instead_of_slug() {
let content = r#"
title = "Hello"
description = "hey there"
url = "hello-world""#;
let res = FrontMatter::parse(content);
assert!(res.is_ok());
let res = res.unwrap();
assert!(res.slug.is_none());
assert_eq!(res.url.unwrap(), "hello-world".to_string());
}

#[test]
fn test_errors_with_empty_front_matter() {
let content = r#" "#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}

#[test]
fn test_errors_with_invalid_front_matter() {
let content = r#"title = 1\n"#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}

#[test]
fn test_errors_with_missing_required_value_front_matter() {
let content = r#"title = """#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}

#[test]
fn test_errors_on_non_string_tag() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
tags = ["rust", 1]"#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}

#[test]
fn test_errors_on_present_but_empty_slug() {
let content = r#"
title = "Hello"
description = "hey there"
slug = """#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}

#[test]
fn test_errors_on_present_but_empty_url() {
let content = r#"
title = "Hello"
description = "hey there"
url = """#;
let res = FrontMatter::parse(content);
assert!(res.is_err());
}

#[test]
fn test_parse_date_yyyy_mm_dd() {
let content = r#"
title = "Hello"
description = "hey there"
date = "2016-10-10""#;
let res = FrontMatter::parse(content).unwrap();
assert!(res.parse_date().is_some());
}

#[test]
fn test_parse_date_rfc3339() {
let content = r#"
title = "Hello"
description = "hey there"
date = "2002-10-02T15:00:00Z""#;
let res = FrontMatter::parse(content).unwrap();
assert!(res.parse_date().is_some());
}

#[test]
fn test_cant_parse_random_date_format() {
let content = r#"
title = "Hello"
description = "hey there"
date = "2002/10/12""#;
let res = FrontMatter::parse(content).unwrap();
assert!(res.parse_date().is_none());
}


#[test]
fn test_can_split_content_valid() {
let content = r#"
+++
title = "Title"
description = "hey there"
date = "2002/10/12"
+++
Hello
"#;
let (front_matter, content) = split_content(Path::new(""), content).unwrap();
assert_eq!(content, "Hello\n");
assert_eq!(front_matter.title, "Title");
}

#[test]
fn test_can_split_content_with_only_frontmatter_valid() {
let content = r#"
+++
title = "Title"
description = "hey there"
date = "2002/10/12"
+++"#;
let (front_matter, content) = split_content(Path::new(""), content).unwrap();
assert_eq!(content, "");
assert_eq!(front_matter.title, "Title");
}

#[test]
fn test_error_if_cannot_locate_frontmatter() {
let content = r#"
+++
title = "Title"
description = "hey there"
date = "2002/10/12"
"#;
let res = split_content(Path::new(""), content);
assert!(res.is_err());
}

+ 249
- 0
tests/page.rs View File

@@ -0,0 +1,249 @@
extern crate gutenberg;
extern crate tempdir;

use tempdir::TempDir;

use std::fs::File;
use std::path::Path;

use gutenberg::{Page, Config};


#[test]
fn test_can_parse_a_valid_page() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse(Path::new("post.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();

assert_eq!(page.meta.title, "Hello".to_string());
assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string());
assert_eq!(page.raw_content, "Hello world".to_string());
assert_eq!(page.content, "<p>Hello world</p>\n".to_string());
}

#[test]
fn test_can_find_one_parent_directory() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse(Path::new("content/posts/intro.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.components, vec!["posts".to_string()]);
}

#[test]
fn test_can_find_multiple_parent_directories() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse(Path::new("content/posts/intro/start.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.components, vec!["posts".to_string(), "intro".to_string()]);
}

#[test]
fn test_can_make_url_from_sections_and_slug() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let mut conf = Config::default();
conf.base_url = "http://hello.com/".to_string();
let res = Page::parse(Path::new("content/posts/intro/start.md"), content, &conf);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.url, "posts/intro/hello-world");
assert_eq!(page.permalink, "http://hello.com/posts/intro/hello-world");
}

#[test]
fn test_can_make_permalink_with_non_trailing_slash_base_url() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let mut conf = Config::default();
conf.base_url = "http://hello.com".to_string();
let res = Page::parse(Path::new("content/posts/intro/hello-world.md"), content, &conf);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.url, "posts/intro/hello-world");
assert_eq!(page.permalink, format!("{}{}", conf.base_url, "/posts/intro/hello-world"));
}

#[test]
fn test_can_make_url_from_slug_only() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse(Path::new("start.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.url, "hello-world");
assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "hello-world"));
}

#[test]
fn test_errors_on_invalid_front_matter_format() {
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse(Path::new("start.md"), content, &Config::default());
assert!(res.is_err());
}

#[test]
fn test_can_make_slug_from_non_slug_filename() {
let content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world"#;
let res = Page::parse(Path::new("file with space.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.slug, "file-with-space");
assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "file-with-space"));
}

#[test]
fn test_trim_slug_if_needed() {
let content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world"#;
let res = Page::parse(Path::new(" file with space.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.slug, "file-with-space");
assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "file-with-space"));
}

#[test]
fn test_reading_analytics_short() {
let content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world"#;
let res = Page::parse(Path::new("hello.md"), content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
let (word_count, reading_time) = page.get_reading_analytics();
assert_eq!(word_count, 2);
assert_eq!(reading_time, 0);
}

#[test]
fn test_reading_analytics_long() {
let mut content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world"#.to_string();
for _ in 0..1000 {
content.push_str(" Hello world");
}
let res = Page::parse(Path::new("hello.md"), &content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
let (word_count, reading_time) = page.get_reading_analytics();
assert_eq!(word_count, 2002);
assert_eq!(reading_time, 10);
}

#[test]
fn test_automatic_summary_is_empty_string() {
let content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world"#.to_string();
let res = Page::parse(Path::new("hello.md"), &content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.summary, "");
}

#[test]
fn test_can_specify_summary() {
let content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world
<!-- more -->
"#.to_string();
let res = Page::parse(Path::new("hello.md"), &content, &Config::default());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.summary, "<p>Hello world</p>\n");
}

#[test]
fn test_can_auto_detect_when_highlighting_needed() {
let content = r#"
+++
title = "Hello"
description = "hey there"
+++
```
Hey there
```
"#.to_string();
let mut config = Config::default();
config.highlight_code = Some(true);
let res = Page::parse(Path::new("hello.md"), &content, &config);
assert!(res.is_ok());
let page = res.unwrap();
assert!(page.content.starts_with("<pre"));
}

#[test]
fn test_file_not_named_index_with_assets() {
let tmp_dir = TempDir::new("example").expect("create temp dir");
File::create(tmp_dir.path().join("something.md")).unwrap();
File::create(tmp_dir.path().join("example.js")).unwrap();
File::create(tmp_dir.path().join("graph.jpg")).unwrap();
File::create(tmp_dir.path().join("fail.png")).unwrap();

let page = Page::from_file(tmp_dir.path().join("something.md"), &Config::default());
assert!(page.is_err());
}

+ 258
- 0
tests/site.rs View File

@@ -0,0 +1,258 @@
extern crate gutenberg;
extern crate tempdir;
extern crate glob;

use std::env;
use std::path::Path;
use std::fs::File;
use std::io::prelude::*;

// use glob::glob;
use tempdir::TempDir;
use gutenberg::{Site};


#[test]
fn test_can_parse_site() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let site = Site::new(&path).unwrap();

// Correct number of pages (sections are pages too)
assert_eq!(site.pages.len(), 10);
let posts_path = path.join("content").join("posts");

// Make sure we remove all the pwd + content from the sections
let basic = &site.pages[&posts_path.join("simple.md")];
assert_eq!(basic.components, vec!["posts".to_string()]);

// Make sure the page with a url doesn't have any sections
let url_post = &site.pages[&posts_path.join("fixed-url.md")];
assert!(url_post.components.is_empty());

// Make sure the article in a folder with only asset doesn't get counted as a section
let asset_folder_post = &site.pages[&posts_path.join("with-assets").join("index.md")];
assert_eq!(asset_folder_post.components, vec!["posts".to_string()]);

// That we have the right number of sections
assert_eq!(site.sections.len(), 4);

// And that the sections are correct
let posts_section = &site.sections[&posts_path];
assert_eq!(posts_section.subsections.len(), 1);
assert_eq!(posts_section.pages.len(), 5);

let tutorials_section = &site.sections[&posts_path.join("tutorials")];
assert_eq!(tutorials_section.subsections.len(), 2);
assert_eq!(tutorials_section.pages.len(), 0);

let devops_section = &site.sections[&posts_path.join("tutorials").join("devops")];
assert_eq!(devops_section.subsections.len(), 0);
assert_eq!(devops_section.pages.len(), 2);

let prog_section = &site.sections[&posts_path.join("tutorials").join("programming")];
assert_eq!(prog_section.subsections.len(), 0);
assert_eq!(prog_section.pages.len(), 2);
}

// 2 helper macros to make all the build testing more bearable
macro_rules! file_exists {
($root: expr, $path: expr) => {
{
let mut path = $root.clone();
for component in $path.split("/") {
path = path.join(component);
}
Path::new(&path).exists()
}
}
}

macro_rules! file_contains {
($root: expr, $path: expr, $text: expr) => {
{
let mut path = $root.clone();
for component in $path.split("/") {
path = path.join(component);
}
let mut file = File::open(&path).unwrap();
let mut s = String::new();
file.read_to_string(&mut s).unwrap();
s.contains($text)
}
}
}

#[test]
fn test_can_build_site_without_live_reload() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path).unwrap();
let tmp_dir = TempDir::new("example").expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
site.build().unwrap();

assert!(Path::new(&public).exists());

assert!(file_exists!(public, "index.html"));
assert!(file_exists!(public, "sitemap.xml"));
assert!(file_exists!(public, "a-fixed-url/index.html"));

assert!(file_exists!(public, "posts/python/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/nix/index.html"));
assert!(file_exists!(public, "posts/with-assets/index.html"));

// Sections
assert!(file_exists!(public, "posts/index.html"));
assert!(file_exists!(public, "posts/tutorials/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/index.html"));
assert!(file_exists!(public, "posts/tutorials/programming/index.html"));
// TODO: add assertion for syntax highlighting

// No tags or categories
assert_eq!(file_exists!(public, "categories/index.html"), false);
assert_eq!(file_exists!(public, "tags/index.html"), false);

// no live reload code
assert_eq!(file_contains!(public, "index.html", "/livereload.js?port=1112&mindelay=10"), false);

// Both pages and sections are in the sitemap
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/posts/simple</loc>"));
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/posts</loc>"));
}

#[test]
fn test_can_build_site_with_live_reload() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path).unwrap();
let tmp_dir = TempDir::new("example").expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
site.enable_live_reload();
site.build().unwrap();

assert!(Path::new(&public).exists());

assert!(file_exists!(public, "index.html"));
assert!(file_exists!(public, "sitemap.xml"));
assert!(file_exists!(public, "a-fixed-url/index.html"));

assert!(file_exists!(public, "posts/python/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/nix/index.html"));
assert!(file_exists!(public, "posts/with-assets/index.html"));

// Sections
assert!(file_exists!(public, "posts/index.html"));
assert!(file_exists!(public, "posts/tutorials/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/index.html"));
assert!(file_exists!(public, "posts/tutorials/programming/index.html"));
// TODO: add assertion for syntax highlighting

// No tags or categories
assert_eq!(file_exists!(public, "categories/index.html"), false);
assert_eq!(file_exists!(public, "tags/index.html"), false);

// no live reload code
assert!(file_contains!(public, "index.html", "/livereload.js?port=1112&mindelay=10"));
}

#[test]
fn test_can_build_site_with_categories() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path).unwrap();

for (i, page) in site.pages.values_mut().enumerate() {
page.meta.category = if i % 2 == 0 {
Some("A".to_string())
} else {
Some("B".to_string())
};
}
site.parse_tags_and_categories();
let tmp_dir = TempDir::new("example").expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
site.build().unwrap();

assert!(Path::new(&public).exists());
assert_eq!(site.categories.len(), 2);

assert!(file_exists!(public, "index.html"));
assert!(file_exists!(public, "sitemap.xml"));
assert!(file_exists!(public, "a-fixed-url/index.html"));

assert!(file_exists!(public, "posts/python/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/nix/index.html"));
assert!(file_exists!(public, "posts/with-assets/index.html"));

// Sections
assert!(file_exists!(public, "posts/index.html"));
assert!(file_exists!(public, "posts/tutorials/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/index.html"));
assert!(file_exists!(public, "posts/tutorials/programming/index.html"));
// TODO: add assertion for syntax highlighting

// Categories are there
assert!(file_exists!(public, "categories/index.html"));
assert!(file_exists!(public, "categories/a/index.html"));
assert!(file_exists!(public, "categories/b/index.html"));
// Tags aren't
assert_eq!(file_exists!(public, "tags/index.html"), false);

// Categories are in the sitemap
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/categories</loc>"));
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/categories/a</loc>"));
}

#[test]
fn test_can_build_site_with_tags() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path).unwrap();

for (i, page) in site.pages.values_mut().enumerate() {
page.meta.tags = if i % 2 == 0 {
Some(vec!["tag1".to_string(), "tag2".to_string()])
} else {
Some(vec!["tag with space".to_string()])
};
}
site.parse_tags_and_categories();

let tmp_dir = TempDir::new("example").expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
site.build().unwrap();

assert!(Path::new(&public).exists());
assert_eq!(site.tags.len(), 3);

assert!(file_exists!(public, "index.html"));
assert!(file_exists!(public, "sitemap.xml"));
assert!(file_exists!(public, "a-fixed-url/index.html"));

assert!(file_exists!(public, "posts/python/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/nix/index.html"));
assert!(file_exists!(public, "posts/with-assets/index.html"));

// Sections
assert!(file_exists!(public, "posts/index.html"));
assert!(file_exists!(public, "posts/tutorials/index.html"));
assert!(file_exists!(public, "posts/tutorials/devops/index.html"));
assert!(file_exists!(public, "posts/tutorials/programming/index.html"));
// TODO: add assertion for syntax highlighting

// Tags are there
assert!(file_exists!(public, "tags/index.html"));
assert!(file_exists!(public, "tags/tag1/index.html"));
assert!(file_exists!(public, "tags/tag2/index.html"));
assert!(file_exists!(public, "tags/tag-with-space/index.html"));
// Categories aren't
assert_eq!(file_exists!(public, "categories/index.html"), false);
// Tags are in the sitemap
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/tags</loc>"));
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/tags/tag-with-space</loc>"));
}

Loading…
Cancel
Save