init
This commit is contained in:
commit
b2e17324a3
|
@ -0,0 +1,5 @@
|
||||||
|
/target
|
||||||
|
|
||||||
|
template.liquid
|
||||||
|
build
|
||||||
|
pages
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
|
@ -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 there’s 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());
|
||||||
|
}
|
Loading…
Reference in New Issue