Skip to main content

max / makenotwork

3.7 KB · 117 lines History Blame Raw
1 //! Apply `Assumptions::substitute` to every markdown file under a
2 //! directory tree. Intended for the `_private/docs/` mirror, which
3 //! references business numbers but isn't part of the public site-docs
4 //! pipeline that runs substitution at boot.
5 //!
6 //! Usage:
7 //!
8 //! substitute_dir <assumptions.toml> <docs_root> [--check]
9 //!
10 //! --check Don't write; exit non-zero if any file would change or
11 //! any placeholder failed to resolve.
12 //!
13 //! Code spans / fenced code blocks are already preserved by
14 //! `Assumptions::substitute` (they hold syntax examples, not live
15 //! placeholders), so docs that only mention `{{ derived.X }}` in
16 //! backticks come through unchanged.
17
18 use mnw_assumptions::Assumptions;
19 use std::fs;
20 use std::path::{Path, PathBuf};
21 use std::process::ExitCode;
22
23 fn main() -> ExitCode {
24 let args: Vec<String> = std::env::args().skip(1).collect();
25 if args.len() < 2 || args.iter().any(|a| a == "-h" || a == "--help") {
26 eprintln!("usage: substitute_dir <assumptions.toml> <docs_root> [--check]");
27 return ExitCode::from(2);
28 }
29 let assumptions_path = PathBuf::from(&args[0]);
30 let root = PathBuf::from(&args[1]);
31 let check_only = args.iter().any(|a| a == "--check");
32
33 let assumptions = match Assumptions::load(&assumptions_path) {
34 Ok(a) => a,
35 Err(e) => {
36 eprintln!("load {}: {e}", assumptions_path.display());
37 return ExitCode::from(2);
38 }
39 };
40 if let Err(e) = assumptions.validate() {
41 eprintln!("validate {}: {e}", assumptions_path.display());
42 return ExitCode::from(2);
43 }
44
45 let mut files = Vec::new();
46 if let Err(e) = collect_markdown(&root, &mut files) {
47 eprintln!("walk {}: {e}", root.display());
48 return ExitCode::from(2);
49 }
50
51 let mut changed: Vec<PathBuf> = Vec::new();
52 let mut errors: Vec<(PathBuf, String)> = Vec::new();
53 for path in &files {
54 let body = match fs::read_to_string(path) {
55 Ok(s) => s,
56 Err(e) => {
57 errors.push((path.clone(), format!("read: {e}")));
58 continue;
59 }
60 };
61 match assumptions.substitute(&body) {
62 Ok(resolved) if resolved != body => {
63 if check_only {
64 changed.push(path.clone());
65 } else if let Err(e) = fs::write(path, &resolved) {
66 errors.push((path.clone(), format!("write: {e}")));
67 } else {
68 changed.push(path.clone());
69 }
70 }
71 Ok(_) => {}
72 Err(e) => errors.push((path.clone(), e.to_string())),
73 }
74 }
75
76 println!(
77 "scanned {} markdown files under {}",
78 files.len(),
79 root.display()
80 );
81 if !changed.is_empty() {
82 println!(
83 "{} {} file(s):",
84 if check_only { "would change" } else { "wrote" },
85 changed.len()
86 );
87 for p in &changed {
88 println!(" {}", p.display());
89 }
90 }
91 if !errors.is_empty() {
92 eprintln!("{} error(s):", errors.len());
93 for (p, e) in &errors {
94 eprintln!(" {}: {e}", p.display());
95 }
96 return ExitCode::from(1);
97 }
98 if check_only && !changed.is_empty() {
99 return ExitCode::from(1);
100 }
101 ExitCode::SUCCESS
102 }
103
104 fn collect_markdown(dir: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
105 for entry in fs::read_dir(dir)? {
106 let entry = entry?;
107 let path = entry.path();
108 let ft = entry.file_type()?;
109 if ft.is_dir() {
110 collect_markdown(&path, out)?;
111 } else if ft.is_file() && path.extension().is_some_and(|e| e == "md") {
112 out.push(path);
113 }
114 }
115 Ok(())
116 }
117