use anyhow::Context;
use chrono::DateTime;
use liquid::*;
use pulldown_cmark::{html, Options, Parser};
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::fs;
use std::path::Path;
use std::{collections::HashMap, fmt};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Date {
pub year: String,
pub short_year: String,
pub month: String,
pub i_month: String,
pub short_month: String,
pub long_month: String,
pub day: String,
pub i_day: String,
pub y_day: String,
pub w_year: String,
pub week: String,
pub w_day: String,
pub short_day: String,
pub long_day: String,
pub hour: String,
pub minute: String,
pub second: String,
pub rfc_3339: String,
pub rfc_2822: String,
}
impl fmt::Display for Date {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.rfc_3339)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Page {
pub data: HashMap<String, serde_yaml::Value>,
pub content: String,
pub permalink: String,
pub date: Date,
pub directory: String,
pub name: String,
pub url: String,
pub markdown: bool,
}
#[inline(always)]
pub fn get_permalink(permalink: &str) -> String {
match &*permalink {
"date" => {
"/{{ page.data.collection }}/{{ page.date.year }}/{{ page.date.month }}/{{ page.date.day }}/{{ page.data.title }}.html".to_owned()
}
"pretty" => {
"/{{ page.data.collection }}/{{ page.date.year }}/{{ page.date.month }}/{{ page.date.day }}/{{ page.data.title }}.html".to_owned()
}
"ordinal" => {
"/{{ page.data.collection }}/{{ page.date.year }}/{{ page.date.y_day }}/{{ page.data.title }}.html"
.to_owned()
}
"weekdate" => {
"/{{ page.data.collection }}/{{ page.date.year }}/W{{ page.date.week }}/{{ page.date.short_day }}/{{ page.data.title }}.html".to_owned()
}
"none" => {
"/{{ page.data.collection }}/{{ page.data.title }}.html".to_owned()
}
_ => {
permalink.to_string()
}
}
}
#[inline(always)]
pub fn split_frontmatter(page_text: String) -> (String, String) {
let mut begin = false;
let mut end = false;
let mut frontmatter = String::new();
let mut contents = String::new();
for line in page_text.lines() {
if !begin && line == "---" {
begin = true;
} else if begin && line == "---" && !end {
end = true;
} else if begin && !end {
frontmatter.push_str(&format!("{}\n", &line));
} else {
contents.push_str(&format!("{}\n", &line));
}
}
if frontmatter.trim().is_empty() {
frontmatter = "empty: true".to_owned();
}
(frontmatter, contents)
}
#[inline(always)]
pub fn get_page_object(page_path: String, collections: &HashMap<String, Vec<Page>>) -> Page {
let split_page = split_frontmatter(fs::read_to_string(&page_path).unwrap());
let frontmatter: HashMap<String, serde_yaml::Value> =
serde_yaml::from_str(&split_page.0).unwrap();
let permalink = frontmatter.get("permalink");
let date = frontmatter.get("date");
let markdown = frontmatter.get("markdown");
let permalink_string: String;
let markdown_bool: bool;
match permalink {
Some(_) => {
permalink_string = permalink.unwrap().as_str().unwrap().to_string();
}
None => {
permalink_string = String::new();
}
}
match markdown {
Some(_) => {
markdown_bool = markdown.unwrap().as_bool().unwrap();
}
None => {
markdown_bool = true;
}
}
let date_object;
match date {
Some(_) => {
let datetime = DateTime::parse_from_rfc3339(date.unwrap().as_str().unwrap());
let global_file = fs::read_to_string("./_global.yml");
let global: HashMap<String, serde_yaml::Value>;
match global_file {
Ok(_) => {
global = serde_yaml::from_str(&global_file.unwrap()).unwrap();
}
Err(_) => {
global = serde_yaml::from_str("locale: \"en_US\"").unwrap();
}
}
let locale_key = global.get("locale");
let locale_value;
match locale_key {
Some(_) => {
locale_value = locale_key.unwrap().as_str().unwrap();
}
None => {
locale_value = "en_US";
}
}
let locale: chrono::Locale = chrono::Locale::try_from(locale_value).unwrap();
date_object = Date {
year: format!("{}", datetime.unwrap().format_localized("%Y", locale)),
short_year: format!("{}", datetime.unwrap().format_localized("%y", locale)),
month: format!("{}", datetime.unwrap().format_localized("%m", locale)),
i_month: format!("{}", datetime.unwrap().format_localized("%-m", locale)),
short_month: format!("{}", datetime.unwrap().format_localized("%b", locale)),
long_month: format!("{}", datetime.unwrap().format_localized("%B", locale)),
day: format!("{}", datetime.unwrap().format_localized("%d", locale)),
i_day: format!("{}", datetime.unwrap().format_localized("%-d", locale)),
y_day: format!("{}", datetime.unwrap().format_localized("%j", locale)),
w_year: format!("{}", datetime.unwrap().format_localized("%G", locale)),
week: format!("{}", datetime.unwrap().format_localized("%U", locale)),
w_day: format!("{}", datetime.unwrap().format_localized("%u", locale)),
short_day: format!("{}", datetime.unwrap().format_localized("%a", locale)),
long_day: format!("{}", datetime.unwrap().format_localized("%A", locale)),
hour: format!("{}", datetime.unwrap().format_localized("%H", locale)),
minute: format!("{}", datetime.unwrap().format_localized("%M", locale)),
second: format!("{}", datetime.unwrap().format_localized("%S", locale)),
rfc_3339: datetime.unwrap().to_rfc3339(),
rfc_2822: datetime.unwrap().to_rfc2822(),
}
}
None => {
date_object = Date {
year: String::new(),
short_year: String::new(),
month: String::new(),
i_month: String::new(),
short_month: String::new(),
long_month: String::new(),
day: String::new(),
i_day: String::new(),
y_day: String::new(),
w_year: String::new(),
week: String::new(),
w_day: String::new(),
short_day: String::new(),
long_day: String::new(),
hour: String::new(),
minute: String::new(),
second: String::new(),
rfc_3339: String::new(),
rfc_2822: String::new(),
}
}
}
let page_path_io = Path::new(&page_path[..]);
let mut page = Page {
data: serde_yaml::from_str(&split_page.0).unwrap(),
content: split_page.1,
permalink: permalink_string,
date: date_object,
directory: page_path_io.parent().unwrap().to_str().unwrap().to_owned(),
name: page_path_io
.file_stem()
.unwrap()
.to_str()
.unwrap()
.to_owned(),
url: String::new(),
markdown: markdown_bool,
};
match &page.permalink[..] {
"" => {}
_ => {
page.url = render(
&page,
&get_permalink(permalink.unwrap().as_str().unwrap()),
true,
collections,
);
}
}
page
}
#[inline(always)]
pub fn get_contexts(page: &Page, collections: &HashMap<String, Vec<Page>>) -> Object {
let global_file = fs::read_to_string("./_global.yml");
let global: HashMap<String, serde_yaml::Value>;
match global_file {
Ok(_) => {
global = serde_yaml::from_str(&global_file.unwrap()).unwrap();
}
Err(_) => {
global = serde_yaml::from_str("locale: \"en_US\"").unwrap();
}
}
let layout_name = page.data.get("layout");
let layout: HashMap<String, serde_yaml::Value>;
match layout_name {
None => {
layout = HashMap::new();
}
Some(_) => {
layout = serde_yaml::from_str(
&split_frontmatter(
fs::read_to_string(format!(
"./layouts/{}.mokkf",
layout_name.unwrap().as_str().unwrap().to_string()
))
.unwrap(),
)
.0,
)
.unwrap();
}
}
let contexts = object!({
"global": global,
"page": page,
"layout": layout,
"collections": collections,
});
contexts
}
#[inline(always)]
pub fn render(
page: &Page,
text_to_render: &str,
only_context: bool,
collections: &HashMap<String, Vec<Page>>,
) -> String {
match only_context {
true => {
let template = create_liquid_parser()
.parse(text_to_render)
.with_context(|| {
format!(
"Could not parse the Mokk file at {}/{}.mokkf",
page.directory, page.name
)
})
.unwrap();
template
.render(&get_contexts(page, collections))
.with_context(|| {
format!(
"Could not render the Mokk file at {}/{}.mokkf",
page.directory, page.name
)
})
.unwrap()
}
false => {
let template = create_liquid_parser()
.parse(text_to_render)
.with_context(|| {
format!(
"Could not parse the Mokk file at {}/{}.mokkf",
page.directory, page.name
)
})
.unwrap();
let liquid_render = template
.render(&get_contexts(page, collections))
.with_context(|| {
format!(
"Could not render the Mokk file at {}/{}.mokkf",
page.directory, page.name
)
})
.unwrap();
render_markdown(liquid_render)
}
}
}
#[inline(always)]
pub fn compile(
mut page: Page,
mut collections: HashMap<String, Vec<Page>>,
) -> (String, HashMap<String, Vec<Page>>) {
let compiled_page;
let layout_name = &page.data.get("layout");
let collection_name = &page.data.get("collection");
page.content = render(&page, &page.content, !page.markdown, &collections);
match layout_name {
None => {
compiled_page = page.content.to_owned();
}
Some(_) => {
let layout_object = get_page_object(
format!(
"./layouts/{}.mokkf",
layout_name.unwrap().as_str().unwrap().to_string()
),
&collections,
);
let layouts = render_layouts(&page, layout_object, &collections);
compiled_page = render(&page, &layouts, true, &collections);
}
}
match collection_name {
None => {}
Some(_) => {
let collection_name_str = collection_name.unwrap().as_str().unwrap();
match collections.contains_key(&collection_name_str.to_string()) {
true => {
(*collections.get_mut(collection_name_str).unwrap()).push(page);
}
false => {
collections.insert(collection_name_str.to_owned(), vec![page]);
}
}
}
}
(compiled_page, collections)
}
#[inline(always)]
pub fn render_layouts(
sub: &Page,
layout: Page,
collections: &HashMap<String, Vec<Page>>,
) -> String {
let rendered: String;
let super_layout = layout.data.get("layout");
match super_layout {
Some(_) => {
let super_layout_object = get_page_object(
format!(
"./layouts/{}.mokkf",
super_layout.unwrap().as_str().unwrap().to_string()
),
collections,
);
rendered = render_layouts(&layout, super_layout_object, collections);
}
None => {
rendered = render(&sub, &layout.content, !layout.markdown, collections);
}
}
rendered
}
pub fn create_liquid_parser() -> liquid::Parser {
let mut partial = liquid::partials::InMemorySource::new();
let snippets = fs::read_dir("./snippets");
if snippets.is_ok() {
for snippet in snippets.unwrap() {
let unwrapped_snippet = snippet.unwrap();
let file_name = &unwrapped_snippet.file_name().into_string().unwrap();
let path = &unwrapped_snippet.path();
partial.add(file_name, &fs::read_to_string(path).unwrap());
}
}
let partial_compiler = liquid::partials::EagerCompiler::new(partial);
liquid::ParserBuilder::with_stdlib()
.tag(liquid_lib::jekyll::IncludeTag)
.filter(liquid_lib::jekyll::ArrayToSentenceString)
.filter(liquid_lib::jekyll::Pop)
.filter(liquid_lib::jekyll::Push)
.filter(liquid_lib::jekyll::Shift)
.filter(liquid_lib::jekyll::Slugify)
.filter(liquid_lib::jekyll::Unshift)
.filter(liquid_lib::shopify::Pluralize)
.filter(liquid_lib::extra::DateInTz)
.partials(partial_compiler)
.build()
.unwrap()
}
#[inline(always)]
pub fn render_markdown(text_to_render: String) -> String {
let mut markdown_options = Options::empty();
markdown_options.insert(Options::ENABLE_TABLES);
markdown_options.insert(Options::ENABLE_FOOTNOTES);
markdown_options.insert(Options::ENABLE_STRIKETHROUGH);
markdown_options.insert(Options::ENABLE_TASKLISTS);
markdown_options.insert(Options::ENABLE_SMART_PUNCTUATION);
let markdown_parser = Parser::new_ext(&text_to_render, markdown_options);
let mut rendered_markdown = String::new();
html::push_html(&mut rendered_markdown, markdown_parser);
rendered_markdown
}