max / docengine
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
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 { |
| @@ -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!("", alt, cdn_base, user_id, path) | |
| 57 | + | format!("") | |
| 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!( |
| @@ -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: ®ex_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 | } |
| @@ -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 <B> & C")); | |
| 118 | 120 | assert!(!result.contains("<B>")); |