Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,44 @@ follow semantic versioning; release dates are ISO 8601.

### Public API

- **A DOCX canvas holds its height.** The page gives a canvas its height whatever it holds. The
export writes what it holds one block after another, and dropped the room under it, so what
followed stood that much higher. The report named the loss only where something followed the
canvas in the flow. Elsewhere the room was lost without a note:
- a canvas that wrote something and was its row's tallest cell;
- a canvas that ended a layer stack's column, or a timeline entry's body;
- a canvas in a shape container's layer, followed there or ending it;
- a canvas that only drew, between the blocks of a band's layer.

What changes:
- **The room under what it writes** is written as the space below it. The next block takes
it: as its space above, or as the space below the paragraph before a table. A cell the
canvas ends holds it on its last paragraph. So a row is as tall as its tallest canvas, and a
column as long as the canvas ending it. At the end of the body, or before a page break,
nothing follows to move, and nothing is written.
- **A canvas that only draws** holds its whole box, its edges included, in a flow, a row's
cell, a column or a band's layer, as a stack of drawings does in the flow. Inside a shape
container's layer it holds it too. Where something round it measures that room already, it
holds none of its own, its edges included, so the room is not counted twice:
- before the first block a band's layer writes, which the band sets the page's distance
down to, past the drawing;
- drawings held whole round it: a stack's, a shape container's or a canvas's;
- a stack of several layers, a shape container or a canvas it is a layer of;
- a block that a canvas round it writes nothing of, and so counts as taking no room.

A timeline's marker, alone in its row's cell, holds its room in the cell.
- **The report names what Word cannot hold:**
- what a canvas writes, one block under another, running past its height, which Word makes
room for and the page does not;
- a drawing in what it writes, which takes no room in Word, so what stands below the
drawing stands higher by its room;
- a height nothing measures: in a table's composed cell, or with no layout.
- Across the DOCX fidelity corpus eight documents' bytes change, each in its timeline's marker
cells, which now hold their markers' room: `CharcoalGold`, `MidnightNavy`, `NavySidebar`,
`ProfessionalSidebar`, `SerifHeadline`, `TerracottaRail`, `VioletGrid` and `ModernReceipt`.
Word and LibreOffice render all eight as they did, every word and drawing where it stood.
- Ledger: `CanvasLayerNode.height` moves from a gap to `REPORTED`; no node-field gap remains.

- **A DOCX export keeps a colour's translucency where Word can hold it, and names where it
cannot.** A translucent colour (`DocumentColor.rgba(...)`, `withOpacity(...)`) was written at full
strength, without a note, on text, a table cell's shading, a panel's fill and borders, and a
Expand Down Expand Up @@ -212,7 +250,7 @@ follow semantic versioning; release dates are ISO 8601.
- that what it writes is written from its corner, one block after another, not where it
places it — unless it stacks it that way;
- that its height is not held, where it stands in a flow and something follows it there
— a timeline's marker, alone in its row's cell, moves nothing;
— no longer named, as the room is written: see "A DOCX canvas holds its height";
- its width, where its text wraps narrower than the column;
- on a painted section in the flow the page lays out page by page — the body and the panels
in it, not a row's or a table's cell, a layer or a layer stack's column, where the page
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/backend-capability-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ honour an option ignores it (documented contract).
| 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. 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 |
| 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. What the page draws inside another layer's fill comes after that fill. Measured in LibreOffice: `CharcoalGold`, `SidebarPortrait` and `SlateOrange` fit on one page, as on the page, with `SidebarPortrait`'s subtitle under its name strip rather than in it; `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 the report names the places, the room and the width it loses, as it does a column's fixed width narrower than its band |
| 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. What the page draws inside another layer's fill comes after that fill. Measured in LibreOffice: `CharcoalGold`, `SidebarPortrait` and `SlateOrange` fit on one page, as on the page, with `SidebarPortrait`'s subtitle under its name strip rather than in it; `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

Expand Down
15 changes: 10 additions & 5 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -714,11 +714,16 @@ recolour what is under it in Word and it keeps the colour it was flattened to.
and on its left are not written round the table; its report note says so. See [charts.md](charts.md).
- **A canvas → its contents.** A canvas's drawings stand where it places them; what it
writes — text, pictures, tables — is written one block after another inside its margin and
padding. The places it gives them, the room it holds and the width its text wraps at are
not carried, and the report names each one it loses — the room where something follows the
canvas in its flow, so a timeline's marker, alone in its row's cell, has no note; a canvas
that is its row's tallest cell does not yet have one either. A canvas's clip policy paints
nothing on the page either.
padding. The places it gives them and the width its text wraps at are not carried, and the
report names each one it loses. Its height is held: the room under what it writes is the
space below it, which the next block takes or the cell it ends holds. One that only draws
holds its whole box in a flow, a cell, a column, a band's layer or a shape container's
layer — not before the first block a band's layer writes, nor in drawings held whole round
it, which measure that room already, nor as a layer of a stack of several layers, a shape
container or a canvas. The report names what Word cannot hold: what it writes, one block
under another, running past its height; a drawing in what it writes, which takes no room in
Word; and a height not measured, in a table's composed cell or with no layout. A canvas's
clip policy paints nothing on the page either.
- **Columns drawn as layers → one table row.** A two-column page can lay its
columns out as the layers of one stack, each inset to its band, so the name
is drawn before the sidebar. Word has no layers. When every layer is a plain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1331,11 +1331,16 @@ private boolean aRowsOwnFill(PlacedFragment fragment) {
if (!(fragment.payload() instanceof com.demcha.compose.document.layout.payloads.ShapeFragmentPayload)) {
return false;
}
return nodesByPath().get(fragment.path()) instanceof com.demcha.compose.document.node.RowNode;
}

/** The nodes this index knows, by path, built when first asked. */
private Map<String, DocumentNode> nodesByPath() {
if (nodesByPath == null) {
nodesByPath = new HashMap<>();
paths.forEach((node, path) -> nodesByPath.putIfAbsent(path, node));
}
return nodesByPath.get(fragment.path()) instanceof com.demcha.compose.document.node.RowNode;
return nodesByPath;
}

/**
Expand Down Expand Up @@ -1556,6 +1561,18 @@ boolean followedInItsParent(DocumentNode node) {
/** The highest child index the layout placed under each parent path, built when first asked. */
private Map<String, Integer> lastChildIndex;

/**
* The node the layout placed a node in.
*
* @param node a placed node
* @return its parent, or {@code null} when the node was not placed or its parent is not a
* node this index knows
*/
DocumentNode parentOf(DocumentNode node) {
PlacedNode box = placedFor(node);
return box == null || box.parentPath() == null ? null : nodesByPath().get(box.parentPath());
}

/** The placed node for a semantic node, or null when this index knows neither. */
private PlacedNode placedFor(DocumentNode node) {
String path = paths.get(node);
Expand Down
Loading
Loading