Skip to main content

max / docengine

clear clippy warnings and export PreProcessor Add PreProcessor to the doc_loader re-exports so consumers can name the type. Convert format_push_string sites to write!, generalise resolve_mentions and post_process_quotes over BuildHasher, and take the machine-applicable lints. Fix an intra-doc link left pointing at a type that moved to mnw-assumptions.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-01 21:28 UTC
Signed with PGP, not checked
Commit: 324a392214649d84edbb226e993ef1588e48d2e9
Parent: 1ce9df5
6 files changed, +51 insertions, -35 deletions
@@ -8,6 +8,7 @@
8 8 //! **Code tabs:** `> [!TABS]` followed by fenced code blocks become a tabbed
9 9 //! interface with language-labelled tabs.
10 10
11 + use std::fmt::Write as _;
11 12 use std::sync::LazyLock;
12 13
13 14 /// Matches any `[!TYPE]` alert marker inside a blockquote paragraph.
@@ -73,12 +74,13 @@
73 74 let after_marker = &remaining[(bq_pos + marker_end)..close_pos];
74 75 let caption = strip_html_tags_simple(after_marker).trim().to_string();
75 76
76 - result.push_str(&format!("<figure class=\"doc-ui\" data-ui=\"{name}\">"));
77 - result.push_str(&format!(
77 + let _ = write!(result, "<figure class=\"doc-ui\" data-ui=\"{name}\">");
78 + let _ = write!(
79 + result,
78 80 "<div class=\"doc-ui-frame\" data-ui=\"{name}\"></div>"
79 - ));
81 + );
80 82 if !caption.is_empty() {
81 - result.push_str(&format!("<figcaption>{caption}</figcaption>"));
83 + let _ = write!(result, "<figcaption>{caption}</figcaption>");
82 84 }
83 85 result.push_str("</figure>");
84 86 }
@@ -248,18 +250,20 @@
248 250 for (i, (lang, _)) in tabs.iter().enumerate() {
249 251 let active = if i == 0 { " active" } else { "" };
250 252 let label = code_language_label(lang);
251 - html.push_str(&format!(
253 + let _ = write!(
254 + html,
252 255 "<button class=\"code-tab{active}\" data-tab-index=\"{i}\">{label}</button>"
253 - ));
256 + );
254 257 }
255 258
256 259 html.push_str("</div>\n");
257 260
258 261 for (i, (_, block)) in tabs.iter().enumerate() {
259 262 let active = if i == 0 { " active" } else { "" };
260 - html.push_str(&format!(
261 - "<div class=\"code-tab-panel{active}\" data-tab-index=\"{i}\">{block}</div>\n"
262 - ));
263 + let _ = writeln!(
264 + html,
265 + "<div class=\"code-tab-panel{active}\" data-tab-index=\"{i}\">{block}</div>"
266 + );
263 267 }
264 268
265 269 html.push_str("</div>");
@@ -56,7 +56,7 @@
56 56 pub examples_path: Option<std::path::PathBuf>,
57 57 /// Optional pre-processor applied to raw markdown before link rewriting.
58 58 /// On `Err`, the page is skipped with a warning. Use to wire
59 - /// [`crate::Assumptions::substitute`] or a similar transform.
59 + /// an assumptions substitution pass or a similar transform.
60 60 pub pre_process: Option<PreProcessor>,
61 61 }
62 62
@@ -160,11 +160,11 @@
160 160 };
161 161
162 162 let mut entries: Vec<_> = read_dir
163 - .filter_map(|e| e.ok())
164 - .filter(|e| e.path().extension().map(|ext| ext == "md").unwrap_or(false))
163 + .filter_map(std::result::Result::ok)
164 + .filter(|e| e.path().extension().is_some_and(|ext| ext == "md"))
165 165 .collect();
166 166
167 - entries.sort_by_key(|e| e.file_name());
167 + entries.sort_by_key(std::fs::DirEntry::file_name);
168 168
169 169 for entry in entries {
170 170 let path = entry.path();
@@ -174,9 +174,8 @@
174 174 .unwrap_or_default()
175 175 .to_string();
176 176
177 - let raw_md = match std::fs::read_to_string(&path) {
178 - Ok(content) => content,
179 - Err(_) => continue,
177 + let Ok(raw_md) = std::fs::read_to_string(&path) else {
178 + continue;
180 179 };
181 180
182 181 let raw_md = match &config.pre_process {
M src/lib.rs +1 -1
@@ -45,7 +45,7 @@
45 45 pub use directives::post_process_directives;
46 46 #[cfg(feature = "doc-loader")]
47 47 pub use doc_loader::{
48 - DocIndexEntry, DocLoader, DocLoaderConfig, DocPage, DocSearchEntry, SlugCollision,
48 + DocIndexEntry, DocLoader, DocLoaderConfig, DocPage, DocSearchEntry, PreProcessor, SlugCollision,
49 49 };
50 50 #[cfg(feature = "frontmatter")]
51 51 pub use frontmatter::{Frontmatter, parse_frontmatter};
@@ -54,7 +54,7 @@
54 54 return caps[0].to_string();
55 55 }
56 56
57 - format!("![{}]({}/{}/media/{})", alt, cdn_base, user_id, path)
57 + format!("![{alt}]({cdn_base}/{user_id}/media/{path})")
58 58 })
59 59 .into_owned()
60 60 }
@@ -70,7 +70,7 @@
70 70 let after_src = &caps[3];
71 71
72 72 // Extract alt text if present
73 - let attrs = format!("{}{}", before_src, after_src);
73 + let attrs = format!("{before_src}{after_src}");
74 74 let alt = ALT_RE
75 75 .captures(&attrs)
76 76 .map(|c| c[1].to_string())
@@ -83,8 +83,7 @@
83 83 let src = crate::escape::html_escape(src);
84 84 if alt.is_empty() {
85 85 format!(
86 - r#"<video controls src="{}">Your browser does not support video.</video>"#,
87 - src
86 + r#"<video controls src="{src}">Your browser does not support video.</video>"#
88 87 )
89 88 } else {
90 89 format!(
M src/mentions.rs +20 -8
@@ -26,9 +26,9 @@
26 26 /// `/p/my-community/u/{username}` becomes `/p/my-community/u/alice`.
27 27 ///
28 28 /// Unknown usernames are left as plain text.
29 - pub fn resolve_mentions(
29 + pub fn resolve_mentions<S: std::hash::BuildHasher>(
30 30 input: &str,
31 - valid_usernames: &HashSet<String>,
31 + valid_usernames: &HashSet<String, S>,
32 32 url_template: &str,
33 33 ) -> String {
34 34 static MENTION_RE: std::sync::LazyLock<regex_lite::Regex> =
@@ -59,9 +59,9 @@
59 59 result
60 60 }
61 61
62 - fn replace_mentions(
62 + fn replace_mentions<S: std::hash::BuildHasher>(
63 63 text: &str,
64 - valid_usernames: &HashSet<String>,
64 + valid_usernames: &HashSet<String, S>,
65 65 url_template: &str,
66 66 re: &regex_lite::Regex,
67 67 ) -> String {
@@ -119,7 +119,10 @@
119 119
120 120 #[test]
121 121 fn resolve_valid_replaced() {
122 - let valid: HashSet<String> = ["alice"].iter().map(|s| s.to_string()).collect();
122 + let valid: HashSet<String> = ["alice"]
123 + .iter()
124 + .map(std::string::ToString::to_string)
125 + .collect();
123 126 let result = resolve_mentions("Hello @alice!", &valid, "/p/test-community/u/{username}");
124 127 assert_eq!(result, "Hello [@alice](/p/test-community/u/alice)!");
125 128 }
@@ -133,21 +136,30 @@
133 136
134 137 #[test]
135 138 fn resolve_in_code_not_replaced() {
136 - let valid: HashSet<String> = ["alice"].iter().map(|s| s.to_string()).collect();
139 + let valid: HashSet<String> = ["alice"]
140 + .iter()
141 + .map(std::string::ToString::to_string)
142 + .collect();
137 143 let result = resolve_mentions("Use `@alice` in code", &valid, "/u/{username}");
138 144 assert_eq!(result, "Use `@alice` in code");
139 145 }
140 146
141 147 #[test]
142 148 fn resolve_mixed_valid_invalid() {
143 - let valid: HashSet<String> = ["alice"].iter().map(|s| s.to_string()).collect();
149 + let valid: HashSet<String> = ["alice"]
150 + .iter()
151 + .map(std::string::ToString::to_string)
152 + .collect();
144 153 let result = resolve_mentions("@alice and @unknown", &valid, "/p/slug/u/{username}");
145 154 assert_eq!(result, "[@alice](/p/slug/u/alice) and @unknown");
146 155 }
147 156
148 157 #[test]
149 158 fn resolve_custom_url_template() {
150 - let valid: HashSet<String> = ["bob"].iter().map(|s| s.to_string()).collect();
159 + let valid: HashSet<String> = ["bob"]
160 + .iter()
161 + .map(std::string::ToString::to_string)
162 + .collect();
151 163 let result = resolve_mentions("Hi @bob", &valid, "/users/{username}/profile");
152 164 assert_eq!(result, "Hi [@bob](/users/bob/profile)");
153 165 }
M src/quotes.rs +8 -6
@@ -11,7 +11,10 @@
11 11
12 12 /// Post-process rendered HTML to replace `[quote:POST_ID:HASH]` markers with
13 13 /// clickable author attribution.
14 - pub fn post_process_quotes(html: &str, quote_authors: &HashMap<uuid::Uuid, QuoteAuthor>) -> String {
14 + pub fn post_process_quotes<S: std::hash::BuildHasher>(
15 + html: &str,
16 + quote_authors: &HashMap<uuid::Uuid, QuoteAuthor, S>,
17 + ) -> String {
15 18 static QUOTE_RE: std::sync::LazyLock<regex_lite::Regex> = std::sync::LazyLock::new(|| {
16 19 regex_lite::Regex::new(r"\[quote:([0-9a-f\-]{36}):([0-9a-f]{8})\]").unwrap()
17 20 });
@@ -25,8 +28,7 @@
25 28 if let Some(author) = resolved {
26 29 if author.is_removed {
27 30 format!(
28 - "<cite class=\"quote-attribution\"><a href=\"#post-{}\">(original post removed)</a></cite>",
29 - post_id_str
31 + "<cite class=\"quote-attribution\"><a href=\"#post-{post_id_str}\">(original post removed)</a></cite>"
30 32 )
31 33 } else {
32 34 format!(
@@ -59,7 +61,7 @@
59 61 is_removed: false,
60 62 },
61 63 );
62 - let input = format!("[quote:{}:abcd1234]", post_id);
64 + let input = format!("[quote:{post_id}:abcd1234]");
63 65 let result = post_process_quotes(&input, &authors);
64 66 assert!(result.contains("Alice Smith"));
65 67 assert!(result.contains("@alice"));
@@ -78,7 +80,7 @@
78 80 is_removed: true,
79 81 },
80 82 );
81 - let input = format!("[quote:{}:abcd1234]", post_id);
83 + let input = format!("[quote:{post_id}:abcd1234]");
82 84 let result = post_process_quotes(&input, &authors);
83 85 assert!(result.contains("original post removed"));
84 86 assert!(!result.contains("Bob"));
@@ -112,7 +114,7 @@
112 114 is_removed: false,
113 115 },
114 116 );
115 - let input = format!("[quote:{}:abcd1234]", post_id);
117 + let input = format!("[quote:{post_id}:abcd1234]");
116 118 let result = post_process_quotes(&input, &authors);
117 119 assert!(result.contains("A &lt;B&gt; &amp; C"));
118 120 assert!(!result.contains("<B>"));