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
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,30 @@ follow semantic versioning; release dates are ISO 8601.

### Public API

- **A DOCX export's report names what a list's items lose.** A centred or right-aligned list was
written flush left, a list's lineSpacing went unwritten wherever the layout's items were not
its own, its continuationIndent was never written before a wrapped line, and an item at the
stated column — a list that nests with `hangingIndent`, a gap its marker does not clear, a tree
of items — stood off the page's column, and a blank item a `hangingIndent` list draws as its
marker alone was not written; the report named none of them. The list's note (`ListNode`) now
names:
- its alignment: its items are written flush left;
- its lineSpacing, where the layout's items are not the list's own and one wraps — an item run
onto the next page wraps, even split a line apiece — and in a list composed in a table cell,
which has no lines of its own to read, saying whether an item wraps is not measured;
- its continuationIndent, in a list the page sets it in — a markerless list, or a tree of
items, without `hangingIndent` — where an item wraps or, its lines unread, saying so;
- how many of its items stand at a stated column, 9pt in and 6pt more a level, or a space past
their marker or two spaces a level in, not where the page sets them;
- the rows the page draws as a marker alone, for blank items.

With no layout behind the export, the section's `measured geometry` note says the space
between lines is the editor's too. Across the DOCX fidelity corpus the report names one list:
`SlateOrange`'s certifications, composed in a table cell, at the stated column. None of this
changes what is written: the 62 documents of the corpus export to the same bytes. In
`DocxNodeFieldLedgerTest` a list's `align`, `lineSpacing`, `continuationIndent`,
`hangingIndent` and `markerGap` move from a gap to `REPORTED`, and its `items` from `WRITTEN`
to `REPORTED` for a blank item drawn as a marker alone; 7 node-field gaps remain.
- **A DOCX export's report names a clip where it cuts something, and a turned container's
transform where no outline names it.** A Word file has no clip a container can set round its
layers: a sidebar's ornament set past its side, a square tile's corners in a disc, a label
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 @@ -64,7 +64,7 @@ Payload records live in `core` under
| Capability (payload) | PDF (fixed) | PPTX (fixed) | DOCX (semantic) |
|---|---|---|---|
| Paragraph — pre-wrapped lines, runs, alignment (`ParagraphFragmentPayload`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` (one absolute, wrap-disabled frame per measured line) | ⚠️ semantic paragraphs (`DocxSemanticBackend`) — each run keeps its own style, falling back to the paragraph's when it has none; a centred or right-aligned left-to-right line of its own, of text alone and untracked, that Word sets a point or more wider or narrower at its half-point size has its letters spaced by the difference (`w:spacing`) and its room reckoned from the page's width; a `linkTarget` becomes a `w:hyperlink`, with a relationship for an address or `w:anchor` for one of the document's own anchors, and a run's own link wins over the paragraph's; a paragraph seated off its baseline (`TextVerticalAlign`) has its runs raised or lowered in the line (`w:position`) by the PDF backend's own correction (`ParagraphSeating`), one shift for the paragraph where the page seats each line by its own; Word and LibreOffice stand an exact line's baseline four fifths of the way down it whatever the face, where the page sets it the face's ascent down, so a paragraph whose face puts the two half a point or more apart — Spectral's, not Lato's — has its text moved to the page's baseline in the same position, matched at its middle line (not yet a list item's or a table text cell's; a picture among it moves with it in Word and stays on its own baseline in LibreOffice); lines a container stacks over one another tighter than their face each end halfway between their letters and the next line's (Word draws an exact line's text on screen only inside the line; its PDF export does not cut it), and the last layer of a shape container on one page, where its line runs past the foot, ends at the foot or below its letters; letters two lines share are split halfway so the page does not move, and a stack that holds a picture keeps its lines' own heights; a `bulletOffset` of spaces becomes the paragraph's indent (`w:ind` left, hanging or first line, by `indentStrategy`) in the flow and in cells, not yet over the flow, in an overlay's left-and-right pair or in a header or footer; one with letters in it is not written, its wrapped lines still set after the spaces that cover it |
| List hanging indent — a marker column and a content column (`ListBuilder.hangingIndent(true)`, `markerGap(...)`) | ✅ marker and content emitted as separate `ParagraphFragmentPayload` fragments at the resolved `markerX` / `contentX` | ✅ the same fragments — the fixed-layout pipeline resolves the geometry before either backend sees it | ⚠️ the top level only. `DocxSemanticBackend` exports a list as a real Word list — `numbering.xml`, `w:numPr` per item, the level carrying the marker — or, with rich items or a drawn marker, as paragraphs; content and nesting are unaffected. With the flag, the top level's marker column is the layout's — the marker's width and `markerGap`, the text and its wrapped lines where the page sets them — where the gap covers what Word may set the marker wider: a picture at its written size, its edges included, or text in the page's face (embedded, or a standard one Word sets in the same widths) grown to its half-point size, half a point clear. A Word list's level then indents and hangs by that column; a list of paragraphs writes the marker, a tab to a stop there, and hangs the item there. Word places content at absolute indents and has no relative-advance primitive, so without the layout's measure the gap could not be honoured; a Word list without the flag that the layout placed and that does not nest takes the page's column too, the spaces the page sets its wrapped lines after, its marker followed by a space (`w:suff`) and an item that wraps measured at Word's half-point size; a list that nests items, a list built as a tree of items (laid out flattened), and a marker the gap does not clear keep the stated column (180 twips, plus 120 per nesting level) — except, in a list of paragraphs, a nested rich item with no marker, which stands where the layout set its text, its measure weighed at Word's half-point sizes, where the layout's items are matched to the list's; a list that nests only such items sets its top level at the page's column too |
| List hanging indent — a marker column and a content column (`ListBuilder.hangingIndent(true)`, `markerGap(...)`) | ✅ marker and content emitted as separate `ParagraphFragmentPayload` fragments at the resolved `markerX` / `contentX` | ✅ the same fragments — the fixed-layout pipeline resolves the geometry before either backend sees it | ⚠️ the top level only. `DocxSemanticBackend` exports a list as a real Word list — `numbering.xml`, `w:numPr` per item, the level carrying the marker — or, with rich items or a drawn marker, as paragraphs; content and nesting are unaffected. With the flag, the top level's marker column is the layout's — the marker's width and `markerGap`, the text and its wrapped lines where the page sets them — where the gap covers what Word may set the marker wider: a picture at its written size, its edges included, or text in the page's face (embedded, or a standard one Word sets in the same widths) grown to its half-point size, half a point clear. A Word list's level then indents and hangs by that column; a list of paragraphs writes the marker, a tab to a stop there, and hangs the item there. Word places content at absolute indents and has no relative-advance primitive, so without the layout's measure the gap could not be honoured; a Word list without the flag that the layout placed and that does not nest takes the page's column too, the spaces the page sets its wrapped lines after, its marker followed by a space (`w:suff`) and an item that wraps measured at Word's half-point size; a list that nests items, a list built as a tree of items (laid out flattened), and a marker the gap does not clear keep the stated column (180 twips, plus 120 per nesting level) — except, in a list of paragraphs, a nested rich item with no marker, which stands where the layout set its text, its measure weighed at Word's half-point sizes, where the layout's items are matched to the list's; a list that nests only such items sets its top level at the page's column too. The report counts, on the list, the items that stand at a stated column, a space past their marker or two spaces a level in, and names a centred or right-aligned list written flush left, a lineSpacing not written where the layout's items are not the list's own and one wraps (in a list composed in a table cell, its wrapping not measured), a continuationIndent not written where an item of a markerless list or a tree of items without the flag wraps or its wrapping is not measured, and the rows the page draws as a marker alone for blank items of a flagged list, which are not written |
| Inline code/badge chips (`InlineBackground` on text spans) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ⚠️ `DocxSemanticBackend` — the fill becomes the run's own `w:shd`, in a paragraph and in a list item alike, so a badge still reads as a badge. What Word has no way to say is the shape: shading covers the glyph box, so the corner radius and the padding above and below the letters are not in the file, and the export records them. The padding beside the letters is written as the room it takes (`spaceAfterTheLastLetter`): character spacing after the chip's last letter, shaded with it, and after the letter before the chip, unshaded. A chip opening its line or following a picture has no letter before it, so its left padding is not in the file; no space is written after right-to-left letters or after a symbol or emoji. The export records, chip by chip, how each side was written. LibreOffice sets no spacing after a line's last letter, so it does not apply the right padding of a chip that ends a line. A `w:shd` fill is opaque, so a translucent chip is flattened first against what this export wrote underneath it — the paragraph's shading, the cell's, or the page — so the chip agrees with the file it is in, which on a white page is the colour the PDF shows. It stops being translucent, and that is recorded with the rest |
| Inline images (`ParagraphImageSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ✅ `DocxSemanticBackend.writeInlinePicture` (a picture in its own run where it sits among the words, at its size, inside the run's or the paragraph's link; raised or lowered by `w:position` to where the page's alignment and `baselineOffset` put it, from the layout's measure of the paragraph's first line — in a list, the list's text on a line as tall as the item's own tallest picture; LibreOffice ignores `w:position` on a picture and stands it on the baseline, so a picture the export draws itself (icon, emoji, shape) that the page raises carries the rise as transparent rows and needs no `w:position`, while one the page lowers stands in LibreOffice higher than on the page by as much as the page lowers it — up to the text's descent for a centred icon as tall as its line; the editor clips a picture to an exact line height, so a paragraph holding a picture that leaves its text — past the ascent or the descent, in Word's placement or on the baseline — has its lines written at least the height the picture reaches, grown by the editor rather than clipped, every line of the paragraph since Word has one line height for it, and each as tall as the editor's font makes it — for 14pt text about 2.5pt taller than the page's in LibreOffice; a picture inside its text in both editors keeps the exact height; a paragraph of one line of text in a Word paragraph of its own, with room above for its pictures' reach, keeps an exact line at the page's height of it, the pictures set in it where the page puts them in Word and what their ink reaches past it taken from the gaps around it, and in LibreOffice a lowered picture there stands higher and loses what passes the line's top; its description is the text it stands for or empty) |
| Inline vector shapes (`ParagraphShapeSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ⚠️ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` (distinct per-corner radii render with the top-left radius — single-adjust preset) | ⚠️ `DocxSemanticBackend.writeInlinePicture` + `DocxShapePictures` (a transparent PNG drawn by the shared `InlineSvgRasters` from the outline, fill and stroke — every outline kind, each layer centred in the run's box — placed as an inline picture is; the picture takes as far as the stroked ink reaches past the outline — half the stroke on an edge, more at a sharp corner's miter — and a pixel on each side, measured side by side, and is lowered by what it takes below, so no edge is cut and a shape takes that much more room in the line; a list marker that draws a disc is its picture; at the top level of a `hangingIndent(true)` list that does not nest it is followed by a tab to where the layout starts the item's text, the item's lines hanging there, when the picture clears that stop, else by a space) |
Expand Down
14 changes: 13 additions & 1 deletion docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,9 @@ goes into the line: Word has one line height for a paragraph and no gap between
so a paragraph that wraps is written with its lines taller. The page has one gap fewer than
lines; what the space above the paragraph can spare of that one comes off it, and the rest is
shared out over the lines, so the paragraph keeps its height on the page. Each list item holds
the gap only when it wraps. A paragraph on one line has no gap to hold.
the gap only when it wraps, and only where the layout's items are the list's own, one for one;
where they are not, the report names the gap (see "What a list becomes"). A paragraph on one
line has no gap to hold.

A gap is written **once, above**. The space a block holds below itself waits for the next
paragraph and is written there as `w:before`, together with whatever that paragraph asks
Expand Down Expand Up @@ -445,6 +447,16 @@ in than the list's edge, and every item of a list the layout did not place or wh
does not report one by one — an item split across pages — keep the spaces, and so does the
top level of a list that nests any of them.

The report names what a list's items lose, on the list (`ListNode`): its items are written
flush left where it is centred or right-aligned; its lineSpacing is not written where the
layout's items are not its own and one wraps — an item run onto the next page wraps, even split
a line apiece — nor in a list composed in a table cell, whose wrapping is not measured; its
continuationIndent is not written in a list the page sets it in, a markerless list or a tree of
items without `hangingIndent`, where an item wraps or its wrapping is not measured; it counts
the items that stand at a stated column, a space past their marker or two spaces a level in,
rather than where the page sets them; and it names the rows a `hangingIndent` list draws as a
marker alone for blank items, which the export does not write.

## What a panel keeps and loses

A container that paints — a fill, per-side borders, a uniform stroke — exports as a table of
Expand Down
8 changes: 7 additions & 1 deletion render-docx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,13 @@ What is not written — each one is named in the export report
- **A row's own fill, outline and side borders**, named in the export report as `row paint`.
- **In a page zone, anything but paragraphs, page fields and spacers** — a logo, a barcode,
a rule — named in the export report as `page zone content`.
- **`markerGap`** on a hanging-indent list: Word places the item text at its own indent.
- **A list's own geometry where Word cannot hold it**, named in the export report on the list:
- a centred or right-aligned list's alignment;
- its `lineSpacing` where the layout's items are not its own;
- its `continuationIndent`;
- the marker column and `markerGap` of an item at a stated column — a list that nests, or a gap
too narrow for its marker — while a flat hanging-indent list keeps the page's column;
- a row the page draws as a marker alone, for a blank item.

Multi-section documents export through `MultiSectionDocument.toDocxBytes()`,
`writeDocx(...)` and `buildDocx(...)` (Experimental): each section becomes a Word section
Expand Down
Loading
Loading