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, WrapMode};
11pub use parser::{extract_frontmatter, parse_markdown};
12
13#[cfg(test)]
14mod tests {
15 use crate::{extract_frontmatter, parse_markdown, Formatter, 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_full(input: &str) -> String {
31 let (frontmatter, content) = extract_frontmatter(input);
32 let events = parse_markdown(content);
33 let mut formatter = Formatter::with_wrap_mode(80, WrapMode::Always);
34 let formatted = formatter.format(events);
35
36 if let Some(fm) = frontmatter {
37 fm + &formatted
38 } else {
39 formatted
40 }
41 }
42
43 #[test]
48 fn test_heading_normalization() {
49 let input = "# Heading 1\n## Heading 2";
50 let output = format_markdown(input);
51 let expected = "# Heading 1\n\n## Heading 2\n";
52 assert_eq!(output, expected);
53 }
54
55 #[test]
56 fn test_list_normalization() {
57 let input = "- Item 1\n- Item 2\n- Item 3";
58 let output = format_markdown(input);
59 let expected = "- Item 1\n- Item 2\n- Item 3\n";
60 assert_eq!(output, expected);
61 }
62
63 #[test]
64 fn test_emphasis() {
65 let input = "This is *italic* and **bold** text.";
66 let output = format_markdown(input);
67 let expected = "This is *italic* and **bold** text.\n";
68 assert_eq!(output, expected);
69 }
70
71 #[test]
72 fn test_code_block() {
73 let input = "```rust\nfn main() {\n println!(\"Hello\");\n}\n```";
74 let output = format_markdown(input);
75 assert!(output.contains("```"));
76 assert!(output.contains("fn main()"));
77 }
78
79 #[test]
80 fn test_text_wrapping() {
81 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.";
82 let output = format_markdown_always(input);
83 assert!(output.lines().count() > 1);
85 }
86
87 #[test]
88 fn test_idempotence() {
89 let input = "# Hello\n\nThis is a paragraph with *emphasis*.\n\n- Item 1\n- Item 2\n";
90 let first_pass = format_markdown(input);
91 let second_pass = format_markdown(&first_pass);
92 assert_eq!(first_pass, second_pass, "Formatter should be idempotent");
93 }
94
95 #[test]
96 fn test_inline_code() {
97 let input = "Use `let x = 5;` for variable declaration.";
98 let output = format_markdown(input);
99 assert!(output.contains("`let x = 5;`"));
100 }
101
102 #[test]
103 fn test_horizontal_rule() {
104 let input = "Before\n\n---\n\nAfter";
105 let output = format_markdown(input);
106 assert!(output.contains("---"));
107 }
108
109 #[test]
110 fn test_nested_lists() {
111 let input = "- Item 1\n- Item 2\n - Nested 1\n - Nested 2\n- Item 3";
112 let output = format_markdown(input);
113 assert!(output.contains(" - Nested"));
114 }
115
116 #[test]
117 fn test_paragraph_wrapping() {
118 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.";
119 let output = format_markdown(input);
120 let parts: Vec<&str> = output.split("\n\n").collect();
122 assert!(parts.len() >= 2);
123 }
124
125 #[test]
126 fn test_blockquote_formatting() {
127 let input = "> This is a blockquote\n> with multiple lines";
128 let output = format_markdown(input);
129 assert!(output.contains(">"));
131 let output2 = format_markdown(&output);
133 assert_eq!(output, output2);
134 }
135
136 #[test]
137 fn test_frontmatter_preservation() {
138 let input = "---\ntitle: Test\nauthor: Me\n---\n\n# Heading\n\nContent.";
139 let (frontmatter, content) = extract_frontmatter(input);
140
141 assert!(frontmatter.is_some());
143 assert!(frontmatter.unwrap().contains("title:"));
144
145 assert!(!content.contains("title:"));
147 assert!(content.contains("# Heading"));
148 }
149
150 #[test]
151 fn test_strikethrough_preservation() {
152 let input = "This has ~~strikethrough~~ text.";
153 let output = format_markdown(input);
154 assert!(output.contains("~~strikethrough~~"));
156 }
157
158 #[test]
159 fn test_gfm_autolinks() {
160 let input = "Visit <https://example.com> for more info.";
161 let output = format_markdown(input);
162 assert!(output.contains("example.com"));
164 }
165
166 #[test]
167 fn test_hard_break_preservation() {
168 let input = "Line one \nLine two";
169 let output = format_markdown(input);
170 assert!(output.contains(" \n"), "Hard break should be preserved");
172 }
173
174 #[test]
175 fn test_no_spurious_hard_breaks() {
176 let input = "This is a very long line that needs to be wrapped because it exceeds eighty characters.";
178 let output = format_markdown_always(input);
179 assert!(
181 !output.contains(" \n"),
182 "Wrapped lines should not have hard breaks"
183 );
184 }
185
186 const SIMPLE_GOOD: &str = include_str!("../tests/fixtures/simple-good.md");
191 const SIMPLE_BAD: &str = include_str!("../tests/fixtures/simple-bad.md");
192 const COMPLEX_GOOD: &str = include_str!("../tests/fixtures/complex-good.md");
193 const COMPLEX_BAD: &str = include_str!("../tests/fixtures/complex-bad.md");
194
195 #[test]
196 fn test_simple_good_is_idempotent() {
197 let formatted = format_markdown(SIMPLE_GOOD);
198 let reformatted = format_markdown(&formatted);
199 assert_eq!(
200 formatted, reformatted,
201 "simple-good.md should be idempotent"
202 );
203 }
204
205 #[test]
206 fn test_simple_bad_formats_correctly() {
207 let formatted = format_markdown(SIMPLE_BAD);
208
209 let reformatted = format_markdown(&formatted);
211 assert_eq!(
212 formatted, reformatted,
213 "Formatted simple-bad.md should be idempotent"
214 );
215
216 assert!(
218 formatted.contains("# Simple Document\n"),
219 "Heading should have single space"
220 );
221 assert!(
222 formatted.contains("- First item\n"),
223 "List items should use dash with single space"
224 );
225 assert!(
226 formatted.contains("1. "),
227 "Ordered list should use 1. format"
228 );
229 }
230
231 #[test]
232 fn test_complex_good_is_idempotent() {
233 let formatted = format_markdown_full(COMPLEX_GOOD);
234 let reformatted = format_markdown_full(&formatted);
235 assert_eq!(
236 formatted, reformatted,
237 "complex-good.md should be idempotent"
238 );
239 }
240
241 #[test]
242 fn test_complex_bad_formats_correctly() {
243 let formatted = format_markdown_full(COMPLEX_BAD);
244
245 let reformatted = format_markdown_full(&formatted);
247 assert_eq!(
248 formatted, reformatted,
249 "Formatted complex-bad.md should be idempotent"
250 );
251
252 assert!(
254 formatted.starts_with("---\n"),
255 "Frontmatter should be preserved"
256 );
257 assert!(
258 formatted.contains("title:"),
259 "Frontmatter content should be preserved"
260 );
261
262 assert!(
264 formatted.contains("```python"),
265 "Python code block should be preserved"
266 );
267 assert!(
268 formatted.contains("def hello_world():"),
269 "Code content should be preserved"
270 );
271
272 assert!(
274 formatted.contains(" \n"),
275 "Hard breaks should be preserved"
276 );
277 }
278
279 #[test]
280 fn test_complex_preserves_code_blocks() {
281 let formatted = format_markdown_full(COMPLEX_GOOD);
282
283 assert!(formatted.contains("def hello_world():"));
285 assert!(formatted.contains(" \"\"\"A simple greeting function.\"\"\""));
286 assert!(formatted.contains("const greet = (name) => {"));
287 }
288
289 #[test]
290 fn test_no_hard_breaks_in_wrapped_output() {
291 let formatted = format_markdown_always(SIMPLE_BAD);
293
294 let hard_break_count = formatted
296 .lines()
297 .filter(|line| line.ends_with(" "))
298 .count();
299
300 assert_eq!(
302 hard_break_count, 0,
303 "Wrapped lines should not introduce hard breaks"
304 );
305 }
306}