This commit is contained in:
Nat 2023-08-29 08:45:14 -07:00
commit b2e17324a3
Signed by: nat
GPG Key ID: B53AB05285D710D6
4 changed files with 1372 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/target
template.liquid
build
pages

1081
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

11
Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "calathea"
version = "1.0.0-beta1"
edition = "2021"
[dependencies]
comrak = "0.18.0"
liquid = "0.26.4"
regex = "1.9.3"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8.7"

275
src/main.rs Normal file
View File

@ -0,0 +1,275 @@
extern crate serde_yaml;
use std::env;
use std::path::Path;
use std::process;
use std::fs;
use std::collections::HashMap;
use regex::{Regex, RegexBuilder, Captures};
use comrak::{markdown_to_html, ComrakOptions};
use serde::{Serialize, Deserialize};
const VERSION: &str = env!("CARGO_PKG_VERSION");
// The frontmatter as it is read directly from the file
#[derive(Serialize, Deserialize, Debug)]
struct Frontmatter {
title: String,
permalink: Option<String>,
data: Option<serde_yaml::Value>
}
// The struct representing the page, as constructed from the frontmatter
#[derive(Clone, Serialize)]
struct Page {
title: String,
permalink: String,
data: Option<serde_yaml::Value>,
}
fn print_help() {
println!("
Usage: calathea [OPTION...]
-o, --output=dir Output directory (default: './build')
-s, --src=dir Source directory of pages (default: './pages')
-t, --template=file Template file path (default: './template.html')
-?,, -h, --help Give this help list
-v, --version Print the version
Mandatory or optional arguments to long options are also mandatory or optional
for any corresponding short options.
");
process::exit(0);
}
fn main() {
let args: Vec<String> = env::args().collect();
let mut output_dir = Path::new("./build");
let mut template_path = Path::new("./template.liquid");
let mut src_dir = Path::new("./pages");
let mut i = 1;
while i < args.len() {
let mut missing_argument = false;
let (option, arg) = match args[i].split_once("=") {
Some((o, a)) => (o, Some(a)),
None => (args[i].as_str(), None)
};
match option {
"-o" | "--output" => match arg {
Some(v) => output_dir = Path::new(v),
None => missing_argument = true
}
"-s" | "--src" => match arg {
Some(v) => src_dir = Path::new(v),
None => missing_argument = true
}
"-t" | "--template" => match arg {
Some(v) => template_path = Path::new(v),
None => missing_argument = true
}
"-v" | "--version" => {
println!("{}", VERSION);
process::exit(0);
}
"-?" | "-h" | "--help" => print_help(),
_ => {
println!("calathea: unknown option: {option}");
process::exit(1);
}
};
if missing_argument {
println!("calatahea: option '{option}' requires an argument");
process::exit(1);
};
i += 1;
}
// Make these fellas immutable since we won't be changing them again
let output_dir = output_dir;
let template_path = template_path;
let src_dir = src_dir;
// Read the template
let template = match fs::read_to_string(template_path) {
Ok(content) => content,
Err(e) => {
println!("calathea: unable to read '{}': {e}", template_path.display());
process::exit(1);
}
};
let mut page_map: HashMap<String, Page> = HashMap::new();
let mut content_map: HashMap<String, String> = HashMap::new();
let mut incoming_map: HashMap<String, Vec<Page>> = HashMap::new();
// Pull the source files into memory
let src_paths = match fs::read_dir(src_dir) {
Ok(p) => p,
Err(e) => {
println!("calathea: error while opening source directory '{}': {}", src_dir.display(), e);
process::exit(1);
}
};
for result in src_paths {
let entry = match result {
Ok(v) => v,
Err(e) => {
// "if theres some sort of intermittent IO error during iteration"
// https://doc.rust-lang.org/std/fs/struct.ReadDir.html
println!("calathea: error while reading source directory: {e}");
process::exit(1);
}
};
let file_name = match entry.path().as_path().file_stem() {
Some(p) => match p.to_str() {
Some(stem) => stem,
// The file has no name and should probably be skipped
None => continue,
}.to_string(),
// In the case it is a directory, for example
None => continue,
};
let file_content = match fs::read_to_string(entry.path()) {
Ok(c) => c,
Err(e) => {
println!("calathea: error while reading file '{}': {}. Skipping...", entry.path().display(), e);
continue;
},
};
let fm_re = RegexBuilder::new(r"---\n(?<frontmatter>.+)\n---")
.dot_matches_new_line(true)
.build()
.unwrap();
let yaml_frontmatter = match fm_re.captures(&file_content) {
Some(c) => c.name("frontmatter").map_or("", |m| m.as_str()),
None => {
println!("calathea: error while reading file '{}': Frontmatter not recognized", entry.path().display());
process::exit(1);
},
};
let parsed_frontmatter: Frontmatter = match serde_yaml::from_str(&yaml_frontmatter) {
Ok(f) => f,
Err(e) => {
println!("calathea: error while parsing frontmatter of file '{}': {}", entry.path().display(), e);
process::exit(1);
},
};
let page = Page {
title: parsed_frontmatter.title,
permalink: match parsed_frontmatter.permalink {
Some(p) => p,
None => format!("{file_name}.html")
},
data: parsed_frontmatter.data
};
if page_map.contains_key(&page.title) {
println!("calathea: duplicate title '{}'. Page titles must be unique.", page.title);
process::exit(1);
}
content_map.insert(page.title.clone(), file_content);
incoming_map.insert(page.title.clone(), Vec::new());
page_map.insert(page.title.clone(), page);
}
// Scan for and render wikilinks
let link_re = Regex::new(r"\[\[(?<link>[^\[\]]+)\]\]").unwrap();
for page in page_map.values() {
let original_content = content_map.get(&page.title).unwrap();
for cap in link_re.captures_iter(original_content.as_str()) {
let name = String::from(cap.name("link").unwrap().as_str());
let incoming_pages = incoming_map.get_mut(&name).unwrap();
incoming_pages.push(page.clone());
}
content_map.insert(page.title.clone(), link_re.replace_all(original_content.as_str(), |caps: &Captures| {
let permalink = page_map.get(&caps["link"])
.unwrap()
.permalink.as_str();
format!("<a href='{}'>{}</a>", permalink, &caps["link"])
}).to_string());
}
// This gives us a clean slate to build the wiki
let _ = fs::remove_dir_all(output_dir);
let _ = fs::create_dir_all(output_dir);
// Render the pages
let mut md_options = ComrakOptions::default();
md_options.render.unsafe_ = true;
md_options.extension.strikethrough = true;
md_options.extension.footnotes = true;
md_options.extension.front_matter_delimiter = Some("---".to_owned());
let liquid_parser = liquid::ParserBuilder::with_stdlib()
.build().expect("calathea: failed to build liquid parser");
let liquid_template = match liquid_parser.parse(template.as_str()) {
Ok(t) => t,
Err(e) => {
println!("calathea: failed to parse template '{}': {}", template_path.display(), e);
process::exit(1);
}
};
for page in page_map.values() {
let incoming_pages: &Vec<Page> = incoming_map.get(&page.title).unwrap();
let content = content_map.get(&page.title).unwrap();
let globals = liquid::object!({
"content": markdown_to_html(&content, &md_options),
"incoming": incoming_pages,
"title": page.title,
"permalink": page.permalink,
"data": page.data,
});
let output = match liquid_template.render(&globals) {
Ok(o) => o,
Err(e) => {
println!("calathea: failed to render liquid templates for '{}': {}", page.permalink, e);
process::exit(1);
}
};
match fs::write(output_dir.join(&page.permalink).with_extension("html"), output) {
Ok(()) => {},
Err(e) => {
println!("calathea: failed to write to '{}': {}", page.permalink, e);
process::exit(1);
}
}
}
println!("Pages successfully generated in {}", output_dir.display());
}