ndg_commonmark/processor/
mod.rs1pub mod core;
18pub mod dom;
19pub mod extensions;
20pub mod process;
21pub mod types;
22
23pub use core::{
25 ProcessorFeature,
26 collect_markdown_files,
27 extract_inline_text,
28 rewrite_cross_page_anchor_links,
29};
30
31#[cfg(feature = "gfm")]
33pub use extensions::apply_gfm_extensions;
34#[cfg(feature = "nixpkgs")]
35pub use extensions::process_manpage_references;
36pub use extensions::process_myst_autolinks;
37#[cfg(feature = "ndg-flavored")]
38pub use extensions::process_option_references;
39#[cfg(any(feature = "nixpkgs", feature = "ndg-flavored"))]
40pub use extensions::process_role_markup;
41#[cfg(feature = "wiki")]
42pub use extensions::process_wikilinks;
43#[cfg(feature = "nixpkgs")]
44pub use extensions::{
45 process_block_elements,
46 process_bracketed_spans,
47 process_file_includes,
48 process_inline_anchors,
49};
50pub use process::{
51 ProcessorPreset,
52 create_processor,
53 process_batch,
54 process_markdown_file,
55 process_markdown_file_with_basedir,
56 process_markdown_string,
57 process_safe,
58 process_with_recovery,
59};
60pub use types::{
61 AstTransformer,
62 MarkdownExtension,
63 MarkdownOptions,
64 MarkdownOptionsBuilder,
65 MarkdownProcessor,
66 PromptTransformer,
67};
68
69#[cfg(test)]
70#[expect(clippy::expect_used, reason = "Fine in tests")]
71mod tests {
72 use html_escape;
73
74 use super::{MarkdownOptions, MarkdownProcessor, types::TabStyle};
75
76 #[test]
77 fn test_html_escaped_roles() {
78 #[cfg(any(feature = "nixpkgs", feature = "ndg-flavored"))]
80 {
81 let result = super::extensions::format_role_markup(
82 "option",
83 "hjem.users.<name>.enable",
84 None,
85 true,
86 None,
87 );
88
89 assert!(result.contains("<name>"));
91 assert!(!result.contains("<code>hjem.users.<name>.enable</code>"));
93 assert!(result.contains(
95 "<code class=\"nixos-option\">hjem.users.<name>.enable</code>"
96 ));
97 assert!(result.contains("option-hjem.users._name_.enable"));
100 }
101 }
102
103 #[test]
104 fn test_html_escape_util() {
105 let input = "test<>&\"'";
106 let escaped = html_escape::encode_text(input);
107
108 assert_eq!(escaped, "test<>&\"'");
110 }
111
112 #[test]
113 fn test_toc_anchor_matches_heading_id_for_angle_brackets() {
114 let processor = MarkdownProcessor::new(MarkdownOptions::default());
120 let result = processor.render("## environments.\\<name\\>.deployment\n");
124
125 let header = result
126 .headers
127 .iter()
128 .find(|h| h.level == 2)
129 .expect("expected an h2 header");
130
131 assert_eq!(header.id, "environments--lt-name-gt--deployment");
133 assert!(
135 result.html.contains(&format!("id=\"{}\"", header.id)),
136 "rendered HTML {:?} is missing id={:?}",
137 result.html,
138 header.id
139 );
140 }
141
142 #[test]
143 fn test_various_role_types_with_html_characters() {
144 #[cfg(any(feature = "nixpkgs", feature = "ndg-flavored"))]
145 {
146 let content = "<script>alert('xss')</script>";
147
148 let command_result = super::extensions::format_role_markup(
149 "command", content, None, true, None,
150 );
151 assert!(command_result.contains("<script>"));
152 assert!(!command_result.contains("<script>alert"));
153
154 let env_result =
155 super::extensions::format_role_markup("env", content, None, true, None);
156 assert!(env_result.contains("<script>"));
157 assert!(!env_result.contains("<script>alert"));
158
159 let file_result = super::extensions::format_role_markup(
160 "file", content, None, true, None,
161 );
162 assert!(file_result.contains("<script>"));
163 assert!(!file_result.contains("<script>alert"));
164 }
165 }
166
167 #[test]
168 fn test_option_role_escaping() {
169 #[cfg(any(feature = "nixpkgs", feature = "ndg-flavored"))]
171 {
172 let result = super::extensions::format_role_markup(
173 "option",
174 "hjem.users.<name>.enable",
175 None,
176 true,
177 None,
178 );
179
180 assert!(!result.contains("</name>"));
183
184 assert!(result.contains("<name>"));
186
187 assert!(result.contains(
189 "<code class=\"nixos-option\">hjem.users.<name>.enable</code>"
190 ));
191
192 assert!(result.contains("options.html#option-hjem.users._name_.enable"));
194 }
195 }
196
197 #[test]
198 fn test_option_role_special_chars_preserved() {
199 #[cfg(any(feature = "nixpkgs", feature = "ndg-flavored"))]
201 {
202 let result = super::extensions::format_role_markup(
203 "option",
204 "services.foo.<bar>.enable",
205 None,
206 true,
207 None,
208 );
209
210 assert!(result.contains("option-services.foo._bar_.enable"));
212
213 assert!(result.contains("<bar>"));
215 }
216 }
217
218 #[test]
219 fn test_hardtab_handling_none() {
220 let options = MarkdownOptions {
221 tab_style: TabStyle::None,
222 highlight_code: false,
223 ..Default::default()
224 };
225 let processor = MarkdownProcessor::new(options);
226
227 let markdown = r#"
228# Test Code
229
230```rust
231fn main() {
232 println!("Hello, world!");
233}
234```
235"#;
236
237 let result = processor.render(markdown);
238 assert!(result.html.contains("\tprintln"));
239 }
240
241 #[test]
242 fn test_hardtab_handling_warn() {
243 let options = MarkdownOptions {
244 tab_style: TabStyle::Warn,
245 highlight_code: false,
246 ..Default::default()
247 };
248 let processor = MarkdownProcessor::new(options);
249
250 let markdown = r#"
251# Test Code
252
253```rust
254fn main() {
255 println!("Hello, world!");
256}
257```
258"#;
259
260 let result = processor.render(markdown);
261 assert!(result.html.contains("\tprintln"));
263 }
264
265 #[test]
266 fn test_hardtab_handling_normalize() {
267 let options = MarkdownOptions {
268 tab_style: TabStyle::Normalize,
269 highlight_code: false,
270 ..Default::default()
271 };
272 let processor = MarkdownProcessor::new(options);
273
274 let markdown = r#"
275# Test Code
276
277```rust
278fn main() {
279 println!("Hello, world!");
280}
281```
282"#;
283
284 let result = processor.render(markdown);
285 assert!(!result.html.contains("\tprintln"));
287 assert!(result.html.contains(" println"));
288 }
289
290 #[test]
291 fn test_hardtab_handling_no_tabs() {
292 let options = MarkdownOptions {
293 tab_style: TabStyle::Warn,
294 highlight_code: false,
295 ..Default::default()
296 };
297 let processor = MarkdownProcessor::new(options);
298
299 let markdown = r#"
300# Test Code
301
302```rust
303fn main() {
304 println!("Hello, world!");
305}
306```
307"#;
308
309 let result = processor.render(markdown);
310 assert!(result.html.contains(" println"));
312 assert!(!result.html.contains('\t'));
313 }
314
315 #[test]
316 fn test_hardtab_handling_mixed_content() {
317 let options = MarkdownOptions {
318 tab_style: TabStyle::Normalize,
319 highlight_code: false,
320 ..Default::default()
321 };
322 let processor = MarkdownProcessor::new(options);
323
324 let markdown = r#"
325# Test Code
326
327```rust
328fn main() {
329 println!("Hello"); // tab here
330 println!("World"); // spaces here
331}
332```
333"#;
334
335 let result = processor.render(markdown);
336 assert!(!result.html.contains("\tprintln"));
338 assert!(result.html.contains(" println"));
339 assert!(result.html.contains(" println"));
340 }
341}