diff --git a/CHANGELOG.md b/CHANGELOG.md index 765382ae0..ca6225688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4549,6 +4549,70 @@ 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, 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 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 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` + 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. 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: 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 + 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 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 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 + 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 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 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.** @@ -5229,6 +5293,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..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; 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 ac34e5e0a..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. 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 fcaa1ac85..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 @@ -1,7 +1,10 @@ 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 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; @@ -101,6 +104,178 @@ 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. 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, or any table outside a kept block, where keepOnOnePage + // 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") + .isTrue(); + } + } + } + + /** + * 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 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, 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: kept with the next instead, the same row + * is held by nothing, which {@link #theSameRowKeptWithTheNextIsHeldByNothing} records.
+ */ + @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(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(); + } + } + + /** + * 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. + * + *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 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 + 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); + + // 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(); + 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(); + } + } + } } }