use chrono::DateTime;
use liquid::*;
use pulldown_cmark::{html, Options, Parser};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fs;
use std::path::Path;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Document {
pub frontmatter: HashMap<String, serde_yaml::Value>,
pub content: String,
pub permalink: String,
pub date: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct Page {
pub document: Document,
pub directory: String,
pub name: String,
pub url: String,
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,
}
#[inline(always)]
pub fn get_permalink(permalink: &str) -> String {
match &*permalink {
"date" => {
"/{{ page.document.frontmatter.collection }}/{{ page.year }}/{{ page.month }}/{{ page.day }}/{{ page.document.frontmatter.title }}.html".to_owned()
}
"pretty" => {
"/{{ page.document.frontmatter.collection }}/{{ page.year }}/{{ page.month }}/{{ page.day }}/{{ page.document.frontmatter.title }}.html".to_owned()
}
"ordinal" => {
"/{{ page.document.frontmatter.collection }}/{{ page.year }}/{{ page.y_day }}/{{ page.document.frontmatter.title }}.html"
.to_owned()
}
"weekdate" => {
"/{{ page.document.frontmatter.collection }}/{{ page.year }}/W{{ page.week }}/{{ page.short_day }}/{{ page.document.frontmatter.title }}.html".to_owned()
}
"none" => {
"/{{ page.document.frontmatter.collection }}/{{ page.document.frontmatter.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 permalink_string: String;
let date_string: String;
match permalink {
Some(_) => {
permalink_string = permalink.unwrap().as_str().unwrap().to_string();
}
None => {
permalink_string = "".to_owned();
}
}
match date {
Some(_) => {
date_string = date.unwrap().as_str().unwrap().to_string();
}
None => {
date_string = "".to_owned();
}
}
let document = Document {
frontmatter: serde_yaml::from_str(&split_page.0).unwrap(),
content: split_page.1,
permalink: permalink_string,
date: date_string,
};
let page_path_io = Path::new(&page_path[..]);
let mut page: Page;
match &document.date[..] {
"" => {
page = Page {
document,
directory: page_path_io.parent().unwrap().to_str().unwrap().to_owned(),
name: page_path_io
.file_stem()
.unwrap()
.to_str()
.unwrap()
.to_owned(),
url: "".to_owned(),
year: "".to_owned(),
short_year: "".to_owned(),
month: "".to_owned(),
i_month: "".to_owned(),
short_month: "".to_owned(),
long_month: "".to_owned(),
day: "".to_owned(),
i_day: "".to_owned(),
y_day: "".to_owned(),
w_year: "".to_owned(),
week: "".to_owned(),
w_day: "".to_owned(),
short_day: "".to_owned(),
long_day: "".to_owned(),
hour: "".to_owned(),
minute: "".to_owned(),
second: "".to_owned(),
};
}
_ => {
let datetime = DateTime::parse_from_rfc3339(date.unwrap().as_str().unwrap());
let global: HashMap<String, serde_yaml::Value> =
serde_yaml::from_str(&fs::read_to_string("./_global.yml").unwrap()).unwrap();
let locale: chrono::Locale =
chrono::Locale::try_from(&(global.get("locale").unwrap().as_str().unwrap()[..]))
.unwrap();
page = Page {
document,
directory: page_path_io.parent().unwrap().to_str().unwrap().to_owned(),
name: page_path_io
.file_stem()
.unwrap()
.to_str()
.unwrap()
.to_owned(),
url: "".to_owned(),
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)),
};
}
}
match &page.document.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>>,
snippet_context: Option<&HashMap<&str, serde_yaml::Value>>,
) -> Object {
let global: HashMap<String, serde_yaml::Value> =
serde_yaml::from_str(&fs::read_to_string("./_global.yml").unwrap()).unwrap();
let layout_name = page.document.frontmatter.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;
match snippet_context {
Some(_) => {
contexts = object!({
"global": global,
"page": page,
"layout": layout,
"collections": collections,
"snippet": snippet_context.unwrap()
});
}
None => {
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 = 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)
.build()
.unwrap()
.parse(text_to_render)
.unwrap();
template
.render(&get_contexts(page, collections, None))
.unwrap()
}
false => {
let template = 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)
.build()
.unwrap()
.parse(text_to_render)
.unwrap();
render_markdown(
template
.render(&get_contexts(page, collections, None))
.unwrap(),
)
}
}
}
#[inline(always)]
pub fn compile(
mut page: Page,
mut collections: HashMap<String, Vec<Page>>,
) -> (String, HashMap<String, Vec<Page>>) {
let compiled_page;
page.document.content = render_snippets(&page, &page.document.content, &collections);
let layout_name = &page.document.frontmatter.get("layout");
let collection_name = &page.document.frontmatter.get("collection");
match layout_name {
None => {
compiled_page = render(&page, &page.document.content, false, &collections);
}
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);
let layouts_and_snippets = render_snippets(&page, &layouts, &collections);
compiled_page = render(&page, &layouts_and_snippets, false, &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.document.frontmatter.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.document.content, true, collections);
}
}
rendered
}
#[inline]
pub fn render_snippets(
page: &Page,
text_to_parse: &str,
collections: &HashMap<String, Vec<Page>>,
) -> String {
let mut snippet_calls: Vec<String> = vec![];
let mut brace_count = 0;
let mut parsing_str: String = "".to_owned();
let mut parsed_str = text_to_parse.to_owned();
for character in text_to_parse.chars() {
match character {
'{' => {
if brace_count == 0 {
brace_count += 1;
parsing_str.push(character);
continue;
}
}
'}' => {
if brace_count == 1 {
brace_count = 0;
parsing_str.push(character);
if parsing_str.contains("{! snippet ") {
snippet_calls.push(parsing_str);
}
parsing_str = String::new();
continue;
}
}
_ => {
if brace_count == 1 {
parsing_str.push(character);
continue;
}
}
}
}
for snippet_call in &snippet_calls {
let call_portions = get_snippet_call_portions(snippet_call.to_owned());
let snippet_path = format!("./snippets/{}", call_portions[2]);
let keys = get_snippet_keys(&call_portions);
let values = get_snippet_values(&call_portions, &keys);
let mut snippet_context: HashMap<&str, serde_yaml::Value> = HashMap::new();
for i in 0..keys.len() {
snippet_context.insert(&keys[i], serde_yaml::from_str(&values[i]).unwrap());
}
parsed_str = parsed_str.replace(
snippet_call,
&render_snippet(page, snippet_path, &snippet_context, collections),
);
}
parsed_str
}
#[inline]
pub fn render_snippet(
page: &Page,
snippet_path: String,
snippet_context: &HashMap<&str, serde_yaml::Value>,
collections: &HashMap<String, Vec<Page>>,
) -> String {
let template = 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)
.build()
.unwrap()
.parse(&fs::read_to_string(snippet_path).unwrap())
.unwrap();
template
.render(&get_contexts(page, collections, Some(snippet_context)))
.unwrap()
}
#[inline]
pub fn get_snippet_call_portions(snippet_call: String) -> Vec<String> {
let mut call_portions: Vec<String> = vec![];
let mut current_argument: String = "".to_owned();
for character in snippet_call.chars() {
match character {
' ' => {
call_portions.push(current_argument);
current_argument = String::new();
continue;
}
_ => {
current_argument.push(character);
continue;
}
}
}
call_portions
}
#[inline]
pub fn get_snippet_keys(call_portions: &[String]) -> Vec<String> {
let mut keys: Vec<String> = vec![];
let mut current_key: String = "".to_owned();
for call_argument in call_portions.iter().skip(3) {
for character in call_argument.chars() {
match character {
'=' => {
keys.push(current_key);
current_key = String::new();
break;
}
_ => {
current_key.push(character);
continue;
}
}
}
}
keys
}
#[inline]
pub fn get_snippet_values(call_portions: &[String], keys: &[String]) -> Vec<String> {
let mut values: Vec<String> = vec![];
let mut current_value: String = "".to_owned();
let mut portions_by_space: Vec<usize> = vec![];
for i in 0..keys.len() {
if portions_by_space.contains(&(i + 3)) {
continue;
}
current_value = format!("{}{}", current_value, call_portions[i + 3]);
current_value = current_value.replace(&format!("{}=", &keys[i]), "");
let start_of_current_value = current_value.chars().next().unwrap();
if start_of_current_value == '"' {
for (j, _) in call_portions.iter().enumerate().skip(i + 4) {
if call_portions[j].contains('=') {
portions_by_space.push(j);
break;
} else {
current_value = format!("{} {}", current_value, call_portions[j]);
continue;
}
}
}
let end_of_current_value = current_value
.chars()
.nth(current_value.chars().count() - 1)
.unwrap();
if start_of_current_value == '"' && end_of_current_value == '"' {
current_value.remove(0);
current_value.remove(current_value.len() - 1);
}
values.push(current_value);
current_value = String::new();
}
values
}
#[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
}