diff --git a/README.md b/README.md index 9ba88a91..3542b115 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ fn main() -> std::io::Result<()> { wrap: true, ellipsis: true, fences: true, - footnotes: false, + ..Default::default() }; let fixed = process_stream_opts(&lines, opts); println!("{}", fixed.join("\n")); diff --git a/src/fences.rs b/src/fences.rs index 13fbf50a..91454e53 100644 --- a/src/fences.rs +++ b/src/fences.rs @@ -66,48 +66,31 @@ pub fn compress_fences(lines: &[String]) -> Vec { /// ``` #[must_use] pub fn attach_orphan_specifiers(lines: &[String]) -> Vec { - let mut out = Vec::with_capacity(lines.len()); + let mut out: Vec = Vec::with_capacity(lines.len()); let mut in_fence = false; - let mut iter = lines.iter().peekable(); - - while let Some(line) = iter.next() { + for line in lines { let trimmed = line.trim(); - if !in_fence && ORPHAN_LANG_RE.is_match(trimmed) { - let mut peek_ahead = iter.clone(); - let mut found_fence = false; - - while let Some(next_line) = peek_ahead.peek() { - let next_trimmed = next_line.trim(); - if next_trimmed.is_empty() { - peek_ahead.next(); - } else if next_trimmed == "```" { - found_fence = true; - break; - } else { - break; + if trimmed.starts_with("```") { + if in_fence { + in_fence = false; + } else { + while matches!(out.last(), Some(l) if l.trim().is_empty()) { + out.pop(); } - } - - if found_fence { - while let Some(next_line) = iter.peek() { - if next_line.trim().is_empty() { - iter.next(); - } else { - break; + if let Some(prev) = out.last() { + let lang = prev.trim().to_string(); + if ORPHAN_LANG_RE.is_match(&lang) { + out.pop(); + out.push(format!("```{lang}")); + in_fence = true; + continue; } } - iter.next(); - out.push(format!("```{trimmed}")); in_fence = true; - continue; } } - if trimmed.starts_with("```") { - in_fence = !in_fence; - } - out.push(line.clone()); } out diff --git a/src/process.rs b/src/process.rs index bc391858..209ca989 100644 --- a/src/process.rs +++ b/src/process.rs @@ -9,12 +9,15 @@ use crate::{ wrap::{self, wrap_text}, }; +/// Column width used when wrapping text. +pub(crate) const WRAP_COLS: usize = 80; + /// Processing options controlling the behaviour of `process_stream_inner`. #[expect( clippy::struct_excessive_bools, reason = "Options map directly to CLI flags" )] -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] pub struct Options { /// Enable paragraph wrapping pub wrap: bool, @@ -96,7 +99,11 @@ pub fn process_stream_inner(lines: &[String], opts: Options) -> Vec { } } - let mut out = if opts.wrap { wrap_text(&out, 80) } else { out }; + let mut out = if opts.wrap { + wrap_text(&out, WRAP_COLS) + } else { + out + }; if opts.ellipsis { out = replace_ellipsis(&out); } @@ -112,24 +119,23 @@ pub fn process_stream(lines: &[String]) -> Vec { lines, Options { wrap: true, - ellipsis: false, - fences: false, - footnotes: false, + ..Default::default() }, ) } +/// Process a Markdown stream without wrapping paragraphs. +/// +/// ``` +/// use mdtablefix::process_stream_no_wrap; +/// let lines = vec!["one".to_string(), "two".to_string()]; +/// let out = process_stream_no_wrap(&lines); +/// assert_eq!(out, lines); +/// ``` #[must_use] +#[inline] pub fn process_stream_no_wrap(lines: &[String]) -> Vec { - process_stream_inner( - lines, - Options { - wrap: false, - ellipsis: false, - fences: false, - footnotes: false, - }, - ) + process_stream_inner(lines, Options::default()) } #[must_use] diff --git a/tests/cli.rs b/tests/cli.rs index 597697f0..d67001b0 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -150,6 +150,21 @@ fn test_cli_fences_option() { ); } +#[test] +fn test_cli_fences_option_tilde() { + let output = Command::cargo_bin("mdtablefix") + .expect("Failed to create cargo command for mdtablefix") + .arg("--fences") + .write_stdin("~~~~rust\nfn main() {}\n~~~~\n") + .output() + .expect("Failed to execute mdtablefix command"); + assert!(output.status.success()); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + "```rust\nfn main() {}\n```\n" + ); +} + /// Ensures fence normalization runs before other processing. #[test] fn test_cli_fences_before_ellipsis() { diff --git a/tests/fences.rs b/tests/fences.rs index 7daff995..bffe9d16 100644 --- a/tests/fences.rs +++ b/tests/fences.rs @@ -25,6 +25,17 @@ fn compresses_tilde_fences() { assert_eq!(out, lines_vec!["```rust", "code", "```"]); } +#[test] +fn does_not_compress_mixed_fences() { + let input = lines_vec!["~~~rust", "code", "```"]; + let out = compress_fences(&input); + assert_eq!(out, lines_vec!["```rust", "code", "```"]); + + let input2 = lines_vec!["```rust", "code", "~~~"]; + let out2 = compress_fences(&input2); + assert_eq!(out2, lines_vec!["```rust", "code", "```"]); +} + #[test] fn leaves_other_lines_untouched() { let input = lines_vec!["~~", "``text``"]; @@ -59,3 +70,45 @@ fn fixes_orphaned_specifier_with_blank_line() { let out = attach_orphan_specifiers(&compress_fences(&input)); assert_eq!(out, lines_vec!["```Rust", "fn main() {}", "```"]); } + +#[test] +fn fixes_multiple_orphaned_specifiers() { + let input = lines_vec![ + "Rust", + "```", + "fn main() {}", + "```", + "Python", + "```", + "print('hi')", + "```", + ]; + let out = attach_orphan_specifiers(&compress_fences(&input)); + assert_eq!( + out, + lines_vec![ + "```Rust", + "fn main() {}", + "```", + "```Python", + "print('hi')", + "```" + ] + ); +} + +#[test] +fn does_not_attach_non_orphan_lines_before_fences() { + let input = lines_vec![ + "Rust code", + "```", + "fn main() {}", + "```", + "rust!", + "```", + "println!(\"hi\");", + "```", + ]; + let out = attach_orphan_specifiers(&input); + assert_eq!(out, input); +}