Skip to content

fix(docx): indent a paragraph's lines by its bulletOffset, as the page sets them - #818

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-bullet-hanging-indent
Oct 2, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-bullet-hanging-indent

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Why

A paragraph's bulletOffset is text the page sets before its first line, its wrapped lines or both, as indentStrategy says. It is not part of the paragraph's text, and the DOCX export did not write it at all.

EditorialProposal's bullets are a dot, three spaces and the text, with bulletOffset(" ") and FROM_SECOND_LINE, so the page sets each wrapped line after a prefix of three spaces in the paragraph's style, under the text. In Word those lines started under the dot, 11.6pt left of the page's: "mobile" at 292.9pt where the page has 304.5, "project" at 388.8 where it has 400.3.

What changed

  • indentAsThePrefixDoes. A prefix of spaces is only a distance, so it is written as one. It is measured in the paragraph's style, not its runs', with the fonts the layout measured with (styleWidth); that is the style the page draws the prefix in.
    • FROM_SECOND_LINE: left indent and an equal hanging indent.
    • FIRST_LINE: first-line indent.
    • ALL_LINES: left indent.
    • It adds to the indent the containers around the paragraph already give it.
    • It is written after a right-to-left paragraph's sides are turned, on w:left, which is the start of the flow in either direction and the side a hanging indent is measured from.
  • A prefix with letters in it. The page draws its letters only before the first line, and sets wrapped lines after as many spaces as cover the prefix and the space that ends it (ParagraphWrapping.computeIndentFromPrefix). spacesCovering counts them the same way, so the wrapped lines get that indent. The letters are text the export does not write, so the first line is not moved for them.
  • styleMetrics(). The measured-font metrics, built on first use, are now shared by styleLineHeight and styleWidth.
  • Baselines. word-windows and libreoffice-windows are rewritten for EditorialProposal, the one document whose numbers this moves.
  • Docs. CHANGELOG.md, the Paragraphs row of docs/recipes/docx-export.md, and the paragraph row of docs/architecture/backend-capability-matrix.md.

Verification

  • Template corpus, re-measured in full against 2.5-dev's baselines. Both gates pass, and no other document's numbers change in either editor.

    Editor Document Lines found Past 2pt Median p90
    Word 16.0.20430 EditorialProposal 128 → 130 of 132 19 → 21 0.50pt 2.09 → 2.16pt
    LibreOffice (Windows) EditorialProposal 127 → 129 of 132 70 → 72 43.26 → 43.12pt 45.15pt
  • Wrapped lines in Word now start where the page starts them: "mobile" at 304.49 against 304.5, and "project" at 400.39 against 400.31.

  • NorthlineProposal's wrapped bullets move as well. In Word they start at 387.91 against the page's 387.81. Its line breaks already matched the page, and the gate measures where lines break and how far they drift down, not their x, so its numbers do not change.

  • The two newly found lines break where the page breaks them now. They are "transfers to Astera Health Co. after" and "full payment is received.", and both stand about 4pt low in Word. That is drift the terms list already had, growing down the list: "50%" is 1.1pt low and "major" 2.7pt. NorthlineProposal's list drifts the same way. "Final files and handoff delivered upon" is still not found, because Word breaks its first line before "upon"; that line has no prefix, and the miss is older than this change.

  • New test class, DocxGeneratedIndentTest, ten tests. Three Helvetica spaces at 10pt are 167 twips.

    • Wrapped lines get left 167 and hanging 167.
    • A first-line prefix gets firstLine 167.
    • An all-lines prefix gets left 167.
    • Inside a section padded 20pt, left is 400 + 167.
    • A right-to-left paragraph gets left 167 (the flow's start) and hanging 167, with no right. In a container padded 20pt it keeps the padding on the right as well (400).
    • A prefix measured in a 14pt paragraph style under a 6pt run is 234 twips.
    • A "-" prefix with FROM_SECOND_LINE or ALL_LINES gives left 167 and hanging 167: "- " is 2.2 spaces, so three. The layout's wrapped line starts with exactly three spaces.
    • With FIRST_LINE a "-" prefix writes no indent, and neither does NONE.
  • Each rule fails the tests when taken out.

    • Without the call, the six tests that expect an indent fail.
    • Measuring a lettered prefix itself instead of the spaces that cover it fails both lettered-prefix tests.
    • Written before the sides are turned, the right-to-left test fails.
  • Full reactor gate: ./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose-qa,:graph-compose-coverage -am gives BUILD SUCCESS. render-docx runs 804 tests, qa 1814.

Known limits

  • The letters of a bulletOffset are still not written, and a first line is not moved for them.
  • Paragraphs written over the flow, as one of an overlay's left-and-right pair, or in a header or footer, do not get the indent yet.
  • An auto-sized paragraph's prefix is measured at the size the export writes its text in, not the size the page fits it to.

Lane: shared-engine (render-docx). No public API change.

@DemchaAV DemchaAV changed the title fix(docx): indent a paragraph's lines by its blank bulletOffset, as the page sets them fix(docx): indent a paragraph's lines by its bulletOffset, as the page sets them Oct 2, 2026
@DemchaAV
DemchaAV merged commit 715f150 into 2.5-dev Oct 2, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-bullet-hanging-indent branch October 2, 2026 14:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant