fix(docx): strip hardcoded numbering prefix from list item text - #80
fix(docx): strip hardcoded numbering prefix from list item text#80Risk-hao wants to merge 3 commits into
Conversation
OOXML numbered lists store the rendered prefix (e.g. "1. ") in w:t text runs while also carrying the numbering model in w:numPr. AnyDoc previously kept both, causing render_list to output "1. 1. text" instead of "1. text". Add strip_list_num_prefix() called in emit_paragraph's ParaKind::ListItem branch to remove the redundant prefix from paragraph inlines before they enter the ListEntry. The function: - Skips bullets (unordered lists never hardcode a prefix) - Matches the expected label or marker.label(number) against the text - Requires a separator (space/tab) after the prefix to avoid false matches like "1.text" as content - Handles prefix text split across multiple inline runs All existing 222 tests pass with no regression on snapshot fixtures.
num_text.len() >= 1 for ordered markers and sep is always 0 or 1, so strip >= 1 always holds. Remove the dead early-return check.
There was a problem hiding this comment.
1 issue found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/formats/docx/content.rs">
<violation number="1" location="src/formats/docx/content.rs:201">
P2: A hard break can be silently removed from list content because `Inline::LineBreak` is treated as transparent while matching the prefix. Stopping the scan at a line break preserves source line boundaries and prevents matching a prefix across it.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| flat.push_str(text); | ||
| lead += 1; | ||
| } | ||
| Inline::LineBreak | Inline::Anchor(_) => lead += 1, |
There was a problem hiding this comment.
P2: A hard break can be silently removed from list content because Inline::LineBreak is treated as transparent while matching the prefix. Stopping the scan at a line break preserves source line boundaries and prevents matching a prefix across it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/formats/docx/content.rs, line 201:
<comment>A hard break can be silently removed from list content because `Inline::LineBreak` is treated as transparent while matching the prefix. Stopping the scan at a line break preserves source line boundaries and prevents matching a prefix across it.</comment>
<file context>
@@ -172,11 +172,90 @@ fn collect_blocks(
+ flat.push_str(text);
+ lead += 1;
+ }
+ Inline::LineBreak | Inline::Anchor(_) => lead += 1,
+ _ => break,
+ }
</file context>
| Inline::LineBreak | Inline::Anchor(_) => lead += 1, | |
| Inline::Anchor(_) => lead += 1, | |
| Inline::LineBreak => break, |
There was a problem hiding this comment.
not a bug. Scanning skips LineBreak in flat but the
prefix still matches correctly ("1. text" starts with "1. "), and Fix 3
explicitly pushes LineBreak back into head, so it's preserved.
1. Use paragraph position instead of blocks.first_mut() to handle
non-Paragraph items (e.g., Rule) appearing before Paragraph in
pieces_into_blocks output.
2. Remove Some("") exact match arm — when paragraph text equals the
prefix exactly (e.g. "1." with no following content), it is real
content, not a hardened prefix to strip.
3. Preserve Anchor and LineBreak inlines during prefix drain instead
of consuming them, since they carry structural meaning (intra-doc
link targets, line boundaries).
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/formats/docx/content.rs">
<violation number="1" location="src/formats/docx/content.rs:191">
P2: The new first-paragraph lookup can target the wrong paragraph when a preceding text box is present. Because text-box content is converted into `Block::Paragraph` (via `walk_drawing` -> `parse_blocks`, and `pieces_into_blocks` always produces a Paragraph), a list item ordered as `[textbox attachment, item text]` yields `[Paragraph(textbox…), Paragraph("1. text")]`, and `position(Paragraph)` matches the text box's paragraph. That both skips stripping the item's real prefix (so the stated text-box-first goal isn't met) and risks stripping text-box content whose first line coincidentally starts with the resolved label. Consider locating the paragraph whose leading inlines actually match the expected prefix, or tracking which blocks derive from the item's own runs vs. text-box attachments, rather than taking the first Paragraph.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| // A list item's pieces may start with block attachments (text boxes, | ||
| // rules, ...) before the paragraph carrying the inline text, so locate | ||
| // the first paragraph rather than assuming it is blocks[0]. | ||
| let para_idx = blocks.iter().position(|b| matches!(b, Block::Paragraph(_))); |
There was a problem hiding this comment.
P2: The new first-paragraph lookup can target the wrong paragraph when a preceding text box is present. Because text-box content is converted into Block::Paragraph (via walk_drawing -> parse_blocks, and pieces_into_blocks always produces a Paragraph), a list item ordered as [textbox attachment, item text] yields [Paragraph(textbox…), Paragraph("1. text")], and position(Paragraph) matches the text box's paragraph. That both skips stripping the item's real prefix (so the stated text-box-first goal isn't met) and risks stripping text-box content whose first line coincidentally starts with the resolved label. Consider locating the paragraph whose leading inlines actually match the expected prefix, or tracking which blocks derive from the item's own runs vs. text-box attachments, rather than taking the first Paragraph.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/formats/docx/content.rs, line 191:
<comment>The new first-paragraph lookup can target the wrong paragraph when a preceding text box is present. Because text-box content is converted into `Block::Paragraph` (via `walk_drawing` -> `parse_blocks`, and `pieces_into_blocks` always produces a Paragraph), a list item ordered as `[textbox attachment, item text]` yields `[Paragraph(textbox…), Paragraph("1. text")]`, and `position(Paragraph)` matches the text box's paragraph. That both skips stripping the item's real prefix (so the stated text-box-first goal isn't met) and risks stripping text-box content whose first line coincidentally starts with the resolved label. Consider locating the paragraph whose leading inlines actually match the expected prefix, or tracking which blocks derive from the item's own runs vs. text-box attachments, rather than taking the first Paragraph.</comment>
<file context>
@@ -185,9 +185,12 @@ fn strip_list_num_prefix(
+ // A list item's pieces may start with block attachments (text boxes,
+ // rules, ...) before the paragraph carrying the inline text, so locate
+ // the first paragraph rather than assuming it is blocks[0].
+ let para_idx = blocks.iter().position(|b| matches!(b, Block::Paragraph(_)));
+ let Some(para_idx) = para_idx else { return };
+ let Block::Paragraph(inlines) = &mut blocks[para_idx] else { unreachable!() };
</file context>
There was a problem hiding this comment.
this is pre-existing, not from Fix 1. The
old blocks.first_mut() and the new position(Paragraph) have the same
behavior when a textbox Paragraph comes first. Fix 1 only deals with
non-Paragraph blocks (Rule, Image) blocking the item. Disambiguating
textbox paragraphs would be a separate change.
Programmatic DOCX generators (python-docx, docx.js, agent-written
reports) are common today, and they typically emit both w:numPr and the
rendered prefix in w:t runs. Result on the page: "1. 1. text" instead
of "1. text". AnyDoc previously honored that double encoding, so
Markdown/HTML downstream looked broken.
This strips the hardcoded prefix from list item text. The numPr model
is preserved, so ordered lists still render correctly.
All tests pass (222).