#! /usr/bin/env rust-script //! ```cargo //! [dependencies] //! chrono = "0.4.26" //! maud = "0.25" //! regex = "1.7.0" //! ``` fn main() -> Result<(), Box> { let before = Instant::now(); struct Article { name: String, content: String, created: DateTime, modified: DateTime } let mut articles: Vec
= Default::default(); let mut exist = HashSet::new(); let mut backlinks = HashMap::>::new(); let relink = Regex::new(r#"([^@]|^)@([[:alnum:]]+)"#)?; let escape = Regex::new(r#"@@"#)?; for file in read_dir("articles")? { let file = file?; let name = file.file_name().into_string().map_err(|_| "Filename not convertible to string")?; let metadata = file.metadata()?; let mut article = Article { name: name.clone(), content: read_to_string(file.path())?, created: Utc.timestamp_millis_opt(metadata.created()?.duration_since(UNIX_EPOCH)?.as_millis() as i64).unwrap(), modified: Utc.timestamp_millis_opt(metadata.modified()?.duration_since(UNIX_EPOCH)?.as_millis() as i64).unwrap(), }; for caps in relink.captures_iter(&article.content).map(|x| x[2].to_string()) { backlinks.entry(caps) .or_insert_with(Default::default) .insert(name.clone()); } let path: PathBuf = ["creation", &name].iter().collect(); let time = match read_to_string(&path) { Ok(time) => time, Err(err) => { if err.kind() == ErrorKind::PermissionDenied { panic!("No permission!"); } let time = article.created.to_rfc2822(); write(&path, &time).unwrap(); time } }; article.created = DateTime::parse_from_rfc2822(time.trim_end()).unwrap().with_timezone(&Utc); articles.push(article); exist.insert(name); } articles.sort_by_key(|x| Reverse(x.created)); let index = html! { @for x in &articles { @if x.name != "index" { div class="index-entry" { a class="index-link" href=(x.name) { (x.name) } span class="index-created" { " " (x.created.format("%a %d %b %Y")) } } } } }.into_string(); let commands = Regex::new(r#"([^@]|^)@\("#)?; let indices = Regex::new(r#"([^@]|^)@\*"#)?; for x in &mut articles { while let Some(pos) = commands.find(&x.content) { let begin = pos.start() + (pos.end() - pos.start()); let mut end = begin; let mut depth: u32 = 1; for byte in &x.content.as_bytes()[end..] { end += 1; match byte { b'(' => { depth += 1; } b')' => { depth = depth.saturating_sub(1); if depth == 0 { break; } } _ => {} } } let matched = std::str::from_utf8(&x.content.as_bytes()[begin..end - 1]).unwrap().to_string(); let mut c = Command::new("bash"); let proc = c.arg("-c").arg(&matched); let stdout = proc.output().unwrap().stdout; x.content.replace_range(begin - 2..end, &String::from_utf8_lossy(&stdout).to_owned()); if !proc.status()?.success() { panic!("Command did not finish successfully: {}", matched); } } x.content = escape .replace_all( &relink.replace_all(&indices.replace_all(&x.content, &index).to_string(), r#"$1$2"#), "@", ) .to_string(); write(&x.name, html! { (DOCTYPE) html { (if x.name == "index" { head(&read_to_string(&"x/title")?) } else { head(&x.name) }) body { div class="content" { (PreEscaped(&x.content)) } hr; div class="footer" { p { "Created: " (x.created) ", modified: " (x.modified) } p { a href="index" { "Home" } } "Backlinks:" br; @if backlinks.get(&x.name).map(BTreeSet::len).unwrap_or(0) == 0 { "(None)" } @for x in backlinks.get(&x.name).unwrap_or(&Default::default()) { a href=(x) { (x) } br; } } } } }.into_string()).unwrap(); } for k in backlinks.keys() { if !exist.contains(k) { println!("Missing: {}", k); } } for file in read_dir(".").unwrap() { let file = file.unwrap(); let name = file .file_name() .into_string() .map_err(|_| "Filename not convertible to string")?; if file.metadata().unwrap().is_file() && !exist.contains(&name) && name != ".gitignore" && name != "flake.lock" && name != "flake.nix" && name != "generate" && name != "regenerate" && name != "deploy" { println!("Removing file: {}", name); remove_file(file.path()).unwrap(); } } for file in read_dir("creation").unwrap() { let file = file.unwrap(); let name = file .file_name() .into_string() .map_err(|_| "Filename not convertible to string").unwrap(); if file.metadata().unwrap().is_file() && !exist.contains(&name) { println!("Removing file: creation/{}", name); remove_file(file.path()).unwrap(); } } let after = Instant::now(); println!("Done! (took {:?})", after.duration_since(before)); Ok(()) } fn head(title: &str) -> PreEscaped { html! { head { meta charset="UTF-8"; meta name="viewport" content="width=device-width,initial-scale=1"; link rel="icon" type="image/png" href="x/favicon.png"; link rel="icon" type="image/png" href="x/favicon16.png"; link rel="icon" type="image/png" href="x/favicon32.png"; link rel="icon" type="image/png" href="x/favicon64.png"; link rel="stylesheet" type="text/css" href="x/main.css"; link rel="stylesheet" type="text/css" href="x/pandoc1.css"; title { (title) } } } } use { chrono::{DateTime, TimeZone, Utc}, maud::{html, PreEscaped, DOCTYPE}, regex::Regex, std::{ cmp::Reverse, collections::{BTreeSet, HashMap, HashSet}, error::Error, fs::{read_dir, read_to_string, remove_file, write}, io::ErrorKind, path::{PathBuf}, process::Command, time::{Instant, UNIX_EPOCH}, }, };