From c73aa199285b5725b815e4a3926ad4388a565034 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 2 Nov 2026 10:28:31 +0000 Subject: [PATCH 1/8] test(docx): hold a composed table to the row that holds it, and record the measurement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A block the layout places on one page is held in Word by two writers that know nothing of each other: keepOnOnePage chains the block's paragraphs, so each row of a table in it stays on the page of the row under it, and breakRowsWhereTheLayoutDoes gives every row the layout placed w:cantSplit, so no row breaks across the page mid-line. Neither alone is enough. A table composed in a cell is reached by neither. The chain reads a cell's own paragraphs, which are not those of a table in that cell, and the layout places no row for such a table, so the table writer gives its rows nothing. What holds it is the row it stands in. Nothing in the backend's tests asserted anything about a composed table's paragraphs, so neither the absence nor the reason it costs nothing was written down anywhere. aTableComposedInACellIsHeldByTheRowRoundIt asserts the two absences together with that row, so the reason stands beside the gap, and names where it would begin to cost something: a row the layout did not place, which carries no w:cantSplit of its own. aTableInsideAKeptBlockIsChainedRowByRow asked only for the chain half of the pair, where the keepWithNext path was already held to both (DocxOneRowTableKeptWithTheNextTest). It now asks for both. The recipe and the capability matrix said that the keep chains a cell's own paragraphs and not the rows of a table in it, and left it there. They now finish it — the table writer holds those rows, a composed table is held by the row round it — and carry the measurement: nine shapes at the foot of a page, in two positions each, eighteen documents, each whole in Word 16.0.20527 and LibreOffice 24.2 with no group and no row broken. The editors differ only over which page holds the whole block: LibreOffice sets rows taller, a three-row table's 33.9pt apart against the page's 21.0, and moved it to the next page for seven of the nine that fit at the foot; Word did the same for the band of two layers. No engine change: every byte of every exported document is as it was. --- CHANGELOG.md | 45 +++++++++++++ .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 2 +- .../semantic/docx/DocxKeepTogetherTest.java | 64 +++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 765382ae0..c0d9736c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4549,6 +4549,42 @@ follow semantic versioning; release dates are ISO 8601. ### Tests +- **A table composed in a cell inside a block held on one page is now held to what actually keeps + it whole — the row round it, since neither of the two writers that hold an ordinary table + reaches it. And a kept block's table is asked for both of those writers on the `keepTogether` + path, where only one half of the pair was asked for before.** + - A table in a kept block is held twice over: the keep chain puts `w:keepNext` on its rows' + paragraphs, every row but the last, which keeps each row on the page of the row under it; and + the table writer gives every row the layout placed `w:cantSplit`, which keeps a row of several + paragraphs from breaking across the page mid-line. Neither alone is enough, and neither writer + knows about the other. The `keepWithNext` path was already held to the pair + (`DocxOneRowTableKeptWithTheNextTest`); on the `keepTogether` path + `aTableInsideAKeptBlockIsChainedRowByRow` asked only for the chain, and now asks for both. + - A table composed in a cell is reached by neither writer: the chain reads a cell's own + paragraphs, which are not those of a table in that cell, and the layout places no row for such + a table, so the table writer gives its rows no `w:cantSplit` either. What holds it is the row + it stands in. `aTableComposedInACellIsHeldByTheRowRoundIt` asserts the two absences together + with that row, so the reason the gap costs nothing stands beside the gap, and names where it + would begin to cost something: a row the layout did not place, which carries no `w:cantSplit` + of its own. Nothing in the backend's tests asserted anything about a composed table's + paragraphs before. + - Measured at the foot of a page in Word 16.0.20527 and LibreOffice 24.2, for a block that fits + there and for one the layout moved to the next page whole: nine shapes over eighteen documents + — a block kept together holding a table, a one-row table of several paragraphs, a table + composed in a cell, a painted panel holding a table, a band, a shape container, a stack + holding a table, a block kept with the next, a painted panel kept with the next — each stood + whole in both editors, with no group and no row broken. Where the editors differ it is which + page holds the whole block, never whether it is broken: LibreOffice sets rows taller (a + three-row table's rows 33.9pt apart against the page's 21.0), so a block that fits at the very + foot of a page there moves to the next one whole, which it did for seven of the nine; Word did + the same for the band of two layers. + - Counted over the 71-document fidelity corpus, with every node of every document classified and + none left unclassified: 1116 blocks are held on one page — 214 in the body, 902 in a cell or a + row's column, layer stacks and shape containers among them as well as the two keep flags — of + which 20 hold a table, 338 hold a band or a container, and none holds a table composed in a + cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are + on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, + lacks `w:cantSplit`. - **A fixture holds the record a page an editor adds past a one-page section is painted from: the export is grown by eighty appended paragraphs through POI and read back, and the fills are in the header of the one section the grown document has.** @@ -5229,6 +5265,15 @@ follow semantic versioning; release dates are ISO 8601. ### Documentation +- **The recipe and the capability matrix now say what was measured of a kept block at the foot of a + page, and finish the half-sentence they carried about a table inside one.** Both said that the + keep chains a cell's own paragraphs and not the rows of a table in it, and left it there — not + saying that the table writer holds those rows instead, that a table composed in a cell is + therefore held by the row it stands in rather than by either writer, or that any of it had ever + been put to the two editors. They now carry the rest of the mechanism and the measurement — nine + shapes at the foot of a page, eighteen documents, each whole in Word 16.0.20527 and LibreOffice + 24.2, the editors differing only over which page holds the whole block — together with the + corpus counts behind it, so the next reader is not left to measure it again. - **A card of a preset shows the code that draws that preset.** The catalogue carries one compiled block per family — the CVs' builds `BoxedSections`, the invoices' `ModernInvoice` — and every other card of the family was shown it under a caption saying it came from the diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index 0f2e76af0..05223dd9e 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -131,7 +131,7 @@ honour an option ignores it (documented contract). | Protection / encryption | ✅ `PdfDocumentPostProcessor` | ❌ (ignored with a one-time warning — no OOXML encryption support planned) | ❌ (not written, so the file opens unprotected; reported `DROPPED`, `protection`) | | Viewer preferences | ✅ `applyViewerPreferences` in `PdfFixedLayoutBackend` | ❌ (ignored with a one-time warning — PDF-viewer concept) | n/a (not written; reported `DROPPED`, `viewer preferences`) | | Debug guide lines / node labels | ✅ `PdfGuideLinesRenderer`, `PdfNodeLabelRenderer` | ❌ (ignored with a one-time warning — render through the PDF backend to see overlays) | n/a | -| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row; in a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | +| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit`, which is what keeps a row of several paragraphs from breaking mid-line; a table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice moved it to the next page for seven of the nine that fit at the foot, its rows being taller, and Word for the band of two layers. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | | Layer stack — layers drawn over one another (`addLayerStack`) | ✅ each layer's fragments at the place the layout gave it | ✅ same | ⚠️ `DocxSemanticBackend` — Word has no layers, so a stack's children are written one after the other, without their positions. The exception is a stack whose layers are side-by-side columns (`DocxLayerColumns`): every layer a plain container at the stack's top-left corner, the bands their padding leaves either the same or apart. That is written as one table row, a cell per band, the way a two-column CV lays its columns out as layers to draw the name first. Layers sharing a band follow one another in its cell. A spacer the layout placed level with content of another layer of the same band only keeps that content's place, and is not written. The space above a later layer's first block is the gap the page shows below the content before it, to the block's content: what the block holds above that content past the gap — its margin, its padding, a contained picture's room — comes out of its own edge (`DocxLayerColumns.resume`) where it is a paragraph, a picture, a list or a table in its layer's own flow with its edge above at zero or more, and a list keeps its own edges after the gap, as a table does. Each layer is held to its box across, past the stack's edge where the page sets it past it, by an indent below zero; in a table cell no further left than the indents round it give, the rest named. The one layer of a stack of one layer is held the same way, its offset across kept and its measure with it; in a table cell, composed in a table's cell with no place of its own, and for an offset down or up, which moves it without making room for it, it is written where the stack starts and named — not where a band or a column's layer sets it at the page's distance below what is above it, which holds the offset. A shape container written as a panel in a table cell keeps its padding round its outline, as the page does — the table's indent and the space above and below it; where its table ends its cell, Word sets the cell's content at its middle or foot without the space below, which a centred cell takes off the space above, the rest named — sets what its cell holds at the top, middle or foot as its first written layer is set, holds a paragraph across to its box, and names a written layer after the first (written under the ones before it), an offset down or up, a box past the shape's side, and a layer set across by a box the layout does not give (`layer offset`). A layer whose content the page starts above where those before it end — a caption over its photo, a label in a card's corner, not a block giving back its edge whose padding or contained picture's room alone reaches there — is written under them, in a column's cell as in a band, and the report names it; a stack composed in a table cell, or exported with no layout, has no place of its own, and one writing more than one of its layers is named with them not measured. What the page draws inside another layer's fill comes after that fill. Drawings stack as the page paints them, by their z-index, each given its place (`wp:anchor/@relativeHeight`) from where the page paints its fragment, whatever order the layers are written in — among the drawings behind the text, and among those in front of it, as Word draws every one behind under every one in front. A painted layer, though, is a shaded cell rather than a drawing, and its fill belongs to the text layer: it is painted over every drawing behind the text, a drawing on a layer *above* it included, so a stack's z-index does not lift a drawing clear of a painted neighbour. An icon laid over a card's corner draws none of its ink in Word 16.0.20527 or LibreOffice 24.2 against 4308 pixels of it at 300 dpi in the engine's PDF; the report names such a drawing and one panel that covers it, taking the panels where it writes their fill, so that a panel bearing borders alone — which writes no shading — is not named and a row written as a panel is. Not named: a drawing under a panel the layout breaks across pages or composed in an authored table cell, one under a table cell's own shading, a cell's paint the table draws itself, and the second panel over a drawing lying over two (`DocxNeighbourFillGuardTest` holds the corpus to the one drawing that is known, and writes those edges out). Measured in LibreOffice: `CharcoalGold`, `SidebarPortrait` and `SlateOrange` fit on one page, as on the page; `NavySidebar` runs one line onto a second page. A stack or shape container the page gives no room — its margins taking back its whole height, as `LumaStudioInvoice`'s sidebar over the page's top margin — is laid over the flow (`laidOverTheFlow`): its drawings where the page draws them, each paragraph in a text box in front of the text where the page sets it, nothing in the flow. Not in a table cell or a panel, and not when it holds anything but drawing and plain paragraphs — a link, an anchor, a picture, a list or a table keeps it in the flow as above. A canvas is written as its contents — what it writes one block after another, its drawings where it places them — and holds its height: the room under what it writes is the space below it, and one that only draws holds its whole box where nothing round it — a band before its layer's first block, drawings held whole, an overlay it is a layer of — measures that room already. The report names the places and the width it loses, as it does a column's fixed width narrower than its band, and what it writes running past its height, a drawing in what it writes that takes no room in Word, or a height not measured | ## Output surface and lifecycle diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index ac34e5e0a..a851544ec 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -75,7 +75,7 @@ creation date is real metadata. | Images | Embedded pictures at the node's declared size. One contained in its box (`fitToBounds`, `CONTAIN`) is embedded at its fitted size, half the room it leaves above and below written as the space round its paragraph and half at its side as its indent; in a band the room below is its paragraph's own, as the band measures what it owes below from the box. In the flow, in a row's cell and held in by an alignment it stands in the box's middle, as the page draws it. In the flow, the margin and padding above and below are the space round the picture's paragraph; on the left they are not written, and the report names them, as it does a barcode's and a page reference's sides. A picture in the flow, a stack of one layer's included, or first in a band of layers there, that the layout moves to a new page keeps the space the layout leaves above it there — its margin, its padding, the room above it in its box, the edges of the containers opening with it and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, as a body paragraph does. A picture drawn beside its text is fitted to its box with its padding in it, which the report names too | | Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's — in a list item as much as in a paragraph. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires; on a section, container, table or image it wraps everything the block wrote, from the start of its first paragraph to the end of its last, so a link to a block lands on its first line; a section or a container written as a layer stack's column wraps what it writes the same way, from a block of it an earlier layer's stand-in holds, written there first, to the last it writes. A block that writes no paragraph — a section, a container or a layer stack's column of drawings alone, whose drawings stand in a paragraph of a block holding it, in the one nearest them or in one written on their page — is bookmarked at the start of the paragraph its first drawing is anchored in, the drawing on the earliest page and highest on it: Word keeps a drawing on its paragraph's page and moves it with that paragraph, so a page reference reads the page the block starts on and goes on reading it once the text above is edited, and a link goes to that page (measured in Word 16, fields updated, and LibreOffice 24.2: a block on page 2 read 2, and 3 with a page put in ahead of it, where the file had held no bookmark and the reference a fixed 2). A block that draws nothing, one whose first drawing stands on a page nothing is written on, and one composed in a table's cell, whose drawings its table draws, have no bookmark, and the report names the anchor: a link to it points at none, and a page reference to it is written as the number it reads. Two anchors that clean to one name, or to a name a suffix gave another, stay two bookmarks; one anchor two nodes declare is one bookmark, the last the file writes one for — the node the page sends a link to and reads a page reference's page from, where that node writes a paragraph to mark — and the report names the anchor, the nodes counted off the document, two cells of one table two of them, and a node of a kind of the caller's own counted where the layout registers it. An anchor in a table's header row the page repeats is drawn again on each page it repeats on, and the page sends a link to the last of them; the file holds the row once, where the table starts, which a link goes to and a page reference reads once Word updates it (measured: the bookmark on page 1, a reference written as 5 reading 1), and the report names it. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is | | Rows | A one-row table spanning the content width — or only its columns, where fixed columns leave part of the row empty — so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; fixed columns that add up to less than the row leave the rest of it empty, the last one at its own width and the table no wider than its columns, so its text wraps where the page wraps it; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin; a cell holds whatever its child is, written as it is anywhere else. The row's `verticalAlign` is every cell's `w:vAlign`, so a child shorter than the row sits at its middle or bottom as on the page — a table of contents' leader on its entry's baseline. A row composed in a table cell (`DocumentTableCell.node(...)`), which the layout places nowhere, that the page divides by what its children measure — an arrangement other than `START`, a spacer that grows, an auto column — takes its columns from where its paragraphs' lines start, the table having laid them out, a spacer where the page spreads the room between them; one holding a child those lines do not place — a picture, a shape, or a spacer in a row of stated columns, which gives it a slot of its own — or written inside a panel or another table in its cell has its columns sized by Word to their text, and the report names it. The row is kept whole across a page break, as the layout keeps it. A row is held at least as tall as the page makes it inside a painted panel, and anywhere its tallest child is a drawing Word holds nothing of in its cell — a badge beside a heading. A section or other container in any cell — a row's, a table's, a panel's — that pulls its first line up with a negative top edge writes that one-line paragraph as much shorter, its text seated where the page sets it — no more than the room above its letters, as Word draws an exact line's text only inside the line. A row that paints its box — `fillColor`, `stroke`, `borders` — is a panel holding its columns (see "What a panel keeps and loses" below): a table of one cell carrying the fill and borders, its margins the row's padding as a panel's are, and in it the columns as a table of their own, without the padding; a corner radius squares, and is named (`corner radius`), and what of its borders above and below Word draws past its box is taken, or named, as a panel's is. A row composed in a table cell, in a page zone's line, in a band of overlapping layers or where a stack's column measures the space round it to its text, with a padding below zero or a column hanging into it, or with a margin below zero above or below it or at a side in a cell, is written as its columns alone, its paint in the report (`row paint`); in the body a row bleeding past the margin is its panel | -| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | +| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). A table composed in a cell is held by neither, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a one-row table of several paragraphs, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets rows taller (a three-row table's rows 33.9pt apart against the page's 21.0), so a block that fits at the very foot of a page there moves to the next one whole, which it did for seven of the nine, and Word did the same for the band of two layers. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | | Spacers | An empty paragraph a tenth of a point tall, which Word keeps; the rest of the spacer's height is the space above the next block, or below this paragraph when a table follows. A spacer shorter than that tenth — a width anchor of no height, as `Panel`'s and `PanelLetter`'s cards open with — has its line reach past it by the rest, which the space below takes, as it takes a line hanging below a block, and what it cannot take is named (`space above`): taken nowhere, the line stood every card a tenth of a point taller in Word. A spacer in the body the layout moves to a new page with the gap before it, because the gap did not fit at the foot of the page above, holds the space the layout leaves above it there — that gap, and the edges of any containers opening with it — in its line, which Word keeps at the top of a page where it drops the space above. So does a spacer opening a stack of one layer, or a band of layers, which the layout moves to a new page whole: the space the layout leaves above it there is held in its line, less, in a stack of one layer, its layer's offset, which is named. It is kept with what follows it in the stack, which Word would otherwise set at the new page's top with the spacer left at the foot of the page above. A spacer's own margin and padding are not written, and the report names them — except below the lowest block of a band, or of a layer another resumes after in its column, where the space is measured from the page, the block's own margin and padding included | | Page breaks | Explicit Word page breaks | diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java index fcaa1ac85..0958f4a00 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java @@ -1,7 +1,9 @@ package com.demcha.compose.document.backend.semantic.docx; +import com.demcha.compose.document.dsl.TableBuilder; import com.demcha.compose.document.style.DocumentColor; import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.table.DocumentTableCell; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFTable; @@ -101,6 +103,68 @@ void aTableInsideAKeptBlockIsChainedRowByRow() throws Exception { assertThat(rowKeepsNext(table.getRow(1))) .as("the table's last row ends the block") .isFalse(); + // Two writers hold such a table, and it needs both: the chain above keeps each row on + // the page of the row under it, and the table writer gives every row the layout placed + // w:cantSplit, which keeps a row of several paragraphs from breaking mid-line + // (breakRowsWhereTheLayoutDoes, blind to the keep flags). Asked for here on the + // keepTogether path as DocxOneRowTableKeptWithTheNextTest asks it on keepWithNext. + assertThat(table.getRows()).hasSize(2); + for (XWPFTableRow row : table.getRows()) { + assertThat(row.isCantSplitRow()) + .as("a row of the kept block's table may not break across a page") + .isTrue(); + } + } + } + + /** + * A table composed in a cell is held by the row round it, having nothing of its own. + * + *

Neither writer reaches it. The chain reads a cell's own paragraphs, which are not a + * table's in that cell, so its paragraphs are told nothing; and the layout places no row for + * a table composed in a cell, so the table writer gives its rows no {@code w:cantSplit} + * either. What holds it is the row it stands in, which carries {@code w:cantSplit} where the + * layout placed that row: measured at the foot of a page in Word 16.0.20527 and LibreOffice + * 24.2, the block stood whole in both, for a block that fits there and for one the layout + * moved to the next page whole.

+ * + *

So the gap costs nothing while the row round it may not split. It would begin to cost + * something where that row may — a row the layout did not place, as in a table too tall to + * lay out ({@code DocxTablePaginationTest#aTableTooTallToLayOutIsLeftFreeToBreak}) — which is + * why the row is asserted here beside the two absences, rather than the absences alone.

+ */ + @Test + void aTableComposedInACellIsHeldByTheRowRoundIt() throws Exception { + try (XWPFDocument document = DocxExports.withLayout(595, 842, 36, page -> page + .addSection(s -> s.keepTogether() + .addParagraph(p -> p.text("Totals")) + .addTable(t -> t.autoColumns(2).rowCells( + DocumentTableCell.node(new TableBuilder().autoColumns(1) + .row("Net").row("Tax").build()), + DocumentTableCell.text("Beside")))))) { + XWPFTable outer = document.getTables().get(0); + XWPFTable composed = outer.getRow(0).getCell(0).getTables().get(0); + + assertThat(outer.getRow(0).isCantSplitRow()) + .as("the row the composed table stands in may not split, and holds it") + .isTrue(); + assertThat(composed.getRows()).hasSize(2); + for (XWPFTableRow row : composed.getRows()) { + assertThat(row.isCantSplitRow()) + .as("the layout places no row for a table composed in a cell, so the " + + "table writer holds none of its rows whole") + .isFalse(); + for (XWPFTableCell cell : row.getTableCells()) { + assertThat(cell.getParagraphs()) + .as("the composed table writes the paragraphs this absence is read off") + .isNotEmpty(); + for (XWPFParagraph paragraph : cell.getParagraphs()) { + assertThat(keepsLines(paragraph) || keepsNext(paragraph)) + .as("the chain reads the cell's own paragraphs, not a table's in it") + .isFalse(); + } + } + } } } From 4b90e5f29d21f55c32d5200b6253c36081df4f06 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 2 Nov 2026 10:49:55 +0000 Subject: [PATCH 2/8] test(docx): state the cantSplit rule with its exceptions, and fix the row-pitch attribution Three corrections to the sentences this change adds. The rule "every row the layout placed carries w:cantSplit" was stated unconditionally, with its exceptions sitting a few sentences away in the same documents: letTheOneRowTablesKeptWithTheNextBreak takes it back off a one-row body table of paragraphs kept with what follows, and the gate in writePanelPiece never writes it for a panel asking only to be kept with the next. Both exceptions are now in the sentence, and the 56 one-row tables of the corpus that carry keeps without it are named in all three documents rather than only in the pull request. The reason given for w:cantSplit was "it keeps a row of several paragraphs from breaking mid-line". A cell of a plain table cannot hold several paragraphs - lines in a cell are one paragraph with line breaks - so the shape named was unreachable, and inside a kept block the chain's w:keepLines already holds a single paragraph's lines together. What w:cantSplit adds is a row the chain cannot reach inside: a cell of more than one paragraph or of a table, which only a composed cell can hold, and any table outside a kept block, where no chain is written at all. The measured shape is renamed from several paragraphs to several lines, which is what the fixture held. The 33.9pt against 21.0 row pitch was measured on one shape, a three-row table, and was made the cause of all seven LibreOffice page moves. It explains six: the band of two layers holds no such table, and both editors moved that one, from a cause not measured. Also: keep-with-next and the table-cell case added to the w:keepNext description, writePanelPiece named as one of three setCantSplitRow sites, and the cross-reference in the recipe pointed above rather than below. --- CHANGELOG.md | 26 +++++++++++-------- .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 2 +- .../semantic/docx/DocxKeepTogetherTest.java | 18 ++++++++----- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0d9736c4..f9c4a09f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4554,12 +4554,16 @@ follow semantic versioning; release dates are ISO 8601. reaches it. And a kept block's table is asked for both of those writers on the `keepTogether` path, where only one half of the pair was asked for before.** - A table in a kept block is held twice over: the keep chain puts `w:keepNext` on its rows' - paragraphs, every row but the last, which keeps each row on the page of the row under it; and - the table writer gives every row the layout placed `w:cantSplit`, which keeps a row of several - paragraphs from breaking across the page mid-line. Neither alone is enough, and neither writer - knows about the other. The `keepWithNext` path was already held to the pair + paragraphs, every row but the last — the last too where the block is kept with the next, and + none of it inside a table cell — which keeps each row on the page of the row under it; and the + table writer gives every row the layout placed `w:cantSplit`, which holds a row whole where + the chain cannot reach inside it, the chain reaching only a cell's own paragraphs. Neither + writer knows about the other. The `keepWithNext` path was already held to the pair (`DocxOneRowTableKeptWithTheNextTest`); on the `keepTogether` path - `aTableInsideAKeptBlockIsChainedRowByRow` asked only for the chain, and now asks for both. + `aTableInsideAKeptBlockIsChainedRowByRow` asked only for the chain, and now asks for both. It + is not unconditional either way: `letTheOneRowTablesKeptWithTheNextBreak` takes `w:cantSplit` + back off a one-row body table kept with what follows, and a panel asking only to be kept with + the next never gets it — 56 one-row tables of the corpus carry keeps without it. - A table composed in a cell is reached by neither writer: the chain reads a cell's own paragraphs, which are not those of a table in that cell, and the layout places no row for such a table, so the table writer gives its rows no `w:cantSplit` either. What holds it is the row @@ -4570,14 +4574,14 @@ follow semantic versioning; release dates are ISO 8601. paragraphs before. - Measured at the foot of a page in Word 16.0.20527 and LibreOffice 24.2, for a block that fits there and for one the layout moved to the next page whole: nine shapes over eighteen documents - — a block kept together holding a table, a one-row table of several paragraphs, a table - composed in a cell, a painted panel holding a table, a band, a shape container, a stack + — a block kept together holding a table, a kept block holding one row of several lines, a + table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next, a painted panel kept with the next — each stood whole in both editors, with no group and no row broken. Where the editors differ it is which - page holds the whole block, never whether it is broken: LibreOffice sets rows taller (a - three-row table's rows 33.9pt apart against the page's 21.0), so a block that fits at the very - foot of a page there moves to the next one whole, which it did for seven of the nine; Word did - the same for the band of two layers. + page holds the whole block, never whether it is broken: LibreOffice sets a three-row table's + rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot of + a page there moves to the next one whole, which six of the nine did; the band of two layers + moved in both editors, from a cause not measured. - Counted over the 71-document fidelity corpus, with every node of every document classified and none left unclassified: 1116 blocks are held on one page — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — of diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index 05223dd9e..add50bd58 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -131,7 +131,7 @@ honour an option ignores it (documented contract). | Protection / encryption | ✅ `PdfDocumentPostProcessor` | ❌ (ignored with a one-time warning — no OOXML encryption support planned) | ❌ (not written, so the file opens unprotected; reported `DROPPED`, `protection`) | | Viewer preferences | ✅ `applyViewerPreferences` in `PdfFixedLayoutBackend` | ❌ (ignored with a one-time warning — PDF-viewer concept) | n/a (not written; reported `DROPPED`, `viewer preferences`) | | Debug guide lines / node labels | ✅ `PdfGuideLinesRenderer`, `PdfNodeLabelRenderer` | ❌ (ignored with a one-time warning — render through the PDF backend to see overlays) | n/a | -| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit`, which is what keeps a row of several paragraphs from breaking mid-line; a table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice moved it to the next page for seven of the nine that fit at the foot, its rows being taller, and Word for the band of two layers. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | +| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit` — but the row of a one-row table of paragraphs kept with what follows, taken back off it by `letTheOneRowTablesKeptWithTheNextBreak` below, and a panel asking only to be kept with the next, which never gets it; what it adds over the chain is a row Word cannot end a page inside, the chain reaching only a cell's own paragraphs. A table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice sets a three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot moves to the next page whole there, which six of the nine did, and the band of two layers moved in both editors from a cause not measured. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`, while 56 one-row tables carry keeps without it. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | | Layer stack — layers drawn over one another (`addLayerStack`) | ✅ each layer's fragments at the place the layout gave it | ✅ same | ⚠️ `DocxSemanticBackend` — Word has no layers, so a stack's children are written one after the other, without their positions. The exception is a stack whose layers are side-by-side columns (`DocxLayerColumns`): every layer a plain container at the stack's top-left corner, the bands their padding leaves either the same or apart. That is written as one table row, a cell per band, the way a two-column CV lays its columns out as layers to draw the name first. Layers sharing a band follow one another in its cell. A spacer the layout placed level with content of another layer of the same band only keeps that content's place, and is not written. The space above a later layer's first block is the gap the page shows below the content before it, to the block's content: what the block holds above that content past the gap — its margin, its padding, a contained picture's room — comes out of its own edge (`DocxLayerColumns.resume`) where it is a paragraph, a picture, a list or a table in its layer's own flow with its edge above at zero or more, and a list keeps its own edges after the gap, as a table does. Each layer is held to its box across, past the stack's edge where the page sets it past it, by an indent below zero; in a table cell no further left than the indents round it give, the rest named. The one layer of a stack of one layer is held the same way, its offset across kept and its measure with it; in a table cell, composed in a table's cell with no place of its own, and for an offset down or up, which moves it without making room for it, it is written where the stack starts and named — not where a band or a column's layer sets it at the page's distance below what is above it, which holds the offset. A shape container written as a panel in a table cell keeps its padding round its outline, as the page does — the table's indent and the space above and below it; where its table ends its cell, Word sets the cell's content at its middle or foot without the space below, which a centred cell takes off the space above, the rest named — sets what its cell holds at the top, middle or foot as its first written layer is set, holds a paragraph across to its box, and names a written layer after the first (written under the ones before it), an offset down or up, a box past the shape's side, and a layer set across by a box the layout does not give (`layer offset`). A layer whose content the page starts above where those before it end — a caption over its photo, a label in a card's corner, not a block giving back its edge whose padding or contained picture's room alone reaches there — is written under them, in a column's cell as in a band, and the report names it; a stack composed in a table cell, or exported with no layout, has no place of its own, and one writing more than one of its layers is named with them not measured. What the page draws inside another layer's fill comes after that fill. Drawings stack as the page paints them, by their z-index, each given its place (`wp:anchor/@relativeHeight`) from where the page paints its fragment, whatever order the layers are written in — among the drawings behind the text, and among those in front of it, as Word draws every one behind under every one in front. A painted layer, though, is a shaded cell rather than a drawing, and its fill belongs to the text layer: it is painted over every drawing behind the text, a drawing on a layer *above* it included, so a stack's z-index does not lift a drawing clear of a painted neighbour. An icon laid over a card's corner draws none of its ink in Word 16.0.20527 or LibreOffice 24.2 against 4308 pixels of it at 300 dpi in the engine's PDF; the report names such a drawing and one panel that covers it, taking the panels where it writes their fill, so that a panel bearing borders alone — which writes no shading — is not named and a row written as a panel is. Not named: a drawing under a panel the layout breaks across pages or composed in an authored table cell, one under a table cell's own shading, a cell's paint the table draws itself, and the second panel over a drawing lying over two (`DocxNeighbourFillGuardTest` holds the corpus to the one drawing that is known, and writes those edges out). Measured in LibreOffice: `CharcoalGold`, `SidebarPortrait` and `SlateOrange` fit on one page, as on the page; `NavySidebar` runs one line onto a second page. A stack or shape container the page gives no room — its margins taking back its whole height, as `LumaStudioInvoice`'s sidebar over the page's top margin — is laid over the flow (`laidOverTheFlow`): its drawings where the page draws them, each paragraph in a text box in front of the text where the page sets it, nothing in the flow. Not in a table cell or a panel, and not when it holds anything but drawing and plain paragraphs — a link, an anchor, a picture, a list or a table keeps it in the flow as above. A canvas is written as its contents — what it writes one block after another, its drawings where it places them — and holds its height: the room under what it writes is the space below it, and one that only draws holds its whole box where nothing round it — a band before its layer's first block, drawings held whole, an overlay it is a layer of — measures that room already. The report names the places and the width it loses, as it does a column's fixed width narrower than its band, and what it writes running past its height, a drawing in what it writes that takes no room in Word, or a height not measured | ## Output surface and lifecycle diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index a851544ec..910baf87d 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -75,7 +75,7 @@ creation date is real metadata. | Images | Embedded pictures at the node's declared size. One contained in its box (`fitToBounds`, `CONTAIN`) is embedded at its fitted size, half the room it leaves above and below written as the space round its paragraph and half at its side as its indent; in a band the room below is its paragraph's own, as the band measures what it owes below from the box. In the flow, in a row's cell and held in by an alignment it stands in the box's middle, as the page draws it. In the flow, the margin and padding above and below are the space round the picture's paragraph; on the left they are not written, and the report names them, as it does a barcode's and a page reference's sides. A picture in the flow, a stack of one layer's included, or first in a band of layers there, that the layout moves to a new page keeps the space the layout leaves above it there — its margin, its padding, the room above it in its box, the edges of the containers opening with it and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, as a body paragraph does. A picture drawn beside its text is fitted to its box with its padding in it, which the report names too | | Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's — in a list item as much as in a paragraph. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires; on a section, container, table or image it wraps everything the block wrote, from the start of its first paragraph to the end of its last, so a link to a block lands on its first line; a section or a container written as a layer stack's column wraps what it writes the same way, from a block of it an earlier layer's stand-in holds, written there first, to the last it writes. A block that writes no paragraph — a section, a container or a layer stack's column of drawings alone, whose drawings stand in a paragraph of a block holding it, in the one nearest them or in one written on their page — is bookmarked at the start of the paragraph its first drawing is anchored in, the drawing on the earliest page and highest on it: Word keeps a drawing on its paragraph's page and moves it with that paragraph, so a page reference reads the page the block starts on and goes on reading it once the text above is edited, and a link goes to that page (measured in Word 16, fields updated, and LibreOffice 24.2: a block on page 2 read 2, and 3 with a page put in ahead of it, where the file had held no bookmark and the reference a fixed 2). A block that draws nothing, one whose first drawing stands on a page nothing is written on, and one composed in a table's cell, whose drawings its table draws, have no bookmark, and the report names the anchor: a link to it points at none, and a page reference to it is written as the number it reads. Two anchors that clean to one name, or to a name a suffix gave another, stay two bookmarks; one anchor two nodes declare is one bookmark, the last the file writes one for — the node the page sends a link to and reads a page reference's page from, where that node writes a paragraph to mark — and the report names the anchor, the nodes counted off the document, two cells of one table two of them, and a node of a kind of the caller's own counted where the layout registers it. An anchor in a table's header row the page repeats is drawn again on each page it repeats on, and the page sends a link to the last of them; the file holds the row once, where the table starts, which a link goes to and a page reference reads once Word updates it (measured: the bookmark on page 1, a reference written as 5 reading 1), and the report names it. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is | | Rows | A one-row table spanning the content width — or only its columns, where fixed columns leave part of the row empty — so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; fixed columns that add up to less than the row leave the rest of it empty, the last one at its own width and the table no wider than its columns, so its text wraps where the page wraps it; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin; a cell holds whatever its child is, written as it is anywhere else. The row's `verticalAlign` is every cell's `w:vAlign`, so a child shorter than the row sits at its middle or bottom as on the page — a table of contents' leader on its entry's baseline. A row composed in a table cell (`DocumentTableCell.node(...)`), which the layout places nowhere, that the page divides by what its children measure — an arrangement other than `START`, a spacer that grows, an auto column — takes its columns from where its paragraphs' lines start, the table having laid them out, a spacer where the page spreads the room between them; one holding a child those lines do not place — a picture, a shape, or a spacer in a row of stated columns, which gives it a slot of its own — or written inside a panel or another table in its cell has its columns sized by Word to their text, and the report names it. The row is kept whole across a page break, as the layout keeps it. A row is held at least as tall as the page makes it inside a painted panel, and anywhere its tallest child is a drawing Word holds nothing of in its cell — a badge beside a heading. A section or other container in any cell — a row's, a table's, a panel's — that pulls its first line up with a negative top edge writes that one-line paragraph as much shorter, its text seated where the page sets it — no more than the room above its letters, as Word draws an exact line's text only inside the line. A row that paints its box — `fillColor`, `stroke`, `borders` — is a panel holding its columns (see "What a panel keeps and loses" below): a table of one cell carrying the fill and borders, its margins the row's padding as a panel's are, and in it the columns as a table of their own, without the padding; a corner radius squares, and is named (`corner radius`), and what of its borders above and below Word draws past its box is taken, or named, as a panel's is. A row composed in a table cell, in a page zone's line, in a band of overlapping layers or where a stack's column measures the space round it to its text, with a padding below zero or a column hanging into it, or with a margin below zero above or below it or at a side in a cell, is written as its columns alone, its paint in the report (`row paint`); in the body a row bleeding past the margin is its panel | -| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). A table composed in a cell is held by neither, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a one-row table of several paragraphs, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets rows taller (a three-row table's rows 33.9pt apart against the page's 21.0), so a block that fits at the very foot of a page there moves to the next one whole, which it did for seven of the nine, and Word did the same for the band of two layers. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | +| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, but the row of a one-row table of paragraphs kept with what follows, which is taken back off it above, and a panel asking only to be kept with the next, which never gets it — so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). The chain reaches inside a row only as far as its cells' own paragraphs, so what `w:cantSplit` adds there is a row Word cannot end a page inside: a cell of more than one paragraph or of a table, which only a composed cell can hold. A table composed in a cell is held by neither writer, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot of a page there moves to the next one whole; it moved six of the nine, and the band of two layers moved in both editors, from a cause not measured. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`; 56 one-row tables do carry keeps without it — panels and containers asking only to be kept with the next — and were measured whole in both editors. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | | Spacers | An empty paragraph a tenth of a point tall, which Word keeps; the rest of the spacer's height is the space above the next block, or below this paragraph when a table follows. A spacer shorter than that tenth — a width anchor of no height, as `Panel`'s and `PanelLetter`'s cards open with — has its line reach past it by the rest, which the space below takes, as it takes a line hanging below a block, and what it cannot take is named (`space above`): taken nowhere, the line stood every card a tenth of a point taller in Word. A spacer in the body the layout moves to a new page with the gap before it, because the gap did not fit at the foot of the page above, holds the space the layout leaves above it there — that gap, and the edges of any containers opening with it — in its line, which Word keeps at the top of a page where it drops the space above. So does a spacer opening a stack of one layer, or a band of layers, which the layout moves to a new page whole: the space the layout leaves above it there is held in its line, less, in a stack of one layer, its layer's offset, which is named. It is kept with what follows it in the stack, which Word would otherwise set at the new page's top with the spacer left at the foot of the page above. A spacer's own margin and padding are not written, and the report names them — except below the lowest block of a band, or of a layer another resumes after in its column, where the space is measured from the page, the block's own margin and padding included | | Page breaks | Explicit Word page breaks | diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java index 0958f4a00..b9eefe9ef 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java @@ -103,11 +103,15 @@ void aTableInsideAKeptBlockIsChainedRowByRow() throws Exception { assertThat(rowKeepsNext(table.getRow(1))) .as("the table's last row ends the block") .isFalse(); - // Two writers hold such a table, and it needs both: the chain above keeps each row on - // the page of the row under it, and the table writer gives every row the layout placed - // w:cantSplit, which keeps a row of several paragraphs from breaking mid-line - // (breakRowsWhereTheLayoutDoes, blind to the keep flags). Asked for here on the - // keepTogether path as DocxOneRowTableKeptWithTheNextTest asks it on keepWithNext. + // Two writers hold such a table. The chain keeps each row with the row under it and + // each paragraph's own lines together; the table writer gives every row the layout + // placed w:cantSplit (breakRowsWhereTheLayoutDoes, blind to the keep flags), which + // holds a row whole where the chain cannot reach inside it — a cell of more than one + // paragraph or of a table, which only a composed cell can hold, and any table outside + // a kept block, where no chain is written at all. Asked for here on the keepTogether + // path as DocxOneRowTableKeptWithTheNextTest asks it on keepWithNext. Not + // unconditional: letTheOneRowTablesKeptWithTheNextBreak takes it back off a one-row + // body table kept with what follows, which that test's own fixtures hold. assertThat(table.getRows()).hasSize(2); for (XWPFTableRow row : table.getRows()) { assertThat(row.isCantSplitRow()) @@ -123,7 +127,9 @@ void aTableInsideAKeptBlockIsChainedRowByRow() throws Exception { *

Neither writer reaches it. The chain reads a cell's own paragraphs, which are not a * table's in that cell, so its paragraphs are told nothing; and the layout places no row for * a table composed in a cell, so the table writer gives its rows no {@code w:cantSplit} - * either. What holds it is the row it stands in, which carries {@code w:cantSplit} where the + * either. ({@code DocxOneRowTableKeptWithTheNextTest} asserts the same silence of a table + * nested in a cell of a kept row; here the table is composed into the cell by the author.) + * What holds it is the row it stands in, which carries {@code w:cantSplit} where the * layout placed that row: measured at the foot of a page in Word 16.0.20527 and LibreOffice * 24.2, the block stood whole in both, for a block that fits there and for one the layout * moved to the next page whole.

From 02cc4152220cafda8baab769c863a1d260588fe6 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 2 Nov 2026 11:10:52 +0000 Subject: [PATCH 3/8] test(docx): hold the row of several paragraphs a plain cell reaches through its line spacing A plain table cell is a paragraph per line, not one paragraph of several lines, when its style sets a line spacing and it holds more than one line (paragraphPerLine in writeCellLines, which DocxCellSeatTest pins, and which the recipe's Tables row already stated). The sentences added here said that only a composed cell can hold several paragraphs, and so left out the one shape on an authored table for which w:cantSplit is load-bearing: the chain gives each of a row's paragraphs w:keepLines, which holds that paragraph's own lines, and w:keepNext only where the row is not the block's last unit, so nothing but the row itself keeps two paragraphs of one row on a page. aRowOfSeveralParagraphsIsHeldWholeByItsRow builds that row, asserts the premise that the line spacing makes it two paragraphs, and then asserts that only the row holds them. A fourth sabotage takes w:cantSplit off a placed row and requires this fixture to fail. Two further sentences narrowed to what the code does. The panel gate is not "a panel asking only to be kept with the next never gets w:cantSplit": a row and a shape container pass its instanceof disjuncts whatever they ask, so the block that goes without it is a painted section. And "nothing asserted anything about a composed table's paragraphs" was too wide - DocxComposedCellTest asserts their text; it is their keeps that nothing asserted. Also: the band's page move is no longer implied to be absent from the corpus, which a static classification cannot establish; the count of unclassified blocks states its denominator; the added cantSplit loop on the keepTogether path is described as documenting the pair at the call site rather than as closing a gap, since breakRowsWhereTheLayoutDoes is blind to the keep flags; and the ninth measured shape is named with its table in all four places. --- CHANGELOG.md | 29 ++++++----- .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 2 +- .../semantic/docx/DocxKeepTogetherTest.java | 52 ++++++++++++++++--- 4 files changed, 63 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9c4a09f6..63d28845a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4554,29 +4554,32 @@ follow semantic versioning; release dates are ISO 8601. reaches it. And a kept block's table is asked for both of those writers on the `keepTogether` path, where only one half of the pair was asked for before.** - A table in a kept block is held twice over: the keep chain puts `w:keepNext` on its rows' - paragraphs, every row but the last — the last too where the block is kept with the next, and - none of it inside a table cell — which keeps each row on the page of the row under it; and the - table writer gives every row the layout placed `w:cantSplit`, which holds a row whole where - the chain cannot reach inside it, the chain reaching only a cell's own paragraphs. Neither - writer knows about the other. The `keepWithNext` path was already held to the pair - (`DocxOneRowTableKeptWithTheNextTest`); on the `keepTogether` path - `aTableInsideAKeptBlockIsChainedRowByRow` asked only for the chain, and now asks for both. It - is not unconditional either way: `letTheOneRowTablesKeptWithTheNextBreak` takes `w:cantSplit` - back off a one-row body table kept with what follows, and a panel asking only to be kept with - the next never gets it — 56 one-row tables of the corpus carry keeps without it. + paragraphs, on every unit of the block but its last — the last too where the block is kept + with the next, and none of it inside a table cell — which keeps each row on the page of the + row under it; and the table writer gives every row the layout placed `w:cantSplit`, which + holds a row whole where the chain cannot reach inside it, the chain reaching only a cell's own + paragraphs. Neither writer knows about the other. The `keepWithNext` path was already held to + the pair (`DocxOneRowTableKeptWithTheNextTest`); on the `keepTogether` path + `aTableInsideAKeptBlockIsChainedRowByRow` asked only for the chain, and now asks for both, and + `aRowOfSeveralParagraphsIsHeldWholeByItsRow` pins the shape for which `w:cantSplit` is the only + thing holding the row — a cell of several paragraphs, which a plain cell is whenever its style + sets a line spacing. It is not unconditional either way: + `letTheOneRowTablesKeptWithTheNextBreak` takes `w:cantSplit` back off a one-row body table kept + with what follows, and a painted section asking only to be kept with the next is the one block + whose own row goes without it — 56 one-row tables of the corpus carry keeps without it. - A table composed in a cell is reached by neither writer: the chain reads a cell's own paragraphs, which are not those of a table in that cell, and the layout places no row for such a table, so the table writer gives its rows no `w:cantSplit` either. What holds it is the row it stands in. `aTableComposedInACellIsHeldByTheRowRoundIt` asserts the two absences together with that row, so the reason the gap costs nothing stands beside the gap, and names where it would begin to cost something: a row the layout did not place, which carries no `w:cantSplit` - of its own. Nothing in the backend's tests asserted anything about a composed table's - paragraphs before. + of its own. Nothing in the backend's tests asserted anything about a composed table's keeps + before. - Measured at the foot of a page in Word 16.0.20527 and LibreOffice 24.2, for a block that fits there and for one the layout moved to the next page whole: nine shapes over eighteen documents — a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack - holding a table, a block kept with the next, a painted panel kept with the next — each stood + holding a table, a block kept with the next, a painted panel kept with the next holding a table — each stood whole in both editors, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot of diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index add50bd58..56e3b8e21 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -131,7 +131,7 @@ honour an option ignores it (documented contract). | Protection / encryption | ✅ `PdfDocumentPostProcessor` | ❌ (ignored with a one-time warning — no OOXML encryption support planned) | ❌ (not written, so the file opens unprotected; reported `DROPPED`, `protection`) | | Viewer preferences | ✅ `applyViewerPreferences` in `PdfFixedLayoutBackend` | ❌ (ignored with a one-time warning — PDF-viewer concept) | n/a (not written; reported `DROPPED`, `viewer preferences`) | | Debug guide lines / node labels | ✅ `PdfGuideLinesRenderer`, `PdfNodeLabelRenderer` | ❌ (ignored with a one-time warning — render through the PDF backend to see overlays) | n/a | -| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit` — but the row of a one-row table of paragraphs kept with what follows, taken back off it by `letTheOneRowTablesKeptWithTheNextBreak` below, and a panel asking only to be kept with the next, which never gets it; what it adds over the chain is a row Word cannot end a page inside, the chain reaching only a cell's own paragraphs. A table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice sets a three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot moves to the next page whole there, which six of the nine did, and the band of two layers moved in both editors from a cause not measured. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`, while 56 one-row tables carry keeps without it. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | +| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit` — but the row of a one-row table of paragraphs kept with what follows, taken back off it by `letTheOneRowTablesKeptWithTheNextBreak` below; a painted section asking only to be kept with the next is the one block whose own row goes without it, a row or a shape container getting it whatever it asks (`writePanelPiece`). What it adds over the chain is a row Word cannot end a page inside — a cell of more than one paragraph, which a plain cell is whenever its style sets a line spacing, or a cell of a table — the chain reaching only a cell's own paragraphs. A table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice sets a three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot moves to the next page whole there, which six of the nine did, and the band of two layers moved in both editors from a cause not measured. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`, while 56 one-row tables carry keeps without it. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | | Layer stack — layers drawn over one another (`addLayerStack`) | ✅ each layer's fragments at the place the layout gave it | ✅ same | ⚠️ `DocxSemanticBackend` — Word has no layers, so a stack's children are written one after the other, without their positions. The exception is a stack whose layers are side-by-side columns (`DocxLayerColumns`): every layer a plain container at the stack's top-left corner, the bands their padding leaves either the same or apart. That is written as one table row, a cell per band, the way a two-column CV lays its columns out as layers to draw the name first. Layers sharing a band follow one another in its cell. A spacer the layout placed level with content of another layer of the same band only keeps that content's place, and is not written. The space above a later layer's first block is the gap the page shows below the content before it, to the block's content: what the block holds above that content past the gap — its margin, its padding, a contained picture's room — comes out of its own edge (`DocxLayerColumns.resume`) where it is a paragraph, a picture, a list or a table in its layer's own flow with its edge above at zero or more, and a list keeps its own edges after the gap, as a table does. Each layer is held to its box across, past the stack's edge where the page sets it past it, by an indent below zero; in a table cell no further left than the indents round it give, the rest named. The one layer of a stack of one layer is held the same way, its offset across kept and its measure with it; in a table cell, composed in a table's cell with no place of its own, and for an offset down or up, which moves it without making room for it, it is written where the stack starts and named — not where a band or a column's layer sets it at the page's distance below what is above it, which holds the offset. A shape container written as a panel in a table cell keeps its padding round its outline, as the page does — the table's indent and the space above and below it; where its table ends its cell, Word sets the cell's content at its middle or foot without the space below, which a centred cell takes off the space above, the rest named — sets what its cell holds at the top, middle or foot as its first written layer is set, holds a paragraph across to its box, and names a written layer after the first (written under the ones before it), an offset down or up, a box past the shape's side, and a layer set across by a box the layout does not give (`layer offset`). A layer whose content the page starts above where those before it end — a caption over its photo, a label in a card's corner, not a block giving back its edge whose padding or contained picture's room alone reaches there — is written under them, in a column's cell as in a band, and the report names it; a stack composed in a table cell, or exported with no layout, has no place of its own, and one writing more than one of its layers is named with them not measured. What the page draws inside another layer's fill comes after that fill. Drawings stack as the page paints them, by their z-index, each given its place (`wp:anchor/@relativeHeight`) from where the page paints its fragment, whatever order the layers are written in — among the drawings behind the text, and among those in front of it, as Word draws every one behind under every one in front. A painted layer, though, is a shaded cell rather than a drawing, and its fill belongs to the text layer: it is painted over every drawing behind the text, a drawing on a layer *above* it included, so a stack's z-index does not lift a drawing clear of a painted neighbour. An icon laid over a card's corner draws none of its ink in Word 16.0.20527 or LibreOffice 24.2 against 4308 pixels of it at 300 dpi in the engine's PDF; the report names such a drawing and one panel that covers it, taking the panels where it writes their fill, so that a panel bearing borders alone — which writes no shading — is not named and a row written as a panel is. Not named: a drawing under a panel the layout breaks across pages or composed in an authored table cell, one under a table cell's own shading, a cell's paint the table draws itself, and the second panel over a drawing lying over two (`DocxNeighbourFillGuardTest` holds the corpus to the one drawing that is known, and writes those edges out). Measured in LibreOffice: `CharcoalGold`, `SidebarPortrait` and `SlateOrange` fit on one page, as on the page; `NavySidebar` runs one line onto a second page. A stack or shape container the page gives no room — its margins taking back its whole height, as `LumaStudioInvoice`'s sidebar over the page's top margin — is laid over the flow (`laidOverTheFlow`): its drawings where the page draws them, each paragraph in a text box in front of the text where the page sets it, nothing in the flow. Not in a table cell or a panel, and not when it holds anything but drawing and plain paragraphs — a link, an anchor, a picture, a list or a table keeps it in the flow as above. A canvas is written as its contents — what it writes one block after another, its drawings where it places them — and holds its height: the room under what it writes is the space below it, and one that only draws holds its whole box where nothing round it — a band before its layer's first block, drawings held whole, an overlay it is a layer of — measures that room already. The report names the places and the width it loses, as it does a column's fixed width narrower than its band, and what it writes running past its height, a drawing in what it writes that takes no room in Word, or a height not measured | ## Output surface and lifecycle diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 910baf87d..f5b98bbe9 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -75,7 +75,7 @@ creation date is real metadata. | Images | Embedded pictures at the node's declared size. One contained in its box (`fitToBounds`, `CONTAIN`) is embedded at its fitted size, half the room it leaves above and below written as the space round its paragraph and half at its side as its indent; in a band the room below is its paragraph's own, as the band measures what it owes below from the box. In the flow, in a row's cell and held in by an alignment it stands in the box's middle, as the page draws it. In the flow, the margin and padding above and below are the space round the picture's paragraph; on the left they are not written, and the report names them, as it does a barcode's and a page reference's sides. A picture in the flow, a stack of one layer's included, or first in a band of layers there, that the layout moves to a new page keeps the space the layout leaves above it there — its margin, its padding, the room above it in its box, the edges of the containers opening with it and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, as a body paragraph does. A picture drawn beside its text is fitted to its box with its padding in it, which the report names too | | Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's — in a list item as much as in a paragraph. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires; on a section, container, table or image it wraps everything the block wrote, from the start of its first paragraph to the end of its last, so a link to a block lands on its first line; a section or a container written as a layer stack's column wraps what it writes the same way, from a block of it an earlier layer's stand-in holds, written there first, to the last it writes. A block that writes no paragraph — a section, a container or a layer stack's column of drawings alone, whose drawings stand in a paragraph of a block holding it, in the one nearest them or in one written on their page — is bookmarked at the start of the paragraph its first drawing is anchored in, the drawing on the earliest page and highest on it: Word keeps a drawing on its paragraph's page and moves it with that paragraph, so a page reference reads the page the block starts on and goes on reading it once the text above is edited, and a link goes to that page (measured in Word 16, fields updated, and LibreOffice 24.2: a block on page 2 read 2, and 3 with a page put in ahead of it, where the file had held no bookmark and the reference a fixed 2). A block that draws nothing, one whose first drawing stands on a page nothing is written on, and one composed in a table's cell, whose drawings its table draws, have no bookmark, and the report names the anchor: a link to it points at none, and a page reference to it is written as the number it reads. Two anchors that clean to one name, or to a name a suffix gave another, stay two bookmarks; one anchor two nodes declare is one bookmark, the last the file writes one for — the node the page sends a link to and reads a page reference's page from, where that node writes a paragraph to mark — and the report names the anchor, the nodes counted off the document, two cells of one table two of them, and a node of a kind of the caller's own counted where the layout registers it. An anchor in a table's header row the page repeats is drawn again on each page it repeats on, and the page sends a link to the last of them; the file holds the row once, where the table starts, which a link goes to and a page reference reads once Word updates it (measured: the bookmark on page 1, a reference written as 5 reading 1), and the report names it. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is | | Rows | A one-row table spanning the content width — or only its columns, where fixed columns leave part of the row empty — so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; fixed columns that add up to less than the row leave the rest of it empty, the last one at its own width and the table no wider than its columns, so its text wraps where the page wraps it; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin; a cell holds whatever its child is, written as it is anywhere else. The row's `verticalAlign` is every cell's `w:vAlign`, so a child shorter than the row sits at its middle or bottom as on the page — a table of contents' leader on its entry's baseline. A row composed in a table cell (`DocumentTableCell.node(...)`), which the layout places nowhere, that the page divides by what its children measure — an arrangement other than `START`, a spacer that grows, an auto column — takes its columns from where its paragraphs' lines start, the table having laid them out, a spacer where the page spreads the room between them; one holding a child those lines do not place — a picture, a shape, or a spacer in a row of stated columns, which gives it a slot of its own — or written inside a panel or another table in its cell has its columns sized by Word to their text, and the report names it. The row is kept whole across a page break, as the layout keeps it. A row is held at least as tall as the page makes it inside a painted panel, and anywhere its tallest child is a drawing Word holds nothing of in its cell — a badge beside a heading. A section or other container in any cell — a row's, a table's, a panel's — that pulls its first line up with a negative top edge writes that one-line paragraph as much shorter, its text seated where the page sets it — no more than the room above its letters, as Word draws an exact line's text only inside the line. A row that paints its box — `fillColor`, `stroke`, `borders` — is a panel holding its columns (see "What a panel keeps and loses" below): a table of one cell carrying the fill and borders, its margins the row's padding as a panel's are, and in it the columns as a table of their own, without the padding; a corner radius squares, and is named (`corner radius`), and what of its borders above and below Word draws past its box is taken, or named, as a panel's is. A row composed in a table cell, in a page zone's line, in a band of overlapping layers or where a stack's column measures the space round it to its text, with a padding below zero or a column hanging into it, or with a margin below zero above or below it or at a side in a cell, is written as its columns alone, its paint in the report (`row paint`); in the body a row bleeding past the margin is its panel | -| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, but the row of a one-row table of paragraphs kept with what follows, which is taken back off it above, and a panel asking only to be kept with the next, which never gets it — so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). The chain reaches inside a row only as far as its cells' own paragraphs, so what `w:cantSplit` adds there is a row Word cannot end a page inside: a cell of more than one paragraph or of a table, which only a composed cell can hold. A table composed in a cell is held by neither writer, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot of a page there moves to the next one whole; it moved six of the nine, and the band of two layers moved in both editors, from a cause not measured. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`; 56 one-row tables do carry keeps without it — panels and containers asking only to be kept with the next — and were measured whole in both editors. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | +| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, but the row of a one-row table of paragraphs kept with what follows, which is taken back off it above — so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). A painted section asking only to be kept with the next is the one block whose own row goes without it, a row or a shape container getting it whatever it asks. The chain reaches inside a row only as far as its cells' own paragraphs, so what `w:cantSplit` adds there is a row Word cannot end a page inside: a cell of more than one paragraph — a plain cell is a paragraph a line where its style sets a line spacing (see Tables), and a composed cell holds whatever it is given — or a cell of a table. A table composed in a cell is held by neither writer, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next holding a table — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot of a page there moves to the next one whole; it moved six of the nine, and the band of two layers moved in both editors, from a cause not measured. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`; 56 one-row tables do carry keeps without it — panels and containers asking only to be kept with the next — and were measured whole in both editors. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | | Spacers | An empty paragraph a tenth of a point tall, which Word keeps; the rest of the spacer's height is the space above the next block, or below this paragraph when a table follows. A spacer shorter than that tenth — a width anchor of no height, as `Panel`'s and `PanelLetter`'s cards open with — has its line reach past it by the rest, which the space below takes, as it takes a line hanging below a block, and what it cannot take is named (`space above`): taken nowhere, the line stood every card a tenth of a point taller in Word. A spacer in the body the layout moves to a new page with the gap before it, because the gap did not fit at the foot of the page above, holds the space the layout leaves above it there — that gap, and the edges of any containers opening with it — in its line, which Word keeps at the top of a page where it drops the space above. So does a spacer opening a stack of one layer, or a band of layers, which the layout moves to a new page whole: the space the layout leaves above it there is held in its line, less, in a stack of one layer, its layer's offset, which is named. It is kept with what follows it in the stack, which Word would otherwise set at the new page's top with the spacer left at the foot of the page above. A spacer's own margin and padding are not written, and the report names them — except below the lowest block of a band, or of a layer another resumes after in its column, where the space is measured from the page, the block's own margin and padding included | | Page breaks | Explicit Word page breaks | diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java index b9eefe9ef..d9e28b4c4 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java @@ -4,6 +4,7 @@ import com.demcha.compose.document.style.DocumentColor; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.table.DocumentTableCell; +import com.demcha.compose.document.table.DocumentTableStyle; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFTable; @@ -107,11 +108,12 @@ void aTableInsideAKeptBlockIsChainedRowByRow() throws Exception { // each paragraph's own lines together; the table writer gives every row the layout // placed w:cantSplit (breakRowsWhereTheLayoutDoes, blind to the keep flags), which // holds a row whole where the chain cannot reach inside it — a cell of more than one - // paragraph or of a table, which only a composed cell can hold, and any table outside - // a kept block, where no chain is written at all. Asked for here on the keepTogether - // path as DocxOneRowTableKeptWithTheNextTest asks it on keepWithNext. Not + // paragraph, or of a table, or any table outside a kept block, where no chain is + // written at all. Documented here at the call site; the pair is also asserted on the + // keepWithNext path by DocxOneRowTableKeptWithTheNextTest, and w:cantSplit alone by + // DocxTablePaginationTest, so this loop is not the only thing holding it. Not // unconditional: letTheOneRowTablesKeptWithTheNextBreak takes it back off a one-row - // body table kept with what follows, which that test's own fixtures hold. + // body table kept with what follows. assertThat(table.getRows()).hasSize(2); for (XWPFTableRow row : table.getRows()) { assertThat(row.isCantSplitRow()) @@ -121,15 +123,51 @@ void aTableInsideAKeptBlockIsChainedRowByRow() throws Exception { } } + /** + * A row of several paragraphs is held whole by {@code w:cantSplit} alone, the chain reaching + * only as far as each paragraph's own lines. + * + *

A plain cell is a paragraph per line, not one paragraph of several lines, when its style + * sets a line spacing and it holds more than one line — the row is sized with that spacing + * between them, and Word holds no space inside one paragraph ({@code paragraphPerLine} in + * {@code writeCellLines}, pinned by {@code DocxCellSeatTest}). So this is the shape on an + * authored table for which {@code w:cantSplit} is load-bearing: the chain writes + * {@code w:keepLines} on each of the row's paragraphs, which holds each one's own lines, and + * {@code w:keepNext} only where the row is not the block's last unit — neither of which stops + * Word ending a page between two paragraphs of the same row.

+ */ + @Test + void aRowOfSeveralParagraphsIsHeldWholeByItsRow() throws Exception { + try (XWPFDocument document = DocxExports.withLayout(595, 842, 36, page -> page + .addSection(s -> s.keepTogether() + .addParagraph(p -> p.text("Totals")) + .addTable(t -> t.autoColumns(2).rowCells( + DocumentTableCell.lines("Net", "of which goods") + .withStyle(DocumentTableStyle.builder().lineSpacing(2).build()), + DocumentTableCell.text("100")))))) { + XWPFTable table = document.getTables().get(0); + XWPFTableRow row = table.getRow(0); + + assertThat(row.getCell(0).getParagraphs()) + .as("the premise: a line spacing makes the cell a paragraph a line") + .hasSize(2); + assertThat(keepsLines(row.getCell(0).getParagraphs().get(0))).isTrue(); + assertThat(keepsNext(row.getCell(0).getParagraphs().get(0))) + .as("the row is the block's last unit, so the chain ties it to nothing after it") + .isFalse(); + assertThat(row.isCantSplitRow()) + .as("so only the row itself keeps its two paragraphs on one page") + .isTrue(); + } + } + /** * A table composed in a cell is held by the row round it, having nothing of its own. * *

Neither writer reaches it. The chain reads a cell's own paragraphs, which are not a * table's in that cell, so its paragraphs are told nothing; and the layout places no row for * a table composed in a cell, so the table writer gives its rows no {@code w:cantSplit} - * either. ({@code DocxOneRowTableKeptWithTheNextTest} asserts the same silence of a table - * nested in a cell of a kept row; here the table is composed into the cell by the author.) - * What holds it is the row it stands in, which carries {@code w:cantSplit} where the + * either. What holds it is the row it stands in, which carries {@code w:cantSplit} where the * layout placed that row: measured at the foot of a page in Word 16.0.20527 and LibreOffice * 24.2, the block stood whole in both, for a block that fits there and for one the layout * moved to the next page whole.

From efd0ead4895ded0652b45643d82c576b3efddf45 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 2 Nov 2026 11:32:19 +0000 Subject: [PATCH 4/8] test(docx): name the three writers that decide w:cantSplit, not one rule for it No one rule says which rows carry w:cantSplit, and the sentences added here tried twice to give one. Three writers decide it between them: breakRowsWhereTheLayoutDoes gives it to every row it lays out; writePanelPiece gives a block written as a panel its own row where it asks to be kept together, or is a row or a shape container the layout put on one page; and letTheOneRowTablesKeptWithTheNextBreak then takes it off a one-row body table of paragraphs all kept with the next, whichever writer put it there. Two existing fixtures refute the rule that was written instead. A painted section asking nothing at all also goes without it (DocxContainerPaintTest.aCardKeptTogetherOnOnePageIsARowThatDoesNotSplit asserts both sides), so "a painted section asking only to be kept with the next is the one block whose own row goes without it" named neither the gate nor the set. And a row asking only to be kept with the next ends up with no w:cantSplit in the file at all (DocxOneRowTableKeptWithTheNextTest.aRowKeptWithTheNextMayBreakAndHoldsOntoIt asserts the element is absent), because the third writer takes back what the gate gave, so "a row or a shape container getting it whatever it asks" was false of the export. The recipe also claimed the 56 one-row tables carrying keeps without w:cantSplit "were measured whole in both editors". They were not: the measurement is nine shapes, of which one has that gate's shape. The clause is gone, and the set is no longer described as panels and containers kept with the next - a one-row body table of paragraphs is neither. aRowOfSeveralParagraphsIsHeldWholeByItsRow is scoped to what it shows: w:cantSplit is the only thing holding such a row inside a kept block, since outside one no chain is written at all and it holds every row. It now asserts both premises before the rule - that the line spacing made two paragraphs, and that the chain ran in the body - and walks both of the row's paragraphs rather than the first. Also: LibreOffice moved seven of the nine, not six, the recipe having made the row-pitch count the editor's whole total; and one CHANGELOG line re-wrapped to the file's width. --- CHANGELOG.md | 28 +++++++++++-------- .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 2 +- .../semantic/docx/DocxKeepTogetherTest.java | 27 ++++++++++++++---- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63d28845a..ba9f5657d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4561,12 +4561,15 @@ follow semantic versioning; release dates are ISO 8601. paragraphs. Neither writer knows about the other. The `keepWithNext` path was already held to the pair (`DocxOneRowTableKeptWithTheNextTest`); on the `keepTogether` path `aTableInsideAKeptBlockIsChainedRowByRow` asked only for the chain, and now asks for both, and - `aRowOfSeveralParagraphsIsHeldWholeByItsRow` pins the shape for which `w:cantSplit` is the only - thing holding the row — a cell of several paragraphs, which a plain cell is whenever its style - sets a line spacing. It is not unconditional either way: - `letTheOneRowTablesKeptWithTheNextBreak` takes `w:cantSplit` back off a one-row body table kept - with what follows, and a painted section asking only to be kept with the next is the one block - whose own row goes without it — 56 one-row tables of the corpus carry keeps without it. + `aRowOfSeveralParagraphsIsHeldWholeByItsRow` pins the shape for which, inside a kept block, + `w:cantSplit` is the only thing holding the row — a cell of several paragraphs, which a plain + cell is whenever its style sets a line spacing. + - No one rule says which rows carry `w:cantSplit`: three writers decide it between them. The + table writer gives it to every row it lays out; `writePanelPiece` gives a block written as a + panel its own row where it asks to be kept together, or is a row or a shape container the + layout put on one page; and `letTheOneRowTablesKeptWithTheNextBreak` then takes it off a + one-row body table of paragraphs all kept with the next, whichever writer put it there. 56 + one-row tables of the corpus carry keeps without it. - A table composed in a cell is reached by neither writer: the chain reads a cell's own paragraphs, which are not those of a table in that cell, and the layout places no row for such a table, so the table writer gives its rows no `w:cantSplit` either. What holds it is the row @@ -4579,12 +4582,13 @@ follow semantic versioning; release dates are ISO 8601. there and for one the layout moved to the next page whole: nine shapes over eighteen documents — a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack - holding a table, a block kept with the next, a painted panel kept with the next holding a table — each stood - whole in both editors, with no group and no row broken. Where the editors differ it is which - page holds the whole block, never whether it is broken: LibreOffice sets a three-row table's - rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot of - a page there moves to the next one whole, which six of the nine did; the band of two layers - moved in both editors, from a cause not measured. + holding a table, a block kept with the next, a painted panel kept with the next holding a + table — each stood whole in both editors, with no group and no row broken. Where the editors + differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a + three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just + fits at the foot of a page there moves to the next one whole, which six of the nine did; the + band of two layers moved in both editors, from a cause not measured, making seven that + LibreOffice moved. - Counted over the 71-document fidelity corpus, with every node of every document classified and none left unclassified: 1116 blocks are held on one page — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — of diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index 56e3b8e21..92aea6a99 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -131,7 +131,7 @@ honour an option ignores it (documented contract). | Protection / encryption | ✅ `PdfDocumentPostProcessor` | ❌ (ignored with a one-time warning — no OOXML encryption support planned) | ❌ (not written, so the file opens unprotected; reported `DROPPED`, `protection`) | | Viewer preferences | ✅ `applyViewerPreferences` in `PdfFixedLayoutBackend` | ❌ (ignored with a one-time warning — PDF-viewer concept) | n/a (not written; reported `DROPPED`, `viewer preferences`) | | Debug guide lines / node labels | ✅ `PdfGuideLinesRenderer`, `PdfNodeLabelRenderer` | ❌ (ignored with a one-time warning — render through the PDF backend to see overlays) | n/a | -| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit` — but the row of a one-row table of paragraphs kept with what follows, taken back off it by `letTheOneRowTablesKeptWithTheNextBreak` below; a painted section asking only to be kept with the next is the one block whose own row goes without it, a row or a shape container getting it whatever it asks (`writePanelPiece`). What it adds over the chain is a row Word cannot end a page inside — a cell of more than one paragraph, which a plain cell is whenever its style sets a line spacing, or a cell of a table — the chain reaching only a cell's own paragraphs. A table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice sets a three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot moves to the next page whole there, which six of the nine did, and the band of two layers moved in both editors from a cause not measured. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`, while 56 one-row tables carry keeps without it. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | +| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit` — but the row of a one-row table of paragraphs kept with what follows, taken back off it by `letTheOneRowTablesKeptWithTheNextBreak` below; three writers decide it between them, so no one rule says which rows carry it — the table writer gives it to every row it lays out, `writePanelPiece` gives a block written as a panel its own row where it asks to be kept together or is a row or a shape container the layout put on one page, and `letTheOneRowTablesKeptWithTheNextBreak` takes it off a one-row body table of paragraphs all kept with the next, whichever writer put it there. What it adds over the chain is a row Word cannot end a page inside — a cell of more than one paragraph, which a plain cell is whenever its style sets a line spacing, or a cell of a table — the chain reaching only a cell's own paragraphs. A table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice sets a three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot moves to the next page whole there, which six of the nine did, and the band of two layers moved in both editors from a cause not measured. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`, while 56 one-row tables carry keeps without it. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | | Layer stack — layers drawn over one another (`addLayerStack`) | ✅ each layer's fragments at the place the layout gave it | ✅ same | ⚠️ `DocxSemanticBackend` — Word has no layers, so a stack's children are written one after the other, without their positions. The exception is a stack whose layers are side-by-side columns (`DocxLayerColumns`): every layer a plain container at the stack's top-left corner, the bands their padding leaves either the same or apart. That is written as one table row, a cell per band, the way a two-column CV lays its columns out as layers to draw the name first. Layers sharing a band follow one another in its cell. A spacer the layout placed level with content of another layer of the same band only keeps that content's place, and is not written. The space above a later layer's first block is the gap the page shows below the content before it, to the block's content: what the block holds above that content past the gap — its margin, its padding, a contained picture's room — comes out of its own edge (`DocxLayerColumns.resume`) where it is a paragraph, a picture, a list or a table in its layer's own flow with its edge above at zero or more, and a list keeps its own edges after the gap, as a table does. Each layer is held to its box across, past the stack's edge where the page sets it past it, by an indent below zero; in a table cell no further left than the indents round it give, the rest named. The one layer of a stack of one layer is held the same way, its offset across kept and its measure with it; in a table cell, composed in a table's cell with no place of its own, and for an offset down or up, which moves it without making room for it, it is written where the stack starts and named — not where a band or a column's layer sets it at the page's distance below what is above it, which holds the offset. A shape container written as a panel in a table cell keeps its padding round its outline, as the page does — the table's indent and the space above and below it; where its table ends its cell, Word sets the cell's content at its middle or foot without the space below, which a centred cell takes off the space above, the rest named — sets what its cell holds at the top, middle or foot as its first written layer is set, holds a paragraph across to its box, and names a written layer after the first (written under the ones before it), an offset down or up, a box past the shape's side, and a layer set across by a box the layout does not give (`layer offset`). A layer whose content the page starts above where those before it end — a caption over its photo, a label in a card's corner, not a block giving back its edge whose padding or contained picture's room alone reaches there — is written under them, in a column's cell as in a band, and the report names it; a stack composed in a table cell, or exported with no layout, has no place of its own, and one writing more than one of its layers is named with them not measured. What the page draws inside another layer's fill comes after that fill. Drawings stack as the page paints them, by their z-index, each given its place (`wp:anchor/@relativeHeight`) from where the page paints its fragment, whatever order the layers are written in — among the drawings behind the text, and among those in front of it, as Word draws every one behind under every one in front. A painted layer, though, is a shaded cell rather than a drawing, and its fill belongs to the text layer: it is painted over every drawing behind the text, a drawing on a layer *above* it included, so a stack's z-index does not lift a drawing clear of a painted neighbour. An icon laid over a card's corner draws none of its ink in Word 16.0.20527 or LibreOffice 24.2 against 4308 pixels of it at 300 dpi in the engine's PDF; the report names such a drawing and one panel that covers it, taking the panels where it writes their fill, so that a panel bearing borders alone — which writes no shading — is not named and a row written as a panel is. Not named: a drawing under a panel the layout breaks across pages or composed in an authored table cell, one under a table cell's own shading, a cell's paint the table draws itself, and the second panel over a drawing lying over two (`DocxNeighbourFillGuardTest` holds the corpus to the one drawing that is known, and writes those edges out). Measured in LibreOffice: `CharcoalGold`, `SidebarPortrait` and `SlateOrange` fit on one page, as on the page; `NavySidebar` runs one line onto a second page. A stack or shape container the page gives no room — its margins taking back its whole height, as `LumaStudioInvoice`'s sidebar over the page's top margin — is laid over the flow (`laidOverTheFlow`): its drawings where the page draws them, each paragraph in a text box in front of the text where the page sets it, nothing in the flow. Not in a table cell or a panel, and not when it holds anything but drawing and plain paragraphs — a link, an anchor, a picture, a list or a table keeps it in the flow as above. A canvas is written as its contents — what it writes one block after another, its drawings where it places them — and holds its height: the room under what it writes is the space below it, and one that only draws holds its whole box where nothing round it — a band before its layer's first block, drawings held whole, an overlay it is a layer of — measures that room already. The report names the places and the width it loses, as it does a column's fixed width narrower than its band, and what it writes running past its height, a drawing in what it writes that takes no room in Word, or a height not measured | ## Output surface and lifecycle diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index f5b98bbe9..4c0d828f3 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -75,7 +75,7 @@ creation date is real metadata. | Images | Embedded pictures at the node's declared size. One contained in its box (`fitToBounds`, `CONTAIN`) is embedded at its fitted size, half the room it leaves above and below written as the space round its paragraph and half at its side as its indent; in a band the room below is its paragraph's own, as the band measures what it owes below from the box. In the flow, in a row's cell and held in by an alignment it stands in the box's middle, as the page draws it. In the flow, the margin and padding above and below are the space round the picture's paragraph; on the left they are not written, and the report names them, as it does a barcode's and a page reference's sides. A picture in the flow, a stack of one layer's included, or first in a band of layers there, that the layout moves to a new page keeps the space the layout leaves above it there — its margin, its padding, the room above it in its box, the edges of the containers opening with it and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, as a body paragraph does. A picture drawn beside its text is fitted to its box with its padding in it, which the report names too | | Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's — in a list item as much as in a paragraph. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires; on a section, container, table or image it wraps everything the block wrote, from the start of its first paragraph to the end of its last, so a link to a block lands on its first line; a section or a container written as a layer stack's column wraps what it writes the same way, from a block of it an earlier layer's stand-in holds, written there first, to the last it writes. A block that writes no paragraph — a section, a container or a layer stack's column of drawings alone, whose drawings stand in a paragraph of a block holding it, in the one nearest them or in one written on their page — is bookmarked at the start of the paragraph its first drawing is anchored in, the drawing on the earliest page and highest on it: Word keeps a drawing on its paragraph's page and moves it with that paragraph, so a page reference reads the page the block starts on and goes on reading it once the text above is edited, and a link goes to that page (measured in Word 16, fields updated, and LibreOffice 24.2: a block on page 2 read 2, and 3 with a page put in ahead of it, where the file had held no bookmark and the reference a fixed 2). A block that draws nothing, one whose first drawing stands on a page nothing is written on, and one composed in a table's cell, whose drawings its table draws, have no bookmark, and the report names the anchor: a link to it points at none, and a page reference to it is written as the number it reads. Two anchors that clean to one name, or to a name a suffix gave another, stay two bookmarks; one anchor two nodes declare is one bookmark, the last the file writes one for — the node the page sends a link to and reads a page reference's page from, where that node writes a paragraph to mark — and the report names the anchor, the nodes counted off the document, two cells of one table two of them, and a node of a kind of the caller's own counted where the layout registers it. An anchor in a table's header row the page repeats is drawn again on each page it repeats on, and the page sends a link to the last of them; the file holds the row once, where the table starts, which a link goes to and a page reference reads once Word updates it (measured: the bookmark on page 1, a reference written as 5 reading 1), and the report names it. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is | | Rows | A one-row table spanning the content width — or only its columns, where fixed columns leave part of the row empty — so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; fixed columns that add up to less than the row leave the rest of it empty, the last one at its own width and the table no wider than its columns, so its text wraps where the page wraps it; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin; a cell holds whatever its child is, written as it is anywhere else. The row's `verticalAlign` is every cell's `w:vAlign`, so a child shorter than the row sits at its middle or bottom as on the page — a table of contents' leader on its entry's baseline. A row composed in a table cell (`DocumentTableCell.node(...)`), which the layout places nowhere, that the page divides by what its children measure — an arrangement other than `START`, a spacer that grows, an auto column — takes its columns from where its paragraphs' lines start, the table having laid them out, a spacer where the page spreads the room between them; one holding a child those lines do not place — a picture, a shape, or a spacer in a row of stated columns, which gives it a slot of its own — or written inside a panel or another table in its cell has its columns sized by Word to their text, and the report names it. The row is kept whole across a page break, as the layout keeps it. A row is held at least as tall as the page makes it inside a painted panel, and anywhere its tallest child is a drawing Word holds nothing of in its cell — a badge beside a heading. A section or other container in any cell — a row's, a table's, a panel's — that pulls its first line up with a negative top edge writes that one-line paragraph as much shorter, its text seated where the page sets it — no more than the room above its letters, as Word draws an exact line's text only inside the line. A row that paints its box — `fillColor`, `stroke`, `borders` — is a panel holding its columns (see "What a panel keeps and loses" below): a table of one cell carrying the fill and borders, its margins the row's padding as a panel's are, and in it the columns as a table of their own, without the padding; a corner radius squares, and is named (`corner radius`), and what of its borders above and below Word draws past its box is taken, or named, as a panel's is. A row composed in a table cell, in a page zone's line, in a band of overlapping layers or where a stack's column measures the space round it to its text, with a padding below zero or a column hanging into it, or with a margin below zero above or below it or at a side in a cell, is written as its columns alone, its paint in the report (`row paint`); in the body a row bleeding past the margin is its panel | -| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, but the row of a one-row table of paragraphs kept with what follows, which is taken back off it above — so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). A painted section asking only to be kept with the next is the one block whose own row goes without it, a row or a shape container getting it whatever it asks. The chain reaches inside a row only as far as its cells' own paragraphs, so what `w:cantSplit` adds there is a row Word cannot end a page inside: a cell of more than one paragraph — a plain cell is a paragraph a line where its style sets a line spacing (see Tables), and a composed cell holds whatever it is given — or a cell of a table. A table composed in a cell is held by neither writer, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next holding a table — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot of a page there moves to the next one whole; it moved six of the nine, and the band of two layers moved in both editors, from a cause not measured. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`; 56 one-row tables do carry keeps without it — panels and containers asking only to be kept with the next — and were measured whole in both editors. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | +| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, but the row of a one-row table of paragraphs kept with what follows, which is taken back off it above — so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). Three writers decide it between them, so no one rule says which rows carry it: the table writer gives it to every row it lays out, a block written as a panel gets it on its own row where it asks to be kept together or is a row or a shape container the layout put on one page, and the pass above then takes it off a one-row body table of paragraphs all kept with the next, whichever writer put it there. The chain reaches inside a row only as far as its cells' own paragraphs, so what `w:cantSplit` adds there is a row Word cannot end a page inside: a cell of more than one paragraph — a plain cell is a paragraph a line where its style sets a line spacing (see Tables), and a composed cell holds whatever it is given — or a cell of a table. A table composed in a cell is held by neither writer, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next holding a table — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot of a page there moves to the next one whole, which six of the nine did; the band of two layers moved in both editors, from a cause not measured, making seven that LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`; 56 one-row tables do carry keeps without it, from those three writers between them. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | | Spacers | An empty paragraph a tenth of a point tall, which Word keeps; the rest of the spacer's height is the space above the next block, or below this paragraph when a table follows. A spacer shorter than that tenth — a width anchor of no height, as `Panel`'s and `PanelLetter`'s cards open with — has its line reach past it by the rest, which the space below takes, as it takes a line hanging below a block, and what it cannot take is named (`space above`): taken nowhere, the line stood every card a tenth of a point taller in Word. A spacer in the body the layout moves to a new page with the gap before it, because the gap did not fit at the foot of the page above, holds the space the layout leaves above it there — that gap, and the edges of any containers opening with it — in its line, which Word keeps at the top of a page where it drops the space above. So does a spacer opening a stack of one layer, or a band of layers, which the layout moves to a new page whole: the space the layout leaves above it there is held in its line, less, in a stack of one layer, its layer's offset, which is named. It is kept with what follows it in the stack, which Word would otherwise set at the new page's top with the spacer left at the foot of the page above. A spacer's own margin and padding are not written, and the report names them — except below the lowest block of a band, or of a layer another resumes after in its column, where the space is measured from the page, the block's own margin and padding included | | Page breaks | Explicit Word page breaks | diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java index d9e28b4c4..8caf30be7 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java @@ -130,11 +130,19 @@ void aTableInsideAKeptBlockIsChainedRowByRow() throws Exception { *

A plain cell is a paragraph per line, not one paragraph of several lines, when its style * sets a line spacing and it holds more than one line — the row is sized with that spacing * between them, and Word holds no space inside one paragraph ({@code paragraphPerLine} in - * {@code writeCellLines}, pinned by {@code DocxCellSeatTest}). So this is the shape on an - * authored table for which {@code w:cantSplit} is load-bearing: the chain writes + * {@code writeCellLines}, pinned by {@code DocxCellSeatTest}). So this is the shape for which + * {@code w:cantSplit} is load-bearing inside a kept block — outside one no chain is + * written at all and it is load-bearing for every row: the chain writes * {@code w:keepLines} on each of the row's paragraphs, which holds each one's own lines, and * {@code w:keepNext} only where the row is not the block's last unit — neither of which stops * Word ending a page between two paragraphs of the same row.

+ * + *

The block is kept together on purpose. Measured on this same document with + * {@code keepWithNext()} instead: the row's paragraphs then carry {@code w:keepNext} as well, + * which makes it a table {@code letTheOneRowTablesKeptWithTheNextBreak} frees, and + * {@code w:cantSplit} is taken off it — leaving the two paragraphs of that row + * held by nothing at all. No document of the corpus has that shape (no block asking only to + * be kept with the next holds a table), so it is left as it is rather than fixed here.

*/ @Test void aRowOfSeveralParagraphsIsHeldWholeByItsRow() throws Exception { @@ -151,10 +159,17 @@ void aRowOfSeveralParagraphsIsHeldWholeByItsRow() throws Exception { assertThat(row.getCell(0).getParagraphs()) .as("the premise: a line spacing makes the cell a paragraph a line") .hasSize(2); - assertThat(keepsLines(row.getCell(0).getParagraphs().get(0))).isTrue(); - assertThat(keepsNext(row.getCell(0).getParagraphs().get(0))) - .as("the row is the block's last unit, so the chain ties it to nothing after it") - .isFalse(); + assertThat(keepsNext(paragraph(document, "Totals"))) + .as("the premise: the chain ran, and ran in the body") + .isTrue(); + for (XWPFParagraph paragraph : row.getCell(0).getParagraphs()) { + assertThat(keepsLines(paragraph)) + .as("each of the row's paragraphs has its own lines held") + .isTrue(); + assertThat(keepsNext(paragraph)) + .as("the row is the block's last unit, so the chain ties it to nothing after it") + .isFalse(); + } assertThat(row.isCantSplitRow()) .as("so only the row itself keeps its two paragraphs on one page") .isTrue(); From e96552c0bb675cbf9ea5c8a74a1e976f2a99def6 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 2 Nov 2026 11:55:30 +0000 Subject: [PATCH 5/8] docs(docx): give the w:cantSplit account its fourth writer and its real conditions Four writers decide whether a row carries w:cantSplit, not three. The account named breakRowsWhereTheLayoutDoes, writePanelPiece and the pass that takes the flag back, and left out writeRow, which gives it to a row written as a table wherever the layout placed that row. That omission made the commonest case in the backend unexplainable: a plain unpainted addRow carries w:cantSplit, and it reaches neither of the two writers the account named - breakRowsWhereTheLayoutDoes sees only a table's rows, and writePanelPiece is reached only through writePaintedRow, which runs only when the row paints. It is also why a row asking only to be kept with the next has no w:cantSplit in the file at all: writeRow gave it one and the removal pass took it back, the panel gate never having run. Three conditions stated more narrowly than the code: - layout.onOnePage gates all three disjuncts of the panel gate, not just the two type tests, so a painted section kept together but taller than a page gets no w:cantSplit either. - A plain cell is a paragraph a line only where its style sets a line spacing AND it holds more than one line; with one line it stays one paragraph. - A block in a table cell is not left without a chain: it gets w:keepLines on every paragraph, and only w:keepNext is withheld. What keeps a table in a cell from being freed is that the removal pass reads the body's tables alone. The local behind the paragraph-a-line rule lives in writeCellContent; writeCellLines, named in the new fixture's javadoc, does not exist. The row-height figure is scoped to the shape it was read off - a three-row table, 33.9pt against the page's 21.0 - rather than offered as the cause of all six moves it covers; the count of 56 one-row tables carrying keeps without w:cantSplit is given as a count, with the shapes that reach it named and no apportionment claimed; and the corpus classification states its denominator again. --- CHANGELOG.md | 37 ++++++++++--------- .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 2 +- .../semantic/docx/DocxKeepTogetherTest.java | 8 ++-- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba9f5657d..b3f67b026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4563,13 +4563,15 @@ follow semantic versioning; release dates are ISO 8601. `aTableInsideAKeptBlockIsChainedRowByRow` asked only for the chain, and now asks for both, and `aRowOfSeveralParagraphsIsHeldWholeByItsRow` pins the shape for which, inside a kept block, `w:cantSplit` is the only thing holding the row — a cell of several paragraphs, which a plain - cell is whenever its style sets a line spacing. - - No one rule says which rows carry `w:cantSplit`: three writers decide it between them. The - table writer gives it to every row it lays out; `writePanelPiece` gives a block written as a - panel its own row where it asks to be kept together, or is a row or a shape container the - layout put on one page; and `letTheOneRowTablesKeptWithTheNextBreak` then takes it off a - one-row body table of paragraphs all kept with the next, whichever writer put it there. 56 - one-row tables of the corpus carry keeps without it. + cell is where its style sets a line spacing and it holds more than one line. + - No one rule says which rows carry `w:cantSplit`: four writers decide it between them. + `breakRowsWhereTheLayoutDoes` gives it to each row of a table the layout placed, `writeRow` to + a row written as a table where the layout placed that row, and `writePanelPiece` to a block + written as a panel on its own row where the layout put the block on one page and it either + asks to be kept together or is a row or a shape container; then + `letTheOneRowTablesKeptWithTheNextBreak` takes it off a one-row body table of paragraphs all + kept with the next, whichever of the three put it there. 56 one-row tables of the corpus carry + keeps without it; the count was not apportioned between the four. - A table composed in a cell is reached by neither writer: the chain reads a cell's own paragraphs, which are not those of a table in that cell, and the layout places no row for such a table, so the table writer gives its rows no `w:cantSplit` either. What holds it is the row @@ -4585,17 +4587,18 @@ follow semantic versioning; release dates are ISO 8601. holding a table, a block kept with the next, a painted panel kept with the next holding a table — each stood whole in both editors, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a - three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just - fits at the foot of a page there moves to the next one whole, which six of the nine did; the - band of two layers moved in both editors, from a cause not measured, making seven that - LibreOffice moved. + table's rows taller — a three-row table's 33.9pt apart against the page's 21.0 — so a block + whose table only just fits at the foot of a page there moves to the next one whole, which six + of the nine did; the band of two layers moved in both editors, from a cause not measured, + making seven that LibreOffice moved. The 33.9pt was read off the three-row shape; the other + five were not measured apart. - Counted over the 71-document fidelity corpus, with every node of every document classified and - none left unclassified: 1116 blocks are held on one page — 214 in the body, 902 in a cell or a - row's column, layer stacks and shape containers among them as well as the two keep flags — of - which 20 hold a table, 338 hold a band or a container, and none holds a table composed in a - cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are - on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, - lacks `w:cantSplit`. + none of the 1116 found left unclassified: 1116 blocks are held on one page — 214 in the body, + 902 in a cell or a row's column, layer stacks and shape containers among them as well as the + two keep flags — of which 20 hold a table, 338 hold a band or a container, and none holds a + table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers + the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of + more than one row, lacks `w:cantSplit`. - **A fixture holds the record a page an editor adds past a one-page section is painted from: the export is grown by eighty appended paragraphs through POI and read back, and the fills are in the header of the one section the grown document has.** diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index 92aea6a99..1b3383008 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -131,7 +131,7 @@ honour an option ignores it (documented contract). | Protection / encryption | ✅ `PdfDocumentPostProcessor` | ❌ (ignored with a one-time warning — no OOXML encryption support planned) | ❌ (not written, so the file opens unprotected; reported `DROPPED`, `protection`) | | Viewer preferences | ✅ `applyViewerPreferences` in `PdfFixedLayoutBackend` | ❌ (ignored with a one-time warning — PDF-viewer concept) | n/a (not written; reported `DROPPED`, `viewer preferences`) | | Debug guide lines / node labels | ✅ `PdfGuideLinesRenderer`, `PdfNodeLabelRenderer` | ❌ (ignored with a one-time warning — render through the PDF backend to see overlays) | n/a | -| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit` — but the row of a one-row table of paragraphs kept with what follows, taken back off it by `letTheOneRowTablesKeptWithTheNextBreak` below; three writers decide it between them, so no one rule says which rows carry it — the table writer gives it to every row it lays out, `writePanelPiece` gives a block written as a panel its own row where it asks to be kept together or is a row or a shape container the layout put on one page, and `letTheOneRowTablesKeptWithTheNextBreak` takes it off a one-row body table of paragraphs all kept with the next, whichever writer put it there. What it adds over the chain is a row Word cannot end a page inside — a cell of more than one paragraph, which a plain cell is whenever its style sets a line spacing, or a cell of a table — the chain reaching only a cell's own paragraphs. A table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice sets a three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot moves to the next page whole there, which six of the nine did, and the band of two layers moved in both editors from a cause not measured. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`, while 56 one-row tables carry keeps without it. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | +| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit` — but the row of a one-row table of paragraphs kept with what follows, taken back off it by `letTheOneRowTablesKeptWithTheNextBreak` below; four writers decide it between them, so no one rule says which rows carry it — `breakRowsWhereTheLayoutDoes` gives it to each row of a table the layout placed, `writeRow` to a row written as a table where the layout placed that row, `writePanelPiece` to a block written as a panel on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container, and `letTheOneRowTablesKeptWithTheNextBreak` then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there. What it adds over the chain is a row Word cannot end a page inside — a cell of more than one paragraph, which a plain cell is where its style sets a line spacing and it holds more than one line, or a cell of a table — the chain reaching only a cell's own paragraphs. A table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot moves to the next page whole there, which six of the nine did, and the band of two layers moved in both editors from a cause not measured, making seven LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`, while 56 one-row tables carry keeps without it. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | | Layer stack — layers drawn over one another (`addLayerStack`) | ✅ each layer's fragments at the place the layout gave it | ✅ same | ⚠️ `DocxSemanticBackend` — Word has no layers, so a stack's children are written one after the other, without their positions. The exception is a stack whose layers are side-by-side columns (`DocxLayerColumns`): every layer a plain container at the stack's top-left corner, the bands their padding leaves either the same or apart. That is written as one table row, a cell per band, the way a two-column CV lays its columns out as layers to draw the name first. Layers sharing a band follow one another in its cell. A spacer the layout placed level with content of another layer of the same band only keeps that content's place, and is not written. The space above a later layer's first block is the gap the page shows below the content before it, to the block's content: what the block holds above that content past the gap — its margin, its padding, a contained picture's room — comes out of its own edge (`DocxLayerColumns.resume`) where it is a paragraph, a picture, a list or a table in its layer's own flow with its edge above at zero or more, and a list keeps its own edges after the gap, as a table does. Each layer is held to its box across, past the stack's edge where the page sets it past it, by an indent below zero; in a table cell no further left than the indents round it give, the rest named. The one layer of a stack of one layer is held the same way, its offset across kept and its measure with it; in a table cell, composed in a table's cell with no place of its own, and for an offset down or up, which moves it without making room for it, it is written where the stack starts and named — not where a band or a column's layer sets it at the page's distance below what is above it, which holds the offset. A shape container written as a panel in a table cell keeps its padding round its outline, as the page does — the table's indent and the space above and below it; where its table ends its cell, Word sets the cell's content at its middle or foot without the space below, which a centred cell takes off the space above, the rest named — sets what its cell holds at the top, middle or foot as its first written layer is set, holds a paragraph across to its box, and names a written layer after the first (written under the ones before it), an offset down or up, a box past the shape's side, and a layer set across by a box the layout does not give (`layer offset`). A layer whose content the page starts above where those before it end — a caption over its photo, a label in a card's corner, not a block giving back its edge whose padding or contained picture's room alone reaches there — is written under them, in a column's cell as in a band, and the report names it; a stack composed in a table cell, or exported with no layout, has no place of its own, and one writing more than one of its layers is named with them not measured. What the page draws inside another layer's fill comes after that fill. Drawings stack as the page paints them, by their z-index, each given its place (`wp:anchor/@relativeHeight`) from where the page paints its fragment, whatever order the layers are written in — among the drawings behind the text, and among those in front of it, as Word draws every one behind under every one in front. A painted layer, though, is a shaded cell rather than a drawing, and its fill belongs to the text layer: it is painted over every drawing behind the text, a drawing on a layer *above* it included, so a stack's z-index does not lift a drawing clear of a painted neighbour. An icon laid over a card's corner draws none of its ink in Word 16.0.20527 or LibreOffice 24.2 against 4308 pixels of it at 300 dpi in the engine's PDF; the report names such a drawing and one panel that covers it, taking the panels where it writes their fill, so that a panel bearing borders alone — which writes no shading — is not named and a row written as a panel is. Not named: a drawing under a panel the layout breaks across pages or composed in an authored table cell, one under a table cell's own shading, a cell's paint the table draws itself, and the second panel over a drawing lying over two (`DocxNeighbourFillGuardTest` holds the corpus to the one drawing that is known, and writes those edges out). Measured in LibreOffice: `CharcoalGold`, `SidebarPortrait` and `SlateOrange` fit on one page, as on the page; `NavySidebar` runs one line onto a second page. A stack or shape container the page gives no room — its margins taking back its whole height, as `LumaStudioInvoice`'s sidebar over the page's top margin — is laid over the flow (`laidOverTheFlow`): its drawings where the page draws them, each paragraph in a text box in front of the text where the page sets it, nothing in the flow. Not in a table cell or a panel, and not when it holds anything but drawing and plain paragraphs — a link, an anchor, a picture, a list or a table keeps it in the flow as above. A canvas is written as its contents — what it writes one block after another, its drawings where it places them — and holds its height: the room under what it writes is the space below it, and one that only draws holds its whole box where nothing round it — a band before its layer's first block, drawings held whole, an overlay it is a layer of — measures that room already. The report names the places and the width it loses, as it does a column's fixed width narrower than its band, and what it writes running past its height, a drawing in what it writes that takes no room in Word, or a height not measured | ## Output surface and lifecycle diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 4c0d828f3..89836f57d 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -75,7 +75,7 @@ creation date is real metadata. | Images | Embedded pictures at the node's declared size. One contained in its box (`fitToBounds`, `CONTAIN`) is embedded at its fitted size, half the room it leaves above and below written as the space round its paragraph and half at its side as its indent; in a band the room below is its paragraph's own, as the band measures what it owes below from the box. In the flow, in a row's cell and held in by an alignment it stands in the box's middle, as the page draws it. In the flow, the margin and padding above and below are the space round the picture's paragraph; on the left they are not written, and the report names them, as it does a barcode's and a page reference's sides. A picture in the flow, a stack of one layer's included, or first in a band of layers there, that the layout moves to a new page keeps the space the layout leaves above it there — its margin, its padding, the room above it in its box, the edges of the containers opening with it and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, as a body paragraph does. A picture drawn beside its text is fitted to its box with its padding in it, which the report names too | | Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's — in a list item as much as in a paragraph. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires; on a section, container, table or image it wraps everything the block wrote, from the start of its first paragraph to the end of its last, so a link to a block lands on its first line; a section or a container written as a layer stack's column wraps what it writes the same way, from a block of it an earlier layer's stand-in holds, written there first, to the last it writes. A block that writes no paragraph — a section, a container or a layer stack's column of drawings alone, whose drawings stand in a paragraph of a block holding it, in the one nearest them or in one written on their page — is bookmarked at the start of the paragraph its first drawing is anchored in, the drawing on the earliest page and highest on it: Word keeps a drawing on its paragraph's page and moves it with that paragraph, so a page reference reads the page the block starts on and goes on reading it once the text above is edited, and a link goes to that page (measured in Word 16, fields updated, and LibreOffice 24.2: a block on page 2 read 2, and 3 with a page put in ahead of it, where the file had held no bookmark and the reference a fixed 2). A block that draws nothing, one whose first drawing stands on a page nothing is written on, and one composed in a table's cell, whose drawings its table draws, have no bookmark, and the report names the anchor: a link to it points at none, and a page reference to it is written as the number it reads. Two anchors that clean to one name, or to a name a suffix gave another, stay two bookmarks; one anchor two nodes declare is one bookmark, the last the file writes one for — the node the page sends a link to and reads a page reference's page from, where that node writes a paragraph to mark — and the report names the anchor, the nodes counted off the document, two cells of one table two of them, and a node of a kind of the caller's own counted where the layout registers it. An anchor in a table's header row the page repeats is drawn again on each page it repeats on, and the page sends a link to the last of them; the file holds the row once, where the table starts, which a link goes to and a page reference reads once Word updates it (measured: the bookmark on page 1, a reference written as 5 reading 1), and the report names it. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is | | Rows | A one-row table spanning the content width — or only its columns, where fixed columns leave part of the row empty — so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; fixed columns that add up to less than the row leave the rest of it empty, the last one at its own width and the table no wider than its columns, so its text wraps where the page wraps it; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin; a cell holds whatever its child is, written as it is anywhere else. The row's `verticalAlign` is every cell's `w:vAlign`, so a child shorter than the row sits at its middle or bottom as on the page — a table of contents' leader on its entry's baseline. A row composed in a table cell (`DocumentTableCell.node(...)`), which the layout places nowhere, that the page divides by what its children measure — an arrangement other than `START`, a spacer that grows, an auto column — takes its columns from where its paragraphs' lines start, the table having laid them out, a spacer where the page spreads the room between them; one holding a child those lines do not place — a picture, a shape, or a spacer in a row of stated columns, which gives it a slot of its own — or written inside a panel or another table in its cell has its columns sized by Word to their text, and the report names it. The row is kept whole across a page break, as the layout keeps it. A row is held at least as tall as the page makes it inside a painted panel, and anywhere its tallest child is a drawing Word holds nothing of in its cell — a badge beside a heading. A section or other container in any cell — a row's, a table's, a panel's — that pulls its first line up with a negative top edge writes that one-line paragraph as much shorter, its text seated where the page sets it — no more than the room above its letters, as Word draws an exact line's text only inside the line. A row that paints its box — `fillColor`, `stroke`, `borders` — is a panel holding its columns (see "What a panel keeps and loses" below): a table of one cell carrying the fill and borders, its margins the row's padding as a panel's are, and in it the columns as a table of their own, without the padding; a corner radius squares, and is named (`corner radius`), and what of its borders above and below Word draws past its box is taken, or named, as a panel's is. A row composed in a table cell, in a page zone's line, in a band of overlapping layers or where a stack's column measures the space round it to its text, with a padding below zero or a column hanging into it, or with a margin below zero above or below it or at a side in a cell, is written as its columns alone, its paint in the report (`row paint`); in the body a row bleeding past the margin is its panel | -| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, but the row of a one-row table of paragraphs kept with what follows, which is taken back off it above — so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). Three writers decide it between them, so no one rule says which rows carry it: the table writer gives it to every row it lays out, a block written as a panel gets it on its own row where it asks to be kept together or is a row or a shape container the layout put on one page, and the pass above then takes it off a one-row body table of paragraphs all kept with the next, whichever writer put it there. The chain reaches inside a row only as far as its cells' own paragraphs, so what `w:cantSplit` adds there is a row Word cannot end a page inside: a cell of more than one paragraph — a plain cell is a paragraph a line where its style sets a line spacing (see Tables), and a composed cell holds whatever it is given — or a cell of a table. A table composed in a cell is held by neither writer, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next holding a table — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a three-row table's rows 33.9pt apart against the page's 21.0, so a table block that only just fits at the foot of a page there moves to the next one whole, which six of the nine did; the band of two layers moved in both editors, from a cause not measured, making seven that LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`; 56 one-row tables do carry keeps without it, from those three writers between them. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | +| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, but the row of a one-row table of paragraphs kept with what follows, which is taken back off it above — so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). Four writers decide it between them, so no one rule says which rows carry it: a table's rows get it where the layout placed each row; a row written as a table gets it where the layout placed that row; a block written as a panel gets it on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container; and the pass above then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there. The chain reaches inside a row only as far as its cells' own paragraphs, so what `w:cantSplit` adds there is a row Word cannot end a page inside: a cell of more than one paragraph — a plain cell is a paragraph a line where its style sets a line spacing and it holds more than one line (see Tables), and a composed cell holds whatever it is given — or a cell of a table. A table composed in a cell is held by neither writer, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next holding a table — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot of a page there moves to the next one whole, which six of the nine did; the band of two layers moved in both editors, from a cause not measured, making seven that LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`; 56 one-row tables do carry keeps without it, from those four writers between them. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | | Spacers | An empty paragraph a tenth of a point tall, which Word keeps; the rest of the spacer's height is the space above the next block, or below this paragraph when a table follows. A spacer shorter than that tenth — a width anchor of no height, as `Panel`'s and `PanelLetter`'s cards open with — has its line reach past it by the rest, which the space below takes, as it takes a line hanging below a block, and what it cannot take is named (`space above`): taken nowhere, the line stood every card a tenth of a point taller in Word. A spacer in the body the layout moves to a new page with the gap before it, because the gap did not fit at the foot of the page above, holds the space the layout leaves above it there — that gap, and the edges of any containers opening with it — in its line, which Word keeps at the top of a page where it drops the space above. So does a spacer opening a stack of one layer, or a band of layers, which the layout moves to a new page whole: the space the layout leaves above it there is held in its line, less, in a stack of one layer, its layer's offset, which is named. It is kept with what follows it in the stack, which Word would otherwise set at the new page's top with the spacer left at the foot of the page above. A spacer's own margin and padding are not written, and the report names them — except below the lowest block of a band, or of a layer another resumes after in its column, where the space is measured from the page, the block's own margin and padding included | | Page breaks | Explicit Word page breaks | diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java index 8caf30be7..2a0ba5ece 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java @@ -130,7 +130,7 @@ void aTableInsideAKeptBlockIsChainedRowByRow() throws Exception { *

A plain cell is a paragraph per line, not one paragraph of several lines, when its style * sets a line spacing and it holds more than one line — the row is sized with that spacing * between them, and Word holds no space inside one paragraph ({@code paragraphPerLine} in - * {@code writeCellLines}, pinned by {@code DocxCellSeatTest}). So this is the shape for which + * {@code writeCellContent}, pinned by {@code DocxCellSeatTest}). So this is the shape for which * {@code w:cantSplit} is load-bearing inside a kept block — outside one no chain is * written at all and it is load-bearing for every row: the chain writes * {@code w:keepLines} on each of the row's paragraphs, which holds each one's own lines, and @@ -141,8 +141,10 @@ void aTableInsideAKeptBlockIsChainedRowByRow() throws Exception { * {@code keepWithNext()} instead: the row's paragraphs then carry {@code w:keepNext} as well, * which makes it a table {@code letTheOneRowTablesKeptWithTheNextBreak} frees, and * {@code w:cantSplit} is taken off it — leaving the two paragraphs of that row - * held by nothing at all. No document of the corpus has that shape (no block asking only to - * be kept with the next holds a table), so it is left as it is rather than fixed here.

+ * held by nothing at all. No document of the corpus has that shape: of its twenty blocks that + * hold a table, the three in the body all ask to be kept together, and the other seventeen + * are in a cell, which that pass never reaches — it reads the body's tables alone. So it is + * left as it is rather than fixed here.

*/ @Test void aRowOfSeveralParagraphsIsHeldWholeByItsRow() throws Exception { From 63fd583944a5ff040fadcc85c9f16c0aab7439f9 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 2 Nov 2026 12:22:23 +0000 Subject: [PATCH 6/8] test(docx): hold the composed table's absences to a premise, and name the panel split aTableComposedInACellIsHeldByTheRowRoundIt asserted that a composed table's paragraphs carry no keep, with the surrounding row's w:cantSplit beside it as the reason that costs nothing. But that flag comes from layout.placedRow, not from the chain, so it did not show that keepOnOnePage had run: a regression that stopped the chain would have left both absences green. The fixture now asserts the chain ran, and ran in the body, as its sibling does, and a sabotage that stops keepOnOnePage writing w:keepNext in the body requires it to fail. w:cantSplit has a fifth source the account did not name. After the four writers have decided, a panel the layout breaks over pages is copied whole by DocxTableSplit.splitInItsPanel, which uses copyXml: the second part's panel row and the copied header rows of its inner table carry whatever the first part's were given, judged by no gate of their own. Two sentences narrowed. A row of several paragraphs is the shape a PLAIN table reaches for which w:cantSplit alone holds the row inside a kept block; a cell holding a table is the other, and the fixture for it sits in the same file. And outside a kept block it is keepOnOnePage that writes nothing, not the export - breakRowsWhereTheLayoutDoes writes w:keepNext itself on a repeated header row. The Verification section now says which of the documented shapes the two editors were never shown: shape 2 of the nine set no line spacing, so its cell was one paragraph, and the multi-paragraph row the new fixture builds is read off the written XML alone, as is the keepWithNext boundary beside it. Also: the per-row gate described per row rather than per table, the composed table said to be held by neither the chain nor the table writer rather than by "neither writer", and the unplaced-row citation given as a document the layout refused rather than a table too tall. --- CHANGELOG.md | 24 +++++++++++-------- .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 2 +- .../semantic/docx/DocxKeepTogetherTest.java | 24 +++++++++++++------ 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3f67b026..213579af6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4561,17 +4561,21 @@ follow semantic versioning; release dates are ISO 8601. paragraphs. Neither writer knows about the other. The `keepWithNext` path was already held to the pair (`DocxOneRowTableKeptWithTheNextTest`); on the `keepTogether` path `aTableInsideAKeptBlockIsChainedRowByRow` asked only for the chain, and now asks for both, and - `aRowOfSeveralParagraphsIsHeldWholeByItsRow` pins the shape for which, inside a kept block, - `w:cantSplit` is the only thing holding the row — a cell of several paragraphs, which a plain - cell is where its style sets a line spacing and it holds more than one line. - - No one rule says which rows carry `w:cantSplit`: four writers decide it between them. - `breakRowsWhereTheLayoutDoes` gives it to each row of a table the layout placed, `writeRow` to - a row written as a table where the layout placed that row, and `writePanelPiece` to a block - written as a panel on its own row where the layout put the block on one page and it either - asks to be kept together or is a row or a shape container; then + `aRowOfSeveralParagraphsIsHeldWholeByItsRow` pins the shape a plain table reaches for which, + inside a kept block, `w:cantSplit` is the only thing holding the row — a cell of several + paragraphs, which a plain cell is where its style sets a line spacing and it holds more than + one line. (The other such shape is a cell holding a table, which the fixture below covers.) + - No one rule says which rows carry `w:cantSplit`: four writers decide which rows are given it. + `breakRowsWhereTheLayoutDoes` gives it to each row of a table where the layout placed that row, + `writeRow` to a row written as a table where the layout placed that row, and `writePanelPiece` + to a block written as a panel on its own row where the layout put the block on one page and it + either asks to be kept together or is a row or a shape container; then `letTheOneRowTablesKeptWithTheNextBreak` takes it off a one-row body table of paragraphs all - kept with the next, whichever of the three put it there. 56 one-row tables of the corpus carry - keeps without it; the count was not apportioned between the four. + kept with the next, whichever of the three put it there. A panel the layout breaks over pages + is copied whole after that, rows and flags together (`DocxTableSplit.splitInItsPanel`), so the + second part's rows carry what the first part's were given, judged by no gate of their own. 56 + one-row tables of the corpus carry keeps without `w:cantSplit`; the count was not apportioned + between the causes. - A table composed in a cell is reached by neither writer: the chain reads a cell's own paragraphs, which are not those of a table in that cell, and the layout places no row for such a table, so the table writer gives its rows no `w:cantSplit` either. What holds it is the row diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index 1b3383008..dda786059 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -131,7 +131,7 @@ honour an option ignores it (documented contract). | Protection / encryption | ✅ `PdfDocumentPostProcessor` | ❌ (ignored with a one-time warning — no OOXML encryption support planned) | ❌ (not written, so the file opens unprotected; reported `DROPPED`, `protection`) | | Viewer preferences | ✅ `applyViewerPreferences` in `PdfFixedLayoutBackend` | ❌ (ignored with a one-time warning — PDF-viewer concept) | n/a (not written; reported `DROPPED`, `viewer preferences`) | | Debug guide lines / node labels | ✅ `PdfGuideLinesRenderer`, `PdfNodeLabelRenderer` | ❌ (ignored with a one-time warning — render through the PDF backend to see overlays) | n/a | -| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit` — but the row of a one-row table of paragraphs kept with what follows, taken back off it by `letTheOneRowTablesKeptWithTheNextBreak` below; four writers decide it between them, so no one rule says which rows carry it — `breakRowsWhereTheLayoutDoes` gives it to each row of a table the layout placed, `writeRow` to a row written as a table where the layout placed that row, `writePanelPiece` to a block written as a panel on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container, and `letTheOneRowTablesKeptWithTheNextBreak` then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there. What it adds over the chain is a row Word cannot end a page inside — a cell of more than one paragraph, which a plain cell is where its style sets a line spacing and it holds more than one line, or a cell of a table — the chain reaching only a cell's own paragraphs. A table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot moves to the next page whole there, which six of the nine did, and the band of two layers moved in both editors from a cause not measured, making seven LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`, while 56 one-row tables carry keeps without it. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | +| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit` — but the row of a one-row table of paragraphs kept with what follows, taken back off it by `letTheOneRowTablesKeptWithTheNextBreak` below; four writers decide which rows are given it, so no one rule says which rows carry it — `breakRowsWhereTheLayoutDoes` gives it to each row of a table where the layout placed that row, `writeRow` to a row written as a table where the layout placed that row, `writePanelPiece` to a block written as a panel on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container, and `letTheOneRowTablesKeptWithTheNextBreak` then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there; a panel the layout breaks over pages is copied whole by `DocxTableSplit.splitInItsPanel`, so the second part's rows carry the first part's flags, judged by no gate of their own. What it adds over the chain is a row Word cannot end a page inside — a cell of more than one paragraph, which a plain cell is where its style sets a line spacing and it holds more than one line, or a cell of a table — the chain reaching only a cell's own paragraphs. A table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot moves to the next page whole there, which six of the nine did, and the band of two layers moved in both editors from a cause not measured, making seven LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`, while 56 one-row tables carry keeps without it. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | | Layer stack — layers drawn over one another (`addLayerStack`) | ✅ each layer's fragments at the place the layout gave it | ✅ same | ⚠️ `DocxSemanticBackend` — Word has no layers, so a stack's children are written one after the other, without their positions. The exception is a stack whose layers are side-by-side columns (`DocxLayerColumns`): every layer a plain container at the stack's top-left corner, the bands their padding leaves either the same or apart. That is written as one table row, a cell per band, the way a two-column CV lays its columns out as layers to draw the name first. Layers sharing a band follow one another in its cell. A spacer the layout placed level with content of another layer of the same band only keeps that content's place, and is not written. The space above a later layer's first block is the gap the page shows below the content before it, to the block's content: what the block holds above that content past the gap — its margin, its padding, a contained picture's room — comes out of its own edge (`DocxLayerColumns.resume`) where it is a paragraph, a picture, a list or a table in its layer's own flow with its edge above at zero or more, and a list keeps its own edges after the gap, as a table does. Each layer is held to its box across, past the stack's edge where the page sets it past it, by an indent below zero; in a table cell no further left than the indents round it give, the rest named. The one layer of a stack of one layer is held the same way, its offset across kept and its measure with it; in a table cell, composed in a table's cell with no place of its own, and for an offset down or up, which moves it without making room for it, it is written where the stack starts and named — not where a band or a column's layer sets it at the page's distance below what is above it, which holds the offset. A shape container written as a panel in a table cell keeps its padding round its outline, as the page does — the table's indent and the space above and below it; where its table ends its cell, Word sets the cell's content at its middle or foot without the space below, which a centred cell takes off the space above, the rest named — sets what its cell holds at the top, middle or foot as its first written layer is set, holds a paragraph across to its box, and names a written layer after the first (written under the ones before it), an offset down or up, a box past the shape's side, and a layer set across by a box the layout does not give (`layer offset`). A layer whose content the page starts above where those before it end — a caption over its photo, a label in a card's corner, not a block giving back its edge whose padding or contained picture's room alone reaches there — is written under them, in a column's cell as in a band, and the report names it; a stack composed in a table cell, or exported with no layout, has no place of its own, and one writing more than one of its layers is named with them not measured. What the page draws inside another layer's fill comes after that fill. Drawings stack as the page paints them, by their z-index, each given its place (`wp:anchor/@relativeHeight`) from where the page paints its fragment, whatever order the layers are written in — among the drawings behind the text, and among those in front of it, as Word draws every one behind under every one in front. A painted layer, though, is a shaded cell rather than a drawing, and its fill belongs to the text layer: it is painted over every drawing behind the text, a drawing on a layer *above* it included, so a stack's z-index does not lift a drawing clear of a painted neighbour. An icon laid over a card's corner draws none of its ink in Word 16.0.20527 or LibreOffice 24.2 against 4308 pixels of it at 300 dpi in the engine's PDF; the report names such a drawing and one panel that covers it, taking the panels where it writes their fill, so that a panel bearing borders alone — which writes no shading — is not named and a row written as a panel is. Not named: a drawing under a panel the layout breaks across pages or composed in an authored table cell, one under a table cell's own shading, a cell's paint the table draws itself, and the second panel over a drawing lying over two (`DocxNeighbourFillGuardTest` holds the corpus to the one drawing that is known, and writes those edges out). Measured in LibreOffice: `CharcoalGold`, `SidebarPortrait` and `SlateOrange` fit on one page, as on the page; `NavySidebar` runs one line onto a second page. A stack or shape container the page gives no room — its margins taking back its whole height, as `LumaStudioInvoice`'s sidebar over the page's top margin — is laid over the flow (`laidOverTheFlow`): its drawings where the page draws them, each paragraph in a text box in front of the text where the page sets it, nothing in the flow. Not in a table cell or a panel, and not when it holds anything but drawing and plain paragraphs — a link, an anchor, a picture, a list or a table keeps it in the flow as above. A canvas is written as its contents — what it writes one block after another, its drawings where it places them — and holds its height: the room under what it writes is the space below it, and one that only draws holds its whole box where nothing round it — a band before its layer's first block, drawings held whole, an overlay it is a layer of — measures that room already. The report names the places and the width it loses, as it does a column's fixed width narrower than its band, and what it writes running past its height, a drawing in what it writes that takes no room in Word, or a height not measured | ## Output surface and lifecycle diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 89836f57d..78ce9710f 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -75,7 +75,7 @@ creation date is real metadata. | Images | Embedded pictures at the node's declared size. One contained in its box (`fitToBounds`, `CONTAIN`) is embedded at its fitted size, half the room it leaves above and below written as the space round its paragraph and half at its side as its indent; in a band the room below is its paragraph's own, as the band measures what it owes below from the box. In the flow, in a row's cell and held in by an alignment it stands in the box's middle, as the page draws it. In the flow, the margin and padding above and below are the space round the picture's paragraph; on the left they are not written, and the report names them, as it does a barcode's and a page reference's sides. A picture in the flow, a stack of one layer's included, or first in a band of layers there, that the layout moves to a new page keeps the space the layout leaves above it there — its margin, its padding, the room above it in its box, the edges of the containers opening with it and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, as a body paragraph does. A picture drawn beside its text is fitted to its box with its padding in it, which the report names too | | Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's — in a list item as much as in a paragraph. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires; on a section, container, table or image it wraps everything the block wrote, from the start of its first paragraph to the end of its last, so a link to a block lands on its first line; a section or a container written as a layer stack's column wraps what it writes the same way, from a block of it an earlier layer's stand-in holds, written there first, to the last it writes. A block that writes no paragraph — a section, a container or a layer stack's column of drawings alone, whose drawings stand in a paragraph of a block holding it, in the one nearest them or in one written on their page — is bookmarked at the start of the paragraph its first drawing is anchored in, the drawing on the earliest page and highest on it: Word keeps a drawing on its paragraph's page and moves it with that paragraph, so a page reference reads the page the block starts on and goes on reading it once the text above is edited, and a link goes to that page (measured in Word 16, fields updated, and LibreOffice 24.2: a block on page 2 read 2, and 3 with a page put in ahead of it, where the file had held no bookmark and the reference a fixed 2). A block that draws nothing, one whose first drawing stands on a page nothing is written on, and one composed in a table's cell, whose drawings its table draws, have no bookmark, and the report names the anchor: a link to it points at none, and a page reference to it is written as the number it reads. Two anchors that clean to one name, or to a name a suffix gave another, stay two bookmarks; one anchor two nodes declare is one bookmark, the last the file writes one for — the node the page sends a link to and reads a page reference's page from, where that node writes a paragraph to mark — and the report names the anchor, the nodes counted off the document, two cells of one table two of them, and a node of a kind of the caller's own counted where the layout registers it. An anchor in a table's header row the page repeats is drawn again on each page it repeats on, and the page sends a link to the last of them; the file holds the row once, where the table starts, which a link goes to and a page reference reads once Word updates it (measured: the bookmark on page 1, a reference written as 5 reading 1), and the report names it. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is | | Rows | A one-row table spanning the content width — or only its columns, where fixed columns leave part of the row empty — so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; fixed columns that add up to less than the row leave the rest of it empty, the last one at its own width and the table no wider than its columns, so its text wraps where the page wraps it; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin; a cell holds whatever its child is, written as it is anywhere else. The row's `verticalAlign` is every cell's `w:vAlign`, so a child shorter than the row sits at its middle or bottom as on the page — a table of contents' leader on its entry's baseline. A row composed in a table cell (`DocumentTableCell.node(...)`), which the layout places nowhere, that the page divides by what its children measure — an arrangement other than `START`, a spacer that grows, an auto column — takes its columns from where its paragraphs' lines start, the table having laid them out, a spacer where the page spreads the room between them; one holding a child those lines do not place — a picture, a shape, or a spacer in a row of stated columns, which gives it a slot of its own — or written inside a panel or another table in its cell has its columns sized by Word to their text, and the report names it. The row is kept whole across a page break, as the layout keeps it. A row is held at least as tall as the page makes it inside a painted panel, and anywhere its tallest child is a drawing Word holds nothing of in its cell — a badge beside a heading. A section or other container in any cell — a row's, a table's, a panel's — that pulls its first line up with a negative top edge writes that one-line paragraph as much shorter, its text seated where the page sets it — no more than the room above its letters, as Word draws an exact line's text only inside the line. A row that paints its box — `fillColor`, `stroke`, `borders` — is a panel holding its columns (see "What a panel keeps and loses" below): a table of one cell carrying the fill and borders, its margins the row's padding as a panel's are, and in it the columns as a table of their own, without the padding; a corner radius squares, and is named (`corner radius`), and what of its borders above and below Word draws past its box is taken, or named, as a panel's is. A row composed in a table cell, in a page zone's line, in a band of overlapping layers or where a stack's column measures the space round it to its text, with a padding below zero or a column hanging into it, or with a margin below zero above or below it or at a side in a cell, is written as its columns alone, its paint in the report (`row paint`); in the body a row bleeding past the margin is its panel | -| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, but the row of a one-row table of paragraphs kept with what follows, which is taken back off it above — so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). Four writers decide it between them, so no one rule says which rows carry it: a table's rows get it where the layout placed each row; a row written as a table gets it where the layout placed that row; a block written as a panel gets it on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container; and the pass above then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there. The chain reaches inside a row only as far as its cells' own paragraphs, so what `w:cantSplit` adds there is a row Word cannot end a page inside: a cell of more than one paragraph — a plain cell is a paragraph a line where its style sets a line spacing and it holds more than one line (see Tables), and a composed cell holds whatever it is given — or a cell of a table. A table composed in a cell is held by neither writer, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next holding a table — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot of a page there moves to the next one whole, which six of the nine did; the band of two layers moved in both editors, from a cause not measured, making seven that LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`; 56 one-row tables do carry keeps without it, from those four writers between them. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | +| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, but the row of a one-row table of paragraphs kept with what follows, which is taken back off it above — so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). Four writers decide which rows are given it, so no one rule says which rows carry it: a table's rows get it where the layout placed each row; a row written as a table gets it where the layout placed that row; a block written as a panel gets it on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container; and the pass above then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there. A panel the layout breaks over pages is then copied whole, rows and flags together, so the second part's rows carry what the first part's were given and no gate judged them. The chain reaches inside a row only as far as its cells' own paragraphs, so what `w:cantSplit` adds there is a row Word cannot end a page inside: a cell of more than one paragraph — a plain cell is a paragraph a line where its style sets a line spacing and it holds more than one line (see Tables), and a composed cell holds whatever it is given — or a cell of a table. A table composed in a cell is held by neither the chain nor the table writer, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next holding a table — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot of a page there moves to the next one whole, which six of the nine did; the band of two layers moved in both editors, from a cause not measured, making seven that LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`; 56 one-row tables do carry keeps without it, from those four writers between them. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | | Spacers | An empty paragraph a tenth of a point tall, which Word keeps; the rest of the spacer's height is the space above the next block, or below this paragraph when a table follows. A spacer shorter than that tenth — a width anchor of no height, as `Panel`'s and `PanelLetter`'s cards open with — has its line reach past it by the rest, which the space below takes, as it takes a line hanging below a block, and what it cannot take is named (`space above`): taken nowhere, the line stood every card a tenth of a point taller in Word. A spacer in the body the layout moves to a new page with the gap before it, because the gap did not fit at the foot of the page above, holds the space the layout leaves above it there — that gap, and the edges of any containers opening with it — in its line, which Word keeps at the top of a page where it drops the space above. So does a spacer opening a stack of one layer, or a band of layers, which the layout moves to a new page whole: the space the layout leaves above it there is held in its line, less, in a stack of one layer, its layer's offset, which is named. It is kept with what follows it in the stack, which Word would otherwise set at the new page's top with the spacer left at the foot of the page above. A spacer's own margin and padding are not written, and the report names them — except below the lowest block of a band, or of a layer another resumes after in its column, where the space is measured from the page, the block's own margin and padding included | | Page breaks | Explicit Word page breaks | diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java index 2a0ba5ece..694475a9f 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java @@ -108,8 +108,10 @@ void aTableInsideAKeptBlockIsChainedRowByRow() throws Exception { // each paragraph's own lines together; the table writer gives every row the layout // placed w:cantSplit (breakRowsWhereTheLayoutDoes, blind to the keep flags), which // holds a row whole where the chain cannot reach inside it — a cell of more than one - // paragraph, or of a table, or any table outside a kept block, where no chain is - // written at all. Documented here at the call site; the pair is also asserted on the + // paragraph, or of a table, or any table outside a kept block, where keepOnOnePage + // writes nothing at all (a repeated header row's own w:keepNext aside, which + // breakRowsWhereTheLayoutDoes writes itself). + // Documented here at the call site; the pair is also asserted on the // keepWithNext path by DocxOneRowTableKeptWithTheNextTest, and w:cantSplit alone by // DocxTablePaginationTest, so this loop is not the only thing holding it. Not // unconditional: letTheOneRowTablesKeptWithTheNextBreak takes it back off a one-row @@ -130,9 +132,11 @@ void aTableInsideAKeptBlockIsChainedRowByRow() throws Exception { *

A plain cell is a paragraph per line, not one paragraph of several lines, when its style * sets a line spacing and it holds more than one line — the row is sized with that spacing * between them, and Word holds no space inside one paragraph ({@code paragraphPerLine} in - * {@code writeCellContent}, pinned by {@code DocxCellSeatTest}). So this is the shape for which - * {@code w:cantSplit} is load-bearing inside a kept block — outside one no chain is - * written at all and it is load-bearing for every row: the chain writes + * {@code writeCellContent}, pinned by {@code DocxCellSeatTest}). So this is the shape a + * plain table reaches for which {@code w:cantSplit} is load-bearing inside a kept + * block — the other is a cell holding a table, which the fixture below covers, and outside a + * kept block {@code keepOnOnePage} writes nothing at all, so there it holds every row. The + * chain writes * {@code w:keepLines} on each of the row's paragraphs, which holds each one's own lines, and * {@code w:keepNext} only where the row is not the block's last unit — neither of which stops * Word ending a page between two paragraphs of the same row.

@@ -190,8 +194,9 @@ void aRowOfSeveralParagraphsIsHeldWholeByItsRow() throws Exception { * moved to the next page whole.

* *

So the gap costs nothing while the row round it may not split. It would begin to cost - * something where that row may — a row the layout did not place, as in a table too tall to - * lay out ({@code DocxTablePaginationTest#aTableTooTallToLayOutIsLeftFreeToBreak}) — which is + * something where that row may — a row the layout did not place, as in a document it refused + * to lay out, where no row is placed at all + * ({@code DocxTablePaginationTest#aTableTooTallToLayOutIsLeftFreeToBreak}) — which is * why the row is asserted here beside the two absences, rather than the absences alone.

*/ @Test @@ -206,6 +211,11 @@ void aTableComposedInACellIsHeldByTheRowRoundIt() throws Exception { XWPFTable outer = document.getTables().get(0); XWPFTable composed = outer.getRow(0).getCell(0).getTables().get(0); + // The outer row's cantSplit comes from layout.placedRow, not from the chain, so it + // does not show that keepOnOnePage ran: the absences below need their own premise. + assertThat(keepsNext(paragraph(document, "Totals"))) + .as("the premise: the chain ran, and ran in the body") + .isTrue(); assertThat(outer.getRow(0).isCantSplitRow()) .as("the row the composed table stands in may not split, and holds it") .isTrue(); From 942c71d231e86549ae4b3af911f771f76349bf45 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 2 Nov 2026 12:43:19 +0000 Subject: [PATCH 7/8] docs(docx): put the split copy on the right side of the removal pass The split copy was described on the wrong side of the pass that frees a one-row table, and as reaching no gate at all. Both are wrong. cutThePanelsAtTheirPages runs at DocxSemanticBackend:1408 and letTheOneRowTablesKeptWithTheNextBreak at :1411, and copyAfter ends with insertTable, so the copied panel is in document.getTables() when the pass runs: its row is judged, and survives only because the gate frees cells of paragraphs and a panel's cell holds a table. What no gate reaches is the copied rows of that inner table, the pass reading the body's tables alone. The copy is also not the panel cut's alone - DocxPageMarginSections splits a table the same way - so the account no longer counts the copiers. It says what a split copy carries and where the panel cut sits relative to the pass, and leaves it there. "Outside a kept block w:cantSplit holds every row" is bounded to every row the layout placed: a document the layout refuses leaves no row placed, and DocxTablePaginationTest asserts the flag absent on all of them. The repeated-header carve-out is given its own condition too - the table writer writes that w:keepNext where the table has body rows under its header. The two shapes w:cantSplit alone holds inside a kept block now name the composed cell beside the plain one, as the recipe already did: a cell of several paragraphs is what a plain cell becomes under a line spacing and what a composed cell is given. The matrix's first mention of the removal pass gains the two qualifiers its later one carries, body and all. Also: the premise on the row count gains a description, and three paragraphs reflowed. --- CHANGELOG.md | 14 ++++---- .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 2 +- .../semantic/docx/DocxKeepTogetherTest.java | 35 ++++++++++--------- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 213579af6..b2a2d89f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4564,18 +4564,20 @@ follow semantic versioning; release dates are ISO 8601. `aRowOfSeveralParagraphsIsHeldWholeByItsRow` pins the shape a plain table reaches for which, inside a kept block, `w:cantSplit` is the only thing holding the row — a cell of several paragraphs, which a plain cell is where its style sets a line spacing and it holds more than - one line. (The other such shape is a cell holding a table, which the fixture below covers.) + one line, and a composed cell is whenever it is given them. (The other such shape is a cell + holding a table, which the fixture below covers.) - No one rule says which rows carry `w:cantSplit`: four writers decide which rows are given it. `breakRowsWhereTheLayoutDoes` gives it to each row of a table where the layout placed that row, `writeRow` to a row written as a table where the layout placed that row, and `writePanelPiece` to a block written as a panel on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container; then `letTheOneRowTablesKeptWithTheNextBreak` takes it off a one-row body table of paragraphs all - kept with the next, whichever of the three put it there. A panel the layout breaks over pages - is copied whole after that, rows and flags together (`DocxTableSplit.splitInItsPanel`), so the - second part's rows carry what the first part's were given, judged by no gate of their own. 56 - one-row tables of the corpus carry keeps without `w:cantSplit`; the count was not apportioned - between the causes. + kept with the next, whichever of the three put it there. Where the export splits a table + (`DocxTableSplit`, by `copyXml`), the second part is copied with its rows' flags, so a copied + row carries what the original was given rather than anything decided for it; the panel cut + runs before that pass, which sees the copy and leaves its row alone, its cell holding a table, + and never reaches the copied rows of the inner table. 56 one-row tables of the corpus carry + keeps without `w:cantSplit`; the count was not apportioned between the causes. - A table composed in a cell is reached by neither writer: the chain reads a cell's own paragraphs, which are not those of a table in that cell, and the layout places no row for such a table, so the table writer gives its rows no `w:cantSplit` either. What holds it is the row diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index dda786059..e8d6f9610 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -131,7 +131,7 @@ honour an option ignores it (documented contract). | Protection / encryption | ✅ `PdfDocumentPostProcessor` | ❌ (ignored with a one-time warning — no OOXML encryption support planned) | ❌ (not written, so the file opens unprotected; reported `DROPPED`, `protection`) | | Viewer preferences | ✅ `applyViewerPreferences` in `PdfFixedLayoutBackend` | ❌ (ignored with a one-time warning — PDF-viewer concept) | n/a (not written; reported `DROPPED`, `viewer preferences`) | | Debug guide lines / node labels | ✅ `PdfGuideLinesRenderer`, `PdfNodeLabelRenderer` | ❌ (ignored with a one-time warning — render through the PDF backend to see overlays) | n/a | -| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit` — but the row of a one-row table of paragraphs kept with what follows, taken back off it by `letTheOneRowTablesKeptWithTheNextBreak` below; four writers decide which rows are given it, so no one rule says which rows carry it — `breakRowsWhereTheLayoutDoes` gives it to each row of a table where the layout placed that row, `writeRow` to a row written as a table where the layout placed that row, `writePanelPiece` to a block written as a panel on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container, and `letTheOneRowTablesKeptWithTheNextBreak` then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there; a panel the layout breaks over pages is copied whole by `DocxTableSplit.splitInItsPanel`, so the second part's rows carry the first part's flags, judged by no gate of their own. What it adds over the chain is a row Word cannot end a page inside — a cell of more than one paragraph, which a plain cell is where its style sets a line spacing and it holds more than one line, or a cell of a table — the chain reaching only a cell's own paragraphs. A table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot moves to the next page whole there, which six of the nine did, and the band of two layers moved in both editors from a cause not measured, making seven LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`, while 56 one-row tables carry keeps without it. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | +| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit` — but the row of a one-row body table of paragraphs all kept with the next, taken back off it by `letTheOneRowTablesKeptWithTheNextBreak` below; four writers decide which rows are given it, so no one rule says which rows carry it — `breakRowsWhereTheLayoutDoes` gives it to each row of a table where the layout placed that row, `writeRow` to a row written as a table where the layout placed that row, `writePanelPiece` to a block written as a panel on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container, and `letTheOneRowTablesKeptWithTheNextBreak` then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there. Where the export splits a table (`DocxTableSplit`, by `copyXml`), the second part is copied with its rows' flags, so a copied row carries what the original was given rather than anything decided for it; the panel cut runs before that pass, which sees the copy and leaves its row alone, its cell holding a table, and never reaches the copied rows of the inner table at all. What it adds over the chain is a row Word cannot end a page inside — a cell of more than one paragraph, which a plain cell is where its style sets a line spacing and it holds more than one line, or a cell of a table — the chain reaching only a cell's own paragraphs. A table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot moves to the next page whole there, which six of the nine did, and the band of two layers moved in both editors from a cause not measured, making seven LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`, while 56 one-row tables carry keeps without it. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | | Layer stack — layers drawn over one another (`addLayerStack`) | ✅ each layer's fragments at the place the layout gave it | ✅ same | ⚠️ `DocxSemanticBackend` — Word has no layers, so a stack's children are written one after the other, without their positions. The exception is a stack whose layers are side-by-side columns (`DocxLayerColumns`): every layer a plain container at the stack's top-left corner, the bands their padding leaves either the same or apart. That is written as one table row, a cell per band, the way a two-column CV lays its columns out as layers to draw the name first. Layers sharing a band follow one another in its cell. A spacer the layout placed level with content of another layer of the same band only keeps that content's place, and is not written. The space above a later layer's first block is the gap the page shows below the content before it, to the block's content: what the block holds above that content past the gap — its margin, its padding, a contained picture's room — comes out of its own edge (`DocxLayerColumns.resume`) where it is a paragraph, a picture, a list or a table in its layer's own flow with its edge above at zero or more, and a list keeps its own edges after the gap, as a table does. Each layer is held to its box across, past the stack's edge where the page sets it past it, by an indent below zero; in a table cell no further left than the indents round it give, the rest named. The one layer of a stack of one layer is held the same way, its offset across kept and its measure with it; in a table cell, composed in a table's cell with no place of its own, and for an offset down or up, which moves it without making room for it, it is written where the stack starts and named — not where a band or a column's layer sets it at the page's distance below what is above it, which holds the offset. A shape container written as a panel in a table cell keeps its padding round its outline, as the page does — the table's indent and the space above and below it; where its table ends its cell, Word sets the cell's content at its middle or foot without the space below, which a centred cell takes off the space above, the rest named — sets what its cell holds at the top, middle or foot as its first written layer is set, holds a paragraph across to its box, and names a written layer after the first (written under the ones before it), an offset down or up, a box past the shape's side, and a layer set across by a box the layout does not give (`layer offset`). A layer whose content the page starts above where those before it end — a caption over its photo, a label in a card's corner, not a block giving back its edge whose padding or contained picture's room alone reaches there — is written under them, in a column's cell as in a band, and the report names it; a stack composed in a table cell, or exported with no layout, has no place of its own, and one writing more than one of its layers is named with them not measured. What the page draws inside another layer's fill comes after that fill. Drawings stack as the page paints them, by their z-index, each given its place (`wp:anchor/@relativeHeight`) from where the page paints its fragment, whatever order the layers are written in — among the drawings behind the text, and among those in front of it, as Word draws every one behind under every one in front. A painted layer, though, is a shaded cell rather than a drawing, and its fill belongs to the text layer: it is painted over every drawing behind the text, a drawing on a layer *above* it included, so a stack's z-index does not lift a drawing clear of a painted neighbour. An icon laid over a card's corner draws none of its ink in Word 16.0.20527 or LibreOffice 24.2 against 4308 pixels of it at 300 dpi in the engine's PDF; the report names such a drawing and one panel that covers it, taking the panels where it writes their fill, so that a panel bearing borders alone — which writes no shading — is not named and a row written as a panel is. Not named: a drawing under a panel the layout breaks across pages or composed in an authored table cell, one under a table cell's own shading, a cell's paint the table draws itself, and the second panel over a drawing lying over two (`DocxNeighbourFillGuardTest` holds the corpus to the one drawing that is known, and writes those edges out). Measured in LibreOffice: `CharcoalGold`, `SidebarPortrait` and `SlateOrange` fit on one page, as on the page; `NavySidebar` runs one line onto a second page. A stack or shape container the page gives no room — its margins taking back its whole height, as `LumaStudioInvoice`'s sidebar over the page's top margin — is laid over the flow (`laidOverTheFlow`): its drawings where the page draws them, each paragraph in a text box in front of the text where the page sets it, nothing in the flow. Not in a table cell or a panel, and not when it holds anything but drawing and plain paragraphs — a link, an anchor, a picture, a list or a table keeps it in the flow as above. A canvas is written as its contents — what it writes one block after another, its drawings where it places them — and holds its height: the room under what it writes is the space below it, and one that only draws holds its whole box where nothing round it — a band before its layer's first block, drawings held whole, an overlay it is a layer of — measures that room already. The report names the places and the width it loses, as it does a column's fixed width narrower than its band, and what it writes running past its height, a drawing in what it writes that takes no room in Word, or a height not measured | ## Output surface and lifecycle diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 78ce9710f..1acb84862 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -75,7 +75,7 @@ creation date is real metadata. | Images | Embedded pictures at the node's declared size. One contained in its box (`fitToBounds`, `CONTAIN`) is embedded at its fitted size, half the room it leaves above and below written as the space round its paragraph and half at its side as its indent; in a band the room below is its paragraph's own, as the band measures what it owes below from the box. In the flow, in a row's cell and held in by an alignment it stands in the box's middle, as the page draws it. In the flow, the margin and padding above and below are the space round the picture's paragraph; on the left they are not written, and the report names them, as it does a barcode's and a page reference's sides. A picture in the flow, a stack of one layer's included, or first in a band of layers there, that the layout moves to a new page keeps the space the layout leaves above it there — its margin, its padding, the room above it in its box, the edges of the containers opening with it and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, as a body paragraph does. A picture drawn beside its text is fitted to its box with its padding in it, which the report names too | | Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's — in a list item as much as in a paragraph. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires; on a section, container, table or image it wraps everything the block wrote, from the start of its first paragraph to the end of its last, so a link to a block lands on its first line; a section or a container written as a layer stack's column wraps what it writes the same way, from a block of it an earlier layer's stand-in holds, written there first, to the last it writes. A block that writes no paragraph — a section, a container or a layer stack's column of drawings alone, whose drawings stand in a paragraph of a block holding it, in the one nearest them or in one written on their page — is bookmarked at the start of the paragraph its first drawing is anchored in, the drawing on the earliest page and highest on it: Word keeps a drawing on its paragraph's page and moves it with that paragraph, so a page reference reads the page the block starts on and goes on reading it once the text above is edited, and a link goes to that page (measured in Word 16, fields updated, and LibreOffice 24.2: a block on page 2 read 2, and 3 with a page put in ahead of it, where the file had held no bookmark and the reference a fixed 2). A block that draws nothing, one whose first drawing stands on a page nothing is written on, and one composed in a table's cell, whose drawings its table draws, have no bookmark, and the report names the anchor: a link to it points at none, and a page reference to it is written as the number it reads. Two anchors that clean to one name, or to a name a suffix gave another, stay two bookmarks; one anchor two nodes declare is one bookmark, the last the file writes one for — the node the page sends a link to and reads a page reference's page from, where that node writes a paragraph to mark — and the report names the anchor, the nodes counted off the document, two cells of one table two of them, and a node of a kind of the caller's own counted where the layout registers it. An anchor in a table's header row the page repeats is drawn again on each page it repeats on, and the page sends a link to the last of them; the file holds the row once, where the table starts, which a link goes to and a page reference reads once Word updates it (measured: the bookmark on page 1, a reference written as 5 reading 1), and the report names it. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is | | Rows | A one-row table spanning the content width — or only its columns, where fixed columns leave part of the row empty — so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; fixed columns that add up to less than the row leave the rest of it empty, the last one at its own width and the table no wider than its columns, so its text wraps where the page wraps it; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin; a cell holds whatever its child is, written as it is anywhere else. The row's `verticalAlign` is every cell's `w:vAlign`, so a child shorter than the row sits at its middle or bottom as on the page — a table of contents' leader on its entry's baseline. A row composed in a table cell (`DocumentTableCell.node(...)`), which the layout places nowhere, that the page divides by what its children measure — an arrangement other than `START`, a spacer that grows, an auto column — takes its columns from where its paragraphs' lines start, the table having laid them out, a spacer where the page spreads the room between them; one holding a child those lines do not place — a picture, a shape, or a spacer in a row of stated columns, which gives it a slot of its own — or written inside a panel or another table in its cell has its columns sized by Word to their text, and the report names it. The row is kept whole across a page break, as the layout keeps it. A row is held at least as tall as the page makes it inside a painted panel, and anywhere its tallest child is a drawing Word holds nothing of in its cell — a badge beside a heading. A section or other container in any cell — a row's, a table's, a panel's — that pulls its first line up with a negative top edge writes that one-line paragraph as much shorter, its text seated where the page sets it — no more than the room above its letters, as Word draws an exact line's text only inside the line. A row that paints its box — `fillColor`, `stroke`, `borders` — is a panel holding its columns (see "What a panel keeps and loses" below): a table of one cell carrying the fill and borders, its margins the row's padding as a panel's are, and in it the columns as a table of their own, without the padding; a corner radius squares, and is named (`corner radius`), and what of its borders above and below Word draws past its box is taken, or named, as a panel's is. A row composed in a table cell, in a page zone's line, in a band of overlapping layers or where a stack's column measures the space round it to its text, with a padding below zero or a column hanging into it, or with a margin below zero above or below it or at a side in a cell, is written as its columns alone, its paint in the report (`row paint`); in the body a row bleeding past the margin is its panel | -| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, but the row of a one-row table of paragraphs kept with what follows, which is taken back off it above — so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). Four writers decide which rows are given it, so no one rule says which rows carry it: a table's rows get it where the layout placed each row; a row written as a table gets it where the layout placed that row; a block written as a panel gets it on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container; and the pass above then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there. A panel the layout breaks over pages is then copied whole, rows and flags together, so the second part's rows carry what the first part's were given and no gate judged them. The chain reaches inside a row only as far as its cells' own paragraphs, so what `w:cantSplit` adds there is a row Word cannot end a page inside: a cell of more than one paragraph — a plain cell is a paragraph a line where its style sets a line spacing and it holds more than one line (see Tables), and a composed cell holds whatever it is given — or a cell of a table. A table composed in a cell is held by neither the chain nor the table writer, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next holding a table — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot of a page there moves to the next one whole, which six of the nine did; the band of two layers moved in both editors, from a cause not measured, making seven that LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`; 56 one-row tables do carry keeps without it, from those four writers between them. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | +| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, but the row of a one-row table of paragraphs kept with what follows, which is taken back off it above — so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). Four writers decide which rows are given it, so no one rule says which rows carry it: a table's rows get it where the layout placed each row; a row written as a table gets it where the layout placed that row; a block written as a panel gets it on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container; and the pass above then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there. Where the export splits a table, the second part is copied with its rows' flags, so a copied row carries what the original was given rather than anything decided for it. The panel cut runs before the pass above, which therefore sees the copy and leaves its row alone — its cell holds a table, and the pass frees only cells of paragraphs. The copied rows of that inner table it never reaches at all, reading the body's tables alone. The chain reaches inside a row only as far as its cells' own paragraphs, so what `w:cantSplit` adds there is a row Word cannot end a page inside: a cell of more than one paragraph — a plain cell is a paragraph a line where its style sets a line spacing and it holds more than one line (see Tables), and a composed cell holds whatever it is given — or a cell of a table. A table composed in a cell is held by neither the chain nor the table writer, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next holding a table — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot of a page there moves to the next one whole, which six of the nine did; the band of two layers moved in both editors, from a cause not measured, making seven that LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`; 56 one-row tables do carry keeps without it, from those four writers between them. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | | Spacers | An empty paragraph a tenth of a point tall, which Word keeps; the rest of the spacer's height is the space above the next block, or below this paragraph when a table follows. A spacer shorter than that tenth — a width anchor of no height, as `Panel`'s and `PanelLetter`'s cards open with — has its line reach past it by the rest, which the space below takes, as it takes a line hanging below a block, and what it cannot take is named (`space above`): taken nowhere, the line stood every card a tenth of a point taller in Word. A spacer in the body the layout moves to a new page with the gap before it, because the gap did not fit at the foot of the page above, holds the space the layout leaves above it there — that gap, and the edges of any containers opening with it — in its line, which Word keeps at the top of a page where it drops the space above. So does a spacer opening a stack of one layer, or a band of layers, which the layout moves to a new page whole: the space the layout leaves above it there is held in its line, less, in a stack of one layer, its layer's offset, which is named. It is kept with what follows it in the stack, which Word would otherwise set at the new page's top with the spacer left at the foot of the page above. A spacer's own margin and padding are not written, and the report names them — except below the lowest block of a band, or of a layer another resumes after in its column, where the space is measured from the page, the block's own margin and padding included | | Page breaks | Explicit Word page breaks | diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java index 694475a9f..f9fd73af4 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java @@ -109,14 +109,16 @@ void aTableInsideAKeptBlockIsChainedRowByRow() throws Exception { // placed w:cantSplit (breakRowsWhereTheLayoutDoes, blind to the keep flags), which // holds a row whole where the chain cannot reach inside it — a cell of more than one // paragraph, or of a table, or any table outside a kept block, where keepOnOnePage - // writes nothing at all (a repeated header row's own w:keepNext aside, which - // breakRowsWhereTheLayoutDoes writes itself). - // Documented here at the call site; the pair is also asserted on the - // keepWithNext path by DocxOneRowTableKeptWithTheNextTest, and w:cantSplit alone by - // DocxTablePaginationTest, so this loop is not the only thing holding it. Not - // unconditional: letTheOneRowTablesKeptWithTheNextBreak takes it back off a one-row - // body table kept with what follows. - assertThat(table.getRows()).hasSize(2); + // writes nothing at all — breakRowsWhereTheLayoutDoes writing a repeated header row's + // own w:keepNext itself, where the table has body rows under its header. Documented + // here at the call site; the pair is also asserted on the keepWithNext path by + // DocxOneRowTableKeptWithTheNextTest, and w:cantSplit alone by DocxTablePaginationTest, + // so this loop is not the only thing holding it. Not unconditional either: + // letTheOneRowTablesKeptWithTheNextBreak takes it back off a one-row body table of + // paragraphs all kept with the next. + assertThat(table.getRows()) + .as("the premise: both rows are there to be held") + .hasSize(2); for (XWPFTableRow row : table.getRows()) { assertThat(row.isCantSplitRow()) .as("a row of the kept block's table may not break across a page") @@ -134,12 +136,13 @@ void aTableInsideAKeptBlockIsChainedRowByRow() throws Exception { * between them, and Word holds no space inside one paragraph ({@code paragraphPerLine} in * {@code writeCellContent}, pinned by {@code DocxCellSeatTest}). So this is the shape a * plain table reaches for which {@code w:cantSplit} is load-bearing inside a kept - * block — the other is a cell holding a table, which the fixture below covers, and outside a - * kept block {@code keepOnOnePage} writes nothing at all, so there it holds every row. The - * chain writes - * {@code w:keepLines} on each of the row's paragraphs, which holds each one's own lines, and - * {@code w:keepNext} only where the row is not the block's last unit — neither of which stops - * Word ending a page between two paragraphs of the same row.

+ * block — the other is a cell holding a table, which the fixture below covers, a composed cell + * holding whatever it is given. Outside a kept block {@code keepOnOnePage} writes nothing at + * all, so there {@code w:cantSplit} holds every row the layout placed, a repeated header row's + * own {@code w:keepNext} aside. The chain writes {@code w:keepLines} on each of the row's + * paragraphs, which holds each one's own lines, and {@code w:keepNext} only where the row is + * not the block's last unit — neither of which stops Word ending a page between two + * paragraphs of the same row.

* *

The block is kept together on purpose. Measured on this same document with * {@code keepWithNext()} instead: the row's paragraphs then carry {@code w:keepNext} as well, @@ -196,8 +199,8 @@ void aRowOfSeveralParagraphsIsHeldWholeByItsRow() throws Exception { *

So the gap costs nothing while the row round it may not split. It would begin to cost * something where that row may — a row the layout did not place, as in a document it refused * to lay out, where no row is placed at all - * ({@code DocxTablePaginationTest#aTableTooTallToLayOutIsLeftFreeToBreak}) — which is - * why the row is asserted here beside the two absences, rather than the absences alone.

+ * ({@code DocxTablePaginationTest.aTableTooTallToLayOutIsLeftFreeToBreak}) — which is why the + * row is asserted here beside the two absences, rather than the absences alone.

*/ @Test void aTableComposedInACellIsHeldByTheRowRoundIt() throws Exception { From bf04d35265d371e54df0de4f4be2e78b50ec7def Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 2 Nov 2026 13:12:32 +0000 Subject: [PATCH 8/8] test(docx): pin the row kept with the next that nothing holds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The export splits a table in two places, not one: cutThePanelsAtTheirPages cuts a panel at its pages, and DocxPageMarginSections cuts a body table for a page's own margins. Both run before the pass that frees a one-row table, so it sees both copies — but the reason a panel's copy survives is its cell's table, and a margin-split copy's cells hold paragraphs, so that reason was being generalised to a copy it does not cover. The account now names both cuts and gives each its own verdict: a panel's row left alone, its inner table's copied rows never reached, and a body table's copy judged like any other. theSameRowKeptWithTheNextIsHeldByNothing pins the boundary that until now only a javadoc recorded. Kept with the next, the block's last unit takes w:keepNext too, which makes its table one the removal pass frees, so w:cantSplit comes off the row that was the only thing holding its two paragraphs together. A sixth sabotage stops the pass freeing it and requires this fixture to fail; the javadoc beside it no longer restates the measurement it now asserts. Also: the removal pass's first mention in the recipe gains the qualifiers its later one carries, body and all; the 68 multi-row tables are said to be the corpus's; and the copied-flag sentences drop copyXml, which the matrix does not need. --- CHANGELOG.md | 24 ++++++--- .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 2 +- .../semantic/docx/DocxKeepTogetherTest.java | 53 ++++++++++++++++--- 4 files changed, 63 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2a2d89f0..ca6225688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4564,8 +4564,14 @@ follow semantic versioning; release dates are ISO 8601. `aRowOfSeveralParagraphsIsHeldWholeByItsRow` pins the shape a plain table reaches for which, inside a kept block, `w:cantSplit` is the only thing holding the row — a cell of several paragraphs, which a plain cell is where its style sets a line spacing and it holds more than - one line, and a composed cell is whenever it is given them. (The other such shape is a cell + one line, and a composed cell is whenever it holds them. (The other such shape is a cell holding a table, which the fixture below covers.) + - `theSameRowKeptWithTheNextIsHeldByNothing` records the boundary beside it: the same row in a + block asking only to be kept with the next takes `w:keepNext` on its last unit too, which makes + its table one the removal pass frees, so `w:cantSplit` comes off and the two paragraphs are held + by nothing. No document of the corpus reaches it — of the twenty blocks that hold a table the + three in the body all ask to be kept together, and the other seventeen are in a cell, which + that pass never reads — so the fixture records the shape rather than guarding a fix. - No one rule says which rows carry `w:cantSplit`: four writers decide which rows are given it. `breakRowsWhereTheLayoutDoes` gives it to each row of a table where the layout placed that row, `writeRow` to a row written as a table where the layout placed that row, and `writePanelPiece` @@ -4573,11 +4579,13 @@ follow semantic versioning; release dates are ISO 8601. either asks to be kept together or is a row or a shape container; then `letTheOneRowTablesKeptWithTheNextBreak` takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there. Where the export splits a table - (`DocxTableSplit`, by `copyXml`), the second part is copied with its rows' flags, so a copied - row carries what the original was given rather than anything decided for it; the panel cut - runs before that pass, which sees the copy and leaves its row alone, its cell holding a table, - and never reaches the copied rows of the inner table. 56 one-row tables of the corpus carry - keeps without `w:cantSplit`; the count was not apportioned between the causes. + (`DocxTableSplit`) — a panel cut at its pages, or a body table cut for a page's own margins — + the second part is copied with its rows' flags, so a copied row carries what the original was + given rather than anything decided for it. Both cuts run before that pass, which sees each + copy: a panel's row it leaves alone, that cell holding a table, and the copied rows of the + inner table it never reaches; a body table's copy it judges like any other. 56 one-row tables + of the corpus carry keeps without `w:cantSplit`; the count was not apportioned between the + causes. - A table composed in a cell is reached by neither writer: the chain reads a cell's own paragraphs, which are not those of a table in that cell, and the layout places no row for such a table, so the table writer gives its rows no `w:cantSplit` either. What holds it is the row @@ -4603,8 +4611,8 @@ follow semantic versioning; release dates are ISO 8601. 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — of which 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers - the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of - more than one row, lacks `w:cantSplit`. + the layout placed are on one page, and no row a keep chain reaches, in any of the corpus's 68 + tables of more than one row, lacks `w:cantSplit`. - **A fixture holds the record a page an editor adds past a one-page section is painted from: the export is grown by eighty appended paragraphs through POI and read back, and the fills are in the header of the one section the grown document has.** diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index e8d6f9610..05c52fe55 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -131,7 +131,7 @@ honour an option ignores it (documented contract). | Protection / encryption | ✅ `PdfDocumentPostProcessor` | ❌ (ignored with a one-time warning — no OOXML encryption support planned) | ❌ (not written, so the file opens unprotected; reported `DROPPED`, `protection`) | | Viewer preferences | ✅ `applyViewerPreferences` in `PdfFixedLayoutBackend` | ❌ (ignored with a one-time warning — PDF-viewer concept) | n/a (not written; reported `DROPPED`, `viewer preferences`) | | Debug guide lines / node labels | ✅ `PdfGuideLinesRenderer`, `PdfNodeLabelRenderer` | ❌ (ignored with a one-time warning — render through the PDF backend to see overlays) | n/a | -| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit` — but the row of a one-row body table of paragraphs all kept with the next, taken back off it by `letTheOneRowTablesKeptWithTheNextBreak` below; four writers decide which rows are given it, so no one rule says which rows carry it — `breakRowsWhereTheLayoutDoes` gives it to each row of a table where the layout placed that row, `writeRow` to a row written as a table where the layout placed that row, `writePanelPiece` to a block written as a panel on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container, and `letTheOneRowTablesKeptWithTheNextBreak` then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there. Where the export splits a table (`DocxTableSplit`, by `copyXml`), the second part is copied with its rows' flags, so a copied row carries what the original was given rather than anything decided for it; the panel cut runs before that pass, which sees the copy and leaves its row alone, its cell holding a table, and never reaches the copied rows of the inner table at all. What it adds over the chain is a row Word cannot end a page inside — a cell of more than one paragraph, which a plain cell is where its style sets a line spacing and it holds more than one line, or a cell of a table — the chain reaching only a cell's own paragraphs. A table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot moves to the next page whole there, which six of the nine did, and the band of two layers moved in both editors from a cause not measured, making seven LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`, while 56 one-row tables carry keeps without it. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | +| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row, and every row the layout placed held whole by the table writer's own `w:cantSplit` — but the row of a one-row body table of paragraphs all kept with the next, taken back off it by `letTheOneRowTablesKeptWithTheNextBreak` below; four writers decide which rows are given it, so no one rule says which rows carry it — `breakRowsWhereTheLayoutDoes` gives it to each row of a table where the layout placed that row, `writeRow` to a row written as a table where the layout placed that row, `writePanelPiece` to a block written as a panel on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container, and `letTheOneRowTablesKeptWithTheNextBreak` then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there. Where the export splits a table (`DocxTableSplit`) — a panel cut at its pages, or a body table cut for a page's own margins — the second part is copied with its rows' flags, so a copied row carries what the original was given rather than anything decided for it; both cuts run before that pass, which sees each copy, leaves a panel's row alone where its cell holds a table, never reaches the copied rows of the inner table at all, and judges a body table's copy like any other. What it adds over the chain is a row Word cannot end a page inside — a cell of more than one paragraph, which a plain cell is where its style sets a line spacing and it holds more than one line, or a cell of a table — the chain reaching only a cell's own paragraphs. A table composed in a cell gets neither and is held by the row it stands in, where the layout placed that row. Measured at the foot of a page, in two positions each — the block fitting there, and the layout moving it to the next page whole — nine shapes over eighteen documents stood whole in Word 16.0.20527 and LibreOffice 24.2, with no group and no row broken, the editors differing only over which page holds the whole block: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot moves to the next page whole there, which six of the nine did, and the band of two layers moved in both editors from a cause not measured, making seven LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus, 20 hold a table and none holds a table composed in a cell; across the whole corpus no row a keep chain reaches, in any of the corpus's 68 tables of more than one row, lacks `w:cantSplit`, while 56 one-row tables carry keeps without it. In a table cell, `w:keepLines` alone, as Word reads `w:keepNext` on the paragraph a cell opens with as keeping the row with what follows its table. A layer stack or a shape container in the flow is kept so too, as the layout moves it to a new page whole, a band of layers included; the first block of a band the layout moves to a new page — a paragraph, a picture, a list, a title and its dates set as one line or a spacer, a backdrop drawn before it or not — holds the space above it there, as one in the flow does: in a line kept with it, or in a spacer's own line; a table, a row or a panel opening it holds the band's top edge so with its own, the gap before the band left at the foot of the page above. A list, such a line or a rule the layout moves to a new page in the flow holds its space above there as a paragraph does, and a table, a row or a panel on that one page holds the gap the layout carries there with its own top edge. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing. A panel in the body the layout breaks over pages inside a table it holds is a table a page, cut at the row each page starts with (`cutThePanelsAtTheirPages`): Word repeats no header row of a table nested in a cell, so each part after a cut opens with copies of the table's header rows, where it has them, and the page breaks between the parts are fixed; where the page after a cut has other top and bottom margins, and the page above has room for it, the paragraph between the parts ends a Word section with them (per-page margins, below) | | Layer stack — layers drawn over one another (`addLayerStack`) | ✅ each layer's fragments at the place the layout gave it | ✅ same | ⚠️ `DocxSemanticBackend` — Word has no layers, so a stack's children are written one after the other, without their positions. The exception is a stack whose layers are side-by-side columns (`DocxLayerColumns`): every layer a plain container at the stack's top-left corner, the bands their padding leaves either the same or apart. That is written as one table row, a cell per band, the way a two-column CV lays its columns out as layers to draw the name first. Layers sharing a band follow one another in its cell. A spacer the layout placed level with content of another layer of the same band only keeps that content's place, and is not written. The space above a later layer's first block is the gap the page shows below the content before it, to the block's content: what the block holds above that content past the gap — its margin, its padding, a contained picture's room — comes out of its own edge (`DocxLayerColumns.resume`) where it is a paragraph, a picture, a list or a table in its layer's own flow with its edge above at zero or more, and a list keeps its own edges after the gap, as a table does. Each layer is held to its box across, past the stack's edge where the page sets it past it, by an indent below zero; in a table cell no further left than the indents round it give, the rest named. The one layer of a stack of one layer is held the same way, its offset across kept and its measure with it; in a table cell, composed in a table's cell with no place of its own, and for an offset down or up, which moves it without making room for it, it is written where the stack starts and named — not where a band or a column's layer sets it at the page's distance below what is above it, which holds the offset. A shape container written as a panel in a table cell keeps its padding round its outline, as the page does — the table's indent and the space above and below it; where its table ends its cell, Word sets the cell's content at its middle or foot without the space below, which a centred cell takes off the space above, the rest named — sets what its cell holds at the top, middle or foot as its first written layer is set, holds a paragraph across to its box, and names a written layer after the first (written under the ones before it), an offset down or up, a box past the shape's side, and a layer set across by a box the layout does not give (`layer offset`). A layer whose content the page starts above where those before it end — a caption over its photo, a label in a card's corner, not a block giving back its edge whose padding or contained picture's room alone reaches there — is written under them, in a column's cell as in a band, and the report names it; a stack composed in a table cell, or exported with no layout, has no place of its own, and one writing more than one of its layers is named with them not measured. What the page draws inside another layer's fill comes after that fill. Drawings stack as the page paints them, by their z-index, each given its place (`wp:anchor/@relativeHeight`) from where the page paints its fragment, whatever order the layers are written in — among the drawings behind the text, and among those in front of it, as Word draws every one behind under every one in front. A painted layer, though, is a shaded cell rather than a drawing, and its fill belongs to the text layer: it is painted over every drawing behind the text, a drawing on a layer *above* it included, so a stack's z-index does not lift a drawing clear of a painted neighbour. An icon laid over a card's corner draws none of its ink in Word 16.0.20527 or LibreOffice 24.2 against 4308 pixels of it at 300 dpi in the engine's PDF; the report names such a drawing and one panel that covers it, taking the panels where it writes their fill, so that a panel bearing borders alone — which writes no shading — is not named and a row written as a panel is. Not named: a drawing under a panel the layout breaks across pages or composed in an authored table cell, one under a table cell's own shading, a cell's paint the table draws itself, and the second panel over a drawing lying over two (`DocxNeighbourFillGuardTest` holds the corpus to the one drawing that is known, and writes those edges out). Measured in LibreOffice: `CharcoalGold`, `SidebarPortrait` and `SlateOrange` fit on one page, as on the page; `NavySidebar` runs one line onto a second page. A stack or shape container the page gives no room — its margins taking back its whole height, as `LumaStudioInvoice`'s sidebar over the page's top margin — is laid over the flow (`laidOverTheFlow`): its drawings where the page draws them, each paragraph in a text box in front of the text where the page sets it, nothing in the flow. Not in a table cell or a panel, and not when it holds anything but drawing and plain paragraphs — a link, an anchor, a picture, a list or a table keeps it in the flow as above. A canvas is written as its contents — what it writes one block after another, its drawings where it places them — and holds its height: the room under what it writes is the space below it, and one that only draws holds its whole box where nothing round it — a band before its layer's first block, drawings held whole, an overlay it is a layer of — measures that room already. The report names the places and the width it loses, as it does a column's fixed width narrower than its band, and what it writes running past its height, a drawing in what it writes that takes no room in Word, or a height not measured | ## Output surface and lifecycle diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 1acb84862..d0ccac4a5 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -75,7 +75,7 @@ creation date is real metadata. | Images | Embedded pictures at the node's declared size. One contained in its box (`fitToBounds`, `CONTAIN`) is embedded at its fitted size, half the room it leaves above and below written as the space round its paragraph and half at its side as its indent; in a band the room below is its paragraph's own, as the band measures what it owes below from the box. In the flow, in a row's cell and held in by an alignment it stands in the box's middle, as the page draws it. In the flow, the margin and padding above and below are the space round the picture's paragraph; on the left they are not written, and the report names them, as it does a barcode's and a page reference's sides. A picture in the flow, a stack of one layer's included, or first in a band of layers there, that the layout moves to a new page keeps the space the layout leaves above it there — its margin, its padding, the room above it in its box, the edges of the containers opening with it and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, as a body paragraph does. A picture drawn beside its text is fitted to its box with its padding in it, which the report names too | | Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's — in a list item as much as in a paragraph. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires; on a section, container, table or image it wraps everything the block wrote, from the start of its first paragraph to the end of its last, so a link to a block lands on its first line; a section or a container written as a layer stack's column wraps what it writes the same way, from a block of it an earlier layer's stand-in holds, written there first, to the last it writes. A block that writes no paragraph — a section, a container or a layer stack's column of drawings alone, whose drawings stand in a paragraph of a block holding it, in the one nearest them or in one written on their page — is bookmarked at the start of the paragraph its first drawing is anchored in, the drawing on the earliest page and highest on it: Word keeps a drawing on its paragraph's page and moves it with that paragraph, so a page reference reads the page the block starts on and goes on reading it once the text above is edited, and a link goes to that page (measured in Word 16, fields updated, and LibreOffice 24.2: a block on page 2 read 2, and 3 with a page put in ahead of it, where the file had held no bookmark and the reference a fixed 2). A block that draws nothing, one whose first drawing stands on a page nothing is written on, and one composed in a table's cell, whose drawings its table draws, have no bookmark, and the report names the anchor: a link to it points at none, and a page reference to it is written as the number it reads. Two anchors that clean to one name, or to a name a suffix gave another, stay two bookmarks; one anchor two nodes declare is one bookmark, the last the file writes one for — the node the page sends a link to and reads a page reference's page from, where that node writes a paragraph to mark — and the report names the anchor, the nodes counted off the document, two cells of one table two of them, and a node of a kind of the caller's own counted where the layout registers it. An anchor in a table's header row the page repeats is drawn again on each page it repeats on, and the page sends a link to the last of them; the file holds the row once, where the table starts, which a link goes to and a page reference reads once Word updates it (measured: the bookmark on page 1, a reference written as 5 reading 1), and the report names it. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is | | Rows | A one-row table spanning the content width — or only its columns, where fixed columns leave part of the row empty — so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; fixed columns that add up to less than the row leave the rest of it empty, the last one at its own width and the table no wider than its columns, so its text wraps where the page wraps it; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin; a cell holds whatever its child is, written as it is anywhere else. The row's `verticalAlign` is every cell's `w:vAlign`, so a child shorter than the row sits at its middle or bottom as on the page — a table of contents' leader on its entry's baseline. A row composed in a table cell (`DocumentTableCell.node(...)`), which the layout places nowhere, that the page divides by what its children measure — an arrangement other than `START`, a spacer that grows, an auto column — takes its columns from where its paragraphs' lines start, the table having laid them out, a spacer where the page spreads the room between them; one holding a child those lines do not place — a picture, a shape, or a spacer in a row of stated columns, which gives it a slot of its own — or written inside a panel or another table in its cell has its columns sized by Word to their text, and the report names it. The row is kept whole across a page break, as the layout keeps it. A row is held at least as tall as the page makes it inside a painted panel, and anywhere its tallest child is a drawing Word holds nothing of in its cell — a badge beside a heading. A section or other container in any cell — a row's, a table's, a panel's — that pulls its first line up with a negative top edge writes that one-line paragraph as much shorter, its text seated where the page sets it — no more than the room above its letters, as Word draws an exact line's text only inside the line. A row that paints its box — `fillColor`, `stroke`, `borders` — is a panel holding its columns (see "What a panel keeps and loses" below): a table of one cell carrying the fill and borders, its margins the row's padding as a panel's are, and in it the columns as a table of their own, without the padding; a corner radius squares, and is named (`corner radius`), and what of its borders above and below Word draws past its box is taken, or named, as a panel's is. A row composed in a table cell, in a page zone's line, in a band of overlapping layers or where a stack's column measures the space round it to its text, with a padding below zero or a column hanging into it, or with a margin below zero above or below it or at a side in a cell, is written as its columns alone, its paint in the report (`row paint`); in the body a row bleeding past the margin is its panel | -| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, but the row of a one-row table of paragraphs kept with what follows, which is taken back off it above — so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). Four writers decide which rows are given it, so no one rule says which rows carry it: a table's rows get it where the layout placed each row; a row written as a table gets it where the layout placed that row; a block written as a panel gets it on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container; and the pass above then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there. Where the export splits a table, the second part is copied with its rows' flags, so a copied row carries what the original was given rather than anything decided for it. The panel cut runs before the pass above, which therefore sees the copy and leaves its row alone — its cell holds a table, and the pass frees only cells of paragraphs. The copied rows of that inner table it never reaches at all, reading the body's tables alone. The chain reaches inside a row only as far as its cells' own paragraphs, so what `w:cantSplit` adds there is a row Word cannot end a page inside: a cell of more than one paragraph — a plain cell is a paragraph a line where its style sets a line spacing and it holds more than one line (see Tables), and a composed cell holds whatever it is given — or a cell of a table. A table composed in a cell is held by neither the chain nor the table writer, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next holding a table — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot of a page there moves to the next one whole, which six of the nine did; the band of two layers moved in both editors, from a cause not measured, making seven that LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the 68 tables of more than one row, lacks `w:cantSplit`; 56 one-row tables do carry keeps without it, from those four writers between them. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | +| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it, which the table writer holds instead — every row the layout placed, but the row of a one-row body table of paragraphs all kept with the next, which is taken back off it above — so a table in a kept block is held by both writers at once, neither knowing of the other (see Tables). Four writers decide which rows are given it, so no one rule says which rows carry it: a table's rows get it where the layout placed each row; a row written as a table gets it where the layout placed that row; a block written as a panel gets it on its own row where the layout put the block on one page and it either asks to be kept together or is a row or a shape container; and the pass above then takes it off a one-row body table of paragraphs all kept with the next, whichever of the three put it there. Where the export splits a table — a panel cut at its pages, or a body table cut for a page's own margins — the second part is copied with its rows' flags, so a copied row carries what the original was given rather than anything decided for it. Both cuts run before the pass above, which sees each copy: a panel's it leaves alone, that cell holding a table where the pass frees only cells of paragraphs, and the copied rows of the inner table it never reaches at all, reading the body's tables alone; a body table's copy it judges like any other. The chain reaches inside a row only as far as its cells' own paragraphs, so what `w:cantSplit` adds there is a row Word cannot end a page inside: a cell of more than one paragraph — a plain cell is a paragraph a line where its style sets a line spacing and it holds more than one line (see Tables), and a composed cell holds whatever it is given — or a cell of a table. A table composed in a cell is held by neither the chain nor the table writer, having no row of its own in the layout: what holds it there is the row it stands in, where the layout placed that row. Measured at the foot of a page, for a block that fits there and for one the layout moved to the next page whole: a block kept together holding a table, a kept block holding one row of several lines, a table composed in a cell, a painted panel holding a table, a band, a shape container, a stack holding a table, a block kept with the next and a painted panel kept with the next holding a table — nine shapes, eighteen documents — each stood whole in Word 16.0.20527 and in LibreOffice 24.2, with no group and no row broken. Where the editors differ it is which page holds the whole block, never whether it is broken: LibreOffice sets a table's rows taller — a three-row table's 33.9pt apart against the page's 21.0, read off that one shape — so a block whose table only just fits at the foot of a page there moves to the next one whole, which six of the nine did; the band of two layers moved in both editors, from a cause not measured, making seven that LibreOffice moved. Of the 1116 blocks the export holds on one page in its corpus — 214 in the body, 902 in a cell or a row's column, layer stacks and shape containers among them as well as the two keep flags — 20 hold a table, 338 hold a band or a container, and none holds a table composed in a cell. Across the whole corpus, all 685 layer stacks and shape containers the layout placed are on one page, and no row a keep chain reaches, in any of the corpus's 68 tables of more than one row, lacks `w:cantSplit`; 56 one-row tables do carry keeps without it, from those four writers between them. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | | Spacers | An empty paragraph a tenth of a point tall, which Word keeps; the rest of the spacer's height is the space above the next block, or below this paragraph when a table follows. A spacer shorter than that tenth — a width anchor of no height, as `Panel`'s and `PanelLetter`'s cards open with — has its line reach past it by the rest, which the space below takes, as it takes a line hanging below a block, and what it cannot take is named (`space above`): taken nowhere, the line stood every card a tenth of a point taller in Word. A spacer in the body the layout moves to a new page with the gap before it, because the gap did not fit at the foot of the page above, holds the space the layout leaves above it there — that gap, and the edges of any containers opening with it — in its line, which Word keeps at the top of a page where it drops the space above. So does a spacer opening a stack of one layer, or a band of layers, which the layout moves to a new page whole: the space the layout leaves above it there is held in its line, less, in a stack of one layer, its layer's offset, which is named. It is kept with what follows it in the stack, which Word would otherwise set at the new page's top with the spacer left at the foot of the page above. A spacer's own margin and padding are not written, and the report names them — except below the lowest block of a band, or of a layer another resumes after in its column, where the space is measured from the page, the block's own margin and padding included | | Page breaks | Explicit Word page breaks | diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java index f9fd73af4..f43f787e7 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxKeepTogetherTest.java @@ -144,14 +144,8 @@ void aTableInsideAKeptBlockIsChainedRowByRow() throws Exception { * not the block's last unit — neither of which stops Word ending a page between two * paragraphs of the same row.

* - *

The block is kept together on purpose. Measured on this same document with - * {@code keepWithNext()} instead: the row's paragraphs then carry {@code w:keepNext} as well, - * which makes it a table {@code letTheOneRowTablesKeptWithTheNextBreak} frees, and - * {@code w:cantSplit} is taken off it — leaving the two paragraphs of that row - * held by nothing at all. No document of the corpus has that shape: of its twenty blocks that - * hold a table, the three in the body all ask to be kept together, and the other seventeen - * are in a cell, which that pass never reaches — it reads the body's tables alone. So it is - * left as it is rather than fixed here.

+ *

The block is kept together on purpose: kept with the next instead, the same row + * is held by nothing, which {@link #theSameRowKeptWithTheNextIsHeldByNothing} records.

*/ @Test void aRowOfSeveralParagraphsIsHeldWholeByItsRow() throws Exception { @@ -185,6 +179,49 @@ void aRowOfSeveralParagraphsIsHeldWholeByItsRow() throws Exception { } } + /** + * The same row, in a block kept with the next instead, is held by nothing. + * + *

Kept with the next, the block's last unit takes {@code w:keepNext} too, which makes its + * table one {@code letTheOneRowTablesKeptWithTheNextBreak} frees — one row, cells of + * paragraphs, every paragraph kept with the next — so {@code w:cantSplit} comes off the row + * that was the only thing holding its two paragraphs together. {@code w:keepLines} holds each + * paragraph's own lines and nothing between them.

+ * + *

No document of the corpus reaches it: of the twenty blocks that hold a table, the three + * in the body all ask to be kept together, and the other seventeen are in a cell, which that + * pass never reads. So this fixture records the shape rather than guarding a fix — it is the + * one place the loss is written down as a test rather than a sentence.

+ */ + @Test + void theSameRowKeptWithTheNextIsHeldByNothing() throws Exception { + try (XWPFDocument document = DocxExports.withLayout(595, 842, 36, page -> page + .addSection(s -> s.keepWithNext() + .addParagraph(p -> p.text("Totals")) + .addTable(t -> t.autoColumns(2).rowCells( + DocumentTableCell.lines("Net", "of which goods") + .withStyle(DocumentTableStyle.builder().lineSpacing(2).build()), + DocumentTableCell.text("100")))) + .addParagraph(p -> p.text("After")))) { + XWPFTableRow row = document.getTables().get(0).getRow(0); + + assertThat(row.getCell(0).getParagraphs()) + .as("the premise: the same two paragraphs as the fixture above") + .hasSize(2); + for (XWPFParagraph paragraph : row.getCell(0).getParagraphs()) { + assertThat(keepsNext(paragraph)) + .as("kept with the next, the last unit is tied forward too") + .isTrue(); + assertThat(keepsLines(paragraph)) + .as("each paragraph's own lines are still held") + .isTrue(); + } + assertThat(row.isCantSplitRow()) + .as("and that is what frees the row, leaving the two paragraphs held by nothing") + .isFalse(); + } + } + /** * A table composed in a cell is held by the row round it, having nothing of its own. *