From 11622a3020929f2457573a7d5676b1fb0eca22a3 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 17 Jul 2025 01:05:39 +0100 Subject: [PATCH 1/4] Add renumber restart test --- src/lists.rs | 21 +++++++++++++++ .../renumber_paragraph_restart_expected.txt | 26 +++++++++++++++++++ .../data/renumber_paragraph_restart_input.txt | 26 +++++++++++++++++++ tests/integration.rs | 13 ++++++++++ 4 files changed, 86 insertions(+) create mode 100644 tests/data/renumber_paragraph_restart_expected.txt create mode 100644 tests/data/renumber_paragraph_restart_input.txt diff --git a/src/lists.rs b/src/lists.rs index 5d001074..e8e8da87 100644 --- a/src/lists.rs +++ b/src/lists.rs @@ -26,21 +26,31 @@ fn drop_deeper(indent: usize, counters: &mut Vec<(usize, usize)>) { } } +fn is_plain_paragraph_line(line: &str) -> bool { + line.trim_start() + .chars() + .next() + .is_some_and(char::is_alphanumeric) +} + #[must_use] pub fn renumber_lists(lines: &[String]) -> Vec { let mut out = Vec::with_capacity(lines.len()); let mut counters: Vec<(usize, usize)> = Vec::new(); let mut in_code = false; + let mut prev_blank = true; for line in lines { if is_fence(line) { in_code = !in_code; out.push(line.clone()); + prev_blank = false; continue; } if in_code { out.push(line.clone()); + prev_blank = line.trim().is_empty(); continue; } @@ -58,6 +68,7 @@ pub fn renumber_lists(lines: &[String]) -> Vec { } }; out.push(format!("{indent_str}{current}.{sep}{rest}")); + prev_blank = false; continue; } @@ -67,8 +78,18 @@ pub fn renumber_lists(lines: &[String]) -> Vec { .map_or_else(|| line.len(), |(i, _)| i); let indent_str = &line[..indent_end]; let indent = indent_len(indent_str); + if prev_blank + && counters + .last() + .is_some_and(|(d, _)| indent <= *d && is_plain_paragraph_line(line)) + { + while counters.last().is_some_and(|(d, _)| *d >= indent) { + counters.pop(); + } + } drop_deeper(indent, &mut counters); out.push(line.clone()); + prev_blank = line.trim().is_empty(); } out diff --git a/tests/data/renumber_paragraph_restart_expected.txt b/tests/data/renumber_paragraph_restart_expected.txt new file mode 100644 index 00000000..0e050c92 --- /dev/null +++ b/tests/data/renumber_paragraph_restart_expected.txt @@ -0,0 +1,26 @@ +1. First item + +2. Second item + + Here’s a plain paragraph within the second item + +3. Third item + +Here's a plain paragraph at a lower indent level + +1. Fourth item + +| A | B | +| --- | --- | +| 1 | 2 | + +2. Fifth item + +3. Sixth item + +```js +// fenced code block at lower indent +console.log('hello'); +``` + +4. Seventh item diff --git a/tests/data/renumber_paragraph_restart_input.txt b/tests/data/renumber_paragraph_restart_input.txt new file mode 100644 index 00000000..df5054b1 --- /dev/null +++ b/tests/data/renumber_paragraph_restart_input.txt @@ -0,0 +1,26 @@ +1. First item + +2. Second item + + Here’s a plain paragraph within the second item + +3. Third item + +Here's a plain paragraph at a lower indent level + +4. Fourth item + +| A | B | +| --- | --- | +| 1 | 2 | + +5. Fifth item + +6. Sixth item + +```js +// fenced code block at lower indent +console.log('hello'); +``` + +7. Seventh item diff --git a/tests/integration.rs b/tests/integration.rs index fce5c95b..e068f27a 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1010,6 +1010,19 @@ fn test_renumber_table_in_list() { assert_eq!(renumber_lists(&input), expected); } +#[test] +fn test_renumber_restart_after_paragraph() { + let input: Vec = include_str!("data/renumber_paragraph_restart_input.txt") + .lines() + .map(str::to_string) + .collect(); + let expected: Vec = include_str!("data/renumber_paragraph_restart_expected.txt") + .lines() + .map(str::to_string) + .collect(); + assert_eq!(renumber_lists(&input), expected); +} + #[test] fn test_format_breaks_basic() { let input = vec!["foo", "***", "bar"] From 2e6292e0802ba42b335ba1ec1284906cd754e0bc Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 17 Jul 2025 01:50:47 +0100 Subject: [PATCH 2/4] Extract pop helper --- src/lists.rs | 11 +++++++--- tests/lists.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 tests/lists.rs diff --git a/src/lists.rs b/src/lists.rs index e8e8da87..6e03333c 100644 --- a/src/lists.rs +++ b/src/lists.rs @@ -33,6 +33,13 @@ fn is_plain_paragraph_line(line: &str) -> bool { .is_some_and(char::is_alphanumeric) } +#[doc(hidden)] +pub fn pop_counters_upto(counters: &mut Vec<(usize, usize)>, indent: usize) { + while counters.last().is_some_and(|(d, _)| *d >= indent) { + counters.pop(); + } +} + #[must_use] pub fn renumber_lists(lines: &[String]) -> Vec { let mut out = Vec::with_capacity(lines.len()); @@ -83,9 +90,7 @@ pub fn renumber_lists(lines: &[String]) -> Vec { .last() .is_some_and(|(d, _)| indent <= *d && is_plain_paragraph_line(line)) { - while counters.last().is_some_and(|(d, _)| *d >= indent) { - counters.pop(); - } + pop_counters_upto(&mut counters, indent); } drop_deeper(indent, &mut counters); out.push(line.clone()); diff --git a/tests/lists.rs b/tests/lists.rs new file mode 100644 index 00000000..e8a07962 --- /dev/null +++ b/tests/lists.rs @@ -0,0 +1,56 @@ +use mdtablefix::{lists::pop_counters_upto, renumber_lists}; + +macro_rules! lines_vec { + ($($line:expr),* $(,)?) => { + vec![$($line.to_string()),*] + }; +} + +#[test] +fn pop_counters_removes_deeper_levels() { + let mut counters = vec![(0usize, 1usize), (4, 2), (8, 3)]; + pop_counters_upto(&mut counters, 4); + assert_eq!(counters, vec![(0, 1)]); +} + +#[test] +fn pop_counters_no_change_when_indent_deeper() { + let mut counters = vec![(0usize, 1usize), (4, 2)]; + pop_counters_upto(&mut counters, 6); + assert_eq!(counters, vec![(0, 1), (4, 2)]); +} + +#[test] +fn restart_after_lower_paragraph() { + let input = lines_vec!("1. One", "", "Paragraph", "3. Next"); + let expected = lines_vec!("1. One", "", "Paragraph", "1. Next"); + assert_eq!(renumber_lists(&input), expected); +} + +#[test] +fn no_restart_without_blank() { + let input = lines_vec!("1. One", "Paragraph", "3. Next"); + let expected = lines_vec!("1. One", "Paragraph", "2. Next"); + assert_eq!(renumber_lists(&input), expected); +} + +#[test] +fn no_restart_for_indented_paragraph() { + let input = lines_vec!("1. One", "", " Indented", "3. Next"); + let expected = lines_vec!("1. One", "", " Indented", "2. Next"); + assert_eq!(renumber_lists(&input), expected); +} + +#[test] +fn no_restart_for_non_plain_line() { + let input = lines_vec!("1. One", "", "# Heading", "3. Next"); + let expected = lines_vec!("1. One", "", "# Heading", "2. Next"); + assert_eq!(renumber_lists(&input), expected); +} + +#[test] +fn restart_after_nested_paragraph() { + let input = lines_vec!("1. One", " 1. Sub", "", "Paragraph", "3. Next"); + let expected = lines_vec!("1. One", " 1. Sub", "", "Paragraph", "1. Next"); + assert_eq!(renumber_lists(&input), expected); +} From 19e5f93ff8dfad7ee23c5390881278e9bb47ba03 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 17 Jul 2025 02:00:38 +0100 Subject: [PATCH 3/4] Refactor paragraph restart logic --- src/lists.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/lists.rs b/src/lists.rs index 6e03333c..0977e1fb 100644 --- a/src/lists.rs +++ b/src/lists.rs @@ -40,12 +40,28 @@ pub fn pop_counters_upto(counters: &mut Vec<(usize, usize)>, indent: usize) { } } +fn handle_paragraph_restart(line: &str, prev_blank: bool, counters: &mut Vec<(usize, usize)>) { + let indent_end = line + .char_indices() + .find(|&(_, c)| !c.is_whitespace()) + .map_or_else(|| line.len(), |(i, _)| i); + let indent = indent_len(&line[..indent_end]); + + if prev_blank + && counters + .last() + .is_some_and(|(d, _)| indent <= *d && is_plain_paragraph_line(line)) + { + pop_counters_upto(counters, indent); + } +} + #[must_use] pub fn renumber_lists(lines: &[String]) -> Vec { let mut out = Vec::with_capacity(lines.len()); let mut counters: Vec<(usize, usize)> = Vec::new(); let mut in_code = false; - let mut prev_blank = true; + let mut prev_blank = lines.first().is_none_or(|l| l.trim().is_empty()); for line in lines { if is_fence(line) { @@ -85,13 +101,7 @@ pub fn renumber_lists(lines: &[String]) -> Vec { .map_or_else(|| line.len(), |(i, _)| i); let indent_str = &line[..indent_end]; let indent = indent_len(indent_str); - if prev_blank - && counters - .last() - .is_some_and(|(d, _)| indent <= *d && is_plain_paragraph_line(line)) - { - pop_counters_upto(&mut counters, indent); - } + handle_paragraph_restart(line, prev_blank, &mut counters); drop_deeper(indent, &mut counters); out.push(line.clone()); prev_blank = line.trim().is_empty(); From baa2d4b9a0ef7dbad33facc5e89ef8931d34abf8 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 17 Jul 2025 02:12:50 +0100 Subject: [PATCH 4/4] Document counter popping helper --- src/lists.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lists.rs b/src/lists.rs index 0977e1fb..fbf2ac1a 100644 --- a/src/lists.rs +++ b/src/lists.rs @@ -33,7 +33,14 @@ fn is_plain_paragraph_line(line: &str) -> bool { .is_some_and(char::is_alphanumeric) } -#[doc(hidden)] +/// Remove counters deeper than or equal to `indent`. +/// +/// ``` +/// use mdtablefix::lists::pop_counters_upto; +/// let mut counters = vec![(0usize, 1usize), (4, 2), (8, 3)]; +/// pop_counters_upto(&mut counters, 4); +/// assert_eq!(counters, vec![(0, 1)]); +/// ``` pub fn pop_counters_upto(counters: &mut Vec<(usize, usize)>, indent: usize) { while counters.last().is_some_and(|(d, _)| *d >= indent) { counters.pop();