Skip to content

fix(docx): end a page-margin section at a panel's cut - #963

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-panel-page-margin-sections
Oct 25, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-panel-page-margin-sections

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Why

A page that DocumentSession.pageMargins sets against other top and bottom margins than the page before it starts a Word section with them, where it starts between blocks of the body or among a body table's rows. A page that starts inside a panel started none and was named: Word set it with the margins in force. Since a panel whose table runs over pages is cut into a table a page, with a paragraph between the parts, there is a place for that section.

Measured in Word 16 and LibreOffice 24.2 on a card (400×600 page, 1pt border, 6pt padding) whose table of 60 rows runs over three pages:

Margins from page 2 Before, Word / LibreOffice Section at the cut, Word / LibreOffice
top 90 (36 on page 1) pages 2–3: every line 52.9pt high +1.0…+1.1pt / +1.0…+1.1pt
top and bottom 20 (36 on page 1) 5 pages / 4 pages, where the page sets 3 3 pages, every line on its page, +1.0…+1.1pt
top 90, bottom 60 on page 2 only page 2: 52.9pt high +1.0pt (pages 2 and 3)
bottom 90 on their pages on their pages, 0.1pt nearer

What is left is the cut's top border, which the report already names.

What changed

  • DocxPageMarginSections
    • PanelCut (new): a cut the export makes in a panel at the row of its table a page starts with — how high the part's last row on the page above ends, the room the part needs under it, and where the section's properties go.
    • plan takes the section's cuts, by the panel's table and the page each opens. A page that starts inside a panel cut there starts a section at the cut. The room is reckoned against the bottom margin Word sets the page above with — the margins in force, as for the hairline under a table. Where that page has room for the cut and not for the paragraph ending a section under the part as well, the page is named and the panel is cut as before.
    • apply hands a cut's section properties to the cut, which writes them when it is made.
  • DocxSemanticBackend
    • A section's margins are planned once it is written, while panels are cut once the document is. cutsOpeningSections decides the cuts of the section's panels when it is written, where its pages are set against more than one set of margins, and cutThePanelsAtTheirPages makes them as decided. A panel in a section of one set of margins, or one whose header or footer counts its own pages (which starts no Word section for a page's margins), is still decided once the document is written.
    • Decided early, a cut reads its rows before the page references are settled and the bookmarks kept: a row holding a page reference leaves the panel uncut at that row's page and every page before it, as any field does, though one whose anchor has no bookmark ends up as its number; so does a header row holding a bookmark the export later takes out. The recipe, the matrix and the CHANGELOG say so.
    • decideTheCuts holds what cutThePanelsAtTheirPages decided inline: it reads the rows in the table as written whole, which gives each later page the same row as the part left after the earlier cuts.
    • Where a section ends at a cut, the paragraph between the parts carries the section's properties and breaks no page before itself: the section's own break opens the page. It stands under the part above, so the section needs the paragraph's 0.1pt of room on that page beyond what the cut needs.
  • DocxTableSplit: NOT_REPEATED is built from the new DRAWINGS list. splitInItsPanel: the copy of the header rows a part after a cut opens with holds no drawing. Where a cut is decided, the header rows hold none (rowStarting); the export anchors a shape there only at the section's end — a shape of the table's first page beside a header's text, or one a page with no other paragraph leaves in its first cell's — and decided early, the cut comes before that. Such a shape is its page's, and stays in the first part.
  • Report: the page's page margin note says the section starts where the panel is cut, in place of "starts inside this block".
  • Docs: the recipe's per-page margins and panel Over pages paragraphs, the capability matrix's per-page margins and keep rows, the CHANGELOG.

ObsidianInvoice

Its pages from 2 on have a larger foot. Its cut where page 2 starts now ends a section, and the pages after it are set with page 2's margins. With its line items repeated 3 to 11 times, every line stands on its page in Word and LibreOffice 24.2, as before; page 2 stands 0.1pt nearer in Word.

With them repeated 12 times, page 5 starts between its info cards and its footer divider. With page 1's margins in force on pages 2 to 4, page 5's differed, and it opened a section, which fixed the break. Its margins are now page 2's, so the editors place that break themselves, and both set it as they do the same invoice with one set of margins on every page:

  • Word sets the info cards on page 5 with the divider, 17 lines lower: keepNext on the paragraphs inside the cards' cell keeps their table with the paragraph after it. With those taken off, Word holds all 121 lines on page 4.
  • Both editors drop the divider's 13pt of space above it at a page they break themselves.

Named, not fixed

  • Where a page a panel runs onto is not cut (its rows too near the foot) and a later one is, LibreOffice 24.2 sets the part after that cut with the earlier section's margins: it reads no section's margins after a table it broke over a page itself, whichever way the section paragraph is written. Word reads them. It was so before too, as no section was written there.
  • The room a cut itself needs on the page above is still reckoned against the bottom margin the page sets that page with. Where that page's own section is refused, Word sets it with the margins in force, and a smaller foot there than in force can leave Word too little room for the page's last rows. This predates the change; the section at the cut is reckoned against the margins in force.

Verification

  • Measured with probes (scratch, not committed) through the engine, Word 16 and LibreOffice 24.2: the table above, a section at page 3 after a plain cut at page 2 (every line on its page in both), ObsidianInvoice ×2 to ×12 against the base, and ×12 with one set of margins on every page.
  • Tests:
    • DocxPanelOverPagesTest (+12, 34):
      • a page with other margins starting at a cut ends a section in the paragraph between the parts, which breaks no page before itself and holds page 1's margins, the body page 2's; the cut is unchanged, and the next cut, at the same margins, breaks the page;
      • each cut where the margins change ends a section of its own;
      • a cut after one on pages of the same margins ends the section where they change;
      • where the page above has room for the cut and not for the paragraph ending a section, the panel is cut, the page breaks before the paragraph, and the margins are named;
      • a panel in a later section of several ends a Word section at its cut;
      • an anchored card cut nowhere leaves the pages it starts inside named;
      • a page reference whose anchor has no bookmark lets its page be cut where the margins do not vary; where they vary, the cuts are decided while it is a field, and its page is left whole and named; a panel of an earlier section of one set of margins, and one of a section whose footer counts its own pages, are decided once the document is written;
      • where page 2's own section is refused, page 3's is reckoned against the 20pt foot Word sets page 2 with, not the 4pt one the page sets it with, and named;
      • a section ending at a body table's split (page 2) and one at a panel's cut (page 3) in one document section are each written in place, with page 1's and page 2's margins.
    • DocxTableSplitTest (+1, 11): a panel's part after a cut copies its header rows without the drawing anchored in them, which the first part keeps. No export test reaches that route: section-end anchoring falls back to a cell's paragraph only on a page with no body paragraph, and every page a card opens on in the layouts tried had one.
    • InvoicePanelOverPagesDocxTest (qa, +1, 3): ObsidianInvoice ×3 and ×6 end one section, at the card's first cut, with page 1's bottom margin, the body page 2's; the parts are found after a paragraph that breaks the page or ends a section.
  • Fail-closed: 16 sabotages, each run alone, all caught: a header row holding a drawing split; the plan ignoring the cuts; a cut ending its section with the opening margins; the cut writing no section; the cut keeping its page break; no room for the paragraph ending a section; no room check at a cut; cuts decided early where the margins do not vary; the early decision lost; a later cut's rows read from the table's start; a part opening its page needing no room for its top; panels of an earlier section decided with this one; a cut read as a hairline carrier; a part copying its header rows with their drawings; cuts decided early in a section counting its own pages; the room reckoned against the page above as the page sets it.
  • Corpus: no document of the 62 has a panel the layout breaks over pages; every document is unchanged part for part, and so is the report (1445 notes).
  • Gate:
    • ./mvnw -B -ntp clean verify -pl <core, render-pdf, render-docx, render-pptx, templates, testing, qa, coverage> -am → BUILD SUCCESS: core 823, render-pdf 339, render-docx 2243, testing 5, render-pptx 142, templates 127, qa 1833.
    • The JDK 17 knowledge checks pass with the API surfaces unchanged, and core javadoc has 0 warnings.
    • ./mvnw -f examples/pom.xml test → BUILD SUCCESS, 93 tests.

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

A page that DocumentSession.pageMargins sets against other top and
bottom margins started no Word section where it started inside a panel:
Word set it with the margins in force. Where the export cuts that panel
at the row of its table the page starts with, the paragraph between the
two parts now ends the section, breaking no page before itself, where
the page above has room for it under the part.

A section's margins are planned once it is written, while panels are
cut once the document is: in a section whose margins vary, its panels'
cuts are decided when it is written and made as decided. The copy of
the header rows a part after a cut opens with holds no drawing: one the
export anchors there at the section's end is the table's first page's.
A row holding a page reference leaves a panel uncut at that row's page
and every page before it, and so does a header row holding a bookmark
the export later takes out; the recipe, the matrix and the CHANGELOG now
say so. The CHANGELOG states how ObsidianInvoice's twelve-fold line
items stood before. NOT_REPEATED is built from DRAWINGS, and a test
writes a section at a body table's split and one at a panel's cut in
one document section.
@DemchaAV
DemchaAV marked this pull request as ready for review October 25, 2026 18:59
@DemchaAV
DemchaAV merged commit 6c151cb into 2.5-dev Oct 25, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-panel-page-margin-sections branch October 25, 2026 19:10
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