Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/lists.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,61 @@ 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)
}
Comment on lines +29 to +34
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Add documentation for the helper function.

Add a doc comment explaining the function's purpose and behaviour.

+/// Determines whether a line represents a plain paragraph by checking if it starts
+/// with an alphanumeric character after any leading whitespace.
 fn is_plain_paragraph_line(line: &str) -> bool {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn is_plain_paragraph_line(line: &str) -> bool {
line.trim_start()
.chars()
.next()
.is_some_and(char::is_alphanumeric)
}
/// Determines whether a line represents a plain paragraph by checking if it starts
/// with an alphanumeric character after any leading whitespace.
fn is_plain_paragraph_line(line: &str) -> bool {
line.trim_start()
.chars()
.next()
.is_some_and(char::is_alphanumeric)
}
🤖 Prompt for AI Agents
In src/lists.rs around lines 29 to 34, the helper function
is_plain_paragraph_line lacks documentation. Add a doc comment above the
function that clearly explains its purpose, which is to check if a line starts
with an alphanumeric character after trimming leading whitespace, indicating it
is a plain paragraph line.


/// 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();
}
}

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);
}
}
Comment on lines +50 to +64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Add documentation for the extracted logic.

Excellent work extracting the complex paragraph restart logic as suggested in past reviews. Add documentation to explain the function's purpose.

+/// Handles resetting list counters when a paragraph line follows a blank line
+/// at or before the indentation level of the last counter.
 fn handle_paragraph_restart(line: &str, prev_blank: bool, counters: &mut Vec<(usize, usize)>) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
}
/// Handles resetting list counters when a paragraph line follows a blank line
/// at or before the indentation level of the last counter.
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);
}
}
🤖 Prompt for AI Agents
In src/lists.rs around lines 43 to 57, the function handle_paragraph_restart
lacks documentation explaining its purpose. Add a clear doc comment above the
function describing that it handles restarting paragraphs based on indentation
and blank line context, detailing the role of its parameters and the effect on
the counters vector.


#[must_use]
pub fn renumber_lists(lines: &[String]) -> Vec<String> {
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 = lines.first().is_none_or(|l| l.trim().is_empty());

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;
}

Expand All @@ -58,6 +98,7 @@ pub fn renumber_lists(lines: &[String]) -> Vec<String> {
}
};
out.push(format!("{indent_str}{current}.{sep}{rest}"));
prev_blank = false;
continue;
}

Expand All @@ -67,8 +108,10 @@ pub fn renumber_lists(lines: &[String]) -> Vec<String> {
.map_or_else(|| line.len(), |(i, _)| i);
let indent_str = &line[..indent_end];
let indent = indent_len(indent_str);
handle_paragraph_restart(line, prev_blank, &mut counters);
drop_deeper(indent, &mut counters);
out.push(line.clone());
prev_blank = line.trim().is_empty();
}

out
Expand Down
26 changes: 26 additions & 0 deletions tests/data/renumber_paragraph_restart_expected.txt
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions tests/data/renumber_paragraph_restart_input.txt
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = include_str!("data/renumber_paragraph_restart_input.txt")
.lines()
.map(str::to_string)
.collect();
let expected: Vec<String> = 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"]
Expand Down
56 changes: 56 additions & 0 deletions tests/lists.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use mdtablefix::{lists::pop_counters_upto, renumber_lists};

macro_rules! lines_vec {
($($line:expr),* $(,)?) => {
vec![$($line.to_string()),*]
};
}
Comment on lines +3 to +7
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Extract the macro into a test utility module.

The lines_vec! macro is useful but should be moved to a shared test utilities module to promote reusability across test files.

Apply this refactor:

-macro_rules! lines_vec {
-    ($($line:expr),* $(,)?) => {
-        vec![$($line.to_string()),*]
-    };
-}

Create tests/common/mod.rs:

#[macro_export]
macro_rules! lines_vec {
    ($($line:expr),* $(,)?) => {
        vec![$($line.to_string()),*]
    };
}

Then import it:

+mod common;
 use mdtablefix::{lists::pop_counters_upto, renumber_lists};
+use common::lines_vec;
🤖 Prompt for AI Agents
In tests/lists.rs around lines 3 to 7, the lines_vec! macro should be moved to a
shared test utility module for reuse. Create a new file tests/common/mod.rs and
define the lines_vec! macro there with #[macro_export]. Then, in tests/lists.rs
and other test files, import the macro by adding `#[macro_use] extern crate
common;` or the appropriate import statement to use lines_vec! from the common
module.


#[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)]);
}
Comment on lines +9 to +21
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add edge case tests for pop_counters_upto.

The unit tests cover basic scenarios but miss important edge cases that could reveal bugs.

Add these additional test cases:

#[test]
fn pop_counters_empty_vec() {
    let mut counters = vec![];
    pop_counters_upto(&mut counters, 0);
    assert_eq!(counters, vec![]);
}

#[test]
fn pop_counters_exact_match() {
    let mut counters = vec![(0usize, 1usize), (4, 2)];
    pop_counters_upto(&mut counters, 4);
    assert_eq!(counters, vec![(0, 1)]);
}
🤖 Prompt for AI Agents
In tests/lists.rs around lines 9 to 21, add two new unit tests for
pop_counters_upto to cover edge cases: one test with an empty counters vector to
ensure no changes occur, and another test where the indent exactly matches an
element in counters to verify correct removal behavior. Implement these tests by
initializing the counters vector accordingly, calling pop_counters_upto with the
specified indent, and asserting the expected resulting vector.


#[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);
}
Comment on lines +23 to +56
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use rstest parameterised tests to reduce duplication.

The integration tests follow a repetitive pattern that violates DRY principles. Replace with parameterised tests as mandated by the coding guidelines.

Apply this refactor:

use rstest::rstest;

#[rstest]
#[case(
    lines_vec!("1. One", "", "Paragraph", "3. Next"),
    lines_vec!("1. One", "", "Paragraph", "1. Next"),
    "restart_after_lower_paragraph"
)]
#[case(
    lines_vec!("1. One", "Paragraph", "3. Next"),
    lines_vec!("1. One", "Paragraph", "2. Next"),
    "no_restart_without_blank"
)]
#[case(
    lines_vec!("1. One", "", "  Indented", "3. Next"),
    lines_vec!("1. One", "", "  Indented", "2. Next"),
    "no_restart_for_indented_paragraph"
)]
#[case(
    lines_vec!("1. One", "", "# Heading", "3. Next"),
    lines_vec!("1. One", "", "# Heading", "2. Next"),
    "no_restart_for_non_plain_line"
)]
#[case(
    lines_vec!("1. One", "    1. Sub", "", "Paragraph", "3. Next"),
    lines_vec!("1. One", "    1. Sub", "", "Paragraph", "1. Next"),
    "restart_after_nested_paragraph"
)]
fn test_renumber_lists_scenarios(
    #[case] input: Vec<String>,
    #[case] expected: Vec<String>,
    #[case] _description: &str,
) {
    assert_eq!(renumber_lists(&input), expected);
}
🤖 Prompt for AI Agents
In tests/lists.rs from lines 23 to 56, the test functions for renumber_lists are
repetitive and violate DRY principles. Refactor by replacing the multiple
individual #[test] functions with a single parameterized test using the rstest
crate. Define a #[rstest] function with multiple #[case] attributes, each
containing the input vector, expected output vector, and a description string.
This will consolidate all test cases into one function that asserts equality for
each case, reducing duplication and improving maintainability.