Repository navigation
fix(docx): one bookmark per anchor, no control characters, and margins below zero written - #911
Merged
Merged
Conversation
…s below zero written
A bookmark name a suffix made for one anchor, "Section_1_2" for
"Section-1", was given again to the anchor that cleans to it, "Section 1
2", so two links went to one place: a name is now taken only once it is
free. An anchor two nodes declare was a bookmark on each, one name
twice, where Word goes to the first and the page to the last: the file
keeps the last it writes, and the report names the anchor, counted on
the nodes the layout registers it on.
A control character in text, a bell or a tab, which the page leaves
out, was written, most of them as a question mark ("Total? due"). It is
taken out of every run of the body, headers and footers, with a space
it leaves at an edge kept, and out of a badge's initials, a list
level's marker and a picture's description where they are written.
A margin below zero on a paragraph, a list or a block round them was
written as none. It is an indent below zero in the flow, as far as the
page sets the text; in a table cell, a panel's included, the side is
held where the cell's text stands and named.
The corner-radius note said a panel kept its fill where it had only a
border; it names what the panel paints.
A markdown test's tab expectation follows the page, which draws no tab.
DemchaAV
marked this pull request as draft
October 16, 2026 18:29
… take out every character the page leaves out In a table cell the paragraph's or list's own margin below zero was held at zero, not the side it made with the blocks round it: 12pt taken back from a block's 20pt of padding stood at 20, named as held at the cell's text, and a block's own margin below zero there was dropped with no note. The running side is held at the cell's text now, and the note names what that holds back. A row's first child the page sets out past the row's left edge starts at it, and is named; one only drawn, an icon, is drawn where the page draws it and is not. The last of a name's bookmarks is the last in the file, whose order is the page's: a block's opens before the nodes inside it, though written after them, so a block and a paragraph inside it sharing an anchor keep the paragraph's, where the page goes. The text pass took out only the controls; the page leaves out every character of Unicode's category C but the direction marks and joiners, so a soft hyphen, a zero-width space, a noncharacter and a private-use one go too, and a barcode's description is cleaned where it is written.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Four DOCX text and bookmark losses, none of them reported:
DocxBookmarkNamesgave "Section-1" the nameSection_1_2after "Section 1", and then gave the sameSection_1_2to the anchor "Section 1 2", which cleans to it: two links went to one place. An anchor two nodes declare was a bookmark on each, one name twice — Word goes to the first, the page (PageIndex, its links) to the last.writeParagraphandwriteListheld a side's margin atMath.max(0, …): a paragraph the page sets 12pt into the left margin (x = 8) stood at the margin in Word (x = 20), its lines broken elsewhere.What changed
DocxBookmarkNames.minttakes a name only once it is free (case-insensitive, as Word reads it): "Section 1 2" now getsSection_1_2_2. Names that do not collide are as before.keepTheLastBookmarkOfEachAnchor, after the body and before the page references are settled, keeps each name's last bookmark in the file — whose order is the page's: a block's bookmark opens at its first paragraph, before those inside it — and takes the earlier starts and ends out by id. It names the anchor (APPROXIMATED,anchor) wherever the layout registers it on two node paths or more —DocxLayoutMetrics.anchorsDeclared, distinct non-zone paths ofAnchorMarkerPayload(a repeated table header's re-drawn marker carries one path: measured, five markers, one path; a table's composed cells carry their table's) — which also covers a last declaration the file writes no bookmark for (a drawn shape's).DocxTextControls(new): at the end of the export, everyw:tof the body, headers and footers loses what the page leaves out — category C but the formatting controls (TextControlSanitizer.isFormattingControl) and\n/\r— withxml:space="preserve"where the kept text starts or ends with a space. Headers and footers are read off the document's relations: a part a header-footer policy creates is in no list of the document's until the file is read back. Text written as XML — a badge's initials, a list level'slvlText, a picture's and a barcode'sdescr— is cleaned where it is written (kept), since XMLBeans writes the question mark at serialisation.applyInsetwrites a negative right side there). In a table cell — a panel's included — the paragraph or list holds its running side, its own with the blocks', no further out than the cell's text: 12pt taken back from a block's 20pt of padding is written as 8; what is held back is named ("a margin below zero, its own or a block's round it, is not written past its table cell's text"), unless a line of one unbroken word stands out byletTheLineStandOut. A placed row's first child the page sets out past the row's left edge starts at the edge, named (reportTheFirstChildPulledOut), unless it is only drawn — an icon, drawn where the page draws it.keptOfnames what a rounded panel keeps — fill, border (a side whose stroke has a width, aspaintCellSidepaints it), both, or neither.Verification
2.5-devfirst, then the fix, in Word (COM export) and LibreOffice (Windows):margin(0,0,0,-12): page x 8 → Word 20 before, 8 after; LO 8.1; lines break where the page breaks them (before: a word sooner). Right-12: Word line ends at 290.8, LO 290.9, page 286.9 (Word's face).DocxTextControlsTest(7),DocxNegativeMarginTest(13: each side in the flow, a block's right, a list, in a panel its own and a block's, a block's padding taken back, a list in a panel, a standing word, a row's first child and its first icon, a picture in a panel's block),DocxLinkTest+6 (suffix collision in written order, last of two kept, a block's bookmark taken out whole, a block and a paragraph inside sharing an anchor, a shape's last declaration named, a split panel marked where it starts),DocxExportReportTest+1 (border only, both, zero-width sides).DocxSessionMarkdownTestexpected "a_b\tc" to keep its tab; the page draws no tab (TextControlSanitizerviaTextFlowSupport/ParagraphWrapping), so it now expects "a_b cd".applyInset,keptOf's fill, border and zero width): all caught../mvnw -B -ntp clean verify→ BUILD SUCCESS (render-docx 1706, qa 1820).Lane: shared semantic backend (
render-docx), report text and docs.Notes for review: measured on a panel (padding 12) by hand-written indents, Word and LibreOffice set a negative indent in a table cell as the page does, past the cell's side too (−6pt → x 26 as the page; −18pt → x 14 against the side at 20); writing it there means letting a cell's indent go below zero, which
leftIndentTwipsholds at zero for a row's hang, and is left to its own change — here it is named.Not changed here: an anchor in a repeated table header row is one bookmark, which Word reads on the header's first page, where the page sends a link to its last repetition; and a table's composed cells share their table's path in the layout, so one anchor in two of them counts as one node.