mirror of
https://github.com/cobalt-org/cobalt.rs
synced 2024-11-15 00:17:29 +00:00
parent
0e90335ada
commit
071e5fac97
27 changed files with 419 additions and 1689 deletions
|
@ -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"]
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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())?;
|
||||
|
|
|
@ -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") => {
|
||||
|
|
|
@ -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)?;
|
||||
|
||||
|
|
|
@ -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())?;
|
||||
|
|
|
@ -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()))?;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
pub use liquid::model::DateTime;
|
|
@ -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, "");
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
61
src/cobalt_model/pagination.rs
Normal file
61
src/cobalt_model/pagination.rs
Normal 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)
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue