krabka_docgen/
snippets.rs1use std::{fs, path::Path};
7
8pub fn extract(source: &str, anchor: &str) -> Result<String, String> {
16 let begin = format!("// docs:begin {anchor}");
17 let end = format!("// docs:end {anchor}");
18 let mut lines = Vec::new();
19 let mut inside = false;
20 let mut completed = false;
21 for line in source.lines() {
22 if line.trim() == begin {
23 if inside || completed {
24 return Err(format!("anchor {anchor}: repeated begin marker"));
25 }
26 inside = true;
27 continue;
28 }
29 if line.trim() == end {
30 if !inside {
31 return Err(format!("anchor {anchor}: end before begin"));
32 }
33 inside = false;
34 completed = true;
35 continue;
36 }
37 if inside {
38 lines.push(line.to_owned());
39 }
40 }
41 if !completed || inside {
42 return Err(format!("anchor {anchor}: markers not found in source"));
43 }
44 let indent = lines
45 .iter()
46 .filter(|line| !line.trim().is_empty())
47 .map(|line| line.len() - line.trim_start_matches([' ', '\t']).len())
48 .min()
49 .unwrap_or(0);
50 Ok(lines
51 .iter()
52 .map(|line| line.get(indent..).unwrap_or(line))
53 .collect::<Vec<_>>()
54 .join("\n"))
55}
56
57pub fn sync_markdown(markdown: &str, crates_dir: &Path) -> Result<String, String> {
65 let mut out = String::new();
66 let mut rest = markdown;
67 while let Some(start) = rest.find("<!-- snippet:") {
68 let (head, after) = rest.split_at(start);
69 out.push_str(head);
70 let (directive, body_start) = after
71 .split_once("-->")
72 .ok_or("unterminated snippet directive")?;
73 let spec = directive.trim_start_matches("<!-- snippet:").trim();
74 let (relpath, anchor) = spec
75 .split_once('#')
76 .ok_or_else(|| format!("snippet directive missing '#': {spec}"))?;
77 let end_marker = "<!-- /snippet -->";
78 let (_, after_body) = body_start
79 .split_once(end_marker)
80 .ok_or("missing <!-- /snippet -->")?;
81 let source = fs::read_to_string(crates_dir.join(relpath.trim()))
82 .map_err(|e| format!("read {relpath}: {e}"))?;
83 let code = extract(&source, anchor.trim())?;
84 out.push_str(directive);
85 out.push_str("-->\n");
86 out.push_str("```rust\n");
87 out.push_str(&code);
88 out.push_str("\n```\n");
89 out.push_str(end_marker);
90 rest = after_body;
91 }
92 out.push_str(rest);
93 Ok(out)
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn extract_trims_indent_and_markers() {
102 let src = "fn main() {\n // docs:begin foo\n let x = 1;\n // docs:end foo\n}\n";
103 assert2::assert!(extract(src, "foo").unwrap() == "let x = 1;");
104 }
105
106 #[test]
107 fn extract_uses_the_unique_exact_comment_marker_pair() {
108 let source = "// docs:begin foobar\nwrong();\n// docs:end foobar\n\
109 // docs:begin foo\nright();\n// docs:end foo\n";
110 assert2::assert!(extract(source, "foo").unwrap() == "right();");
111 }
112
113 #[yare::parameterized(
114 wrong_marker_name = { "// docs:begin foobar\nwrong();\n// docs:end foobar\n" },
115 marker_text_in_strings = {
116 "let s = \"// docs:begin foo\";\nwrong();\nlet e = \"// docs:end foo\";\n",
117 },
118 duplicate_begin = { "// docs:begin foo\n// docs:begin foo\n// docs:end foo\n" },
119 duplicate_pairs = {
120 "// docs:begin foo\none();\n// docs:end foo\n// docs:begin foo\ntwo();\n// docs:end foo\n",
121 },
122 reversed_markers = { "// docs:end foo\n// docs:begin foo\n" },
123 )]
124 fn extract_requires_an_exact_comment_marker_and_unique_pair(invalid: &str) {
125 assert2::assert!(extract(invalid, "foo").is_err());
126 }
127
128 #[test]
129 fn sync_is_idempotent() {
130 let dir = tempfile::tempdir().unwrap();
131 fs::create_dir_all(dir.path().join("c/examples")).unwrap();
132 fs::write(
133 dir.path().join("c/examples/e.rs"),
134 "// docs:begin a\nlet y = 2;\n// docs:end a\n",
135 )
136 .unwrap();
137 let md = "intro\n<!-- snippet: c/examples/e.rs#a -->\nOLD\n<!-- /snippet -->\nend\n";
138 let once = sync_markdown(md, dir.path()).unwrap();
139 let twice = sync_markdown(&once, dir.path()).unwrap();
140 assert2::assert!(once == twice);
141 assert2::assert!(once.contains("```rust\nlet y = 2;\n```"));
142 assert2::assert!(!once.contains("OLD"));
143 }
144}