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_ordered_list_one_mode() {
93 let input = "1. First\n2. Second\n3. Third";
95 let output = format_markdown_one(input);
96 assert!(output.contains("1. First"));
97 assert!(output.contains("1. Second"));
98 assert!(output.contains("1. Third"));
99 }
100
101 #[test]
102 fn test_emphasis() {
103 let input = "This is *italic* and **bold** text.";
104 let output = format_markdown(input);
105 let expected = "This is *italic* and **bold** text.\n";
106 assert_eq!(output, expected);
107 }
108
109 #[test]
110 fn test_code_block() {
111 let input = "```rust\nfn main() {\n println!(\"Hello\");\n}\n```";
112 let output = format_markdown(input);
113 assert!(output.contains("```"));
114 assert!(output.contains("fn main()"));
115 }
116
117 #[test]
118 fn test_text_wrapping() {
119 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.";
120 let output = format_markdown_always(input);
121 assert!(output.lines().count() > 1);
123 }
124
125 #[test]
126 fn test_idempotence() {
127 let input = "# Hello\n\nThis is a paragraph with *emphasis*.\n\n- Item 1\n- Item 2\n";
128 let first_pass = format_markdown(input);
129 let second_pass = format_markdown(&first_pass);
130 assert_eq!(first_pass, second_pass, "Formatter should be idempotent");
131 }
132
133 #[test]
134 fn test_inline_code() {
135 let input = "Use `let x = 5;` for variable declaration.";
136 let output = format_markdown(input);
137 assert!(output.contains("`let x = 5;`"));
138 }
139
140 #[test]
141 fn test_horizontal_rule() {
142 let input = "Before\n\n---\n\nAfter";
143 let output = format_markdown(input);
144 assert!(output.contains("---"));
145 }
146
147 #[test]
148 fn test_nested_lists() {
149 let input = "- Item 1\n- Item 2\n - Nested 1\n - Nested 2\n- Item 3";
150 let output = format_markdown(input);
151 assert!(output.contains(" - Nested"));
152 }
153
154 #[test]
155 fn test_paragraph_wrapping() {
156 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.";
157 let output = format_markdown(input);
158 let parts: Vec<&str> = output.split("\n\n").collect();
160 assert!(parts.len() >= 2);
161 }
162
163 #[test]
164 fn test_blockquote_formatting() {
165 let input = "> This is a blockquote\n> with multiple lines";
166 let output = format_markdown(input);
167 assert!(output.contains(">"));
169 let output2 = format_markdown(&output);
171 assert_eq!(output, output2);
172 }
173
174 #[test]
175 fn test_frontmatter_preservation() {
176 let input = "---\ntitle: Test\nauthor: Me\n---\n\n# Heading\n\nContent.";
177 let (frontmatter, content) = extract_frontmatter(input);
178
179 assert!(frontmatter.is_some());
181 assert!(frontmatter.unwrap().contains("title:"));
182
183 assert!(!content.contains("title:"));
185 assert!(content.contains("# Heading"));
186 }
187
188 #[test]
189 fn test_strikethrough_preservation() {
190 let input = "This has ~~strikethrough~~ text.";
191 let output = format_markdown(input);
192 assert!(output.contains("~~strikethrough~~"));
194 }
195
196 #[test]
197 fn test_gfm_autolinks() {
198 let input = "Visit <https://example.com> for more info.";
199 let output = format_markdown(input);
200 assert!(output.contains("example.com"));
202 }
203
204 #[test]
205 fn test_hard_break_preservation() {
206 let input = "Line one \nLine two";
207 let output = format_markdown(input);
208 assert!(output.contains(" \n"), "Hard break should be preserved");
210 }
211
212 #[test]
213 fn test_no_spurious_hard_breaks() {
214 let input = "This is a very long line that needs to be wrapped because it exceeds eighty characters.";
216 let output = format_markdown_always(input);
217 assert!(
219 !output.contains(" \n"),
220 "Wrapped lines should not have hard breaks"
221 );
222 }
223
224 const SIMPLE_GOOD: &str = include_str!("../tests/fixtures/simple-good.md");
229 const SIMPLE_BAD: &str = include_str!("../tests/fixtures/simple-bad.md");
230 const COMPLEX_GOOD: &str = include_str!("../tests/fixtures/complex-good.md");
231 const COMPLEX_BAD: &str = include_str!("../tests/fixtures/complex-bad.md");
232
233 #[test]
234 fn test_simple_good_is_idempotent() {
235 let formatted = format_markdown(SIMPLE_GOOD);
236 let reformatted = format_markdown(&formatted);
237 assert_eq!(
238 formatted, reformatted,
239 "simple-good.md should be idempotent"
240 );
241 }
242
243 #[test]
244 fn test_simple_bad_formats_correctly() {
245 let formatted = format_markdown(SIMPLE_BAD);
246
247 let reformatted = format_markdown(&formatted);
249 assert_eq!(
250 formatted, reformatted,
251 "Formatted simple-bad.md should be idempotent"
252 );
253
254 assert!(
256 formatted.contains("# Simple Document\n"),
257 "Heading should have single space"
258 );
259 assert!(
260 formatted.contains("- First item\n"),
261 "List items should use dash with single space"
262 );
263 assert!(
264 formatted.contains("1. "),
265 "Ordered list should use 1. format"
266 );
267 }
268
269 #[test]
270 fn test_complex_good_is_idempotent() {
271 let formatted = format_markdown_full(COMPLEX_GOOD);
272 let reformatted = format_markdown_full(&formatted);
273 assert_eq!(
274 formatted, reformatted,
275 "complex-good.md should be idempotent"
276 );
277 }
278
279 #[test]
280 fn test_complex_bad_formats_correctly() {
281 let formatted = format_markdown_full(COMPLEX_BAD);
282
283 let reformatted = format_markdown_full(&formatted);
285 assert_eq!(
286 formatted, reformatted,
287 "Formatted complex-bad.md should be idempotent"
288 );
289
290 assert!(
292 formatted.starts_with("---\n"),
293 "Frontmatter should be preserved"
294 );
295 assert!(
296 formatted.contains("title:"),
297 "Frontmatter content should be preserved"
298 );
299
300 assert!(
302 formatted.contains("```python"),
303 "Python code block should be preserved"
304 );
305 assert!(
306 formatted.contains("def hello_world():"),
307 "Code content should be preserved"
308 );
309
310 assert!(
312 formatted.contains(" \n"),
313 "Hard breaks should be preserved"
314 );
315 }
316
317 #[test]
318 fn test_complex_preserves_code_blocks() {
319 let formatted = format_markdown_full(COMPLEX_GOOD);
320
321 assert!(formatted.contains("def hello_world():"));
323 assert!(formatted.contains(" \"\"\"A simple greeting function.\"\"\""));
324 assert!(formatted.contains("const greet = (name) => {"));
325 }
326
327 #[test]
328 fn test_no_hard_breaks_in_wrapped_output() {
329 let formatted = format_markdown_always(SIMPLE_BAD);
331
332 let hard_break_count = formatted
334 .lines()
335 .filter(|line| line.ends_with(" "))
336 .count();
337
338 assert_eq!(
340 hard_break_count, 0,
341 "Wrapped lines should not introduce hard breaks"
342 );
343 }
344}