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

Cherry-pick: 0b3ef05
This commit is contained in:
Ed Page 2021-07-08 14:38:51 -05:00
parent 0e90335ada
commit 071e5fac97
27 changed files with 419 additions and 1689 deletions

View file

@ -88,6 +88,7 @@ difference = "2.0"
[features]
default = ["syntax-highlight", "sass", "serve", "html-minifier"]
unstable = []
preview_unstable = ["cobalt-config/preview_unstable"]
serve = ["tiny_http", "notify", "mime_guess"]
syntax-highlight = ["syntect"]

View file

@ -48,6 +48,7 @@ fn parse_frontmatter(front: &str) -> Result<Frontmatter> {
Ok(front)
}
#[cfg(feature = "preview_unstable")]
static FRONT_MATTER: once_cell::sync::Lazy<regex::Regex> = once_cell::sync::Lazy::new(|| {
regex::RegexBuilder::new(r"\A---\s*\r?\n([\s\S]*\n)?---\s*\r?\n(.*)")
.dot_matches_new_line(true)
@ -55,6 +56,7 @@ static FRONT_MATTER: once_cell::sync::Lazy<regex::Regex> = once_cell::sync::Lazy
.unwrap()
});
#[cfg(feature = "preview_unstable")]
fn split_document(content: &str) -> (Option<&str>, &str) {
if let Some(captures) = FRONT_MATTER.captures(content) {
let front_split = captures.get(1).map(|m| m.as_str()).unwrap_or_default();
@ -70,6 +72,72 @@ fn split_document(content: &str) -> (Option<&str>, &str) {
}
}
#[cfg(not(feature = "preview_unstable"))]
fn split_document(content: &str) -> (Option<&str>, &str) {
static FRONT_MATTER_DIVIDE: once_cell::sync::Lazy<regex::Regex> =
once_cell::sync::Lazy::new(|| {
regex::RegexBuilder::new(r"---\s*\r?\n")
.dot_matches_new_line(true)
.build()
.unwrap()
});
static FRONT_MATTER: once_cell::sync::Lazy<regex::Regex> = once_cell::sync::Lazy::new(|| {
regex::RegexBuilder::new(r"\A---\s*\r?\n([\s\S]*\n)?---\s*\r?\n")
.dot_matches_new_line(true)
.build()
.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() {
(None, content_split)
} else {
(Some(front_split), content_split)
}
} else {
deprecated_split_front_matter(content)
}
}
#[cfg(not(feature = "preview_unstable"))]
fn deprecated_split_front_matter(content: &str) -> (Option<&str>, &str) {
static FRONT_MATTER_DIVIDE: once_cell::sync::Lazy<regex::Regex> =
once_cell::sync::Lazy::new(|| {
regex::RegexBuilder::new(r"(\A|\n)---\s*\r?\n")
.dot_matches_new_line(true)
.build()
.unwrap()
});
if FRONT_MATTER_DIVIDE.is_match(content) {
log::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() {
(None, content_split)
} else {
(Some(front_split), content_split)
}
} else {
(None, content)
}
}
#[cfg(test)]
mod test {
use super::*;

View file

@ -7,7 +7,6 @@ use env_logger;
use failure::ResultExt;
use crate::error::*;
use cobalt;
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()
}
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");
// Fetch config information if available
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))?
} else {
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);

View file

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

View file

@ -30,7 +30,7 @@ pub fn debug_command(matches: &clap::ArgMatches) -> Result<()> {
match matches.subcommand() {
("config", _) => {
let config = args::get_config(matches)?;
let config = config.build()?;
let config = cobalt::cobalt_model::Config::from_config(config)?;
println!("{}", config);
}
("highlight", Some(matches)) => match matches.subcommand() {
@ -48,7 +48,7 @@ pub fn debug_command(matches: &clap::ArgMatches) -> Result<()> {
},
("files", Some(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");
match collection {
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<()> {
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();
@ -107,7 +107,7 @@ pub fn rename_command_args() -> clap::App<'static, 'static> {
pub fn rename_command(matches: &clap::ArgMatches) -> Result<()> {
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());
@ -144,7 +144,7 @@ pub fn publish_command(matches: &clap::ArgMatches) -> Result<()> {
file.push(path::Path::new(filename));
let file = file;
let config = args::get_config(matches)?;
let config = config.build()?;
let config = cobalt::cobalt_model::Config::from_config(config)?;
publish_document(&config, &file)
.with_context(|_| failure::format_err!("Could not publish `{:?}`", file))?;
@ -311,11 +311,10 @@ pub fn create_new_document(
default.to_string()
};
let doc = cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::parse(&source)?;
let (front, content) = doc.parts();
let front = front.set_title(title.to_owned());
let doc =
cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::new(front, content);
let doc = cobalt_model::Document::parse(&source)?;
let (mut front, content) = doc.into_parts();
front.title = Some(title.to_owned());
let doc = cobalt_model::Document::new(front, content);
let doc = doc.to_string();
create_file(&file, &doc)?;
@ -359,8 +358,8 @@ pub fn rename_document(
};
let doc = cobalt_model::files::read_file(&source)?;
let doc = cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::parse(&doc)?;
let (front, content) = doc.parts();
let doc = cobalt_model::Document::parse(&doc)?;
let (mut front, content) = doc.into_parts();
let pages = config.pages.clone().build()?;
let posts = config.posts.clone().build()?;
@ -374,10 +373,7 @@ pub fn rename_document(
let rel_src = target
.strip_prefix(&config.source)
.expect("file was found under the root");
front
.clone()
.merge_path(rel_src)
.merge(posts.default.clone())
front.clone().merge_path(rel_src).merge(&posts.default)
} else if pages.pages.includes_file(&target)
|| pages
.drafts
@ -388,21 +384,17 @@ pub fn rename_document(
let rel_src = target
.strip_prefix(&config.source)
.expect("file was found under the root");
front
.clone()
.merge_path(rel_src)
.merge(pages.default.clone())
front.clone().merge_path(rel_src).merge(&pages.default)
} else {
failure::bail!(
"Target file wouldn't be a member of any collection: {:?}",
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()));
let doc =
cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::new(new_front, content);
front.title = Some(title.to_string());
let doc = cobalt_model::Document::new(front, content);
let doc = doc.to_string();
cobalt_model::files::write_document_file(doc, target)?;
@ -421,8 +413,12 @@ fn prepend_date_to_filename(
) -> Result<()> {
// avoid prepend to existing date prefix
let file_stem = cobalt_model::file_stem(file);
let (_, file_stem) = cobalt_model::parse_file_stem(file_stem);
let file_stem = file
.file_stem()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
let (_, file_stem) = cobalt_config::path::parse_file_stem(file_stem);
let file_name = format!(
"{}{}.{}",
(**date).format("%Y-%m-%d-"),
@ -476,14 +472,14 @@ fn move_from_drafts_to_posts(
pub fn publish_document(config: &cobalt_model::Config, file: &path::Path) -> Result<()> {
let doc = cobalt_model::files::read_file(file)?;
let doc = cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::parse(&doc)?;
let (front, content) = doc.parts();
let doc = cobalt_model::Document::parse(&doc)?;
let (mut front, content) = doc.into_parts();
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 =
cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::new(front, content);
let doc = cobalt_model::Document::new(front, content);
let doc = doc.to_string();
cobalt_model::files::write_document_file(doc, file)?;

View file

@ -64,7 +64,7 @@ pub fn serve_command(matches: &clap::ArgMatches) -> Result<()> {
let mut config = args::get_config(matches)?;
debug!("Overriding config `site.base_url` with `{}`", 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();
build::build(config.clone())?;

View file

@ -331,7 +331,11 @@ fn parse_drafts(
.expect("file was found under the root");
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)
.with_context(|_| failure::format_err!("Failed to parse {}", rel_src.display()))?;

View file

@ -1,11 +1,12 @@
use std::ffi::OsStr;
use std::path;
use failure::ResultExt;
use super::sass;
use super::{files, Minify};
use crate::error::*;
use failure::ResultExt;
use std::ffi::OsStr;
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
@ -17,6 +18,20 @@ pub struct 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> {
let AssetsBuilder {
sass,

View file

@ -1,12 +1,12 @@
use std::path;
use cobalt_config::Frontmatter;
use cobalt_config::SortOrder;
use liquid;
use super::files;
use super::slug;
use super::FrontmatterBuilder;
use crate::error::*;
pub use cobalt_config::SortOrder;
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
@ -25,15 +25,111 @@ pub struct CollectionBuilder {
pub jsonfeed: Option<String>,
pub base_url: Option<String>,
pub publish_date_in_filename: bool,
pub default: FrontmatterBuilder,
pub default: Frontmatter,
}
impl CollectionBuilder {
pub fn new() -> Self {
Self::default()
pub fn from_page_config(
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(
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
}
@ -90,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 {
title,
@ -151,7 +250,7 @@ pub struct Collection {
pub rss: Option<String>,
pub jsonfeed: Option<String>,
pub base_url: Option<String>,
pub default: FrontmatterBuilder,
pub default: Frontmatter,
pub attributes: liquid::Object,
}

View file

@ -1,8 +1,6 @@
use std::fmt;
use std::path;
use failure::ResultExt;
use liquid;
use serde_yaml;
use crate::error::*;
@ -10,351 +8,31 @@ use crate::error::*;
use super::assets;
use super::collection;
use super::files;
use super::frontmatter;
use super::mark;
use super::sass;
use super::site;
use super::template;
use super::vwiki;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(deny_unknown_fields, default)]
pub struct SyntaxHighlight {
pub theme: String,
pub enabled: bool,
}
impl Default for SyntaxHighlight {
fn default() -> Self {
Self {
theme: "base16-ocean.dark".to_owned(),
enabled: true,
}
}
}
#[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 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 vimwiki: vwiki::VimwikiBuilder,
pub assets: assets::AssetsBuilder,
pub sitemap: Option<String>,
pub data: Option<liquid::Object>,
#[serde(skip)]
pub data_dir: &'static str,
pub minify: cobalt_config::Minify,
}
impl SiteConfig {
fn builder(self, source: &path::Path) -> site::SiteBuilder {
site::SiteBuilder {
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 Minify {
pub html: bool,
pub css: bool,
pub js: bool,
}
impl Default for Minify {
fn default() -> Self {
Minify {
html: false,
css: false,
js: false,
}
}
}
#[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,
pub minify: Minify,
}
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(), "wiki".to_owned(), "liquid".to_owned()],
ignore: Default::default(),
syntax_highlight: SyntaxHighlight::default(),
layouts_dir: "_layouts",
includes_dir: "_includes",
assets: AssetsConfig::default(),
minify: Minify::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 {
impl Config {
pub fn from_config(source: cobalt_config::Config) -> Result<Self> {
let cobalt_config::Config {
root,
source,
destination,
@ -371,7 +49,7 @@ impl ConfigBuilder {
includes_dir,
assets,
minify,
} = self;
} = source;
if include_drafts {
debug!("Draft mode enabled");
@ -395,7 +73,8 @@ impl ConfigBuilder {
let source = root.join(source);
let destination = abs_dest.unwrap_or_else(|| root.join(destination));
let pages = pages.builder(
let pages = collection::CollectionBuilder::from_page_config(
pages,
&source,
&site,
&posts,
@ -404,7 +83,8 @@ impl ConfigBuilder {
&template_extensions,
);
let posts = posts.builder(
let posts = collection::CollectionBuilder::from_post_config(
posts,
&source,
&site,
include_drafts,
@ -414,9 +94,10 @@ impl ConfigBuilder {
);
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 layouts_dir = source.join(layouts_dir);
@ -453,35 +134,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 vimwiki: vwiki::VimwikiBuilder,
pub assets: assets::AssetsBuilder,
pub sitemap: Option<String>,
pub minify: Minify,
}
impl Default for Config {
fn default() -> Config {
ConfigBuilder::default()
.build()
Config::from_config(cobalt_config::Config::default())
.expect("default config should not fail")
}
}
@ -494,70 +149,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]
fn test_build_default() {
let config = ConfigBuilder::default();
config.build().unwrap();
let config = cobalt_config::Config::default();
Config::from_config(config).unwrap();
}
#[test]
fn test_build_dest() {
let result = ConfigBuilder::from_file("tests/fixtures/config/_cobalt.yml").unwrap();
let result = result.build().unwrap();
let config = cobalt_config::Config::from_file("tests/fixtures/config/_cobalt.yml").unwrap();
let result = Config::from_config(config).unwrap();
assert_eq!(
result.source,
path::Path::new("tests/fixtures/config").to_path_buf()
@ -570,9 +171,9 @@ fn test_build_dest() {
#[test]
fn test_build_abs_dest() {
let mut result = ConfigBuilder::from_file("tests/fixtures/config/_cobalt.yml").unwrap();
result.abs_dest = Some(path::PathBuf::from("hello/world"));
let result = result.build().unwrap();
let mut config = cobalt_config::Config::from_file("tests/fixtures/config/_cobalt.yml").unwrap();
config.abs_dest = Some(path::PathBuf::from("hello/world"));
let result = Config::from_config(config).unwrap();
assert_eq!(
result.source,
path::Path::new("tests/fixtures/config").to_path_buf()

View file

@ -1 +0,0 @@
pub use liquid::model::DateTime;

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,292 +1,37 @@
use std::collections::HashMap;
use std::fmt;
use std::path;
use chrono::Datelike;
use cobalt_config::DateTime;
use cobalt_config::SourceFormat;
use liquid;
use regex;
use serde;
use serde_yaml;
use super::pagination;
use crate::error::Result;
use super::datetime;
use super::pagination_config;
use super::slug;
pub use cobalt_config::SourceFormat;
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();
}
// 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 converted = serde_yaml::to_string(self)?;
println!("Before: {:?}", converted);
let subset = converted
.strip_prefix("---")
.unwrap_or_else(|| converted.as_str())
.trim();
let converted = if subset == "{}" { "" } else { subset }.to_owned();
println!("After: {:?}", converted);
Ok(converted)
}
}
#[derive(Debug, Eq, PartialEq, Default, Clone, Serialize, Deserialize)]
#[derive(Debug, Eq, PartialEq, Default, Clone, Serialize)]
#[serde(deny_unknown_fields, default)]
pub struct FrontmatterBuilder {
#[serde(skip_serializing_if = "Option::is_none")]
pub permalink: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub slug: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub struct Frontmatter {
pub permalink: cobalt_config::Permalink,
pub slug: String,
pub title: String,
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub excerpt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub categories: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub categories: Vec<String>,
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub excerpt_separator: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
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 templated: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub excerpt_separator: String,
pub published_date: Option<DateTime>,
pub format: SourceFormat,
pub templated: bool,
pub layout: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_draft: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub weight: Option<i32>,
#[serde(skip_serializing_if = "liquid::Object::is_empty")]
pub is_draft: bool,
pub weight: i32,
pub collection: String,
pub data: liquid::Object,
#[serde(skip_serializing_if = "Option::is_none")]
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>,
pub pagination: Option<pagination::PaginationConfig>,
}
impl FrontmatterBuilder {
pub fn new() -> Self {
Self::default()
}
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
}
}
#[cfg(test)]
pub fn set_templated<S: Into<Option<bool>>>(self, templated: S) -> Self {
Self {
templated: templated.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()))
}
#[cfg(test)]
pub fn merge_templated<S: Into<Option<bool>>>(self, templated: S) -> Self {
self.merge(Self::new().set_templated(templated.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::Object) -> Self {
let Self {
impl Frontmatter {
pub fn from_config(config: cobalt_config::Frontmatter) -> Result<Frontmatter> {
let cobalt_config::Frontmatter {
permalink,
slug,
title,
@ -304,160 +49,17 @@ impl FrontmatterBuilder {
collection,
data,
pagination,
} = self;
Self {
permalink,
slug,
title,
description,
excerpt,
categories,
tags,
excerpt_separator,
published_date,
format,
templated,
layout,
is_draft,
weight,
collection,
data: merge_objects(data, other_data),
pagination,
}
}
} = config;
pub fn merge(self, other: Self) -> Self {
let Self {
permalink,
slug,
title,
description,
excerpt,
categories,
tags,
excerpt_separator,
published_date,
format,
templated,
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,
templated: other_templated,
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),
templated: templated.or_else(|| other_templated),
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),
}
}
let collection = collection.unwrap_or_default();
pub fn merge_path<P: AsRef<path::Path>>(self, relpath: P) -> Self {
self.merge_path_ref(relpath.as_ref())
}
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,
"wiki" => SourceFormat::Vimwiki,
_ => 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);
let permalink = permalink.unwrap_or_default();
if let cobalt_config::Permalink::Explicit(permalink) = &permalink {
if !permalink.starts_with('/') {
failure::bail!("Unsupported permalink alias '{}'", permalink);
}
}
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,
templated,
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 tags.iter().any(|x| x.trim().is_empty()) {
failure::bail!("Empty strings are not allowed in tags");
@ -469,7 +71,8 @@ impl FrontmatterBuilder {
tags
};
let fm = Frontmatter {
pagination: pagination.and_then(|p| p.build(&permalink)),
pagination: pagination
.and_then(|p| pagination::PaginationConfig::from_config(p, &permalink)),
permalink,
slug: slug.ok_or_else(|| failure::err_msg("No slug"))?,
title: title.ok_or_else(|| failure::err_msg("No title"))?,
@ -479,7 +82,10 @@ impl FrontmatterBuilder {
tags,
excerpt_separator: excerpt_separator.unwrap_or_else(|| "\n\n".to_owned()),
published_date,
format: format.unwrap_or_else(SourceFormat::default),
format: format.unwrap_or_else(super::SourceFormat::default),
#[cfg(feature = "preview_unstable")]
templated: templated.unwrap_or(false),
#[cfg(not(feature = "preview_unstable"))]
templated: templated.unwrap_or(true),
layout,
is_draft: is_draft.unwrap_or(false),
@ -489,7 +95,7 @@ impl FrontmatterBuilder {
};
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...");
}
}
@ -497,408 +103,18 @@ 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 templated: bool,
pub layout: Option<String>,
pub is_draft: bool,
pub weight: i32,
pub collection: String,
pub data: liquid::Object,
pub pagination: Option<pagination_config::PaginationConfig>,
}
impl Front for Frontmatter {}
impl fmt::Display for Frontmatter {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let converted = Front::to_string(self).map_err(|_| fmt::Error)?;
write!(f, "{}", converted)
}
}
/// Shallow merge of `liquid::Object`'s
fn merge_objects(mut primary: liquid::Object, secondary: liquid::Object) -> liquid::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),
templated: Some(true),
layout: Some("layout a".to_owned()),
is_draft: Some(true),
weight: Some(0),
collection: Some("pages".to_owned()),
data: liquid::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),
templated: Some(false),
layout: Some("layout b".to_owned()),
is_draft: Some(true),
weight: Some(0),
collection: Some("posts".to_owned()),
data: liquid::Object::new(),
pagination: Some(Default::default()),
};
let merge_b_into_a = a.clone().merge(b);
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.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),
templated: Some(true),
layout: Some("layout a".to_owned()),
is_draft: Some(true),
weight: Some(0),
collection: Some("pages".to_owned()),
data: liquid::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_templated(false)
.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_templated(true)
.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();
let converted = serde_yaml::to_string(self).expect("should always be valid");
let subset = converted
.strip_prefix("---")
.unwrap_or_else(|| converted.as_str())
.trim();
let converted = if subset == "{}" { "" } else { subset };
if converted.is_empty() {
Ok(())
} else {
write!(f, "{}", converted)
}
}
}

View file

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

View file

@ -1,8 +1,6 @@
mod assets;
mod collection;
mod config;
mod datetime;
mod document;
mod frontmatter;
mod mark;
mod sass;
@ -11,37 +9,28 @@ mod template;
mod vwiki;
pub mod files;
pub mod pagination_config;
pub mod pagination;
pub mod permalink;
pub mod slug;
pub use cobalt_config::DateTime;
pub use cobalt_config::Document;
pub use cobalt_config::Minify;
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::AssetsBuilder;
pub use self::collection::Collection;
pub use self::collection::CollectionBuilder;
pub use self::collection::SortOrder;
pub use self::config::AssetsConfig;
pub use self::config::Config;
pub use self::config::ConfigBuilder;
pub use self::config::Minify;
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::FrontmatterBuilder;
pub use self::frontmatter::SourceFormat;
pub use self::mark::Markdown;
pub use self::mark::MarkdownBuilder;
pub use self::sass::SassBuilder;
pub use self::sass::SassCompiler;
pub use self::sass::SassOutputStyle;
pub use self::site::SiteBuilder;
pub use self::template::Liquid;
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,158 +0,0 @@
use std::convert::Into;
use std::vec::Vec;
use super::SortOrder;
pub use cobalt_config::DateIndex;
pub use cobalt_config::Include;
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];
}
// TODO to be replaced by a call to `is_sorted()` once it's stabilized
pub fn is_date_index_sorted(v: &[DateIndex]) -> bool {
let mut copy = v.to_vec();
copy.sort_unstable();
copy.as_slice().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

@ -17,8 +17,15 @@ pub struct SassBuilder {
}
impl SassBuilder {
pub fn new() -> Self {
Default::default()
pub fn from_config(config: cobalt_config::Sass, source: &path::Path) -> Self {
Self {
style: config.style,
import_dir: source
.join(config.import_dir)
.into_os_string()
.into_string()
.ok(),
}
}
pub fn build(self) -> SassCompiler {

View file

@ -19,10 +19,20 @@ pub struct SiteBuilder {
pub description: Option<String>,
pub base_url: Option<String>,
pub data: Option<liquid::Object>,
pub data_dir: Option<path::PathBuf>,
pub data_dir: path::PathBuf,
}
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::Object> {
let SiteBuilder {
title,
@ -53,9 +63,7 @@ impl SiteBuilder {
attributes.insert("base_url".into(), liquid::model::Value::scalar(base_url));
}
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() {
attributes.insert("data".into(), liquid::model::Value::Object(data));
}

View file

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

View file

@ -212,23 +212,23 @@ impl Document {
pub fn parse(
src_path: &Path,
rel_path: &Path,
default_front: cobalt_model::FrontmatterBuilder,
default_front: cobalt_config::Frontmatter,
) -> Result<Document> {
trace!("Parsing {:?}", rel_path);
let content = files::read_file(src_path)?;
let builder =
cobalt_model::DocumentBuilder::<cobalt_model::FrontmatterBuilder>::parse(&content)?;
let (front, content) = builder.parts();
let front = front.merge_path(rel_path).merge(default_front);
let builder = cobalt_config::Document::parse(&content)?;
let (front, content) = builder.into_parts();
let front = front.merge_path(rel_path).merge(&default_front);
let front = front.build()?;
let front = cobalt_model::Frontmatter::from_config(front)?;
let (file_path, url_path) = {
let perma_attributes = permalink_attributes(&front, rel_path);
let url_path = permalink::explode_permalink(&front.permalink, &perma_attributes)
.with_context(|_| {
failure::format_err!("Failed to create permalink `{}`", front.permalink)
})?;
let url_path =
permalink::explode_permalink(front.permalink.as_str(), &perma_attributes)
.with_context(|_| {
failure::format_err!("Failed to create permalink `{}`", front.permalink)
})?;
let file_path = permalink::format_url_as_file(&url_path);
(file_path, url_path)
};

View file

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

View file

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

View file

@ -2,8 +2,8 @@ use std::cmp::Ordering;
use liquid::ValueView;
use crate::cobalt_model::pagination_config::Include;
use crate::cobalt_model::pagination_config::PaginationConfig;
use crate::cobalt_model::pagination::Include;
use crate::cobalt_model::pagination::PaginationConfig;
use crate::cobalt_model::permalink;
use crate::cobalt_model::slug;
use crate::cobalt_model::SortOrder;

View file

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

View file

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