1#[cfg(feature = "cli")]
2pub mod cli;
3pub mod formatter;
4pub mod parser;
5
6#[cfg(feature = "napi")]
8pub mod napi;
9
10pub use formatter::{Formatter, OrderedListMode, WrapMode};
11pub use parser::{extract_frontmatter, parse_markdown};
12
13#[cfg(test)]
14mod tests {
15 use crate::{extract_frontmatter, parse_markdown, Formatter, OrderedListMode, WrapMode};
16
17 fn format_markdown(input: &str) -> String {
18 let events = parse_markdown(input);
19 let mut formatter = Formatter::new(80);
20 formatter.format(events)
21 }
22
23 fn format_markdown_always(input: &str) -> String {
24 let events = parse_markdown(input);
25 let mut formatter = Formatter::with_wrap_mode(80, WrapMode::Always);
26 formatter.format(events)
27 }
28
29 fn format_markdown_one(input: &str) -> String {
30 let events = parse_markdown(input);
31 let mut formatter = Formatter::with_options(80, WrapMode::default(), OrderedListMode::One);
32 formatter.format(events)
33 }
34
35 fn format_markdown_full(input: &str) -> String {
37 let (frontmatter, content) = extract_frontmatter(input);
38 let events = parse_markdown(content);
39 let mut formatter = Formatter::with_wrap_mode(80, WrapMode::Always);
40 let formatted = formatter.format(events);
41
42 if let Some(fm) = frontmatter {
43 fm + &formatted
44 } else {
45 formatted
46 }
47 }
48
49 #[test]
54 fn test_heading_normalization() {
55 let input = "# Heading 1\n## Heading 2";
56 let output = format_markdown(input);
57 let expected = "# Heading 1\n\n## Heading 2\n";
58 assert_eq!(output, expected);
59 }
60
61 #[test]
62 fn test_list_normalization() {
63 let input = "- Item 1\n- Item 2\n- Item 3";
64 let output = format_markdown(input);
65 let expected = "- Item 1\n- Item 2\n- Item 3\n";
66 assert_eq!(output, expected);
67 }
68
69 #[test]
70 fn test_ordered_list_ascending_mode() {
71 let input = "1. First\n1. Second\n1. Third";
73 let output = format_markdown(input);
74 assert!(output.contains("1. First"));
75 assert!(output.contains("2. Second"));
76 assert!(output.contains("3. Third"));
77 }
78
79 #[test]
80 fn test_multiple_ordered_list_ascending_mode() {
81 let input = "1. First\n1. Second\n1. Third\n\nHere is one more\n\n1. Another\n1. List";
83 let output = format_markdown(input);
84 assert!(output.contains("1. First"));
85 assert!(output.contains("2. Second"));
86 assert!(output.contains("3. Third"));
87 assert!(output.contains("1. Another"));
88 assert!(output.contains("2. List"));
89 }
90
91 #[test]
92 fn test_nested_lists_have_no_empty_lines() {
93 let input = "- First\n- Second\n - Subitem one\n - Subitem two\n- Third";
94 let output = format_markdown(input);
95 assert!(!output.contains("\n\n - Subitem one"));
96 }
97
98 #[test]
99 fn test_ordered_list_one_mode() {
100 let input = "1. First\n2. Second\n3. Third";
102 let output = format_markdown_one(input);
103 assert!(output.contains("1. First"));
104 assert!(output.contains("1. Second"));
105 assert!(output.contains("1. Third"));
106 }
107
108 #[test]
109 fn test_emphasis() {
110 let input = "This is *italic* and **bold** text.";
111 let output = format_markdown(input);
112 let expected = "This is *italic* and **bold** text.\n";
113 assert_eq!(output, expected);
114 }
115
116 #[test]
117 fn test_code_block() {
118 let input = "```rust\nfn main() {\n println!(\"Hello\");\n}\n```";
119 let output = format_markdown(input);
120 assert!(output.contains("```"));
121 assert!(output.contains("fn main()"));
122 }
123
124 #[test]
125 fn test_text_wrapping() {
126 let input = "This is a very long line that should probably be wrapped because it exceeds the line width limit that we have set for the formatter.";
127 let output = format_markdown_always(input);
128 assert!(output.lines().count() > 1);
130 }
131
132 #[test]
133 fn test_idempotence() {
134 let input = "# Hello\n\nThis is a paragraph with *emphasis*.\n\n- Item 1\n- Item 2\n";
135 let first_pass = format_markdown(input);
136 let second_pass = format_markdown(&first_pass);
137 assert_eq!(first_pass, second_pass, "Formatter should be idempotent");
138 }
139
140 #[test]
141 fn test_inline_code() {
142 let input = "Use `let x = 5;` for variable declaration.";
143 let output = format_markdown(input);
144 assert!(output.contains("`let x = 5;`"));
145 }
146
147 #[test]
148 fn test_horizontal_rule() {
149 let input = "Before\n\n---\n\nAfter";
150 let output = format_markdown(input);
151 assert!(output.contains("---"));
152 }
153
154 #[test]
155 fn test_nested_lists() {
156 let input = "- Item 1\n- Item 2\n - Nested 1\n - Nested 2\n- Item 3";
157 let output = format_markdown(input);
158 assert!(output.contains(" - Nested"));
159 }
160
161 #[test]
162 fn test_paragraph_wrapping() {
163 let input = "This is a short intro paragraph.\n\nThis is another paragraph that is quite long and should be wrapped nicely across multiple lines if needed based on the formatter's width settings.";
164 let output = format_markdown(input);
165 let parts: Vec<&str> = output.split("\n\n").collect();
167 assert!(parts.len() >= 2);
168 }
169
170 #[test]
171 fn test_blockquote_formatting() {
172 let input = "> This is a blockquote\n> with multiple lines";
173 let output = format_markdown(input);
174 assert!(output.contains(">"));
176 let output2 = format_markdown(&output);
178 assert_eq!(output, output2);
179 }
180
181 #[test]
182 fn test_frontmatter_preservation() {
183 let input = "---\ntitle: Test\nauthor: Me\n---\n\n# Heading\n\nContent.";
184 let (frontmatter, content) = extract_frontmatter(input);
185
186 assert!(frontmatter.is_some());
188 assert!(frontmatter.unwrap().contains("title:"));
189
190 assert!(!content.contains("title:"));
192 assert!(content.contains("# Heading"));
193 }
194
195 #[test]
196 fn test_strikethrough_preservation() {
197 let input = "This has ~~strikethrough~~ text.";
198 let output = format_markdown(input);
199 assert!(output.contains("~~strikethrough~~"));
201 }
202
203 #[test]
204 fn test_gfm_autolinks() {
205 let input = "Visit <https://example.com> for more info.";
206 let output = format_markdown(input);
207 assert!(output.contains("example.com"));
209 }
210
211 #[test]
212 fn test_hard_break_preservation() {
213 let input = "Line one \nLine two";
214 let output = format_markdown(input);
215 assert!(output.contains(" \n"), "Hard break should be preserved");
217 }
218
219 #[test]
220 fn test_no_spurious_hard_breaks() {
221 let input = "This is a very long line that needs to be wrapped because it exceeds eighty characters.";
223 let output = format_markdown_always(input);
224 assert!(
226 !output.contains(" \n"),
227 "Wrapped lines should not have hard breaks"
228 );
229 }
230
231 const SIMPLE_GOOD: &str = include_str!("../tests/fixtures/simple-good.md");
236 const SIMPLE_BAD: &str = include_str!("../tests/fixtures/simple-bad.md");
237 const COMPLEX_GOOD: &str = include_str!("../tests/fixtures/complex-good.md");
238 const COMPLEX_BAD: &str = include_str!("../tests/fixtures/complex-bad.md");
239
240 #[test]
241 fn test_simple_good_is_idempotent() {
242 let formatted = format_markdown(SIMPLE_GOOD);
243 let reformatted = format_markdown(&formatted);
244 assert_eq!(
245 formatted, reformatted,
246 "simple-good.md should be idempotent"
247 );
248 }
249
250 #[test]
251 fn test_simple_bad_formats_correctly() {
252 let formatted = format_markdown(SIMPLE_BAD);
253
254 let reformatted = format_markdown(&formatted);
256 assert_eq!(
257 formatted, reformatted,
258 "Formatted simple-bad.md should be idempotent"
259 );
260
261 assert!(
263 formatted.contains("# Simple Document\n"),
264 "Heading should have single space"
265 );
266 assert!(
267 formatted.contains("- First item\n"),
268 "List items should use dash with single space"
269 );
270 assert!(
271 formatted.contains("1. "),
272 "Ordered list should use 1. format"
273 );
274 }
275
276 #[test]
277 fn test_complex_good_is_idempotent() {
278 let formatted = format_markdown_full(COMPLEX_GOOD);
279 let reformatted = format_markdown_full(&formatted);
280 assert_eq!(
281 formatted, reformatted,
282 "complex-good.md should be idempotent"
283 );
284 }
285
286 #[test]
287 fn test_complex_bad_formats_correctly() {
288 let formatted = format_markdown_full(COMPLEX_BAD);
289
290 let reformatted = format_markdown_full(&formatted);
292 assert_eq!(
293 formatted, reformatted,
294 "Formatted complex-bad.md should be idempotent"
295 );
296
297 assert!(
299 formatted.starts_with("---\n"),
300 "Frontmatter should be preserved"
301 );
302 assert!(
303 formatted.contains("title:"),
304 "Frontmatter content should be preserved"
305 );
306
307 assert!(
309 formatted.contains("```python"),
310 "Python code block should be preserved"
311 );
312 assert!(
313 formatted.contains("def hello_world():"),
314 "Code content should be preserved"
315 );
316
317 assert!(
319 formatted.contains(" \n"),
320 "Hard breaks should be preserved"
321 );
322 }
323
324 #[test]
325 fn test_complex_preserves_code_blocks() {
326 let formatted = format_markdown_full(COMPLEX_GOOD);
327
328 assert!(formatted.contains("def hello_world():"));
330 assert!(formatted.contains(" \"\"\"A simple greeting function.\"\"\""));
331 assert!(formatted.contains("const greet = (name) => {"));
332 }
333
334 #[test]
335 fn test_no_hard_breaks_in_wrapped_output() {
336 let formatted = format_markdown_always(SIMPLE_BAD);
338
339 let hard_break_count = formatted
341 .lines()
342 .filter(|line| line.ends_with(" "))
343 .count();
344
345 assert_eq!(
347 hard_break_count, 0,
348 "Wrapped lines should not introduce hard breaks"
349 );
350 }
351}