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
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@
## Rust Specific Guidance

This repository is written in Rust and uses Cargo for building and dependency
management. Contributors should follow these best practices when working on the
project:
management. Contributors should follow these best practices when working on
the project:

- Run `make fmt`, `make lint`, and `make test` before committing. These targets
wrap `cargo fmt`, `cargo clippy`, and `cargo test` with the appropriate flags.
Expand Down Expand Up @@ -130,6 +130,8 @@ project:
- Replace duplicated tests with `#[rstest(...)]` parameterised cases.
- Prefer `mockall` for mocks/stubs.
- Prefer `.expect()` over `.unwrap()`.
- Use `concat!()` to combine long string literals rather than escaping newlines
with a backslash.

## Markdown Guidance

Expand Down
49 changes: 37 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ static BULLET_RE: std::sync::LazyLock<Regex> =
static NUMBERED_RE: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"^(\s*)([1-9][0-9]*)\.(\s+)(.*)").unwrap());

static FOOTNOTE_RE: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"^(\s*)(\[\^[^]]+\]:\s*)(.*)$").unwrap());

/// Parses a line beginning with a numbered list marker.
///
/// Returns the indentation prefix, separator following the number, and the
Expand Down Expand Up @@ -400,6 +403,26 @@ fn flush_paragraph(out: &mut Vec<String>, buf: &[(String, bool)], indent: &str,
}
}

fn append_wrapped_with_prefix(out: &mut Vec<String>, prefix: &str, text: &str, width: usize) {
use unicode_width::UnicodeWidthStr;

let prefix_width = UnicodeWidthStr::width(prefix);
let indent_str: String = prefix.chars().take_while(|c| c.is_whitespace()).collect();
let indent_width = UnicodeWidthStr::width(indent_str.as_str());
let wrapped_indent = format!("{}{}", indent_str, " ".repeat(prefix_width - indent_width));

for (i, line) in wrap_preserving_code(text, width - prefix_width)
.iter()
.enumerate()
{
if i == 0 {
out.push(format!("{prefix}{line}"));
} else {
out.push(format!("{wrapped_indent}{line}"));
}
}
}

/// Wraps text lines to a specified width, preserving markdown structure.
///
/// Paragraphs and list items are reflowed to the given width, while code blocks, tables, headers,
Expand Down Expand Up @@ -485,18 +508,20 @@ pub fn wrap_text(lines: &[String], width: usize) -> Vec<String> {
buf.clear();
indent.clear();
let prefix = cap.get(1).unwrap().as_str();
let rest = cap.get(2).unwrap().as_str().trim();
let spaces = " ".repeat(prefix.len());
for (i, l) in wrap_preserving_code(rest, width - prefix.len())
.iter()
.enumerate()
{
if i == 0 {
out.push(format!("{prefix}{l}"));
} else {
out.push(format!("{spaces}{l}"));
}
}
let rest = cap.get(2).unwrap().as_str();
append_wrapped_with_prefix(&mut out, prefix, rest, width);
continue;
}
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

if let Some(cap) = FOOTNOTE_RE.captures(line) {
flush_paragraph(&mut out, &buf, &indent, width);
buf.clear();
indent.clear();
let indent_part = cap.get(1).unwrap().as_str();
let label_part = cap.get(2).unwrap().as_str();
let prefix = format!("{indent_part}{label_part}");
let rest = cap.get(3).unwrap().as_str();
append_wrapped_with_prefix(&mut out, &prefix, rest, width);
continue;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down
26 changes: 26 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,32 @@ fn test_wrap_multiple_inline_code_spans() {
common::assert_wrapped_list_item(&output, "- ", 2);
}

#[test]
fn test_wrap_footnote_multiline() {
let input = vec![
concat!(
"[^note]: This footnote is sufficiently long to require wrapping ",
"across multiple lines so we can verify indentation."
)
.to_string(),
];
let output = process_stream(&input);
common::assert_wrapped_list_item(&output, "[^note]: ", 2);
}

#[test]
fn test_wrap_footnote_with_inline_code() {
let input = vec![
concat!(
" [^code_note]: A footnote containing inline `code` that should wrap ",
"across multiple lines without breaking the span."
)
.to_string(),
];
let output = process_stream(&input);
common::assert_wrapped_list_item(&output, " [^code_note]: ", 2);
}

#[test]
/// Verifies that short list items are not wrapped or altered by the stream processing logic.
///
Expand Down