Skip to main content

max / makenotwork

3.7 KB · 119 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 docengine::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!(
27 "usage: substitute_dir <assumptions.toml> <docs_root> [--check]"
28 );
29 return ExitCode::from(2);
30 }
31 let assumptions_path = PathBuf::from(&args[0]);
32 let root = PathBuf::from(&args[1]);
33 let check_only = args.iter().any(|a| a == "--check");
34
35 let assumptions = match Assumptions::load(&assumptions_path) {
36 Ok(a) => a,
37 Err(e) => {
38 eprintln!("load {}: {e}", assumptions_path.display());
39 return ExitCode::from(2);
40 }
41 };
42 if let Err(e) = assumptions.validate() {
43 eprintln!("validate {}: {e}", assumptions_path.display());
44 return ExitCode::from(2);
45 }
46
47 let mut files = Vec::new();
48 if let Err(e) = collect_markdown(&root, &mut files) {
49 eprintln!("walk {}: {e}", root.display());
50 return ExitCode::from(2);
51 }
52
53 let mut changed: Vec<PathBuf> = Vec::new();
54 let mut errors: Vec<(PathBuf, String)> = Vec::new();
55 for path in &files {
56 let body = match fs::read_to_string(path) {
57 Ok(s) => s,
58 Err(e) => {
59 errors.push((path.clone(), format!("read: {e}")));
60 continue;
61 }
62 };
63 match assumptions.substitute(&body) {
64 Ok(resolved) if resolved != body => {
65 if check_only {
66 changed.push(path.clone());
67 } else if let Err(e) = fs::write(path, &resolved) {
68 errors.push((path.clone(), format!("write: {e}")));
69 } else {
70 changed.push(path.clone());
71 }
72 }
73 Ok(_) => {}
74 Err(e) => errors.push((path.clone(), e.to_string())),
75 }
76 }
77
78 println!(
79 "scanned {} markdown files under {}",
80 files.len(),
81 root.display()
82 );
83 if !changed.is_empty() {
84 println!(
85 "{} {} file(s):",
86 if check_only { "would change" } else { "wrote" },
87 changed.len()
88 );
89 for p in &changed {
90 println!(" {}", p.display());
91 }
92 }
93 if !errors.is_empty() {
94 eprintln!("{} error(s):", errors.len());
95 for (p, e) in &errors {
96 eprintln!(" {}: {e}", p.display());
97 }
98 return ExitCode::from(1);
99 }
100 if check_only && !changed.is_empty() {
101 return ExitCode::from(1);
102 }
103 ExitCode::SUCCESS
104 }
105
106 fn collect_markdown(dir: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
107 for entry in fs::read_dir(dir)? {
108 let entry = entry?;
109 let path = entry.path();
110 let ft = entry.file_type()?;
111 if ft.is_dir() {
112 collect_markdown(&path, out)?;
113 } else if ft.is_file() && path.extension().is_some_and(|e| e == "md") {
114 out.push(path);
115 }
116 }
117 Ok(())
118 }
119