refactor(config): Port to new cobalt-config crate

BREAKING CHANGE: Old-style frontmatter is removed and some config keys
are renamed:
- Sort Order: Asc / Desc were lowercased
- Sass Style: Nested / Expanded / Compact / Compressed were lowercased
- Pagination: All, Tags, Categories, Dates were lowercased
This commit is contained in:
Ed Page 2019-12-05 15:29:46 -07:00
parent 3e5bdad921
commit 0b3ef05ae3
42 changed files with 394 additions and 1994 deletions

32
Cargo.lock generated
View file

@ -482,6 +482,7 @@ dependencies = [
"assert_fs 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", "assert_fs 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cobalt-config 0.16.0",
"deunicode 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "deunicode 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -510,6 +511,22 @@ dependencies = [
"walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "cobalt-config"
version = "0.16.0"
dependencies = [
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"deunicode 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
"liquid 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"once_cell 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)",
"status 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "const-random" name = "const-random"
version = "0.1.6" version = "0.1.6"
@ -1368,6 +1385,11 @@ dependencies = [
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "once_cell"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "onig" name = "onig"
version = "4.3.3" version = "4.3.3"
@ -1988,6 +2010,14 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "status"
version = "0.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "string" name = "string"
version = "0.2.1" version = "0.2.1"
@ -2657,6 +2687,7 @@ dependencies = [
"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" "checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09"
"checksum num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c81ffc11c212fa327657cb19dd85eb7419e163b5b076bede2bdb5c974c07e4" "checksum num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c81ffc11c212fa327657cb19dd85eb7419e163b5b076bede2bdb5c974c07e4"
"checksum num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72" "checksum num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72"
"checksum once_cell 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "891f486f630e5c5a4916c7e16c4b24a53e78c860b646e9f8e005e4f16847bfed"
"checksum onig 4.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8518fcb2b1b8c2f45f0ad499df4fda6087fc3475ca69a185c173b8315d2fb383" "checksum onig 4.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8518fcb2b1b8c2f45f0ad499df4fda6087fc3475ca69a185c173b8315d2fb383"
"checksum onig_sys 69.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388410bf5fa341f10e58e6db3975f4bea1ac30247dd79d37a9e5ced3cb4cc3b0" "checksum onig_sys 69.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388410bf5fa341f10e58e6db3975f4bea1ac30247dd79d37a9e5ced3cb4cc3b0"
"checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" "checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
@ -2729,6 +2760,7 @@ dependencies = [
"checksum smallvec 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6" "checksum smallvec 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6"
"checksum smallvec 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ecf3b85f68e8abaa7555aa5abdb1153079387e60b718283d732f03897fcfc86" "checksum smallvec 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ecf3b85f68e8abaa7555aa5abdb1153079387e60b718283d732f03897fcfc86"
"checksum socket2 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "e8b74de517221a2cb01a53349cf54182acdc31a074727d3079068448c0676d85" "checksum socket2 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "e8b74de517221a2cb01a53349cf54182acdc31a074727d3079068448c0676d85"
"checksum status 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "36f7e6db3d58ab9e499a2722b0ddaaffeda2d76865a1749d5411182176c594f9"
"checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" "checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d"
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"

View file

@ -21,6 +21,7 @@ name = "cobalt"
doc = false doc = false
[dependencies] [dependencies]
cobalt-config = { version = "0.16", path = "crates/config" }
clap = "2.33" clap = "2.33"
liquid = { version = "0.19.0", features = ["jekyll-filters"] } liquid = { version = "0.19.0", features = ["jekyll-filters"] }
deunicode = "1.0.0" deunicode = "1.0.0"

View file

@ -7,7 +7,6 @@ use env_logger;
use failure::ResultExt; use failure::ResultExt;
use crate::error::*; use crate::error::*;
use cobalt;
pub fn get_config_args() -> Vec<clap::Arg<'static, 'static>> { pub fn get_config_args() -> Vec<clap::Arg<'static, 'static>> {
[ [
@ -36,16 +35,16 @@ pub fn get_config_args() -> Vec<clap::Arg<'static, 'static>> {
.to_vec() .to_vec()
} }
pub fn get_config(matches: &clap::ArgMatches) -> Result<cobalt::ConfigBuilder> { pub fn get_config(matches: &clap::ArgMatches) -> Result<cobalt_config::Config> {
let config_path = matches.value_of("config"); let config_path = matches.value_of("config");
// Fetch config information if available // Fetch config information if available
let mut config = if let Some(config_path) = config_path { let mut config = if let Some(config_path) = config_path {
cobalt::ConfigBuilder::from_file(config_path) cobalt_config::Config::from_file(config_path)
.with_context(|_| failure::format_err!("Error reading config file {:?}", config_path))? .with_context(|_| failure::format_err!("Error reading config file {:?}", config_path))?
} else { } else {
let cwd = env::current_dir().expect("How does this fail?"); let cwd = env::current_dir().expect("How does this fail?");
cobalt::ConfigBuilder::from_cwd(cwd)? cobalt_config::Config::from_cwd(cwd)?
}; };
config.abs_dest = matches.value_of("destination").map(path::PathBuf::from); config.abs_dest = matches.value_of("destination").map(path::PathBuf::from);

View file

@ -17,7 +17,7 @@ pub fn build_command_args() -> clap::App<'static, 'static> {
pub fn build_command(matches: &clap::ArgMatches) -> Result<()> { pub fn build_command(matches: &clap::ArgMatches) -> Result<()> {
let config = args::get_config(matches)?; let config = args::get_config(matches)?;
let config = config.build()?; let config = cobalt::cobalt_model::Config::from_config(config)?;
build(config.clone())?; build(config.clone())?;
info!("Build successful"); info!("Build successful");
@ -43,7 +43,7 @@ pub fn clean_command_args() -> clap::App<'static, 'static> {
pub fn clean_command(matches: &clap::ArgMatches) -> Result<()> { pub fn clean_command(matches: &clap::ArgMatches) -> Result<()> {
let config = args::get_config(matches)?; let config = args::get_config(matches)?;
let config = config.build()?; let config = cobalt::cobalt_model::Config::from_config(config)?;
clean(&config) clean(&config)
} }
@ -100,7 +100,7 @@ pub fn import_command_args() -> clap::App<'static, 'static> {
pub fn import_command(matches: &clap::ArgMatches) -> Result<()> { pub fn import_command(matches: &clap::ArgMatches) -> Result<()> {
let config = args::get_config(matches)?; let config = args::get_config(matches)?;
let config = config.build()?; let config = cobalt::cobalt_model::Config::from_config(config)?;
clean(&config)?; clean(&config)?;
build(config.clone())?; build(config.clone())?;

View file

@ -30,7 +30,7 @@ pub fn debug_command(matches: &clap::ArgMatches) -> Result<()> {
match matches.subcommand() { match matches.subcommand() {
("config", _) => { ("config", _) => {
let config = args::get_config(matches)?; let config = args::get_config(matches)?;
let config = config.build()?; let config = cobalt::cobalt_model::Config::from_config(config)?;
println!("{}", config); println!("{}", config);
} }
("highlight", Some(matches)) => match matches.subcommand() { ("highlight", Some(matches)) => match matches.subcommand() {
@ -48,7 +48,7 @@ pub fn debug_command(matches: &clap::ArgMatches) -> Result<()> {
}, },
("files", Some(matches)) => { ("files", Some(matches)) => {
let config = args::get_config(matches)?; let config = args::get_config(matches)?;
let config = config.build()?; let config = cobalt::cobalt_model::Config::from_config(config)?;
let collection = matches.value_of("COLLECTION"); let collection = matches.value_of("COLLECTION");
match collection { match collection {
Some("assets") => { Some("assets") => {

View file

@ -62,7 +62,7 @@ pub fn new_command_args() -> clap::App<'static, 'static> {
pub fn new_command(matches: &clap::ArgMatches) -> Result<()> { pub fn new_command(matches: &clap::ArgMatches) -> Result<()> {
let config = args::get_config(matches)?; let config = args::get_config(matches)?;
let config = config.build()?; let config = cobalt::cobalt_model::Config::from_config(config)?;
let title = matches.value_of("TITLE").unwrap(); let title = matches.value_of("TITLE").unwrap();
@ -107,7 +107,7 @@ pub fn rename_command_args() -> clap::App<'static, 'static> {
pub fn rename_command(matches: &clap::ArgMatches) -> Result<()> { pub fn rename_command(matches: &clap::ArgMatches) -> Result<()> {
let config = args::get_config(matches)?; let config = args::get_config(matches)?;
let config = config.build()?; let config = cobalt::cobalt_model::Config::from_config(config)?;
let source = path::PathBuf::from(matches.value_of("SRC").unwrap()); let source = path::PathBuf::from(matches.value_of("SRC").unwrap());
@ -144,7 +144,7 @@ pub fn publish_command(matches: &clap::ArgMatches) -> Result<()> {
file.push(path::Path::new(filename)); file.push(path::Path::new(filename));
let file = file; let file = file;
let config = args::get_config(matches)?; let config = args::get_config(matches)?;
let config = config.build()?; let config = cobalt::cobalt_model::Config::from_config(config)?;
publish_document(&config, &file) publish_document(&config, &file)
.with_context(|_| failure::format_err!("Could not publish `{:?}`", file))?; .with_context(|_| failure::format_err!("Could not publish `{:?}`", file))?;
@ -310,11 +310,10 @@ pub fn create_new_document(
DEFAULT.get(file_type).unwrap_or(&POST_MD).to_string() DEFAULT.get(file_type).unwrap_or(&POST_MD).to_string()
}; };
let doc = cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::parse(&source)?; let doc = cobalt_model::Document::parse(&source)?;
let (front, content) = doc.parts(); let (mut front, content) = doc.into_parts();
let front = front.set_title(title.to_owned()); front.title = Some(title.to_owned());
let doc = let doc = cobalt_model::Document::new(front, content);
cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::new(front, content);
let doc = doc.to_string(); let doc = doc.to_string();
create_file(&file, &doc)?; create_file(&file, &doc)?;
@ -358,8 +357,8 @@ pub fn rename_document(
}; };
let doc = cobalt_model::files::read_file(&source)?; let doc = cobalt_model::files::read_file(&source)?;
let doc = cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::parse(&doc)?; let doc = cobalt_model::Document::parse(&doc)?;
let (front, content) = doc.parts(); let (mut front, content) = doc.into_parts();
let pages = config.pages.clone().build()?; let pages = config.pages.clone().build()?;
let posts = config.posts.clone().build()?; let posts = config.posts.clone().build()?;
@ -375,8 +374,8 @@ pub fn rename_document(
.expect("file was found under the root"); .expect("file was found under the root");
front front
.clone() .clone()
.merge_path(rel_src) .merge(&cobalt_config::Frontmatter::from_path(rel_src))
.merge(posts.default.clone()) .merge(&posts.default)
} else if pages.pages.includes_file(&target) } else if pages.pages.includes_file(&target)
|| pages || pages
.drafts .drafts
@ -389,19 +388,18 @@ pub fn rename_document(
.expect("file was found under the root"); .expect("file was found under the root");
front front
.clone() .clone()
.merge_path(rel_src) .merge(&cobalt_config::Frontmatter::from_path(rel_src))
.merge(pages.default.clone()) .merge(&pages.default)
} else { } else {
failure::bail!( failure::bail!(
"Target file wouldn't be a member of any collection: {:?}", "Target file wouldn't be a member of any collection: {:?}",
target target
); );
}; };
let full_front = full_front.build()?; let full_front = cobalt_model::Frontmatter::from_config(full_front)?;
let new_front = front.set_title(Some(title.to_string())); front.title = Some(title.to_string());
let doc = let doc = cobalt_model::Document::new(front, content);
cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::new(new_front, content);
let doc = doc.to_string(); let doc = doc.to_string();
cobalt_model::files::write_document_file(doc, target)?; cobalt_model::files::write_document_file(doc, target)?;
@ -420,8 +418,8 @@ fn prepend_date_to_filename(
) -> Result<()> { ) -> Result<()> {
// avoid prepend to existing date prefix // avoid prepend to existing date prefix
let file_stem = cobalt_model::file_stem(file); let file_stem = cobalt_config::path::file_stem(file);
let (_, file_stem) = cobalt_model::parse_file_stem(file_stem); let (_, file_stem) = cobalt_config::path::parse_file_stem(file_stem);
let file_name = format!( let file_name = format!(
"{}{}.{}", "{}{}.{}",
(**date).format("%Y-%m-%d-"), (**date).format("%Y-%m-%d-"),
@ -475,14 +473,14 @@ fn move_from_drafts_to_posts(
pub fn publish_document(config: &cobalt_model::Config, file: &path::Path) -> Result<()> { pub fn publish_document(config: &cobalt_model::Config, file: &path::Path) -> Result<()> {
let doc = cobalt_model::files::read_file(file)?; let doc = cobalt_model::files::read_file(file)?;
let doc = cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::parse(&doc)?; let doc = cobalt_model::Document::parse(&doc)?;
let (front, content) = doc.parts(); let (mut front, content) = doc.into_parts();
let date = cobalt_model::DateTime::now(); let date = cobalt_model::DateTime::now();
let front = front.set_draft(false).set_published_date(date); front.is_draft = Some(false);
front.published_date = Some(date);
let doc = let doc = cobalt_model::Document::new(front, content);
cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::new(front, content);
let doc = doc.to_string(); let doc = doc.to_string();
cobalt_model::files::write_document_file(doc, file)?; cobalt_model::files::write_document_file(doc, file)?;

View file

@ -57,7 +57,7 @@ pub fn serve_command(matches: &clap::ArgMatches) -> Result<()> {
let mut config = args::get_config(matches)?; let mut config = args::get_config(matches)?;
debug!("Overriding config `site.base_url` with `{}`", ip); debug!("Overriding config `site.base_url` with `{}`", ip);
config.site.base_url = Some(format!("http://{}", ip)); config.site.base_url = Some(format!("http://{}", ip));
let config = config.build()?; let config = cobalt::cobalt_model::Config::from_config(config)?;
let dest = path::Path::new(&config.destination).to_owned(); let dest = path::Path::new(&config.destination).to_owned();
build::build(config.clone())?; build::build(config.clone())?;

View file

@ -309,7 +309,11 @@ fn parse_drafts(
.expect("file was found under the root"); .expect("file was found under the root");
let new_path = rel_real.join(rel_src); let new_path = rel_real.join(rel_src);
let default_front = collection.default.clone().set_draft(true); let default_front = cobalt_config::Frontmatter {
is_draft: Some(true),
..Default::default()
}
.merge(&collection.default);
let doc = Document::parse(&file_path, &new_path, default_front) let doc = Document::parse(&file_path, &new_path, default_front)
.with_context(|_| failure::format_err!("Failed to parse {}", rel_src.display()))?; .with_context(|_| failure::format_err!("Failed to parse {}", rel_src.display()))?;

View file

@ -15,6 +15,20 @@ pub struct AssetsBuilder {
} }
impl AssetsBuilder { impl AssetsBuilder {
pub fn from_config(
config: cobalt_config::Assets,
source: &path::Path,
ignore: &[String],
template_extensions: &[String],
) -> Self {
Self {
sass: sass::SassBuilder::from_config(config.sass, source),
source: Some(source.to_owned()),
ignore: ignore.to_vec(),
template_extensions: template_extensions.to_vec(),
}
}
pub fn build(self) -> Result<Assets> { pub fn build(self) -> Result<Assets> {
let AssetsBuilder { let AssetsBuilder {
sass, sass,

View file

@ -1,26 +1,13 @@
use std::path; use std::path;
use cobalt_config::Frontmatter;
use cobalt_config::SortOrder;
use liquid; use liquid;
use super::files; use super::files;
use super::slug; use super::slug;
use super::FrontmatterBuilder;
use crate::error::*; use crate::error::*;
#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum SortOrder {
None,
Asc,
Desc,
}
impl Default for SortOrder {
fn default() -> SortOrder {
SortOrder::Desc
}
}
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)] #[serde(deny_unknown_fields, default)]
pub struct CollectionBuilder { pub struct CollectionBuilder {
@ -38,16 +25,112 @@ pub struct CollectionBuilder {
pub jsonfeed: Option<String>, pub jsonfeed: Option<String>,
pub base_url: Option<String>, pub base_url: Option<String>,
pub publish_date_in_filename: bool, pub publish_date_in_filename: bool,
pub default: FrontmatterBuilder, pub default: Frontmatter,
} }
impl CollectionBuilder { impl CollectionBuilder {
pub fn new() -> Self { pub fn from_page_config(
Self::default() config: cobalt_config::PageCollection,
source: &path::Path,
site: &cobalt_config::Site,
posts: &cobalt_config::PostCollection,
common_default: &cobalt_config::Frontmatter,
ignore: &[String],
template_extensions: &[String],
) -> Self {
let mut ignore = ignore.to_vec();
ignore.push(format!("/{}", posts.dir));
if let Some(ref drafts_dir) = posts.drafts_dir {
ignore.push(format!("/{}", drafts_dir));
}
let mut config: cobalt_config::Collection = config.into();
// Use `site` because the pages are effectively the site
config.title = Some(site.title.clone().unwrap_or_else(|| "".to_owned()));
config.description = site.description.clone();
Self::from_config(
config,
"pages",
false,
source,
site,
common_default,
ignore,
template_extensions,
)
} }
pub fn merge_frontmatter(mut self, secondary: FrontmatterBuilder) -> Self { pub fn from_post_config(
self.default = self.default.merge(secondary); config: cobalt_config::PostCollection,
source: &path::Path,
site: &cobalt_config::Site,
include_drafts: bool,
common_default: &cobalt_config::Frontmatter,
ignore: &[String],
template_extensions: &[String],
) -> Self {
let mut config: cobalt_config::Collection = config.into();
// Default with `site` for people quickly bootstrapping a blog, the blog and site are
// effectively equivalent.
if config.title.is_none() {
config.title = Some(site.title.clone().unwrap_or_else(|| "".to_owned()));
}
if config.description.is_none() {
config.description = site.description.clone();
}
Self::from_config(
config,
"posts",
include_drafts,
source,
site,
common_default,
ignore.to_vec(),
template_extensions,
)
}
fn from_config(
config: cobalt_config::Collection,
slug: &str,
include_drafts: bool,
source: &path::Path,
site: &cobalt_config::Site,
common_default: &cobalt_config::Frontmatter,
ignore: Vec<String>,
template_extensions: &[String],
) -> Self {
let cobalt_config::Collection {
title,
description,
dir,
drafts_dir,
order,
rss,
jsonfeed,
publish_date_in_filename,
default,
} = config;
Self {
title: title,
slug: Some(slug.to_owned()),
description: description,
source: Some(source.to_owned()),
dir: dir,
drafts_dir: drafts_dir,
include_drafts,
template_extensions: template_extensions.to_vec(),
ignore,
order,
rss,
jsonfeed,
base_url: site.base_url.clone(),
publish_date_in_filename,
default: default.merge(&common_default),
}
}
pub fn merge_frontmatter(mut self, secondary: &Frontmatter) -> Self {
self.default = self.default.merge(&secondary);
self self
} }
@ -103,7 +186,10 @@ impl CollectionBuilder {
); );
} }
let default = default.set_collection(slug.clone()); let default = default.merge(&Frontmatter {
collection: Some(slug.clone()),
..Default::default()
});
let new = Collection { let new = Collection {
title, title,
@ -164,7 +250,7 @@ pub struct Collection {
pub rss: Option<String>, pub rss: Option<String>,
pub jsonfeed: Option<String>, pub jsonfeed: Option<String>,
pub base_url: Option<String>, pub base_url: Option<String>,
pub default: FrontmatterBuilder, pub default: Frontmatter,
pub attributes: liquid::value::Object, pub attributes: liquid::value::Object,
} }

View file

@ -1,8 +1,6 @@
use std::fmt; use std::fmt;
use std::path; use std::path;
use failure::ResultExt;
use liquid;
use serde_yaml; use serde_yaml;
use crate::error::*; use crate::error::*;
@ -10,330 +8,28 @@ use crate::error::*;
use super::assets; use super::assets;
use super::collection; use super::collection;
use super::files; use super::files;
use super::frontmatter;
use super::mark; use super::mark;
use super::sass;
use super::site; use super::site;
use super::template; use super::template;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(deny_unknown_fields, default)] #[serde(deny_unknown_fields, default)]
pub struct SyntaxHighlight { pub struct Config {
pub theme: String, pub source: path::PathBuf,
pub enabled: bool, pub destination: path::PathBuf,
} pub pages: collection::CollectionBuilder,
pub posts: collection::CollectionBuilder,
impl Default for SyntaxHighlight { pub site: site::SiteBuilder,
fn default() -> Self { pub layouts_dir: path::PathBuf,
Self { pub liquid: template::LiquidBuilder,
theme: "base16-ocean.dark".to_owned(), pub markdown: mark::MarkdownBuilder,
enabled: true, pub assets: assets::AssetsBuilder,
}
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct PageConfig {
pub default: frontmatter::FrontmatterBuilder,
}
impl PageConfig {
fn builder(
self,
source: &path::Path,
site: &SiteConfig,
posts: &PostConfig,
common_default: &frontmatter::FrontmatterBuilder,
ignore: &[String],
template_extensions: &[String],
) -> collection::CollectionBuilder {
let mut ignore = ignore.to_vec();
ignore.push(format!("/{}", posts.dir));
if let Some(ref drafts_dir) = posts.drafts_dir {
ignore.push(format!("/{}", drafts_dir));
}
// Use `site` because the pages are effectively the site
collection::CollectionBuilder {
title: Some(site.title.clone().unwrap_or_else(|| "".to_owned())),
slug: Some("pages".to_owned()),
description: site.description.clone(),
source: Some(source.to_owned()),
dir: Some(".".to_owned()),
drafts_dir: None,
include_drafts: false,
template_extensions: template_extensions.to_vec(),
ignore,
order: collection::SortOrder::None,
rss: None,
jsonfeed: None,
base_url: site.base_url.clone(),
publish_date_in_filename: false,
default: self
.default
.merge_excerpt_separator("".to_owned())
.merge(common_default.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct PostConfig {
pub title: Option<String>,
pub description: Option<String>,
pub dir: String,
pub drafts_dir: Option<String>,
pub order: collection::SortOrder,
pub rss: Option<String>,
pub jsonfeed: Option<String>,
pub publish_date_in_filename: bool,
pub default: frontmatter::FrontmatterBuilder,
}
impl PostConfig {
fn builder(
self,
source: &path::Path,
site: &SiteConfig,
include_drafts: bool,
common_default: &frontmatter::FrontmatterBuilder,
ignore: &[String],
template_extensions: &[String],
) -> collection::CollectionBuilder {
let PostConfig {
title,
description,
dir,
drafts_dir,
order,
rss,
jsonfeed,
publish_date_in_filename,
default,
} = self;
// Default with `site` for people quickly bootstrapping a blog, the blog and site are
// effectively equivalent.
collection::CollectionBuilder {
title: Some(
title
.or_else(|| site.title.clone())
.unwrap_or_else(|| "".to_owned()),
),
slug: Some("posts".to_owned()),
description: description.or_else(|| site.description.clone()),
source: Some(source.to_owned()),
dir: Some(dir),
drafts_dir,
include_drafts,
template_extensions: template_extensions.to_vec(),
ignore: ignore.to_vec(),
order,
rss,
jsonfeed,
base_url: site.base_url.clone(),
publish_date_in_filename,
default: default.merge(common_default.clone()),
}
}
}
impl Default for PostConfig {
fn default() -> Self {
Self {
title: Default::default(),
description: Default::default(),
dir: "posts".to_owned(),
drafts_dir: Default::default(),
order: Default::default(),
rss: Default::default(),
jsonfeed: Default::default(),
publish_date_in_filename: true,
default: Default::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct SiteConfig {
pub title: Option<String>,
pub description: Option<String>,
pub base_url: Option<String>,
pub sitemap: Option<String>, pub sitemap: Option<String>,
pub data: Option<liquid::value::Object>,
#[serde(skip)]
pub data_dir: &'static str,
} }
impl SiteConfig { impl Config {
fn builder(self, source: &path::Path) -> site::SiteBuilder { pub fn from_config(source: cobalt_config::Config) -> Result<Self> {
site::SiteBuilder { let cobalt_config::Config {
title: self.title,
description: self.description,
base_url: self.base_url,
data: self.data,
data_dir: Some(source.join(self.data_dir)),
}
}
}
impl Default for SiteConfig {
fn default() -> Self {
Self {
title: Default::default(),
description: Default::default(),
base_url: Default::default(),
sitemap: Default::default(),
data: Default::default(),
data_dir: "_data",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct SassConfig {
#[serde(skip)]
pub import_dir: &'static str,
pub style: sass::SassOutputStyle,
}
impl SassConfig {
fn builder(self, source: &path::Path) -> sass::SassBuilder {
let mut sass = sass::SassBuilder::new();
sass.style = self.style;
sass.import_dir = source
.join(self.import_dir)
.into_os_string()
.into_string()
.ok();
sass
}
}
impl Default for SassConfig {
fn default() -> Self {
Self {
import_dir: "_sass",
style: Default::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct AssetsConfig {
pub sass: SassConfig,
}
impl AssetsConfig {
fn builder(
self,
source: &path::Path,
ignore: &[String],
template_extensions: &[String],
) -> assets::AssetsBuilder {
assets::AssetsBuilder {
sass: self.sass.builder(source),
source: Some(source.to_owned()),
ignore: ignore.to_vec(),
template_extensions: template_extensions.to_vec(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct ConfigBuilder {
#[serde(skip)]
pub root: path::PathBuf,
pub source: String,
pub destination: String,
#[serde(skip)]
pub abs_dest: Option<path::PathBuf>,
pub include_drafts: bool,
pub default: frontmatter::FrontmatterBuilder,
pub pages: PageConfig,
pub posts: PostConfig,
pub site: SiteConfig,
pub template_extensions: Vec<String>,
pub ignore: Vec<String>,
pub syntax_highlight: SyntaxHighlight,
#[serde(skip)]
pub layouts_dir: &'static str,
#[serde(skip)]
pub includes_dir: &'static str,
pub assets: AssetsConfig,
}
impl Default for ConfigBuilder {
fn default() -> ConfigBuilder {
ConfigBuilder {
root: Default::default(),
source: "./".to_owned(),
destination: "./_site".to_owned(),
abs_dest: Default::default(),
include_drafts: false,
default: Default::default(),
pages: Default::default(),
posts: Default::default(),
site: Default::default(),
template_extensions: vec!["md".to_owned(), "liquid".to_owned()],
ignore: Default::default(),
syntax_highlight: SyntaxHighlight::default(),
layouts_dir: "_layouts",
includes_dir: "_includes",
assets: AssetsConfig::default(),
}
}
}
impl ConfigBuilder {
pub fn from_file<P: Into<path::PathBuf>>(path: P) -> Result<ConfigBuilder> {
Self::from_file_internal(path.into())
}
fn from_file_internal(path: path::PathBuf) -> Result<ConfigBuilder> {
let content = files::read_file(&path)?;
let mut config = if content.trim().is_empty() {
ConfigBuilder::default()
} else {
serde_yaml::from_str(&content)?
};
let mut root = path;
root.pop(); // Remove filename
config.root = root;
Ok(config)
}
pub fn from_cwd<P: Into<path::PathBuf>>(cwd: P) -> Result<ConfigBuilder> {
Self::from_cwd_internal(cwd.into())
}
fn from_cwd_internal(cwd: path::PathBuf) -> Result<ConfigBuilder> {
let file_path = files::find_project_file(&cwd, "_cobalt.yml");
let config = file_path
.map(|p| {
debug!("Using config file {:?}", &p);
Self::from_file(&p).with_context(|_| format!("Error reading config file {:?}", p))
})
.unwrap_or_else(|| {
warn!("No _cobalt.yml file found in current directory, using default config.");
let config = ConfigBuilder {
root: cwd,
..Default::default()
};
Ok(config)
})?;
Ok(config)
}
pub fn build(self) -> Result<Config> {
let ConfigBuilder {
root, root,
source, source,
destination, destination,
@ -349,7 +45,7 @@ impl ConfigBuilder {
layouts_dir, layouts_dir,
includes_dir, includes_dir,
assets, assets,
} = self; } = source;
if include_drafts { if include_drafts {
debug!("Draft mode enabled"); debug!("Draft mode enabled");
@ -373,7 +69,8 @@ impl ConfigBuilder {
let source = root.join(source); let source = root.join(source);
let destination = abs_dest.unwrap_or_else(|| root.join(destination)); let destination = abs_dest.unwrap_or_else(|| root.join(destination));
let pages = pages.builder( let pages = collection::CollectionBuilder::from_page_config(
pages,
&source, &source,
&site, &site,
&posts, &posts,
@ -382,7 +79,8 @@ impl ConfigBuilder {
&template_extensions, &template_extensions,
); );
let posts = posts.builder( let posts = collection::CollectionBuilder::from_post_config(
posts,
&source, &source,
&site, &site,
include_drafts, include_drafts,
@ -392,9 +90,10 @@ impl ConfigBuilder {
); );
let sitemap = site.sitemap.clone(); let sitemap = site.sitemap.clone();
let site = site.builder(&source); let site = site::SiteBuilder::from_config(site, &source);
let assets = assets.builder(&source, &ignore, &template_extensions); let assets =
assets::AssetsBuilder::from_config(assets, &source, &ignore, &template_extensions);
let includes_dir = source.join(includes_dir); let includes_dir = source.join(includes_dir);
let layouts_dir = source.join(layouts_dir); let layouts_dir = source.join(layouts_dir);
@ -425,33 +124,9 @@ impl ConfigBuilder {
} }
} }
impl fmt::Display for ConfigBuilder {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut converted = serde_yaml::to_string(self).map_err(|_| fmt::Error)?;
converted.drain(..4);
write!(f, "{}", converted)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
pub source: path::PathBuf,
pub destination: path::PathBuf,
pub pages: collection::CollectionBuilder,
pub posts: collection::CollectionBuilder,
pub site: site::SiteBuilder,
pub layouts_dir: path::PathBuf,
pub liquid: template::LiquidBuilder,
pub markdown: mark::MarkdownBuilder,
pub assets: assets::AssetsBuilder,
pub sitemap: Option<String>,
}
impl Default for Config { impl Default for Config {
fn default() -> Config { fn default() -> Config {
ConfigBuilder::default() Config::from_config(cobalt_config::Config::default())
.build()
.expect("default config should not fail") .expect("default config should not fail")
} }
} }
@ -464,70 +139,16 @@ impl fmt::Display for Config {
} }
} }
#[test]
fn test_from_file_ok() {
let result = ConfigBuilder::from_file("tests/fixtures/config/_cobalt.yml").unwrap();
assert_eq!(
result.root,
path::Path::new("tests/fixtures/config").to_path_buf()
);
}
#[test]
fn test_from_file_alternate_name() {
let result = ConfigBuilder::from_file("tests/fixtures/config/rss.yml").unwrap();
assert_eq!(
result.root,
path::Path::new("tests/fixtures/config").to_path_buf()
);
}
#[test]
fn test_from_file_empty() {
let result = ConfigBuilder::from_file("tests/fixtures/config/empty.yml").unwrap();
assert_eq!(
result.root,
path::Path::new("tests/fixtures/config").to_path_buf()
);
}
#[test]
fn test_from_file_invalid_syntax() {
let result = ConfigBuilder::from_file("tests/fixtures/config/invalid_syntax.yml");
assert!(result.is_err());
}
#[test]
fn test_from_file_not_found() {
let result = ConfigBuilder::from_file("tests/fixtures/config/config_does_not_exist.yml");
assert!(result.is_err());
}
#[test]
fn test_from_cwd_ok() {
let result = ConfigBuilder::from_cwd("tests/fixtures/config/child").unwrap();
assert_eq!(
result.root,
path::Path::new("tests/fixtures/config").to_path_buf()
);
}
#[test]
fn test_from_cwd_not_found() {
let result = ConfigBuilder::from_cwd("tests/fixtures").unwrap();
assert_eq!(result.root, path::Path::new("tests/fixtures").to_path_buf());
}
#[test] #[test]
fn test_build_default() { fn test_build_default() {
let config = ConfigBuilder::default(); let config = cobalt_config::Config::default();
config.build().unwrap(); Config::from_config(config).unwrap();
} }
#[test] #[test]
fn test_build_dest() { fn test_build_dest() {
let result = ConfigBuilder::from_file("tests/fixtures/config/_cobalt.yml").unwrap(); let config = cobalt_config::Config::from_file("tests/fixtures/config/_cobalt.yml").unwrap();
let result = result.build().unwrap(); let result = Config::from_config(config).unwrap();
assert_eq!( assert_eq!(
result.source, result.source,
path::Path::new("tests/fixtures/config").to_path_buf() path::Path::new("tests/fixtures/config").to_path_buf()
@ -540,9 +161,9 @@ fn test_build_dest() {
#[test] #[test]
fn test_build_abs_dest() { fn test_build_abs_dest() {
let mut result = ConfigBuilder::from_file("tests/fixtures/config/_cobalt.yml").unwrap(); let mut config = cobalt_config::Config::from_file("tests/fixtures/config/_cobalt.yml").unwrap();
result.abs_dest = Some(path::PathBuf::from("hello/world")); config.abs_dest = Some(path::PathBuf::from("hello/world"));
let result = result.build().unwrap(); let result = Config::from_config(config).unwrap();
assert_eq!( assert_eq!(
result.source, result.source,
path::Path::new("tests/fixtures/config").to_path_buf() path::Path::new("tests/fixtures/config").to_path_buf()

View file

@ -1,268 +0,0 @@
use std::convert;
use std::fmt;
use std::ops;
use chrono;
use chrono::TimeZone;
use serde;
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)]
pub struct DateTime(chrono::DateTime<chrono::FixedOffset>);
impl DateTime {
pub fn now() -> Self {
let d = chrono::Utc::now().with_timezone(&chrono::FixedOffset::east(0));
DateTime(d)
}
pub fn parse<S: AsRef<str>>(d: S) -> Option<Self> {
Self::parse_str(d.as_ref())
}
fn parse_str(d: &str) -> Option<DateTime> {
chrono::DateTime::parse_from_str(d, "%Y-%m-%d %H:%M:%S %z")
.ok()
.map(DateTime)
}
pub fn format(&self) -> String {
self.0.format("%Y-%m-%d %H:%M:%S %z").to_string()
}
pub fn with_offset(&self, secs: i32) -> Option<Self> {
let timezone = chrono::FixedOffset::east_opt(secs);
timezone.map(|tz| self.0.with_timezone(&tz).into())
}
}
impl Default for DateTime {
fn default() -> Self {
let d = chrono::Utc
.timestamp(0, 0)
.with_timezone(&chrono::FixedOffset::east(0));
DateTime(d)
}
}
impl ops::Deref for DateTime {
type Target = chrono::DateTime<chrono::FixedOffset>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ops::DerefMut for DateTime {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl convert::From<chrono::DateTime<chrono::FixedOffset>> for DateTime {
fn from(v: chrono::DateTime<chrono::FixedOffset>) -> Self {
DateTime(v)
}
}
impl convert::From<DateTime> for chrono::DateTime<chrono::FixedOffset> {
fn from(v: DateTime) -> Self {
v.0
}
}
impl chrono::Datelike for DateTime {
#[inline]
fn year(&self) -> i32 {
self.0.year()
}
#[inline]
fn month(&self) -> u32 {
self.0.month()
}
#[inline]
fn month0(&self) -> u32 {
self.0.month0()
}
#[inline]
fn day(&self) -> u32 {
self.0.day()
}
#[inline]
fn day0(&self) -> u32 {
self.0.day0()
}
#[inline]
fn ordinal(&self) -> u32 {
self.0.ordinal()
}
#[inline]
fn ordinal0(&self) -> u32 {
self.0.ordinal0()
}
#[inline]
fn weekday(&self) -> chrono::Weekday {
self.0.weekday()
}
#[inline]
fn iso_week(&self) -> chrono::IsoWeek {
self.0.iso_week()
}
#[inline]
fn with_year(&self, year: i32) -> Option<DateTime> {
self.0.with_year(year).map(|d| d.into())
}
#[inline]
fn with_month(&self, month: u32) -> Option<DateTime> {
self.0.with_month(month).map(|d| d.into())
}
#[inline]
fn with_month0(&self, month0: u32) -> Option<DateTime> {
self.0.with_month0(month0).map(|d| d.into())
}
#[inline]
fn with_day(&self, day: u32) -> Option<DateTime> {
self.0.with_day(day).map(|d| d.into())
}
#[inline]
fn with_day0(&self, day0: u32) -> Option<DateTime> {
self.0.with_day(day0).map(|d| d.into())
}
#[inline]
fn with_ordinal(&self, ordinal: u32) -> Option<DateTime> {
self.0.with_ordinal(ordinal).map(|d| d.into())
}
#[inline]
fn with_ordinal0(&self, ordinal0: u32) -> Option<DateTime> {
self.0.with_ordinal0(ordinal0).map(|d| d.into())
}
}
impl chrono::Timelike for DateTime {
#[inline]
fn hour(&self) -> u32 {
self.0.hour()
}
#[inline]
fn minute(&self) -> u32 {
self.0.minute()
}
#[inline]
fn second(&self) -> u32 {
self.0.second()
}
#[inline]
fn nanosecond(&self) -> u32 {
self.0.nanosecond()
}
#[inline]
fn with_hour(&self, hour: u32) -> Option<DateTime> {
self.0.with_hour(hour).map(|d| d.into())
}
#[inline]
fn with_minute(&self, min: u32) -> Option<DateTime> {
self.0.with_minute(min).map(|d| d.into())
}
#[inline]
fn with_second(&self, sec: u32) -> Option<DateTime> {
self.0.with_second(sec).map(|d| d.into())
}
#[inline]
fn with_nanosecond(&self, nano: u32) -> Option<DateTime> {
self.0.with_nanosecond(nano).map(|d| d.into())
}
}
impl serde::Serialize for DateTime {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(&self.format())
}
}
struct DateTimeVisitor;
impl<'de> serde::de::Visitor<'de> for DateTimeVisitor {
type Value = DateTime;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a formatted date and time string")
}
fn visit_str<E>(self, value: &str) -> Result<DateTime, E>
where
E: serde::de::Error,
{
DateTime::parse(value).ok_or_else(|| {
E::custom(format!(
"Invalid datetime '{}', must be `YYYY-MM-DD HH:MM:SS +/-TTTT",
value
))
})
}
}
impl<'de> serde::de::Deserialize<'de> for DateTime {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
deserializer.deserialize_str(DateTimeVisitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{Datelike, Timelike};
#[test]
fn format() {
let d = DateTime::default()
.with_year(2016)
.and_then(|d| d.with_month(1))
.and_then(|d| d.with_day(1))
.and_then(|d| d.with_hour(20))
.and_then(|d| d.with_offset(1 * 60 * 60))
.unwrap();
assert_eq!(d.format(), "2016-01-01 21:00:00 +0100");
}
#[test]
fn parse() {
let expected = DateTime::default()
.with_year(2016)
.and_then(|d| d.with_month(1))
.and_then(|d| d.with_day(1))
.and_then(|d| d.with_hour(3))
.and_then(|d| d.with_offset(1 * 60 * 60))
.unwrap();
assert_eq!(DateTime::parse("2016-1-1 4:00:00 +0100").unwrap(), expected);
}
#[test]
fn parse_leading_zero() {
let expected = DateTime::default()
.with_year(2016)
.and_then(|d| d.with_month(1))
.and_then(|d| d.with_day(1))
.and_then(|d| d.with_hour(3))
.and_then(|d| d.with_offset(1 * 60 * 60))
.unwrap();
assert_eq!(
DateTime::parse("2016-01-01 04:00:00 +0100").unwrap(),
expected
);
}
}

View file

@ -1,173 +0,0 @@
use std::fmt;
use regex;
use super::frontmatter;
use crate::error::*;
#[derive(Debug, Eq, PartialEq, Default, Clone)]
pub struct DocumentBuilder<T: frontmatter::Front> {
front: T,
content: String,
}
impl<T: frontmatter::Front> DocumentBuilder<T> {
pub fn new(front: T, content: String) -> Self {
Self { front, content }
}
pub fn parts(self) -> (T, String) {
let Self { front, content } = self;
(front, content)
}
pub fn parse(content: &str) -> Result<Self> {
let (front, content) = split_document(content)?;
let front = front
.map(|s| T::parse(s))
.map_or(Ok(None), |r| r.map(Some))?
.unwrap_or_else(T::default);
let content = content.to_owned();
Ok(Self { front, content })
}
}
impl<T: frontmatter::Front> fmt::Display for DocumentBuilder<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let front = self.front.to_string().map_err(|_| fmt::Error)?;
if front.trim().is_empty() {
write!(f, "{}", self.content)
} else {
write!(f, "---\n{}\n---\n{}", front, self.content)
}
}
}
fn split_document(content: &str) -> Result<(Option<&str>, &str)> {
lazy_static! {
static ref FRONT_MATTER_DIVIDE: regex::Regex = regex::Regex::new(r"---\s*\r?\n").unwrap();
static ref FRONT_MATTER: regex::Regex =
regex::Regex::new(r"\A---\s*\r?\n([\s\S]*\n)?---\s*\r?\n").unwrap();
}
if FRONT_MATTER.is_match(content) {
// skip first empty string
let mut splits = FRONT_MATTER_DIVIDE.splitn(content, 3).skip(1);
// split between dividers
let front_split = splits.next().unwrap_or("");
// split after second divider
let content_split = splits.next().unwrap_or("");
if front_split.is_empty() {
Ok((None, content_split))
} else {
Ok((Some(front_split), content_split))
}
} else {
deprecated_split_front_matter(content)
}
}
fn deprecated_split_front_matter(content: &str) -> Result<(Option<&str>, &str)> {
lazy_static! {
static ref FRONT_MATTER_DIVIDE: regex::Regex =
regex::Regex::new(r"(\A|\n)---\s*\r?\n").unwrap();
}
if FRONT_MATTER_DIVIDE.is_match(content) {
warn!("Trailing separators are deprecated. We recommend frontmatters be surrounded, above and below, with ---");
let mut splits = FRONT_MATTER_DIVIDE.splitn(content, 2);
// above the split are the attributes
let front_split = splits.next().unwrap_or("");
// everything below the split becomes the new content
let content_split = splits.next().unwrap_or("");
if front_split.is_empty() {
Ok((None, content_split))
} else {
Ok((Some(front_split), content_split))
}
} else {
Ok((None, content))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn split_document_empty() {
let input = "";
let (cobalt_model, content) = split_document(input).unwrap();
assert!(cobalt_model.is_none());
assert_eq!(content, "");
}
#[test]
fn split_document_no_front_matter() {
let input = "Body";
let (cobalt_model, content) = split_document(input).unwrap();
assert!(cobalt_model.is_none());
assert_eq!(content, "Body");
}
#[test]
fn split_document_deprecated_empty_front_matter() {
let input = "---\nBody";
let (cobalt_model, content) = split_document(input).unwrap();
assert!(cobalt_model.is_none());
assert_eq!(content, "Body");
}
#[test]
fn split_document_empty_front_matter() {
let input = "---\n---\nBody";
let (cobalt_model, content) = split_document(input).unwrap();
assert!(cobalt_model.is_none());
assert_eq!(content, "Body");
}
#[test]
fn split_document_deprecated_empty_body() {
let input = "cobalt_model\n---\n";
let (cobalt_model, content) = split_document(input).unwrap();
assert_eq!(cobalt_model.unwrap(), "cobalt_model");
assert_eq!(content, "");
}
#[test]
fn split_document_empty_body() {
let input = "---\ncobalt_model\n---\n";
let (cobalt_model, content) = split_document(input).unwrap();
assert_eq!(cobalt_model.unwrap(), "cobalt_model\n");
assert_eq!(content, "");
}
#[test]
fn split_document_front_matter_and_body() {
let input = "---\ncobalt_model\n---\nbody";
let (cobalt_model, content) = split_document(input).unwrap();
assert_eq!(cobalt_model.unwrap(), "cobalt_model\n");
assert_eq!(content, "body");
}
#[test]
fn split_document_no_new_line_after_front_matter() {
let input = "invalid_front_matter---\nbody";
let (cobalt_model, content) = split_document(input).unwrap();
assert!(cobalt_model.is_none());
assert_eq!(content, input);
}
#[test]
fn document_format_empty_has_no_front() {
let doc = DocumentBuilder::<frontmatter::FrontmatterBuilder>::default();
let doc = doc.to_string();
assert_eq!(doc, "");
}
}

View file

@ -1,286 +1,36 @@
use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::path;
use chrono::Datelike; use cobalt_config::DateTime;
use cobalt_config::SourceFormat;
use liquid; use liquid;
use regex;
use serde;
use serde_yaml;
use super::pagination;
use crate::error::Result; use crate::error::Result;
use super::datetime; #[derive(Debug, Eq, PartialEq, Default, Clone, Serialize)]
use super::pagination_config;
use super::slug;
const PATH_ALIAS: &str = "/{{parent}}/{{name}}{{ext}}";
lazy_static! {
static ref PERMALINK_ALIASES: HashMap<&'static str, &'static str> = [("path", PATH_ALIAS),]
.iter()
.map(|&(k, v)| (k, v))
.collect();
}
#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum SourceFormat {
Raw,
Markdown,
}
impl Default for SourceFormat {
fn default() -> SourceFormat {
SourceFormat::Raw
}
}
// TODO(epage): Remove the serde traits and instead provide an impl based on if serde traits exist
pub trait Front:
Default + fmt::Display + for<'de> serde::Deserialize<'de> + serde::Serialize
{
fn parse(content: &str) -> Result<Self> {
let front: Self = serde_yaml::from_str(content)?;
Ok(front)
}
fn to_string(&self) -> Result<String> {
let mut converted = serde_yaml::to_string(self)?;
converted.drain(..4);
if converted == "{}" {
converted.clear();
}
Ok(converted)
}
}
#[derive(Debug, Eq, PartialEq, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)] #[serde(deny_unknown_fields, default)]
pub struct FrontmatterBuilder { pub struct Frontmatter {
#[serde(skip_serializing_if = "Option::is_none")] pub permalink: cobalt_config::Permalink,
pub permalink: Option<String>, pub slug: String,
#[serde(skip_serializing_if = "Option::is_none")] pub title: String,
pub slug: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub excerpt: Option<String>, pub excerpt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] pub categories: Vec<String>,
pub categories: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>, pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")] pub excerpt_separator: String,
pub excerpt_separator: Option<String>, pub published_date: Option<DateTime>,
#[serde(skip_serializing_if = "Option::is_none")] pub format: SourceFormat,
pub published_date: Option<datetime::DateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<SourceFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub layout: Option<String>, pub layout: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] pub is_draft: bool,
pub is_draft: Option<bool>, pub weight: i32,
#[serde(skip_serializing_if = "Option::is_none")] pub collection: String,
pub weight: Option<i32>,
#[serde(skip_serializing_if = "liquid::value::Object::is_empty")]
pub data: liquid::value::Object, pub data: liquid::value::Object,
#[serde(skip_serializing_if = "Option::is_none")] pub pagination: Option<pagination::PaginationConfig>,
pub pagination: Option<pagination_config::PaginationConfigBuilder>,
// Controlled by where the file is found. We might allow control over the type at a later
// point but we need to first define those semantics.
#[serde(skip)]
pub collection: Option<String>,
} }
impl FrontmatterBuilder { impl Frontmatter {
pub fn new() -> Self { pub fn from_config(config: cobalt_config::Frontmatter) -> Result<Frontmatter> {
Self::default() let cobalt_config::Frontmatter {
}
pub fn set_permalink<S: Into<Option<String>>>(self, permalink: S) -> Self {
Self {
permalink: permalink.into(),
..self
}
}
pub fn set_slug<S: Into<Option<String>>>(self, slug: S) -> Self {
Self {
slug: slug.into(),
..self
}
}
pub fn set_title<S: Into<Option<String>>>(self, title: S) -> Self {
Self {
title: title.into(),
..self
}
}
pub fn set_description<S: Into<Option<String>>>(self, description: S) -> Self {
Self {
description: description.into(),
..self
}
}
pub fn set_excerpt<S: Into<Option<String>>>(self, excerpt: S) -> Self {
Self {
excerpt: excerpt.into(),
..self
}
}
pub fn set_categories<S: Into<Option<Vec<String>>>>(self, categories: S) -> Self {
Self {
categories: categories.into(),
..self
}
}
pub fn set_tags<S: Into<Option<Vec<String>>>>(self, tags: S) -> Self {
Self {
tags: tags.into(),
..self
}
}
pub fn set_pagination<S: Into<Option<pagination_config::PaginationConfigBuilder>>>(
self,
pagination: S,
) -> Self {
Self {
pagination: pagination.into(),
..self
}
}
pub fn set_excerpt_separator<S: Into<Option<String>>>(self, excerpt_separator: S) -> Self {
Self {
excerpt_separator: excerpt_separator.into(),
..self
}
}
pub fn set_published_date<D: Into<Option<datetime::DateTime>>>(
self,
published_date: D,
) -> Self {
Self {
published_date: published_date.into(),
..self
}
}
#[cfg(test)]
pub fn set_format<S: Into<Option<SourceFormat>>>(self, format: S) -> Self {
Self {
format: format.into(),
..self
}
}
pub fn set_layout<S: Into<Option<String>>>(self, layout: S) -> Self {
Self {
layout: layout.into(),
..self
}
}
pub fn set_draft<B: Into<Option<bool>>>(self, is_draft: B) -> Self {
Self {
is_draft: is_draft.into(),
..self
}
}
pub fn set_weight<I: Into<Option<i32>>>(self, weight: I) -> Self {
Self {
weight: weight.into(),
..self
}
}
pub fn set_collection<S: Into<Option<String>>>(self, collection: S) -> Self {
Self {
collection: collection.into(),
..self
}
}
pub fn merge_permalink<S: Into<Option<String>>>(self, permalink: S) -> Self {
self.merge(Self::new().set_permalink(permalink.into()))
}
pub fn merge_slug<S: Into<Option<String>>>(self, slug: S) -> Self {
self.merge(Self::new().set_slug(slug.into()))
}
pub fn merge_title<S: Into<Option<String>>>(self, title: S) -> Self {
self.merge(Self::new().set_title(title.into()))
}
pub fn merge_description<S: Into<Option<String>>>(self, description: S) -> Self {
self.merge(Self::new().set_description(description.into()))
}
pub fn merge_excerpt<S: Into<Option<String>>>(self, excerpt: S) -> Self {
self.merge(Self::new().set_excerpt(excerpt.into()))
}
pub fn merge_categories<S: Into<Option<Vec<String>>>>(self, categories: S) -> Self {
self.merge(Self::new().set_categories(categories.into()))
}
pub fn merge_tags<S: Into<Option<Vec<String>>>>(self, tags: S) -> Self {
self.merge(Self::new().set_tags(tags.into()))
}
pub fn merge_excerpt_separator<S: Into<Option<String>>>(self, excerpt_separator: S) -> Self {
self.merge(Self::new().set_excerpt_separator(excerpt_separator.into()))
}
pub fn merge_published_date<D: Into<Option<datetime::DateTime>>>(
self,
published_date: D,
) -> Self {
self.merge(Self::new().set_published_date(published_date.into()))
}
pub fn merge_pagination<S: Into<Option<pagination_config::PaginationConfigBuilder>>>(
self,
secondary: S,
) -> Self {
self.merge(Self::new().set_pagination(secondary.into()))
}
#[cfg(test)]
pub fn merge_format<S: Into<Option<SourceFormat>>>(self, format: S) -> Self {
self.merge(Self::new().set_format(format.into()))
}
pub fn merge_layout<S: Into<Option<String>>>(self, layout: S) -> Self {
self.merge(Self::new().set_layout(layout.into()))
}
pub fn merge_draft<B: Into<Option<bool>>>(self, draft: B) -> Self {
self.merge(Self::new().set_draft(draft.into()))
}
pub fn merge_weight<I: Into<Option<i32>>>(self, weight: I) -> Self {
self.merge(Self::new().set_weight(weight.into()))
}
#[cfg(test)]
pub fn merge_collection<S: Into<Option<String>>>(self, collection: S) -> Self {
self.merge(Self::new().set_collection(collection.into()))
}
pub fn merge_data(self, other_data: liquid::value::Object) -> Self {
let Self {
permalink, permalink,
slug, slug,
title, title,
@ -297,154 +47,17 @@ impl FrontmatterBuilder {
collection, collection,
data, data,
pagination, pagination,
} = self; } = config;
Self {
permalink,
slug,
title,
description,
excerpt,
categories,
tags,
excerpt_separator,
published_date,
format,
layout,
is_draft,
weight,
collection,
data: merge_objects(data, other_data),
pagination,
}
}
pub fn merge(self, other: Self) -> Self { let collection = collection.unwrap_or_default();
let Self {
permalink,
slug,
title,
description,
excerpt,
categories,
tags,
excerpt_separator,
published_date,
format,
layout,
is_draft,
weight,
collection,
data,
pagination,
} = self;
let Self {
permalink: other_permalink,
slug: other_slug,
title: other_title,
description: other_description,
excerpt: other_excerpt,
categories: other_categories,
tags: other_tags,
excerpt_separator: other_excerpt_separator,
published_date: other_published_date,
format: other_format,
layout: other_layout,
is_draft: other_is_draft,
weight: other_weight,
collection: other_collection,
data: other_data,
pagination: other_pagination,
} = other;
Self {
permalink: permalink.or_else(|| other_permalink),
slug: slug.or_else(|| other_slug),
title: title.or_else(|| other_title),
description: description.or_else(|| other_description),
excerpt: excerpt.or_else(|| other_excerpt),
categories: categories.or_else(|| other_categories),
tags: tags.or_else(|| other_tags),
excerpt_separator: excerpt_separator.or_else(|| other_excerpt_separator),
published_date: published_date.or_else(|| other_published_date),
format: format.or_else(|| other_format),
layout: layout.or_else(|| other_layout),
is_draft: is_draft.or_else(|| other_is_draft),
weight: weight.or_else(|| other_weight),
collection: collection.or_else(|| other_collection),
data: merge_objects(data, other_data),
pagination: merge_pagination(pagination, other_pagination),
}
}
pub fn merge_path<P: AsRef<path::Path>>(self, relpath: P) -> Self { let permalink = permalink.unwrap_or_default();
self.merge_path_ref(relpath.as_ref()) if let cobalt_config::Permalink::Explicit(permalink) = &permalink {
} if !permalink.starts_with('/') {
failure::bail!("Unsupported permalink alias '{}'", permalink);
fn merge_path_ref(mut self, relpath: &path::Path) -> Self {
if self.format.is_none() {
let ext = relpath.extension().and_then(|os| os.to_str()).unwrap_or("");
let format = match ext {
"md" => SourceFormat::Markdown,
_ => SourceFormat::Raw,
};
self.format = Some(format);
}
if self.published_date.is_none() || self.slug.is_none() {
let file_stem = file_stem(relpath);
let (file_date, file_stem) = parse_file_stem(file_stem);
if self.published_date.is_none() {
self.published_date = file_date;
}
if self.slug.is_none() {
let slug = slug::slugify(file_stem);
self.slug = Some(slug);
} }
} }
if self.title.is_none() {
let slug = self
.slug
.as_ref()
.expect("slug has been unconditionally initialized");
let title = slug::titleize_slug(slug);
self.title = Some(title);
}
self
}
pub fn build(self) -> Result<Frontmatter> {
let Self {
permalink,
slug,
title,
description,
excerpt,
categories,
tags,
excerpt_separator,
published_date,
format,
layout,
is_draft,
weight,
collection,
data,
pagination,
} = self;
let collection = collection.unwrap_or_else(|| "".to_owned());
let permalink = permalink.unwrap_or_else(|| PATH_ALIAS.to_owned());
let permalink = if !permalink.starts_with('/') {
let resolved = *PERMALINK_ALIASES.get(permalink.as_str()).ok_or_else(|| {
failure::format_err!("Unsupported permalink alias '{}'", permalink)
})?;
resolved.to_owned()
} else {
permalink
};
if let Some(ref tags) = tags { if let Some(ref tags) = tags {
if tags.iter().any(|x| x.trim().is_empty()) { if tags.iter().any(|x| x.trim().is_empty()) {
failure::bail!("Empty strings are not allowed in tags"); failure::bail!("Empty strings are not allowed in tags");
@ -456,7 +69,8 @@ impl FrontmatterBuilder {
tags tags
}; };
let fm = Frontmatter { let fm = Frontmatter {
pagination: pagination.and_then(|p| p.build(&permalink)), pagination: pagination
.and_then(|p| pagination::PaginationConfig::from_config(p, &permalink)),
permalink, permalink,
slug: slug.ok_or_else(|| failure::err_msg("No slug"))?, slug: slug.ok_or_else(|| failure::err_msg("No slug"))?,
title: title.ok_or_else(|| failure::err_msg("No title"))?, title: title.ok_or_else(|| failure::err_msg("No title"))?,
@ -466,7 +80,7 @@ impl FrontmatterBuilder {
tags, tags,
excerpt_separator: excerpt_separator.unwrap_or_else(|| "\n\n".to_owned()), excerpt_separator: excerpt_separator.unwrap_or_else(|| "\n\n".to_owned()),
published_date, published_date,
format: format.unwrap_or_else(SourceFormat::default), format: format.unwrap_or_else(super::SourceFormat::default),
layout, layout,
is_draft: is_draft.unwrap_or(false), is_draft: is_draft.unwrap_or(false),
weight: weight.unwrap_or(0), weight: weight.unwrap_or(0),
@ -478,7 +92,7 @@ impl FrontmatterBuilder {
failure::bail!("Unsupported `pagination` field"); failure::bail!("Unsupported `pagination` field");
} else { } else {
if let Some(pagination) = &fm.pagination { if let Some(pagination) = &fm.pagination {
if !pagination_config::is_date_index_sorted(&pagination.date_index) { if !pagination::is_date_index_sorted(&pagination.date_index) {
failure::bail!("date_index is not correctly sorted: Year > Month > Day..."); failure::bail!("date_index is not correctly sorted: Year > Month > Day...");
} }
} }
@ -487,405 +101,13 @@ impl FrontmatterBuilder {
} }
} }
impl fmt::Display for FrontmatterBuilder {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let converted = Front::to_string(self).map_err(|_| fmt::Error)?;
write!(f, "{}", converted)
}
}
impl Front for FrontmatterBuilder {}
#[derive(Debug, Eq, PartialEq, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Frontmatter {
pub permalink: String,
pub slug: String,
pub title: String,
pub description: Option<String>,
pub excerpt: Option<String>,
pub categories: Vec<String>,
pub tags: Option<Vec<String>>,
pub excerpt_separator: String,
pub published_date: Option<datetime::DateTime>,
pub format: SourceFormat,
pub layout: Option<String>,
pub is_draft: bool,
pub weight: i32,
pub collection: String,
pub data: liquid::value::Object,
pub pagination: Option<pagination_config::PaginationConfig>,
}
impl Front for Frontmatter {}
impl fmt::Display for Frontmatter { impl fmt::Display for Frontmatter {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let converted = Front::to_string(self).map_err(|_| fmt::Error)?; let converted = serde_yaml::to_string(self).ok();
write!(f, "{}", converted) if converted.as_ref().map(|s| s.as_str()) == Some("---\n{}") {
} Ok(())
} } else {
write!(f, "{}", &converted.unwrap()[4..])
/// Shallow merge of `liquid::value::Object`'s }
fn merge_objects(
mut primary: liquid::value::Object,
secondary: liquid::value::Object,
) -> liquid::value::Object {
for (key, value) in secondary {
primary
.entry(key.to_owned())
.or_insert_with(|| value.clone());
}
primary
}
fn merge_pagination(
primary: Option<pagination_config::PaginationConfigBuilder>,
secondary: Option<pagination_config::PaginationConfigBuilder>,
) -> Option<pagination_config::PaginationConfigBuilder> {
match (primary, secondary) {
(Some(primary), Some(secondary)) => Some(primary.merge(&secondary)),
(Some(primary), None) => Some(primary),
(None, Some(secondary)) => Some(secondary),
(None, None) => None,
}
}
/// The base-name without an extension. Correlates to Jekyll's :name path tag
pub fn file_stem<P: AsRef<path::Path>>(p: P) -> String {
file_stem_path(p.as_ref())
}
fn file_stem_path(p: &path::Path) -> String {
p.file_stem()
.map(|os| os.to_string_lossy().into_owned())
.unwrap_or_else(|| "".to_owned())
}
pub fn parse_file_stem(stem: String) -> (Option<datetime::DateTime>, String) {
lazy_static! {
static ref DATE_PREFIX_REF: regex::Regex =
regex::Regex::new(r"^(\d{4})-(\d{1,2})-(\d{1,2})[- ](.*)$").unwrap();
}
let parts = DATE_PREFIX_REF.captures(&stem).and_then(|caps| {
let year: i32 = caps
.get(1)
.expect("unconditional capture")
.as_str()
.parse()
.expect("regex gets back an integer");
let month: u32 = caps
.get(2)
.expect("unconditional capture")
.as_str()
.parse()
.expect("regex gets back an integer");
let day: u32 = caps
.get(3)
.expect("unconditional capture")
.as_str()
.parse()
.expect("regex gets back an integer");
let published = datetime::DateTime::default()
.with_year(year)
.and_then(|d| d.with_month(month))
.and_then(|d| d.with_day(day));
published.map(|p| {
(
Some(p),
caps.get(4)
.expect("unconditional capture")
.as_str()
.to_owned(),
)
})
});
parts.unwrap_or((None, stem))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn file_stem_absolute_path() {
let input = path::PathBuf::from("/embedded/path/___filE-worlD-__09___.md");
let actual = file_stem(input.as_path());
assert_eq!(actual, "___filE-worlD-__09___");
}
#[test]
fn parse_file_stem_empty() {
assert_eq!(parse_file_stem("".to_owned()), (None, "".to_owned()));
}
#[test]
fn parse_file_stem_none() {
assert_eq!(
parse_file_stem("First Blog Post".to_owned()),
(None, "First Blog Post".to_owned())
);
}
#[test]
fn parse_file_stem_out_of_range_month() {
assert_eq!(
parse_file_stem("2017-30-5 First Blog Post".to_owned()),
(None, "2017-30-5 First Blog Post".to_owned())
);
}
#[test]
fn parse_file_stem_out_of_range_day() {
assert_eq!(
parse_file_stem("2017-3-50 First Blog Post".to_owned()),
(None, "2017-3-50 First Blog Post".to_owned())
);
}
#[test]
fn parse_file_stem_single_digit() {
assert_eq!(
parse_file_stem("2017-3-5 First Blog Post".to_owned()),
(
Some(
datetime::DateTime::default()
.with_year(2017)
.unwrap()
.with_month(3)
.unwrap()
.with_day(5)
.unwrap()
),
"First Blog Post".to_owned()
)
);
}
#[test]
fn parse_file_stem_double_digit() {
assert_eq!(
parse_file_stem("2017-12-25 First Blog Post".to_owned()),
(
Some(
datetime::DateTime::default()
.with_year(2017)
.unwrap()
.with_month(12)
.unwrap()
.with_day(25)
.unwrap()
),
"First Blog Post".to_owned()
)
);
}
#[test]
fn parse_file_stem_double_digit_leading_zero() {
assert_eq!(
parse_file_stem("2017-03-05 First Blog Post".to_owned()),
(
Some(
datetime::DateTime::default()
.with_year(2017)
.unwrap()
.with_month(3)
.unwrap()
.with_day(5)
.unwrap()
),
"First Blog Post".to_owned()
)
);
}
#[test]
fn parse_file_stem_dashed() {
assert_eq!(
parse_file_stem("2017-3-5-First-Blog-Post".to_owned()),
(
Some(
datetime::DateTime::default()
.with_year(2017)
.unwrap()
.with_month(3)
.unwrap()
.with_day(5)
.unwrap()
),
"First-Blog-Post".to_owned()
)
);
}
#[test]
fn frontmatter_title_from_path() {
let front = FrontmatterBuilder::new()
.merge_path("./parent/file.md")
.build()
.unwrap();
assert_eq!(front.title, "File");
}
#[test]
fn frontmatter_slug_from_md_path() {
let front = FrontmatterBuilder::new()
.merge_path("./parent/file.md")
.build()
.unwrap();
assert_eq!(front.slug, "file");
}
#[test]
fn frontmatter_markdown_from_path() {
let front = FrontmatterBuilder::new()
.merge_path("./parent/file.md")
.build()
.unwrap();
assert_eq!(front.format, SourceFormat::Markdown);
}
#[test]
fn frontmatter_raw_from_path() {
let front = FrontmatterBuilder::new()
.merge_path("./parent/file.liquid")
.build()
.unwrap();
assert_eq!(front.format, SourceFormat::Raw);
}
#[test]
fn frontmatter_global_merge() {
let empty = FrontmatterBuilder::new();
let a = FrontmatterBuilder {
permalink: Some("permalink a".to_owned()),
slug: Some("slug a".to_owned()),
title: Some("title a".to_owned()),
description: Some("description a".to_owned()),
excerpt: Some("excerpt a".to_owned()),
categories: Some(vec!["a".to_owned(), "b".to_owned()]),
tags: Some(vec!["a".to_owned(), "b".to_owned()]),
excerpt_separator: Some("excerpt_separator a".to_owned()),
published_date: Some(datetime::DateTime::default()),
format: Some(SourceFormat::Markdown),
layout: Some("layout a".to_owned()),
is_draft: Some(true),
weight: Some(0),
collection: Some("pages".to_owned()),
data: liquid::value::Object::new(),
pagination: Some(Default::default()),
};
let b = FrontmatterBuilder {
permalink: Some("permalink b".to_owned()),
slug: Some("slug b".to_owned()),
title: Some("title b".to_owned()),
description: Some("description b".to_owned()),
excerpt: Some("excerpt b".to_owned()),
categories: Some(vec!["b".to_owned(), "a".to_owned()]),
tags: Some(vec!["b".to_owned(), "a".to_owned()]),
excerpt_separator: Some("excerpt_separator b".to_owned()),
published_date: Some(datetime::DateTime::default()),
format: Some(SourceFormat::Raw),
layout: Some("layout b".to_owned()),
is_draft: Some(true),
weight: Some(0),
collection: Some("posts".to_owned()),
data: liquid::value::Object::new(),
pagination: Some(Default::default()),
};
let merge_b_into_a = a.clone().merge(b.clone());
assert_eq!(merge_b_into_a, a);
let merge_empty_into_a = a.clone().merge(empty.clone());
assert_eq!(merge_empty_into_a, a);
let merge_a_into_empty = empty.clone().merge(a.clone());
assert_eq!(merge_a_into_empty, a);
}
#[test]
fn frontmatter_local_merge() {
let a = FrontmatterBuilder {
permalink: Some("permalink a".to_owned()),
slug: Some("slug a".to_owned()),
title: Some("title a".to_owned()),
description: Some("description a".to_owned()),
excerpt: Some("excerpt a".to_owned()),
categories: Some(vec!["a".to_owned(), "b".to_owned()]),
tags: Some(vec!["a".to_owned(), "b".to_owned()]),
excerpt_separator: Some("excerpt_separator a".to_owned()),
published_date: None,
format: Some(SourceFormat::Markdown),
layout: Some("layout a".to_owned()),
is_draft: Some(true),
weight: Some(0),
collection: Some("pages".to_owned()),
data: liquid::value::Object::new(),
pagination: Some(Default::default()),
};
let merge_b_into_a = a
.clone()
.merge_permalink("permalink b".to_owned())
.merge_slug("slug b".to_owned())
.merge_title("title b".to_owned())
.merge_description("description b".to_owned())
.merge_excerpt("excerpt b".to_owned())
.merge_categories(vec!["a".to_owned(), "b".to_owned()])
.merge_tags(vec!["a".to_owned(), "b".to_owned()])
.merge_excerpt_separator("excerpt_separator b".to_owned())
.merge_format(SourceFormat::Raw)
.merge_layout("layout b".to_owned())
.merge_draft(true)
.merge_weight(0)
.merge_collection("posts".to_owned());
assert_eq!(merge_b_into_a, a);
let merge_empty_into_a = a
.clone()
.merge_permalink(None)
.merge_slug(None)
.merge_title(None)
.merge_description(None)
.merge_excerpt(None)
.merge_categories(None)
.merge_tags(None)
.merge_excerpt_separator(None)
.merge_format(None)
.merge_layout(None)
.merge_draft(None)
.merge_weight(None)
.merge_collection(None);
assert_eq!(merge_empty_into_a, a);
let merge_a_into_empty = FrontmatterBuilder::new()
.merge_permalink("permalink a".to_owned())
.merge_slug("slug a".to_owned())
.merge_title("title a".to_owned())
.merge_description("description a".to_owned())
.merge_excerpt("excerpt a".to_owned())
.merge_categories(vec!["a".to_owned(), "b".to_owned()])
.merge_tags(vec!["a".to_owned(), "b".to_owned()])
.merge_excerpt_separator("excerpt_separator a".to_owned())
.merge_format(SourceFormat::Markdown)
.merge_layout("layout a".to_owned())
.merge_draft(true)
.merge_weight(0)
.merge_collection("pages".to_owned())
.merge_pagination(Some(Default::default()));
assert_eq!(merge_a_into_empty, a);
}
#[test]
fn frontmatter_defaults() {
FrontmatterBuilder::new()
.set_title("Title".to_owned())
.set_slug("Slug".to_owned())
.build()
.unwrap();
} }
} }

View file

@ -3,7 +3,7 @@ use pulldown_cmark as cmark;
use crate::error::*; use crate::error::*;
use crate::syntax_highlight::decorate_markdown; use crate::syntax_highlight::decorate_markdown;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct MarkdownBuilder { pub struct MarkdownBuilder {
pub theme: String, pub theme: String,

View file

@ -1,8 +1,6 @@
mod assets; mod assets;
mod collection; mod collection;
mod config; mod config;
mod datetime;
mod document;
mod frontmatter; mod frontmatter;
mod mark; mod mark;
mod sass; mod sass;
@ -10,36 +8,27 @@ mod site;
mod template; mod template;
pub mod files; pub mod files;
pub mod pagination_config; pub mod pagination;
pub mod permalink; pub mod permalink;
pub mod slug; pub mod slug;
pub use cobalt_config::DateTime;
pub use cobalt_config::Document;
pub use cobalt_config::Permalink;
pub use cobalt_config::SassOutputStyle;
pub use cobalt_config::SortOrder;
pub use cobalt_config::SourceFormat;
pub use self::assets::Assets; pub use self::assets::Assets;
pub use self::assets::AssetsBuilder; pub use self::assets::AssetsBuilder;
pub use self::collection::Collection; pub use self::collection::Collection;
pub use self::collection::CollectionBuilder; pub use self::collection::CollectionBuilder;
pub use self::collection::SortOrder;
pub use self::config::AssetsConfig;
pub use self::config::Config; pub use self::config::Config;
pub use self::config::ConfigBuilder;
pub use self::config::PageConfig;
pub use self::config::PostConfig;
pub use self::config::SassConfig;
pub use self::config::SiteConfig;
pub use self::config::SyntaxHighlight;
pub use self::datetime::DateTime;
pub use self::document::DocumentBuilder;
pub use self::frontmatter::file_stem;
pub use self::frontmatter::parse_file_stem;
pub use self::frontmatter::Front;
pub use self::frontmatter::Frontmatter; pub use self::frontmatter::Frontmatter;
pub use self::frontmatter::FrontmatterBuilder;
pub use self::frontmatter::SourceFormat;
pub use self::mark::Markdown; pub use self::mark::Markdown;
pub use self::mark::MarkdownBuilder; pub use self::mark::MarkdownBuilder;
pub use self::sass::SassBuilder; pub use self::sass::SassBuilder;
pub use self::sass::SassCompiler; pub use self::sass::SassCompiler;
pub use self::sass::SassOutputStyle;
pub use self::site::SiteBuilder; pub use self::site::SiteBuilder;
pub use self::template::Liquid; pub use self::template::Liquid;
pub use self::template::LiquidBuilder; pub use self::template::LiquidBuilder;

View file

@ -0,0 +1,61 @@
use std::vec::Vec;
use cobalt_config::SortOrder;
pub use cobalt_config::DateIndex;
pub use cobalt_config::Include;
#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields, default)]
pub struct PaginationConfig {
pub include: Include,
pub per_page: i32,
pub front_permalink: cobalt_config::Permalink,
pub permalink_suffix: String,
pub order: SortOrder,
pub sort_by: Vec<String>,
pub date_index: Vec<DateIndex>,
}
impl PaginationConfig {
pub fn from_config(
config: cobalt_config::Pagination,
permalink: &cobalt_config::Permalink,
) -> Option<Self> {
let config = config.merge(&cobalt_config::Pagination::with_defaults());
let cobalt_config::Pagination {
include,
per_page,
permalink_suffix,
order,
sort_by,
date_index,
} = config;
let include = include.expect("default applied");
let per_page = per_page.expect("default applied");
let permalink_suffix = permalink_suffix.expect("default applied");
let order = order.expect("default applied");
let sort_by = sort_by.expect("default applied");
let date_index = date_index.expect("default applied");
if include == Include::None {
return None;
}
Some(Self {
include,
per_page,
front_permalink: permalink.to_owned(),
permalink_suffix,
order,
sort_by,
date_index,
})
}
}
// TODO to be replaced by a call to `is_sorted()` once it's stabilized
pub fn is_date_index_sorted(v: &Vec<DateIndex>) -> bool {
let mut copy = v.clone();
copy.sort_unstable();
copy.eq(v)
}

View file

@ -1,194 +0,0 @@
use std::convert::Into;
use std::vec::Vec;
use super::SortOrder;
pub const DEFAULT_PERMALINK_SUFFIX: &str = "{{num}}/";
pub const DEFAULT_PER_PAGE: i32 = 10;
lazy_static! {
static ref DEFAULT_SORT: Vec<String> = vec!["weight".to_string(), "published_date".to_string()];
}
lazy_static! {
static ref DEFAULT_DATE_INDEX: Vec<DateIndex> = vec![DateIndex::Year, DateIndex::Month];
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub enum Include {
None,
All,
Tags,
Categories,
Dates,
}
impl Into<&'static str> for Include {
fn into(self) -> &'static str {
match self {
Include::None => "",
Include::All => "all",
Include::Tags => "tags",
Include::Categories => "categories",
Include::Dates => "dates",
}
}
}
impl Default for Include {
fn default() -> Include {
Include::None
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(deny_unknown_fields)]
pub enum DateIndex {
Year,
Month,
Day,
Hour,
Minute,
}
// TODO to be replaced by a call to `is_sorted()` once it's stabilized
pub fn is_date_index_sorted(v: &Vec<DateIndex>) -> bool {
let mut copy = v.clone();
copy.sort_unstable();
copy.eq(v)
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(deny_unknown_fields, default)]
pub struct PaginationConfigBuilder {
#[serde(skip_serializing_if = "Option::is_none")]
pub include: Option<Include>,
#[serde(skip_serializing_if = "Option::is_none")]
pub per_page: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permalink_suffix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order: Option<SortOrder>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_by: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date_index: Option<Vec<DateIndex>>,
}
impl PaginationConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn set_include<S: Into<Option<Include>>>(self, include: S) -> Self {
Self {
include: include.into(),
..self
}
}
pub fn set_per_page<S: Into<Option<i32>>>(self, per_page: S) -> Self {
Self {
per_page: per_page.into(),
..self
}
}
pub fn set_permalink_suffix<S: Into<Option<String>>>(self, permalink_suffix: S) -> Self {
Self {
permalink_suffix: permalink_suffix.into(),
..self
}
}
pub fn set_order<S: Into<Option<SortOrder>>>(self, order: S) -> Self {
Self {
order: order.into(),
..self
}
}
pub fn set_sort_by<S: Into<Option<Vec<String>>>>(self, sort_by: S) -> Self {
Self {
sort_by: sort_by.into(),
..self
}
}
pub fn merge(mut self, secondary: &PaginationConfigBuilder) -> PaginationConfigBuilder {
if self.include.is_none() {
self.include = secondary.include;
}
if self.per_page.is_none() {
self.per_page = secondary.per_page;
}
if self.permalink_suffix.is_none() {
self.permalink_suffix = secondary.permalink_suffix.clone();
}
if self.order.is_none() {
self.order = secondary.order;
}
if self.sort_by.is_none() {
self.sort_by = secondary.sort_by.clone();
}
self
}
pub fn build(self, permalink: &str) -> Option<PaginationConfig> {
let Self {
include,
per_page,
permalink_suffix,
order,
sort_by,
date_index,
} = self;
let include = include.unwrap_or(Include::None);
if include == Include::None {
return None;
}
let per_page = per_page.unwrap_or(DEFAULT_PER_PAGE);
let permalink_suffix =
permalink_suffix.unwrap_or_else(|| DEFAULT_PERMALINK_SUFFIX.to_owned());
let order = order.unwrap_or(SortOrder::Desc);
let sort_by = sort_by.unwrap_or_else(|| DEFAULT_SORT.to_vec());
let date_index = date_index.unwrap_or_else(|| DEFAULT_DATE_INDEX.to_vec());
Some(PaginationConfig {
include,
per_page,
front_permalink: permalink.to_owned(),
permalink_suffix,
order,
sort_by,
date_index,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields, default)]
pub struct PaginationConfig {
pub include: Include,
pub per_page: i32,
pub front_permalink: String,
pub permalink_suffix: String,
pub order: SortOrder,
pub sort_by: Vec<String>,
pub date_index: Vec<DateIndex>,
}
impl Default for PaginationConfig {
fn default() -> PaginationConfig {
PaginationConfig {
include: Default::default(),
per_page: DEFAULT_PER_PAGE,
permalink_suffix: DEFAULT_PERMALINK_SUFFIX.to_owned(),
front_permalink: Default::default(),
order: SortOrder::Desc,
sort_by: DEFAULT_SORT.to_vec(),
date_index: DEFAULT_DATE_INDEX.to_vec(),
}
}
}

View file

@ -1,27 +1,13 @@
use std::ffi; use std::ffi;
use std::path; use std::path;
use cobalt_config::SassOutputStyle;
#[cfg(feature = "sass")] #[cfg(feature = "sass")]
use sass_rs; use sass_rs;
use super::files; use super::files;
use crate::error::*; use crate::error::*;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum SassOutputStyle {
Nested,
Expanded,
Compact,
Compressed,
}
impl Default for SassOutputStyle {
fn default() -> Self {
SassOutputStyle::Nested
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)] #[serde(deny_unknown_fields, default)]
pub struct SassBuilder { pub struct SassBuilder {
@ -30,8 +16,15 @@ pub struct SassBuilder {
} }
impl SassBuilder { impl SassBuilder {
pub fn new() -> Self { pub fn from_config(config: cobalt_config::Sass, source: &path::Path) -> Self {
Default::default() Self {
style: config.style,
import_dir: source
.join(config.import_dir)
.into_os_string()
.into_string()
.ok(),
}
} }
pub fn build(self) -> SassCompiler { pub fn build(self) -> SassCompiler {

View file

@ -19,10 +19,20 @@ pub struct SiteBuilder {
pub description: Option<String>, pub description: Option<String>,
pub base_url: Option<String>, pub base_url: Option<String>,
pub data: Option<liquid::value::Object>, pub data: Option<liquid::value::Object>,
pub data_dir: Option<path::PathBuf>, pub data_dir: path::PathBuf,
} }
impl SiteBuilder { impl SiteBuilder {
pub fn from_config(config: cobalt_config::Site, source: &path::Path) -> Self {
Self {
title: config.title,
description: config.description,
base_url: config.base_url,
data: config.data,
data_dir: source.join(config.data_dir),
}
}
pub fn build(self) -> Result<liquid::value::Object> { pub fn build(self) -> Result<liquid::value::Object> {
let SiteBuilder { let SiteBuilder {
title, title,
@ -53,9 +63,7 @@ impl SiteBuilder {
attributes.insert("base_url".into(), liquid::value::Value::scalar(base_url)); attributes.insert("base_url".into(), liquid::value::Value::scalar(base_url));
} }
let mut data = data.unwrap_or_default(); let mut data = data.unwrap_or_default();
if let Some(ref data_dir) = data_dir { insert_data_dir(&mut data, &data_dir)?;
insert_data_dir(&mut data, data_dir)?;
}
if !data.is_empty() { if !data.is_empty() {
attributes.insert("data".into(), liquid::value::Value::Object(data)); attributes.insert("data".into(), liquid::value::Value::Object(data));
} }

View file

@ -27,7 +27,7 @@ fn load_partials_from_path(root: path::PathBuf) -> Result<liquid::Partials> {
Ok(source) Ok(source)
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct LiquidBuilder { pub struct LiquidBuilder {
pub includes_dir: path::PathBuf, pub includes_dir: path::PathBuf,

View file

@ -194,23 +194,25 @@ impl Document {
pub fn parse( pub fn parse(
src_path: &Path, src_path: &Path,
rel_path: &Path, rel_path: &Path,
default_front: cobalt_model::FrontmatterBuilder, default_front: cobalt_config::Frontmatter,
) -> Result<Document> { ) -> Result<Document> {
trace!("Parsing {:?}", rel_path); trace!("Parsing {:?}", rel_path);
let content = files::read_file(src_path)?; let content = files::read_file(src_path)?;
let builder = let builder = cobalt_config::Document::parse(&content)?;
cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::parse(&content)?; let (front, content) = builder.into_parts();
let (front, content) = builder.parts(); let front = front
let front = front.merge_path(rel_path).merge(default_front); .merge(&cobalt_config::Frontmatter::from_path(rel_path))
.merge(&default_front);
let front = front.build()?; let front = cobalt_model::Frontmatter::from_config(front)?;
let (file_path, url_path) = { let (file_path, url_path) = {
let perma_attributes = permalink_attributes(&front, rel_path); let perma_attributes = permalink_attributes(&front, rel_path);
let url_path = permalink::explode_permalink(&front.permalink, &perma_attributes) let url_path =
.with_context(|_| { permalink::explode_permalink(front.permalink.as_str(), &perma_attributes)
failure::format_err!("Failed to create permalink `{}`", front.permalink) .with_context(|_| {
})?; failure::format_err!("Failed to create permalink `{}`", front.permalink)
})?;
let file_path = permalink::format_url_as_file(&url_path); let file_path = permalink::format_url_as_file(&url_path);
(file_path, url_path) (file_path, url_path)
}; };
@ -314,7 +316,7 @@ impl Document {
cobalt_model::SourceFormat::Raw => html, cobalt_model::SourceFormat::Raw => html,
cobalt_model::SourceFormat::Markdown => markdown.parse(&html)?, cobalt_model::SourceFormat::Markdown => markdown.parse(&html)?,
}; };
Ok(html.to_owned()) Ok(html)
} }
/// Renders excerpt and adds it to attributes of the document. /// Renders excerpt and adds it to attributes of the document.

View file

@ -15,7 +15,6 @@ extern crate difference;
pub use crate::cobalt::build; pub use crate::cobalt::build;
pub use crate::cobalt_model::Config; pub use crate::cobalt_model::Config;
pub use crate::cobalt_model::ConfigBuilder;
pub use crate::error::Error; pub use crate::error::Error;
pub mod cobalt_model; pub mod cobalt_model;

View file

@ -1,7 +1,7 @@
use chrono::Datelike; use chrono::Datelike;
use chrono::Timelike; use chrono::Timelike;
use crate::cobalt_model::pagination_config::DateIndex; use crate::cobalt_model::pagination::DateIndex;
use crate::cobalt_model::DateTime; use crate::cobalt_model::DateTime;
use crate::document::Document; use crate::document::Document;

View file

@ -1,7 +1,7 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use crate::cobalt_model::pagination_config::Include; use crate::cobalt_model::pagination::Include;
use crate::cobalt_model::pagination_config::PaginationConfig; use crate::cobalt_model::pagination::PaginationConfig;
use crate::cobalt_model::permalink; use crate::cobalt_model::permalink;
use crate::cobalt_model::slug; use crate::cobalt_model::slug;
use crate::cobalt_model::SortOrder; use crate::cobalt_model::SortOrder;
@ -139,7 +139,7 @@ fn interpret_permalink(
index: Option<&liquid::value::Value>, index: Option<&liquid::value::Value>,
) -> Result<String> { ) -> Result<String> {
let mut attributes = document::permalink_attributes(&doc.front, &doc.file_path); let mut attributes = document::permalink_attributes(&doc.front, &doc.file_path);
let permalink = permalink::explode_permalink(&config.front_permalink, &attributes)?; let permalink = permalink::explode_permalink(config.front_permalink.as_str(), &attributes)?;
let permalink_path = std::path::Path::new(&permalink); let permalink_path = std::path::Path::new(&permalink);
let pagination_root = permalink_path.extension().map_or_else( let pagination_root = permalink_path.extension().map_or_else(
|| permalink.clone(), || permalink.clone(),

View file

@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::cobalt_model::pagination_config::PaginationConfig; use crate::cobalt_model::pagination::PaginationConfig;
use crate::cobalt_model::slug; use crate::cobalt_model::slug;
use crate::document::Document; use crate::document::Document;

View file

@ -66,7 +66,11 @@ struct CodeBlock {
} }
impl Renderable for CodeBlock { impl Renderable for CodeBlock {
fn render_to(&self, writer: &mut Write, _context: &mut Context) -> Result<(), liquid::Error> { fn render_to(
&self,
writer: &mut dyn Write,
_context: &mut Context,
) -> Result<(), liquid::Error> {
if let Some(ref lang) = self.lang { if let Some(ref lang) = self.lang {
write!( write!(
writer, writer,
@ -110,7 +114,7 @@ impl liquid::compiler::ParseBlock for CodeBlockParser {
mut arguments: TagTokenIter, mut arguments: TagTokenIter,
mut tokens: TagBlock, mut tokens: TagBlock,
_options: &Language, _options: &Language,
) -> Result<Box<Renderable>, liquid::Error> { ) -> Result<Box<dyn Renderable>, liquid::Error> {
let lang = arguments let lang = arguments
.expect_next("Identifier or literal expected.") .expect_next("Identifier or literal expected.")
.ok() .ok()
@ -167,7 +171,7 @@ mod test {
#[test] #[test]
fn codeblock_renders_rust() { fn codeblock_renders_rust() {
let highlight: Box<liquid::compiler::ParseBlock> = let highlight: Box<dyn liquid::compiler::ParseBlock> =
Box::new(CodeBlockParser::new("base16-ocean.dark".to_owned())); Box::new(CodeBlockParser::new("base16-ocean.dark".to_owned()));
let parser = liquid::ParserBuilder::new() let parser = liquid::ParserBuilder::new()
.block(highlight) .block(highlight)

View file

@ -249,7 +249,7 @@ mod test {
#[test] #[test]
fn highlight_block_renders_rust() { fn highlight_block_renders_rust() {
let highlight: Box<liquid::compiler::ParseBlock> = let highlight: Box<dyn liquid::compiler::ParseBlock> =
Box::new(CodeBlockParser::new("base16-ocean.dark".to_owned())); Box::new(CodeBlockParser::new("base16-ocean.dark".to_owned()));
let parser = liquid::ParserBuilder::new() let parser = liquid::ParserBuilder::new()
.block(highlight) .block(highlight)

View file

@ -1,3 +1,4 @@
---
layout: default.txt layout: default.txt
--- ---
This is my Index page! This is my Index page!

View file

@ -2,7 +2,7 @@
permalink: / permalink: /
layout: default.liquid layout: default.liquid
pagination: pagination:
include: All include: all
--- ---
This is my Index page! This is my Index page!

View file

@ -2,8 +2,8 @@
layout: default.liquid layout: default.liquid
permalink: / permalink: /
pagination: pagination:
include: All include: all
order: Asc order: asc
permalink_suffix: ./_p/{{num}} permalink_suffix: ./_p/{{num}}
--- ---
This is my Index page! This is my Index page!

View file

@ -2,7 +2,7 @@
layout: default.liquid layout: default.liquid
permalink: / permalink: /
pagination: pagination:
include: All include: all
sort_by: ["title"] sort_by: ["title"]
permalink_suffix: ./_p/{{num}} permalink_suffix: ./_p/{{num}}
--- ---

View file

@ -2,7 +2,7 @@
permalink: /categories.html permalink: /categories.html
layout: default.liquid layout: default.liquid
pagination: pagination:
include: Categories include: categories
permalink_suffix: ./_p/{{num}} permalink_suffix: ./_p/{{num}}
--- ---
{% if paginator.index_title %} {% if paginator.index_title %}

View file

@ -2,7 +2,7 @@
permalink: /dates.html permalink: /dates.html
layout: default.liquid layout: default.liquid
pagination: pagination:
include: Dates include: dates
permalink_suffix: ./_p/{{num}} permalink_suffix: ./_p/{{num}}
--- ---
{% if paginator.index_title %} {% if paginator.index_title %}

View file

@ -2,7 +2,7 @@
layout: default.liquid layout: default.liquid
permalink: / permalink: /
pagination: pagination:
include: All include: all
per_page: 1 per_page: 1
permalink_suffix: ./_p/{{num}} permalink_suffix: ./_p/{{num}}
--- ---

View file

@ -2,7 +2,7 @@
permalink: / permalink: /
layout: default.liquid layout: default.liquid
pagination: pagination:
include: All include: all
sort_by: ["weight"] sort_by: ["weight"]
--- ---
This is my Index page! This is my Index page!

View file

@ -2,7 +2,7 @@
permalink: /tags permalink: /tags
layout: default.liquid layout: default.liquid
pagination: pagination:
include: Tags include: tags
permalink_suffix: _p/{{num}} permalink_suffix: _p/{{num}}
per_page: 2 per_page: 2
--- ---

View file

@ -1,4 +1,4 @@
posts: posts:
order: Asc order: asc
site: site:
title: cobalt blog title: cobalt blog

View file

@ -1,3 +1,4 @@
---
layout: default.liquid layout: default.liquid
title: a post inside posts/2017/01/05/ title: a post inside posts/2017/01/05/

View file

@ -1,3 +1,4 @@
---
layout: default.liquid layout: default.liquid
title: a post inside posts/2017/01/08/ title: a post inside posts/2017/01/08/

View file

@ -1,3 +1,3 @@
assets: assets:
sass: sass:
style: Compressed style: compressed

View file

@ -69,9 +69,9 @@ fn run_test(name: &str) -> Result<(), cobalt::Error> {
.copy_from(format!("tests/fixtures/{}", name), &["**"]) .copy_from(format!("tests/fixtures/{}", name), &["**"])
.unwrap(); .unwrap();
let mut config = cobalt::ConfigBuilder::from_cwd(target.path())?; let mut config = cobalt_config::Config::from_cwd(target.path())?;
config.destination = "./_dest".into(); config.destination = "./_dest".into();
let config = config.build()?; let config = cobalt::cobalt_model::Config::from_config(config)?;
let result = cobalt::build(config); let result = cobalt::build(config);
// Always explicitly close to catch errors, especially on Windows. // Always explicitly close to catch errors, especially on Windows.
@ -86,9 +86,9 @@ fn test_with_expected(name: &str) -> Result<(), cobalt::Error> {
.copy_from(format!("tests/fixtures/{}", name), &["**"]) .copy_from(format!("tests/fixtures/{}", name), &["**"])
.unwrap(); .unwrap();
let mut config = cobalt::ConfigBuilder::from_cwd(target.path())?; let mut config = cobalt_config::Config::from_cwd(target.path())?;
config.destination = "./_dest".into(); config.destination = "./_dest".into();
let config = config.build()?; let config = cobalt::cobalt_model::Config::from_config(config)?;
let destination = config.destination.clone(); let destination = config.destination.clone();
let result = cobalt::build(config); let result = cobalt::build(config);
@ -229,7 +229,6 @@ pub fn ignore_files() {
pub fn yaml_error() { pub fn yaml_error() {
let err = test_with_expected("yaml_error"); let err = test_with_expected("yaml_error");
assert!(err.is_err()); assert!(err.is_err());
let err: exitfailure::ExitFailure = err.unwrap_err().into();
let error_message = format!("{:?}", err); let error_message = format!("{:?}", err);
assert_contains!(error_message, "unexpected character"); assert_contains!(error_message, "unexpected character");
} }