use std::borrow;
use std::collections::HashMap;
use std::fs;
use std::io::Write;
use std::path;
use jsonfeed;
use jsonfeed::Feed;
use liquid;
use rss;
use crate::cobalt_model;
use crate::cobalt_model::files;
use crate::cobalt_model::permalink;
use crate::cobalt_model::Collection;
use crate::cobalt_model::{Config, SortOrder};
use crate::document::Document;
use crate::error::*;
use crate::pagination;
struct Context {
pub source: path::PathBuf,
pub destination: path::PathBuf,
pub pages: cobalt_model::Collection,
pub posts: cobalt_model::Collection,
pub site: liquid::value::Object,
pub layouts: HashMap<String, String>,
pub liquid: cobalt_model::Liquid,
pub markdown: cobalt_model::Markdown,
pub assets: cobalt_model::Assets,
}
impl Context {
fn with_config(config: Config) -> Result<Self> {
let Config {
source,
destination,
pages,
posts,
site,
layouts_dir,
liquid,
markdown,
assets,
} = config;
let pages = pages.build()?;
let posts = posts.build()?;
let site = site.build()?;
let liquid = liquid.build()?;
let markdown = markdown.build();
let assets = assets.build()?;
let layouts = find_layouts(&layouts_dir)?;
let layouts = parse_layouts(&layouts);
let context = Context {
source,
destination,
pages,
posts,
site,
layouts,
liquid,
markdown,
assets,
};
Ok(context)
}
}
pub fn build(config: Config) -> Result<()> {
let context = Context::with_config(config)?;
let post_files = &context.posts.pages;
let mut posts = parse_pages(post_files, &context.posts, &context.source)?;
if let Some(ref drafts) = context.posts.drafts {
let drafts_root = drafts.subtree();
parse_drafts(drafts_root, drafts, &mut posts, &context.posts)?;
}
let page_files = &context.pages.pages;
let documents = parse_pages(page_files, &context.pages, &context.source)?;
sort_pages(&mut posts, &context.posts)?;
generate_posts(&mut posts, &context)?;
if let Some(ref path) = context.posts.rss {
create_rss(path, &context.destination, &context.posts, &posts)?;
}
if let Some(ref path) = context.posts.jsonfeed {
create_jsonfeed(path, &context.destination, &context.posts, &posts)?;
}
generate_pages(posts, documents, &context)?;
context.assets.populate(&context.destination)?;
Ok(())
}
fn generate_collections_var(
posts_data: &[liquid::value::Value],
context: &Context,
) -> (borrow::Cow<'static, str>, liquid::value::Value) {
let mut posts_variable = context.posts.attributes.clone();
posts_variable.insert(
"pages".into(),
liquid::value::Value::Array(posts_data.to_vec()),
);
let global_collection: liquid::value::Object = vec![(
context.posts.slug.clone().into(),
liquid::value::Value::Object(posts_variable),
)]
.into_iter()
.collect();
(
"collections".into(),
liquid::value::Value::Object(global_collection),
)
}
fn generate_doc(
doc: &mut Document,
context: &Context,
global_collection: (borrow::Cow<'static, str>, liquid::value::Value),
) -> Result<()> {
let mut globals: liquid::value::Object = vec![
(
"site".into(),
liquid::value::Value::Object(context.site.clone()),
),
global_collection,
]
.into_iter()
.collect();
globals.insert(
"page".into(),
liquid::value::Value::Object(doc.attributes.clone()),
);
doc.render_excerpt(&globals, &context.liquid, &context.markdown)
.chain_err(|| format!("Failed to render excerpt for {:?}", doc.file_path))?;
doc.render_content(&globals, &context.liquid, &context.markdown)
.chain_err(|| format!("Failed to render content for {:?}", doc.file_path))?;
globals.insert(
"page".into(),
liquid::value::Value::Object(doc.attributes.clone()),
);
let doc_html = doc
.render(&globals, &context.liquid, &context.layouts)
.chain_err(|| format!("Failed to render for {:?}", doc.file_path))?;
files::write_document_file(doc_html, context.destination.join(&doc.file_path))?;
Ok(())
}
fn generate_pages(posts: Vec<Document>, documents: Vec<Document>, context: &Context) -> Result<()> {
let posts_data: Vec<liquid::value::Value> = posts
.into_iter()
.map(|x| liquid::value::Value::Object(x.attributes))
.collect();
trace!("Generating other documents");
for mut doc in documents {
trace!("Generating {}", doc.url_path);
if doc.front.pagination.is_some() {
let paginators = pagination::generate_paginators(&mut doc, &posts_data)?;
let mut paginators = paginators.into_iter();
let paginator = paginators
.next()
.expect("We detected pagination enabled but we have no paginator");
generate_doc(
&mut doc,
context,
(
"paginator".into(),
liquid::value::Value::Object(paginator.into()),
),
)?;
for paginator in paginators {
let mut doc_page = doc.clone();
doc_page.file_path = permalink::format_url_as_file(&paginator.index_permalink);
generate_doc(
&mut doc_page,
context,
(
"paginator".into(),
liquid::value::Value::Object(paginator.into()),
),
)?;
}
} else {
generate_doc(
&mut doc,
context,
generate_collections_var(&posts_data, &context),
)?;
};
}
Ok(())
}
fn generate_posts(posts: &mut Vec<Document>, context: &Context) -> Result<()> {
let simple_posts_data: Vec<liquid::value::Value> = posts
.iter()
.map(|x| liquid::value::Value::Object(x.attributes.clone()))
.collect();
trace!("Generating posts");
for (i, post) in &mut posts.iter_mut().enumerate() {
trace!("Generating {}", post.url_path);
let previous = simple_posts_data
.get(i + 1)
.cloned()
.unwrap_or(liquid::value::Value::Nil);
post.attributes.insert("previous".into(), previous);
let next = if i >= 1 {
simple_posts_data.get(i - 1)
} else {
None
}
.cloned()
.unwrap_or(liquid::value::Value::Nil);
post.attributes.insert("next".into(), next);
generate_doc(
post,
context,
generate_collections_var(&simple_posts_data, &context),
)?;
}
Ok(())
}
fn sort_pages(posts: &mut Vec<Document>, collection: &Collection) -> Result<()> {
let default_date = cobalt_model::DateTime::default();
posts.sort_by(|a, b| {
b.front
.published_date
.unwrap_or(default_date)
.cmp(&a.front.published_date.unwrap_or(default_date))
});
match collection.order {
SortOrder::Asc => posts.reverse(),
SortOrder::Desc | SortOrder::None => (),
}
Ok(())
}
fn parse_drafts(
drafts_root: &path::Path,
draft_files: &files::Files,
documents: &mut Vec<Document>,
collection: &Collection,
) -> Result<()> {
let rel_real = collection
.pages
.subtree()
.strip_prefix(collection.pages.root())
.expect("subtree is under root");
for file_path in draft_files.files() {
let rel_src = file_path
.strip_prefix(&drafts_root)
.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 doc = Document::parse(&file_path, &new_path, default_front)
.chain_err(|| format!("Failed to parse {:?}", rel_src))?;
documents.push(doc);
}
Ok(())
}
fn find_layouts(layouts: &path::Path) -> Result<files::Files> {
let mut files = files::FilesBuilder::new(layouts)?;
files.ignore_hidden(false)?;
files.build()
}
fn parse_layouts(files: &files::Files) -> HashMap<String, String> {
let (entries, errors): (Vec<_>, Vec<_>) = files
.files()
.map(|file_path| {
let rel_src = file_path
.strip_prefix(files.root())
.expect("file was found under the root");
let layout_data = files::read_file(&file_path)
.map_err(|e| format!("Failed to load layout {:?}: {}", rel_src, e))?;
let path = rel_src
.to_str()
.ok_or_else(|| format!("File name not valid liquid path: {:?}", rel_src))?
.to_owned();
Ok((path, layout_data))
})
.partition(Result::is_ok);
for error in errors {
warn!("{}", error.expect_err("partition to filter out oks"));
}
entries
.into_iter()
.map(|entry| entry.expect("partition to filter out errors"))
.collect()
}
fn parse_pages(
page_files: &files::Files,
collection: &Collection,
source: &path::Path,
) -> Result<Vec<Document>> {
let mut documents = vec![];
for file_path in page_files.files() {
let rel_src = file_path
.strip_prefix(source)
.expect("file was found under the root");
let default_front = collection.default.clone();
let doc = Document::parse(&file_path, rel_src, default_front)
.chain_err(|| format!("Failed to parse {:?}", rel_src))?;
if !doc.front.is_draft || collection.include_drafts {
documents.push(doc);
}
}
Ok(documents)
}
fn create_rss(
path: &str,
dest: &path::Path,
collection: &Collection,
documents: &[Document],
) -> Result<()> {
let rss_path = dest.join(path);
debug!("Creating RSS file at {}", rss_path.display());
let title = &collection.title;
let description = collection
.description
.as_ref()
.map(|s| s.as_str())
.unwrap_or("");
let link = collection
.base_url
.as_ref()
.ok_or(ErrorKind::ConfigFileMissingFields)?;
let items: Result<Vec<rss::Item>> = documents.iter().map(|doc| doc.to_rss(link)).collect();
let items = items?;
let channel = rss::ChannelBuilder::default()
.title(title.to_owned())
.link(link.to_owned())
.description(description.to_owned())
.items(items)
.build()?;
let rss_string = channel.to_string();
trace!("RSS data: {}", rss_string);
if let Some(parent) = rss_path.parent() {
fs::create_dir_all(parent).map_err(|e| format!("Could not create {:?}: {}", parent, e))?;
}
let mut rss_file = fs::File::create(&rss_path)?;
rss_file.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
rss_file.write_all(&rss_string.into_bytes())?;
rss_file.write_all(b"\n")?;
Ok(())
}
fn create_jsonfeed(
path: &str,
dest: &path::Path,
collection: &Collection,
documents: &[Document],
) -> Result<()> {
let jsonfeed_path = dest.join(path);
debug!("Creating jsonfeed file at {}", jsonfeed_path.display());
let title = &collection.title;
let description = collection
.description
.as_ref()
.map(|s| s.as_str())
.unwrap_or("");
let link = collection
.base_url
.as_ref()
.ok_or(ErrorKind::ConfigFileMissingFields)?;
let jsonitems = documents.iter().map(|doc| doc.to_jsonfeed(link)).collect();
let feed = Feed {
title: title.to_string(),
items: jsonitems,
home_page_url: Some(link.to_string()),
description: Some(description.to_string()),
..Default::default()
};
let jsonfeed_string = jsonfeed::to_string(&feed).unwrap();
files::write_document_file(jsonfeed_string, jsonfeed_path)?;
Ok(())
}