Skip to content

fix(docx): name in the report what a paragraph's own fields lose - #862

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-report-paragraph-zone-losses
Oct 6, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-report-paragraph-zone-losses

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Why

DocxExportReport promises to name every loss. For a paragraph's own fields it named none, while the Word file lost these:

  • autoSize. The page fits an auto-sized paragraph's text to the largest size on its grid, from its maximum down, at which it takes one line, or to its minimum where none does. That size can be above its style's. The file writes the style's size.
  • bulletOffset.
    • A prefix with letters in it is drawn before the first line (FIRST_LINE, ALL_LINES) and never written.
    • Some paths write no prefix at all, a blank one included, which the body path writes as the indent: a paragraph written over the flow, one side of an overlay's left-and-right pair, a badge's initials.
  • bookmarkOptions. A bookmark becomes Word's HeadingN, which Word's outline lists by the text of its Word paragraph.
    • The title the PDF's outline shows is not in the file.
    • Levels past the ninth are clamped to it, so two of them become one level.
    • A line an overlay's pair shares is one Word paragraph. Word lists both sides' text under one level: the left side's where it has one, else the right side's. Where both have one, the right side's entry is lost.

DocxNodeFieldLedgerTest listed autoSize, bulletOffset and bookmarkOptions as gaps — the latter two for their letters and title alone.

What changed

  • paragraphLost(node, roomLost, listedAs) names the losses.
    • Every path that writes a paragraph in the body calls it, and reports a ParagraphNode note where something is lost.
    • The caller says whether the path leaves out the prefix's room (roomLost), and which text Word's outline lists the heading by (listedAs, null where the path writes no level).
    • The calls:
      • writeParagraph: "written as a paragraph"; it writes a blank prefix as the indent, and the heading is listed by its own text.
      • writeLinePair: "written as one side of a line it shares".
        • The left side starts where its line starts, prefix and all, so its room is lost wherever the page lays out a prefix.
        • The right side's room is lost only where a left tab holds its start (fromItsStart); a right tab holds its end where the page ends it.
        • The side applyHeadingRole gives the line's level to is listed by both sides' text.
      • writeTextBadge: "written as its badge's text".
      • writeTextOverTheFlow: the losses are added to its existing "laid over the flow…" note. For the badge and the text box, the room is lost unless the paragraph is one line set from the end away from its prefix (roomLostInItsBox): aligned right, or aligned left right to left, whose prefix stands at the right. Over more lines than one, Word breaks the lines without the room, and they take more words.
    • A page zone's paragraphs are written apart; they stay a gap of the zones option.
  • The phrases:
    • "its text is written at 24pt, where the page fits it to 19.5pt". The written size is the one the file holds, to Word's half point, and the note is written only where the fitted size is another. Where the lines are not read or do not tell the fitted size, the phrase says it "is not measured".
    • "its bulletOffset's letters, "•", are not written before its first line": on any path, for a strategy that sets the prefix before a first line, where that line holds something once the control characters the page drops are dropped.
    • "the room its bulletOffset sets its lines in by is not written": where the path loses the room, and the page lays out the prefix before a first line that holds something or before a later line that holds something (an empty line takes none).
    • "its outline entry shows "…", not its title "…"": white space, no-break spaces among it, collapsed on both sides, as Word shows a heading.
    • "its outline entry is written at Word's ninth level, which it shares with a level the page nests apart from it": a level past the ninth, where the document declares another from the ninth on. One deep level alone is nested one step down by both outlines.
    • "its outline entry is not written": the right side of a pair beside a left heading. The text box and the badge pass no level too, but hold no paragraph with an outline entry.
  • The fitted size is read from the layout, not computed again. The fragment's textStyle is the paragraph's authored one; the size the page fits it to is on the text spans.
    • A run with a style of its own is laid out at that style's size. So the paragraph's size is a span size no such run has, or the one size the lines hold.
    • Text in the paragraph's style counts: its plain text, a run without a style, and a prefix the page lays out. The page sets the prefix in the paragraph's style at the fitted size, while the indent is measured at the style's.
    • A paragraph whose runs all carry their own style, and that lays out no prefix, loses nothing to autoSize.
  • Ledger.
    • autoSize, bulletOffset and bookmarkOptions move from a gap to REPORTED.
    • The zones gap now names a zone paragraph's prefix, fitted size and outline entry. A zone line writes none of them.
    • 4 node-field gaps remain.
  • Docs. These now say what the note names, and that a page zone's paragraphs are not named yet:
    • the capability matrix's paragraph row;
    • the recipe's Paragraphs row, its outline paragraph, and its pair paragraph, which said each side keeps its outline level;
    • render-docx/README.md;
    • the CHANGELOG.

Verification

  • ./mvnw -B -ntp install -pl :graph-compose-render-docx → BUILD SUCCESS: 1080 tests, 0 failures, 1 skipped (the property-gated fidelity probe).
  • DocxParagraphReportTest is new, with 9 tests.
    • Named:
      • autoSize:
        • text fitted smaller than its style;
        • a short line fitted larger;
        • a run without a style after one with its own, at another size and at the fitted size;
        • on a side of a pair;
        • a prefix laid out at the fitted size beside runs that all keep their own, read from the prefix's own span;
        • with no layout, not measured;
      • bulletOffset:
        • letters with FIRST_LINE and ALL_LINES, and with FIRST_LINE in runs;
        • letters and room on a side of a pair and in a text box;
        • room for a blank prefix:
          • on the left side of a pair;
          • on a right side held by its start;
          • on a right-aligned right-to-left text box;
          • on a right-aligned text box of more lines than one;
          • on a badge;
      • outline:
        • a title that is not the text, in plain text and in runs;
        • level 9 beside level 8;
        • a pair's line listed by both sides' text, under the left side's level or the right side's;
        • the right side's entry beside a left heading.
    • Not named:
      • autoSize:
        • fitted at its own size;
        • not auto-sized, with a layout and without;
        • every run in a style of its own;
        • 10.3 and 10.5, one size to Word;
      • bulletOffset:
        • letters before the wrapped lines (FROM_SECOND_LINE);
        • a blank prefix on the body path;
        • a first line ended at once, in plain text and in runs, or holding only a character the page drops;
        • NONE;
        • a pair side's FROM_SECOND_LINE on one line;
        • a prefix before a right side held by its end, aligned right or centred;
        • a left-aligned right-to-left text box;
        • an empty line after the first;
      • outline:
        • a title equal to the text, white space collapsed on either side, a no-break space among it;
        • a title equal to the text of all of its runs;
        • level 9 alone under level 0;
        • a pair's title equal to the whole line.
  • The tests fail without the code they cover. 38 sabotages were run, each breaking one thing, and each made the tests that cover it fail:
    • every path's note: the body path, a pair's left side, the text box, the badge;
    • the size comparison: never, finer than Word's half point, and the written size unrounded;
    • the own-style text check, and the prefix counted as such;
    • the own-run sizes left out;
    • the one-size rule;
    • the not-measured phrase;
    • a paragraph that is not auto-sized;
    • the strategies that set a first line;
    • the empty first line: plain, in runs, and holding a dropped character;
    • an empty later line;
    • blank letters;
    • a one-line pair side;
    • the pair's room:
      • a left side as indent;
      • a right side's start ignored;
      • a right side's start never;
    • the box's room:
      • the alignment ignored;
      • the lines ignored;
      • right to left read as left to right;
    • the title comparison:
      • never;
      • its white space on either side;
      • its no-break space;
      • its runs;
    • a pair listed by one side;
    • the body path's outline written as none;
    • the ninth level: off by one, or named alone;
    • the right side's level, always and never;
    • the unwritten outline phrase.
  • The DOCX bytes do not change. The 62 corpus documents exported deterministically are byte-identical to the export before the change: DocxFidelityCorpusTest -Dgraphcompose.docxFidelity=export, SHA-256 per file, 0 of 62 differ.
  • Across the corpus the report names no paragraph; there are 950 notes, as before. EngineeringResume's auto-sized name ("JORDAN RIVERA") fits at its own 24.5pt. The proposals' blank prefixes are written as indents.
  • Documentation guards:
    • core: -pl :graph-compose-core -Dtest='com.demcha.documentation.**' → 166 tests, 0 failures;
    • qa: the documentation guards plus DocxPageZoneTest, DocxTransparentWrapperTest, TimelineRailAcrossBackendsTest and RtlAcrossBackendsTest → 50 tests, 0 failures.
  • The full reactor gate was not run; no public signature, POM or workflow file changed.

Known limits

  • A size shared by a run with its own style can hide the fitted one. Suppose the paragraph's own text and runs with their own styles are laid out at more than one size, and every one of those sizes is a run's own. Then the fitted size is not read, and the note says it is not measured, even where it equals the style's.
  • A one-line markdown heading is laid out at a multiple of the fitted size. The note gives the size the page sets it in.
  • Two edge cases of white space:
    • A blank prefix before a first line of spaces alone is named on a path that writes no prefix, though the page's line holds nothing.
    • A run of spaces alone, with no style of its own, among runs that have one, is not counted as text in the paragraph's style.
  • A page zone's paragraphs are still a gap, recorded on the zones output option.

Lane: render-docx backend (report only, no change to what is written) plus tests and docs.

An auto-sized paragraph was written at its style's size, a bulletOffset
prefix's letters were never written and some paths wrote no prefix at all,
and Word's outline listed a heading by its paragraph's text, not by its
bookmark title. The paragraph's note now names the size the page fits
its text to, the prefix's letters and the room a path leaves out, and an
outline title, level or pair-side entry the file does not carry. The
written bytes do not change.
…s line

A pair's left side starts where its line does, prefix and all, and its
right side loses the room only where a left tab holds its start; a text
box or a badge loses it unless it is one line set from the end away from
its prefix. A level past Word's ninth is named only where it shares the
ninth with another. The written size is the one Word holds, to the half
point; control characters and no-break spaces are read as the page reads
them, and an empty later line takes no prefix.
@DemchaAV
DemchaAV merged commit 5e0aeaa into 2.5-dev Oct 6, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-report-paragraph-zone-losses branch October 6, 2026 17:38
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