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