From 004f0239bb79a911e84e7a6bf53a579bb21ccad5 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Wed, 21 Oct 2026 20:49:30 +0100
Subject: [PATCH 1/2] test(docx): give every value a node holds a fate, and
name the known gaps
DocxNodeFieldLedgerTest gives every field of every node a fate and
stopped at the field: a part added to a value one holds - a style's
colour, a run's alignment, a border's width, an enum's constant -
passed it with no decision.
DocxValueFieldLedgerTest walks the value types from the node classes
and the output options, down to the last one they hold, through
generics, arrays and sealed types, and gives each of their 421 parts a
fate. It fails on a type or part with no entry, on an entry for a part
that is gone, on a type any class may implement or extend, and on a
node type held from outside the node ledger's packages.
Read against the export, some parts are lost in a case the report does
not name. Each is a gap, a fourth fate whose note opens with one of the
known cases: 39 parts of values and 48 node fields, among them a top
below zero, a panel's or a cell's padding below zero, a right border
drawn short of its cell's edge, a cell margin Word gives the whole row,
pictures in a list's items, a text badge's seat and a drawing painted
over a layer of text. Node entries that said those cases were written
now say so. A node's alignment, a row's arrangement and a paragraph's
indent strategy move from written to reported, which the report
already names.
No production code changes.
---
CHANGELOG.md | 28 +
.../docx/DocxNodeFieldLedgerTest.java | 187 +++-
.../docx/DocxValueFieldLedgerTest.java | 996 ++++++++++++++++++
3 files changed, 1163 insertions(+), 48 deletions(-)
create mode 100644 render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxValueFieldLedgerTest.java
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff94681b0..b24e55840 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3648,6 +3648,34 @@ follow semantic versioning; release dates are ISO 8601.
### Tests
+- **The DOCX field ledger reaches the values a node holds, and names the losses it knows of
+ and its report does not.** `DocxNodeFieldLedgerTest` gives every field of every node a fate,
+ and stopped at the field: a part added to a value one holds — a style's colour, a run's
+ alignment, a border's width, an enum's constant — passed it with no decision.
+ - `DocxValueFieldLedgerTest` reads the value types from the node classes and the output
+ options, down to the last one they hold, and gives each of their 421 parts a fate: 150
+ written, 220 reported, 12 with nothing for Word to carry and 39 gaps. It fails on a value
+ type or a part with no entry, on an entry for a part that is gone, on a type any class may
+ implement or extend, and on a node type held from outside the node ledger's packages.
+ - Read against the export, some parts are lost in a case the report does not name. Each is a
+ gap, a fourth fate whose entry opens with the case, in 39 parts of values and 48 node fields:
+ - a top below zero, not written on a picture, a barcode, a table or a row, and partly on
+ another block;
+ - a table cell's or a panel's padding below zero, written as none;
+ - the space below a table that ends a cell set at its middle or foot;
+ - a panel's right border drawn short of its table cell's edge;
+ - a cell's top or bottom margin that Word gives the whole row;
+ - pictures in a list's items, placed by the list's first line;
+ - a text badge's vertical seat, and a paragraph's where its lines are not read;
+ - a drawing the page paints over a layer of text, drawn behind it in Word;
+ - a stack's one layer composed in a table cell, written at the cell's top left; a layer set
+ past its stack's right edge in a table cell; with no layout, a stack's one layer moved down;
+ - with no layout, a row's fixed or weighted column beside an auto one;
+ - a box shape written as a panel in a table cell narrower than it.
+ - A gap whose note does not open with one of these cases fails the ledger.
+ - A node's alignment, a row's arrangement and a paragraph's indent strategy move from written
+ to reported: the report already names where Word sets them otherwise. A row's columns move
+ to a gap.
- **Word's editing protocol fails a restyle that leaves text without room for it.**
`restyle-body-through-normal` checked only that the body's letters grew with Normal by 2pt,
and passed every corpus document. Most paragraphs of an exported DOCX stand on exact lines,
diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxNodeFieldLedgerTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxNodeFieldLedgerTest.java
index c6f08468e..95ea790d8 100644
--- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxNodeFieldLedgerTest.java
+++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxNodeFieldLedgerTest.java
@@ -47,8 +47,10 @@
/**
* What the DOCX export does with every field of every node, and with every output option:
- * writes it, reports it when the author set it, or has nothing for Word to carry. There is no
- * fourth answer: a field the export neither writes nor names is not one this ledger can hold.
+ * writes it, reports it when the author set it, or has nothing for Word to carry. A field the
+ * export loses in a case its report does not name yet is a gap, and its entry says which case: a
+ * known defect, held here so the ledger stays true while it is open. A field the export neither
+ * writes nor names, with no gap to say where, is not one this ledger can hold.
*
* The export's report promises a caller that what the file does not carry is said, not
* approximated in silence. That promise is only as good as the fields someone thought about:
@@ -64,11 +66,12 @@
*
*
The entries are claims about the export, not proofs of it: a {@code WRITTEN} field is
* proved by the export's own tests, a {@code REPORTED} one by a report test. A field added later
- * takes one of the three fates in the change that adds it.
+ * takes a fate in the change that adds it. The values a field holds have their own entries, in
+ * {@link DocxValueFieldLedgerTest}, which shares the gaps' notes below.
*/
class DocxNodeFieldLedgerTest {
- private enum Fate {
+ enum Fate {
/** Reaches the Word file, directly or through the geometry the layout resolved. */
WRITTEN,
/**
@@ -81,11 +84,17 @@ private enum Fate {
* identity the layout resolves a wrapper's place by, or a field the page itself does
* not apply.
*/
- INERT
+ INERT,
+ /**
+ * Lost in a case the export knows of and its report does not name yet: the note opens with
+ * that case, one of the known ones below, then says the field's fate in every other.
+ * Closing the gap moves the field to one of the fates above.
+ */
+ GAP
}
/** A field's fate, and a note on it: for a report the cases it names, for another fate why. */
- private record Entry(Fate fate, String note) {
+ record Entry(Fate fate, String note) {
}
// A text colour's alpha is written, as Word's text fill, and a drawing's in DrawingML; what a
@@ -131,6 +140,48 @@ private record Entry(Fate fate, String note) {
+ "a border, " + BORDER_WIDTH + ", as a panel's; any other is "
+ "written, as a panel";
+ // The gaps: losses the export knows of and its report does not name yet.
+ static final String TOP_PULL = "a top below zero: on a picture, a barcode, a table or a row, not written; on "
+ + "another block, what the space above it and the lines round it cannot give, "
+ + "not written; neither named";
+ static final String PADDING_BELOW_ZERO = "a table cell's or a panel's padding below zero, written as none, not "
+ + "named";
+ static final String SPACE_BELOW_AT_THE_FOOT = "below a block written as a table that ends a table cell or a "
+ + "row's column set at its middle or foot, which Word does not "
+ + "count there, the block set that much lower, not named but for "
+ + "a shape container";
+ static final String RIGHT_BORDER = "a panel's right border reaching its table cell's edge, drawn up to its width "
+ + "and half a point short of it so the cell's gridline does not cover it, not "
+ + "named";
+ static final String CELL_PADDING = "a top or bottom a cell keeps where Word sets it, one opening with a table or "
+ + "spanning rows, which Word gives the row's other cells too, not named";
+ static final String LIST_PICTURE = "in a list's item or marker, placed by the list's first line and the item's "
+ + "tallest picture rather than its own line, and with no line read not moved "
+ + "off the baseline, not named";
+ static final String SEAT_UNREAD = "in a text badge, not applied, and where a paragraph's lines are not read, no "
+ + "seat written, neither named";
+ static final String ROW_COLUMNS_UNLAID = "with no layout, a row's fixed or weighted column beside an auto one, "
+ + "written with no width, not named";
+ static final String DRAWING_OVER_TEXT = "a drawing the page paints over a text or picture layer, drawn behind it, "
+ + "not named";
+ static final String COMPOSED_STACK_LAYER = "of the one layer a stack composed in a table cell writes beside drawn "
+ + "ones, written at the cell's top left, not named";
+ static final String LAYER_PAST_THE_EDGE = "set past its stack's right edge in a table cell the layout places, its "
+ + "lines held to the edge and broken narrower, not named";
+ static final String LAYER_UNLAID = "with no layout, a stack's one layer written where the stack starts, not named";
+ static final String PANEL_WIDER_THAN_ITS_CELL = "as a panel in a table cell narrower than it, held to the cell "
+ + "where the page draws it past, not named";
+ // A row whose children the layout does not place has columns Word sizes to their text.
+ private static final String ROW_COLUMNS = "its columns Word's, sized to their text, where the layout does not "
+ + "show where its children start: composed in a table cell with a "
+ + "child it does not place, or in a panel or another table in its "
+ + "cell; with no layout, in the section's note; any other is written";
+
+ /** The known cases: a gap's note opens with one of them, or the ledger fails. */
+ private static final List GAPS = List.of(TOP_PULL, PADDING_BELOW_ZERO, SPACE_BELOW_AT_THE_FOOT,
+ RIGHT_BORDER, CELL_PADDING, LIST_PICTURE, SEAT_UNREAD, ROW_COLUMNS_UNLAID, DRAWING_OVER_TEXT,
+ COMPOSED_STACK_LAYER, LAYER_PAST_THE_EDGE, LAYER_UNLAID, PANEL_WIDER_THAN_ITS_CELL);
+
private static final Map, Map> NODES = new LinkedHashMap<>();
private static final Map OUTPUT_OPTIONS = fields(
"metadata:WRITTEN",
@@ -156,11 +207,16 @@ private record Entry(Fate fate, String note) {
+ "with another page zone; its pictures are written in the line at the size the page draws them");
static {
- node(AlignNode.class, "name:INERT", "child:WRITTEN", "align:WRITTEN", "margin:WRITTEN");
+ node(AlignNode.class, "name:INERT", "child:WRITTEN",
+ "align:REPORTED:composed in a table cell where the layout does not say where its child stands, "
+ + "written from the cell's left; with no layout, in the section's note; any other is written",
+ "margin:GAP:" + TOP_PULL + "; any other is written");
node(BarcodeNode.class, "name:INERT", "barcodeOptions:WRITTEN", "width:WRITTEN", "height:WRITTEN",
"linkTarget:REPORTED", "bookmarkOptions:REPORTED",
- "padding:REPORTED:its left side; its right moves nothing in a paragraph set from the left",
- "margin:REPORTED:its left side; its right moves nothing in a paragraph set from the left",
+ "padding:GAP:" + TOP_PULL + "; its left side; its right moves nothing in a paragraph set from the "
+ + "left",
+ "margin:GAP:" + TOP_PULL + "; its left side; its right moves nothing in a paragraph set from the "
+ + "left",
"transform:REPORTED", "anchor:WRITTEN");
node(CanvasLayerNode.class, "name:INERT",
"width:REPORTED:the width its text wraps at",
@@ -171,13 +227,17 @@ private record Entry(Fate fate, String note) {
+ "as taking no room, the room is theirs; any other is written, as the room under what it "
+ "writes, or as its whole box where it only draws",
"placements:REPORTED:where what it writes stands; its drawings stand where it places them",
- "clipPolicy:INERT:the page clips no canvas", "padding:WRITTEN", "margin:WRITTEN");
+ "clipPolicy:INERT:the page clips no canvas", "padding:GAP:" + TOP_PULL + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; any other is written");
node(ChartNode.class, "name:INERT", "spec:REPORTED", "style:REPORTED:in the chart's note",
"margin:REPORTED:in the chart's note; above and below; below a block a band or a column measures from, written",
"padding:REPORTED:in the chart's note; above and below; below a block a band or a column measures from, written");
- node(ContainerNode.class, "name:INERT", "children:WRITTEN", "spacing:WRITTEN", "padding:WRITTEN",
- "margin:REPORTED:" + PULL, "fillColor:REPORTED:" + PANEL_TRANSLUCENCY, "stroke:REPORTED:" + PANEL_BORDERS,
- "cornerRadius:REPORTED", "borders:REPORTED:" + PANEL_BORDERS,
+ node(ContainerNode.class, "name:INERT", "children:WRITTEN", "spacing:WRITTEN",
+ "padding:GAP:" + PADDING_BELOW_ZERO + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; " + PULL,
+ "fillColor:REPORTED:" + PANEL_TRANSLUCENCY,
+ "stroke:GAP:" + RIGHT_BORDER + "; " + PANEL_BORDERS,
+ "cornerRadius:REPORTED", "borders:GAP:" + RIGHT_BORDER + "; " + PANEL_BORDERS,
"anchor:REPORTED:as a layer stack's column, or where it writes no paragraph, as of drawings alone, "
+ "which has no bookmark; otherwise, in the flow, it is bookmarked",
"bookmarkOptions:REPORTED", "flowWidth:REPORTED:of an unpainted one, a panel in a table cell and a layer stack's column");
@@ -187,34 +247,42 @@ private record Entry(Fate fate, String note) {
"anchor:REPORTED");
node(ImageNode.class, "name:INERT", "imageData:WRITTEN", "width:WRITTEN", "height:WRITTEN",
"scale:WRITTEN", "fitMode:WRITTEN", "linkTarget:REPORTED",
- "bookmarkOptions:REPORTED", "padding:REPORTED:its left side; its right moves nothing in a paragraph set from the left; drawn beside its text or over its badge, all of it",
- "margin:REPORTED:its left side; its right moves nothing in a paragraph set from the left", "transform:REPORTED",
+ "bookmarkOptions:REPORTED",
+ "padding:GAP:" + TOP_PULL + "; its left side; its right moves nothing in a paragraph set from the "
+ + "left; drawn beside its text or over its badge, all of it",
+ "margin:GAP:" + TOP_PULL + "; its left side; its right moves nothing in a paragraph set from the "
+ + "left",
+ "transform:REPORTED",
"anchor:WRITTEN");
node(LayerStackNode.class, "name:INERT",
- "layers:REPORTED:one the page starts above where the layers before it end, which is written under "
+ "layers:GAP:" + DRAWING_OVER_TEXT + "; " + COMPOSED_STACK_LAYER + "; " + LAYER_PAST_THE_EDGE + "; "
+ + LAYER_UNLAID + "; one the page starts above where the layers before it end, which is written under "
+ "them; composed in a table cell or with no layout, more than one of them written, not measured; "
+ "their drawings stack as the page paints them, by their z-index, among those behind the text "
+ "and among those in front of it; the one of a stack of one layer its offset moves down or up, "
+ "or across in a table cell, or at all with no place of its own in the layout, and one set left "
+ "of the stack in a table cell further than the indents round it give",
- "padding:WRITTEN", "margin:WRITTEN",
+ "padding:GAP:" + TOP_PULL + "; any other is written", "margin:GAP:" + TOP_PULL + "; any other is written",
"clipToBounds:REPORTED:where it cuts what its layers paint; composed in a table cell, on its table");
node(LineNode.class, "name:INERT", "width:WRITTEN", "height:WRITTEN", "startX:WRITTEN", "startY:WRITTEN",
"endX:WRITTEN", "endY:WRITTEN",
"stroke:REPORTED:its translucency, as a rule's, flattened against the colour under it, and as a "
+ "rule, " + BORDER_WIDTH + "; drawn, its alpha is written; any other is written",
"linkTarget:REPORTED",
- "bookmarkOptions:REPORTED", "padding:WRITTEN", "margin:WRITTEN", "transform:REPORTED",
+ "bookmarkOptions:REPORTED", "padding:GAP:" + TOP_PULL + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; any other is written", "transform:REPORTED",
"dashPattern:REPORTED",
"anchor:REPORTED:drawn; a rule in the flow is bookmarked", "lineCap:REPORTED:a rule's other than butt, a border ending flat; drawn, it is written",
"fillWidth:WRITTEN", "keepWithNext:REPORTED:of a line drawn in the flow the page keeps blocks together in");
node(ListNode.class, "name:INERT",
- "items:REPORTED:a blank one a hangingIndent list draws as its marker alone, the marks of one "
+ "items:GAP:" + LIST_PICTURE + "; a blank one a hangingIndent list draws as its marker alone, the "
+ + "marks of one "
+ "the page reads as markdown, and a chip's translucent fill, flattened against the colour under "
+ "it; any other is written",
- "nestedItems:REPORTED:the marks of one the page reads as markdown, and a chip's translucent fill, "
+ "nestedItems:GAP:" + LIST_PICTURE + "; the marks of one the page reads as markdown, and a chip's "
+ + "translucent fill, "
+ "flattened against the colour under it; any other is written",
- "marker:WRITTEN",
+ "marker:GAP:" + LIST_PICTURE + "; any other is written",
"textStyle:WRITTEN", "align:REPORTED",
"lineSpacing:REPORTED:where the layout's items are not its own and one wraps, an item run "
+ "onto the next page among them; composed in a table cell, its wrapping not measured; "
@@ -222,7 +290,8 @@ private record Entry(Fate fate, String note) {
"itemSpacing:WRITTEN",
"continuationIndent:REPORTED:where an item wraps, or its lines are not read, in a markerless "
+ "list or a tree of items without hangingIndent, the only lists the page sets it in",
- "normalizeMarkers:WRITTEN", "padding:WRITTEN", "margin:WRITTEN",
+ "normalizeMarkers:WRITTEN", "padding:GAP:" + TOP_PULL + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; any other is written",
"hangingIndent:REPORTED:where an item stands at a stated column, a space past its marker or "
+ "two spaces a level in",
"markerGap:REPORTED:where an item stands at a stated column, a space past its marker or "
@@ -240,8 +309,8 @@ private record Entry(Fate fate, String note) {
node(PageReferenceNode.class, "name:INERT",
"anchor:REPORTED:where the anchor has no bookmark, its number is written as text",
"textStyle:WRITTEN", "align:WRITTEN", "placeholderText:WRITTEN",
- "padding:REPORTED:the sides its alignment sets it from",
- "margin:REPORTED:the sides its alignment sets it from");
+ "padding:GAP:" + TOP_PULL + "; the sides its alignment sets it from",
+ "margin:GAP:" + TOP_PULL + "; the sides its alignment sets it from");
node(ParagraphNode.class, "name:INERT",
"text:REPORTED:where the page reads it as markdown, written as the page sets it where its lines "
+ "hold the pieces read so, a heading written taller than its line named; its marks written as letters "
@@ -252,27 +321,34 @@ private record Entry(Fate fate, String note) {
"textStyle:WRITTEN", "align:WRITTEN", "lineSpacing:WRITTEN",
"bulletOffset:REPORTED:its letters before the first line; over the flow, as a side of an "
+ "overlay's pair or as a badge's text, the room it sets lines in by where that moves one",
- "indentStrategy:WRITTEN", "linkTarget:WRITTEN",
+ "indentStrategy:REPORTED:as its bulletOffset's: a prefix's letters before the first line, and over "
+ + "the flow the room it sets lines in by where that moves one; any other is written",
+ "linkTarget:WRITTEN",
"bookmarkOptions:REPORTED:a title that is not the text Word lists it by, a pair's whole line for "
+ "a side of one; a level past Word's ninth that shares it with another; the right side's of a "
+ "pair whose left holds the level",
- "padding:REPORTED:" + PULL, "margin:REPORTED:" + PULL,
+ "padding:GAP:" + TOP_PULL + "; " + PULL, "margin:GAP:" + TOP_PULL + "; " + PULL,
"autoSize:REPORTED:where the layout does not tell the size the page fits the text in the "
+ "paragraph's style to — no lines read, lines in sizes that do not say which is the "
+ "paragraph's, or one paragraph added at more than one place — the text written at its "
+ "style's, not measured; any other is written, at the size the page fits it to",
- "verticalAlign:WRITTEN", "anchor:WRITTEN", "direction:WRITTEN");
+ "verticalAlign:GAP:" + SEAT_UNREAD + "; its first text line's seat for all its lines, a line in "
+ + "another size or face named from half a point; any other is written",
+ "anchor:WRITTEN", "direction:WRITTEN");
node(PathNode.class, "name:INERT", "width:WRITTEN", "height:WRITTEN", "segments:WRITTEN",
"fillColor:WRITTEN", "fillPaint:REPORTED", "stroke:WRITTEN", "strokePaint:REPORTED",
"padding:WRITTEN", "margin:WRITTEN", "dashPattern:REPORTED", "lineCap:WRITTEN", "lineJoin:WRITTEN");
node(PolygonNode.class, "name:INERT", "width:WRITTEN", "height:WRITTEN", "points:WRITTEN",
"fillColor:WRITTEN", "stroke:WRITTEN", "padding:WRITTEN", "margin:WRITTEN");
- node(RowNode.class, "name:INERT", "children:WRITTEN", "weights:WRITTEN", "gap:WRITTEN", "padding:WRITTEN",
- "margin:WRITTEN",
- "fillColor:REPORTED:" + ROW_PAINT, "stroke:REPORTED:" + ROW_PAINT,
+ node(RowNode.class, "name:INERT", "children:WRITTEN", "weights:WRITTEN", "gap:WRITTEN",
+ "padding:GAP:" + TOP_PULL + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; any other is written",
+ "fillColor:REPORTED:" + ROW_PAINT, "stroke:GAP:" + RIGHT_BORDER + "; " + ROW_PAINT,
"cornerRadius:REPORTED:a panel's corners, which a Word table cell does not round; with no paint "
+ "it paints nothing",
- "borders:REPORTED:" + ROW_PAINT, "columns:WRITTEN", "verticalAlign:WRITTEN", "arrangement:WRITTEN");
+ "borders:GAP:" + RIGHT_BORDER + "; " + ROW_PAINT,
+ "columns:GAP:" + ROW_COLUMNS_UNLAID + "; " + ROW_COLUMNS, "verticalAlign:WRITTEN",
+ "arrangement:REPORTED:" + ROW_COLUMNS);
// The wrappers the layout adds round a timeline's parts, public records the export writes
// through to their child.
node(com.demcha.compose.document.layout.LayoutAnchorNode.class, "name:INERT", "id:INERT", "child:WRITTEN");
@@ -280,9 +356,12 @@ private record Entry(Fate fate, String note) {
"child:WRITTEN");
node(com.demcha.compose.document.layout.HorizontalBandContentNode.class, "name:INERT", "key:INERT",
"slot:WRITTEN", "child:WRITTEN");
- node(SectionNode.class, "name:INERT", "children:WRITTEN", "spacing:WRITTEN", "padding:WRITTEN",
- "margin:REPORTED:" + PULL, "fillColor:REPORTED:" + PANEL_TRANSLUCENCY, "stroke:REPORTED:" + PANEL_BORDERS,
- "cornerRadius:REPORTED", "borders:REPORTED:" + PANEL_BORDERS,
+ node(SectionNode.class, "name:INERT", "children:WRITTEN", "spacing:WRITTEN",
+ "padding:GAP:" + PADDING_BELOW_ZERO + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; " + PULL,
+ "fillColor:REPORTED:" + PANEL_TRANSLUCENCY,
+ "stroke:GAP:" + RIGHT_BORDER + "; " + PANEL_BORDERS,
+ "cornerRadius:REPORTED", "borders:GAP:" + RIGHT_BORDER + "; " + PANEL_BORDERS,
"keepTogether:WRITTEN",
"anchor:REPORTED:as a layer stack's column, or where it writes no paragraph, as of drawings alone, "
+ "which has no bookmark; otherwise, in the flow, it is bookmarked",
@@ -290,11 +369,13 @@ private record Entry(Fate fate, String note) {
"bookmarkOptions:REPORTED", "keepWithNext:WRITTEN",
"flowWidth:REPORTED:of an unpainted one, a panel in a table cell and a layer stack's column");
node(ShapeContainerNode.class, "name:INERT",
- "outline:REPORTED:drawn as a shape, corners whose radii differ once held to half its shorter side "
+ "outline:GAP:" + PANEL_WIDER_THAN_ITS_CELL + "; drawn as a shape, corners whose radii differ once "
+ + "held to half its shorter side "
+ "at the largest of them; a box "
+ "written as a panel in a table cell, its corners square; one no shape draws, or composed in a "
+ "table cell with nowhere to draw it, dropped",
- "layers:REPORTED:written inline one after another in source order where it writes them in the "
+ "layers:GAP:" + DRAWING_OVER_TEXT + "; written inline one after another in source order where it "
+ + "writes them in the "
+ "flow; drawn where the page puts them, unreported, where they are all drawing, stacked as the "
+ "page paints them among those behind the text and among those in front of it; a panel's in its "
+ "cell, at its first written layer's height, a paragraph held across to its box, and named where "
@@ -304,21 +385,24 @@ private record Entry(Fate fate, String note) {
"clipPolicy:REPORTED:where it cuts what its layers paint; composed in a table cell, on its table",
"fillColor:REPORTED:its translucency, as a panel's, flattened against the colour under it; "
+ "drawn, its alpha is written; any other is written",
- "stroke:REPORTED:its translucency, as a panel's border, flattened against the panel's fill or "
+ "stroke:GAP:" + RIGHT_BORDER + "; its translucency, as a panel's border, flattened against the "
+ + "panel's fill or "
+ "the colour under it, and as a panel's, " + BORDER_WIDTH + "; drawn, its alpha and its width "
+ "are written; any other is written",
- "padding:REPORTED:written as a panel's in a table cell, round its outline as a margin is, and "
- + "below it, where its table ends its cell and the cell sets what it holds at its middle or "
+ "padding:GAP:" + TOP_PULL + "; written as a panel's in a table cell, round its outline as a margin "
+ + "is, and below it, where its table ends its cell and the cell sets what it holds at its middle or "
+ "foot, as far as Word does not count it there; any other is written",
- "margin:REPORTED:written as a panel's in a table cell, below it, where its table ends its cell "
+ "margin:GAP:" + TOP_PULL + "; written as a panel's in a table cell, below it, where its table ends "
+ + "its cell "
+ "and the cell sets what it holds at its middle or foot, as far as Word does not count it "
+ "there; any other is written",
"transform:REPORTED");
node(ShapeNode.class, "name:INERT", "width:WRITTEN", "height:REPORTED:" + RULE_WIDTH,
"fillColor:REPORTED:" + RULE_TRANSLUCENCY,
"stroke:WRITTEN", "cornerRadius:REPORTED:corners unequal once held to half its shorter side, drawn at the largest radius",
- "linkTarget:REPORTED", "bookmarkOptions:REPORTED", "padding:WRITTEN",
- "margin:WRITTEN", "transform:REPORTED", "fillPaint:REPORTED",
+ "linkTarget:REPORTED", "bookmarkOptions:REPORTED",
+ "padding:GAP:" + TOP_PULL + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; any other is written", "transform:REPORTED", "fillPaint:REPORTED",
"anchor:REPORTED:drawn; a rule in the flow is bookmarked");
node(SpacerNode.class, "name:INERT", "width:WRITTEN", "height:WRITTEN",
"padding:REPORTED:above and below; below a block a band or a column measures from, written",
@@ -328,12 +412,16 @@ private record Entry(Fate fate, String note) {
"columns:REPORTED:where one is auto and the layout does not show their widths — composed in a "
+ "table cell, or with a column edge no row shows — the editor's, a fixed one beside it with "
+ "them; with no layout, in the section's note; any other is written",
- "rows:REPORTED:" + CELL_TRANSLUCENCY,
- "defaultCellStyle:REPORTED:" + CELL_TRANSLUCENCY, "rowStyles:REPORTED:" + CELL_TRANSLUCENCY,
- "columnStyles:REPORTED:" + CELL_TRANSLUCENCY, "width:WRITTEN", "linkTarget:REPORTED",
+ "rows:GAP:" + CELL_PADDING + "; " + PADDING_BELOW_ZERO + "; " + CELL_TRANSLUCENCY,
+ "defaultCellStyle:GAP:" + CELL_PADDING + "; " + PADDING_BELOW_ZERO + "; " + CELL_TRANSLUCENCY,
+ "rowStyles:GAP:" + CELL_PADDING + "; " + PADDING_BELOW_ZERO + "; " + CELL_TRANSLUCENCY,
+ "columnStyles:GAP:" + CELL_PADDING + "; " + PADDING_BELOW_ZERO + "; " + CELL_TRANSLUCENCY,
+ "width:WRITTEN", "linkTarget:REPORTED",
"bookmarkOptions:REPORTED",
- "padding:WRITTEN",
- "margin:WRITTEN", "repeatedHeaderRowCount:WRITTEN", "anchor:WRITTEN");
+ "padding:GAP:" + TOP_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; any other is written",
+ "repeatedHeaderRowCount:WRITTEN",
+ "anchor:WRITTEN");
}
@Test
@@ -388,11 +476,14 @@ private static void node(Class extends DocumentNode> kind, String... specs) {
}
/** {@code field:FATE}, or {@code field:FATE:a note}, as {@code field:REPORTED:the cases the report names}. */
- private static Map fields(String... specs) {
+ static Map fields(String... specs) {
Map entries = new LinkedHashMap<>();
for (String spec : specs) {
String[] parts = spec.split(":", 3);
Entry entry = new Entry(Fate.valueOf(parts[1]), parts.length > 2 ? parts[2] : "");
+ if (entry.fate() == Fate.GAP && GAPS.stream().noneMatch(entry.note()::startsWith)) {
+ throw new IllegalStateException("a gap whose note opens with no known case, for " + parts[0]);
+ }
if (entries.put(parts[0], entry) != null) {
throw new IllegalStateException("two entries for " + parts[0]);
}
diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxValueFieldLedgerTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxValueFieldLedgerTest.java
new file mode 100644
index 000000000..5641ba00c
--- /dev/null
+++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxValueFieldLedgerTest.java
@@ -0,0 +1,996 @@
+package com.demcha.compose.document.backend.semantic.docx;
+
+import com.demcha.compose.document.node.DocumentNode;
+import com.demcha.compose.document.output.DocumentOutputOptions;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.RecordComponent;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.CELL_PADDING;
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.COMPOSED_STACK_LAYER;
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.DRAWING_OVER_TEXT;
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.LAYER_PAST_THE_EDGE;
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.LAYER_UNLAID;
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.LIST_PICTURE;
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.PADDING_BELOW_ZERO;
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.PANEL_WIDER_THAN_ITS_CELL;
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.RIGHT_BORDER;
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.ROW_COLUMNS_UNLAID;
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.SEAT_UNREAD;
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.SPACE_BELOW_AT_THE_FOOT;
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.TOP_PULL;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * What the DOCX export does with every part of every value a node's field or an output option
+ * holds: a style's colour, a run's alignment, a border's width, a chart's legend, a header's
+ * separator. Its parts are a record's components, a final class's fields and an enum's constants,
+ * and each takes one of the fates {@link DocxNodeFieldLedgerTest} gives a node's fields: written,
+ * reported where the author set it and the file does not carry it, nothing for Word to carry, or a
+ * gap, a case the export knows it loses and its report does not name yet, which the entry says.
+ *
+ * The node ledger stops at a node's fields, and the values in them are where most of what a
+ * page draws is said. A part added to one of them later is drawn by the PDF backend at once and,
+ * until the DOCX export decides, passes every node entry with no decision of its own. This ledger
+ * reads the values from the node classes and the output options, down to the last one they hold,
+ * and fails on any value type or part it has no entry for, and on any entry for a part that is
+ * gone. An enum's constant is a part: a value added to one is an answer the export has not been
+ * asked for.
+ *
+ * A value used in several places has the fate of all of them: reported where any loses it, a
+ * gap where any loses it unnamed, and its note says where. The entries are claims about the
+ * export, as the node ledger's are: a written or a reported one proved by its tests, a gap a known
+ * defect held here until it is closed.
+ */
+class DocxValueFieldLedgerTest {
+
+ // A drawing's gradient is not carried, an inline icon's is drawn in one colour.
+ private static final String GRADIENT = "a shape's or a path's gradient is not drawn: the drawing keeps what else "
+ + "it paints, or is dropped; an inline icon's is drawn flat, in its first "
+ + "colour; a chart's, with its drawing";
+
+ private static final Map, Map> VALUES = new LinkedHashMap<>();
+
+ static {
+ value(com.demcha.compose.document.node.HorizontalAlign.class,
+ "CENTER:REPORTED:composed in a table cell where the layout does not say where its child stands, "
+ + "written from the cell's left; with no layout, in the section's note; any other is written, as "
+ + "the indents that hold the child where the layout places it",
+ "LEFT:WRITTEN",
+ "RIGHT:REPORTED:composed in a table cell where the layout does not say where its child stands, "
+ + "written from the cell's left; with no layout, in the section's note; any other is written, as "
+ + "the indents that hold the child where the layout places it");
+ value(com.demcha.compose.document.node.DocumentBookmarkOptions.class,
+ "level:REPORTED:its paragraph's heading level, past Word's ninth at the ninth, named where a level "
+ + "the page nests apart shares it; not written, named, for a line pair's right side whose left holds "
+ + "one, in a page zone and on any node but a paragraph",
+ "title:REPORTED:Word lists a heading by its paragraph's text: a title that is not that text, and "
+ + "one side's of a line pair, which lists the whole line, named; not written where its level is not");
+ value(com.demcha.compose.document.node.ListMarker.class,
+ "runs:WRITTEN:each run written as in a paragraph, before its item; what a chip or a picture among "
+ + "them loses is the run's own",
+ "value:WRITTEN:as a Word list level's text where every item at its depth shares it and it holds no "
+ + "percent sign before a digit, otherwise as letters before the item; a marker of runs is written by "
+ + "its runs, this being their plain reading");
+ value(com.demcha.compose.document.style.DocumentTextStyle.class,
+ "color:WRITTEN:its alpha as Word's text fill",
+ "decoration:WRITTEN",
+ "fontName:REPORTED:a face that may not travel or cannot be read, named as an embedded font; any "
+ + "other is written, a face alias as its family, the weight its decoration's",
+ "letterSpacing:WRITTEN:as Word's character spacing",
+ "size:WRITTEN:to Word's half point; an auto-sized paragraph's text at the size the layout fits it "
+ + "to, where its lines tell it, as its autoSize says");
+ value(com.demcha.compose.document.node.TextAlign.class,
+ "CENTER:REPORTED:a list's items, written flush left, named; in a page zone, where its part stands "
+ + "is the zone's note; a page field's the page does not apply; any other is written as Word's "
+ + "alignment, turned to the flow in a right-to-left paragraph",
+ "LEFT:WRITTEN:turned to the flow in a right-to-left paragraph",
+ "RIGHT:REPORTED:a list's items, written flush left, named; in a page zone, where its part stands "
+ + "is the zone's note; a page field's the page does not apply; any other is written as Word's "
+ + "alignment, turned to the flow in a right-to-left paragraph");
+ value(com.demcha.compose.document.node.PageFieldKind.class,
+ "NUMBER:WRITTEN:as Word's page field on a page zone's line, each section counting from 1 as on the "
+ + "page; a field in the body is not written, which its node's note names",
+ "TOTAL:WRITTEN:as Word's count of the pages, a section's in a document of several, reading the "
+ + "layout's count until it updates; a field in the body is not written, which its node's note names");
+ value(com.demcha.compose.document.style.DocumentTextIndent.class,
+ "ALL_LINES:REPORTED:a blank prefix as every line's indent, one with letters as the spaces covering "
+ + "it after the first line, its letters named; in a text box, line pair or badge, its room named "
+ + "where it moves a line; in a page zone, the zone's note",
+ "FIRST_LINE:REPORTED:a blank prefix written as the first line's indent; one with letters, not "
+ + "written, named; in a text box, line pair or badge, its room named where it moves a line; in a "
+ + "page zone, the zone's note",
+ "FROM_SECOND_LINE:REPORTED:written as the indent of the lines after the first, a prefix with letters "
+ + "as the spaces covering it; in a text box, line pair or badge, its room named where it moves a "
+ + "line; in a page zone, the zone's note",
+ "NONE:WRITTEN:the page sets no prefix, and no indent is written for one");
+ value(com.demcha.compose.document.style.DocumentTextAutoSize.class,
+ "maxSize:REPORTED:read through the size the layout fits the text to, off its lines; where they do "
+ + "not tell it, unread, in sizes that do not say which is the paragraph's, or one place's of several, "
+ + "the text is written at its style's size, named",
+ "minSize:REPORTED:as maxSize: through the fitted size where the lines tell it, the style's size "
+ + "named where not",
+ "step:REPORTED:as maxSize: through the fitted size where the lines tell it, the style's size named "
+ + "where not");
+ value(com.demcha.compose.document.node.TextVerticalAlign.class,
+ "BOTTOM:GAP:" + SEAT_UNREAD
+ + "; written as its text's position in its lines, the first text line's seat for all "
+ + "of them; a line in another size or face, seated as the first, named from half a point; in a page "
+ + "zone, as its part of the zone's line",
+ "CENTER:GAP:" + SEAT_UNREAD
+ + "; written as its text's position in its lines, the first text line's seat for all "
+ + "of them; a line in another size or face, seated as the first, named from half a point; in a page "
+ + "zone, as its part of the zone's line",
+ "DEFAULT:WRITTEN:on the baseline, with no seat to write",
+ "TOP:GAP:" + SEAT_UNREAD
+ + "; written as its text's position in its lines, the first text line's seat for all of "
+ + "them; a line in another size or face, seated as the first, named from half a point; in a page "
+ + "zone, as its part of the zone's line");
+ value(com.demcha.compose.document.node.TextDirection.class,
+ "AUTO:REPORTED:resolved as the page resolves it and written as the direction it resolves to; "
+ + "resolved right to left on a page zone's paragraph, named as written left to right",
+ "LTR:WRITTEN",
+ "RTL:REPORTED:on a page zone's paragraph, written left to right, named; any other is written as "
+ + "Word's right-to-left paragraph and runs");
+ value(com.demcha.compose.document.node.ExternalLinkTarget.class,
+ "options:WRITTEN:as an external link from the part the text is in, a header's or footer's included");
+ value(com.demcha.compose.document.node.InternalLinkTarget.class,
+ "anchor:REPORTED:written as a link to its bookmark; where the node declaring the anchor writes "
+ + "none, drawn, in a page zone, a stack's column or a block writing no paragraph, named on that "
+ + "node; an anchor several nodes declare or a repeated header row holds, named");
+ value(com.demcha.compose.document.node.ListItem.class,
+ "children:WRITTEN:each a depth further in, as the next level of a Word list or two spaces more; "
+ + "where an item stands against the page's column is the list's note",
+ "label:REPORTED:where the page reads it as markdown, its marks named where they are written as "
+ + "letters; an item of runs is written by its runs, this being their plain reading; any other is "
+ + "written",
+ "marker:WRITTEN:its own, or its depth's default as on the page; a Word list's level where every "
+ + "item at its depth shares it, otherwise its letters or runs before the item",
+ "runs:WRITTEN:each run written as in a paragraph; what a chip or a picture among them loses is the "
+ + "run's own");
+ value(com.demcha.compose.font.FontName.class,
+ "name:REPORTED:resolved to its family as the layout resolves it and written as that family's Word "
+ + "name; a face of it that may not travel or cannot be read, named as an embedded font",
+ "normalizedName:INERT:the name without case, the identity its family is found by; nothing Word reads");
+ value(com.demcha.compose.document.style.DocumentTextDecoration.class,
+ "BOLD:WRITTEN",
+ "BOLD_ITALIC:WRITTEN",
+ "DEFAULT:WRITTEN:no face of its own, and the Normal style sets none",
+ "ITALIC:WRITTEN",
+ "STRIKETHROUGH:WRITTEN",
+ "UNDERLINE:WRITTEN:as Word's single underline");
+ value(com.demcha.compose.document.style.DocumentLetterSpacing.class,
+ "type:WRITTEN:through the points it resolves the value to",
+ "value:WRITTEN:in twentieths of a point; one past what Word can hold fails the export rather than "
+ + "wrap");
+ value(com.demcha.compose.document.style.DocumentLetterSpacing.Type.class,
+ "FONT_SIZE:WRITTEN:against the size the run is written at, an auto-sized paragraph's fitted size",
+ "POINTS:WRITTEN");
+ value(com.demcha.compose.document.node.DocumentLinkOptions.class,
+ "uri:WRITTEN:as the link's target");
+ value(com.demcha.compose.document.node.InlineTextRun.class,
+ "linkTarget:WRITTEN:its own link before its paragraph's",
+ "text:WRITTEN:each line break as Word's; characters the page draws nothing for left out",
+ "textStyle:WRITTEN:its own, else its paragraph's or list's, as the page falls back");
+ value(com.demcha.compose.document.node.InlineImageRun.class,
+ "alignment:GAP:" + LIST_PICTURE
+ + "; written as the picture's raise off the baseline, by the first line's measure; "
+ + "where its paragraph's lines are not read, set other than on the baseline, and on a later line of "
+ + "another height, named; in a page zone, named; any other is written",
+ "baselineOffset:GAP:" + LIST_PICTURE
+ + "; added to that raise; where its paragraph's lines are not read, named from "
+ + "half a point or with its alignment; in a page zone, named; any other is written",
+ "height:WRITTEN",
+ "imageData:REPORTED:empty data, nothing to write, named; any other is written as given, typed by "
+ + "its signature",
+ "linkTarget:WRITTEN:its own link before its paragraph's",
+ "width:WRITTEN");
+ value(com.demcha.compose.document.node.InlineShapeRun.class,
+ "alignment:GAP:" + LIST_PICTURE
+ + "; as an inline image's, a raised picture carrying its rise as transparent rows; "
+ + "where its paragraph's lines are not read, set other than on the baseline, and on a later line of "
+ + "another height, named; in a page zone, named; any other is written",
+ "baselineOffset:GAP:" + LIST_PICTURE
+ + "; as an inline image's: where its paragraph's lines are not read, named from "
+ + "half a point or with its alignment; in a page zone, named; any other is written",
+ "layers:REPORTED:drawn as one picture of its layers, named; where no text beside it gives back how "
+ + "much wider than its box the picture is, how far that moves its line, named from half a point",
+ "linkTarget:WRITTEN:its own link before its paragraph's");
+ value(com.demcha.compose.document.node.InlineSvgRun.class,
+ "alignment:GAP:" + LIST_PICTURE
+ + "; as an inline image's, a raised picture carrying its rise as transparent rows; "
+ + "where its paragraph's lines are not read, set other than on the baseline, and on a later line of "
+ + "another height, named; in a page zone, named; any other is written",
+ "baselineOffset:GAP:" + LIST_PICTURE
+ + "; as an inline image's: where its paragraph's lines are not read, named from "
+ + "half a point or with its alignment; in a page zone, named; any other is written",
+ "height:WRITTEN:the picture's, a raised one grown by its rise",
+ "icon:REPORTED:drawn as a picture, named, the text it stands for its description; a gradient, "
+ + "drawn flat in its first colour, named",
+ "linkTarget:WRITTEN:its own link before its paragraph's",
+ "width:WRITTEN");
+ value(com.demcha.compose.document.node.InlineHighlightRun.class,
+ "background:REPORTED:as the run's shading; what Word's shading does not hold of it, named on the chip",
+ "linkTarget:WRITTEN:its own link before its paragraph's",
+ "text:WRITTEN:its line breaks as spaces, as the page keeps a chip on one line",
+ "textStyle:WRITTEN:its own, else its paragraph's or list's");
+ value(com.demcha.compose.document.node.InlineImageAlignment.class,
+ "BASELINE:GAP:" + LIST_PICTURE
+ + "; its foot on the baseline, where Word stands a picture; where its paragraph's lines "
+ + "are not read, a drawn shape's picture, which reaches below its box, stands that much high, named "
+ + "from half a point; any other is written",
+ "CENTER:GAP:" + LIST_PICTURE
+ + "; written as a raise off the baseline, by the first line's measure; where its "
+ + "paragraph's lines are not read, standing on the baseline, and on a later line of another height, "
+ + "named; in a page zone, named; any other is written",
+ "TEXT_BOTTOM:GAP:" + LIST_PICTURE
+ + "; written as a raise off the baseline, by the first line's measure; where its "
+ + "paragraph's lines are not read, standing on the baseline, and on a later line of another height, "
+ + "named; in a page zone, named; any other is written",
+ "TEXT_TOP:GAP:" + LIST_PICTURE
+ + "; written as a raise off the baseline, by the first line's measure; where its "
+ + "paragraph's lines are not read, standing on the baseline, and on a later line of another height, "
+ + "named; in a page zone, named; any other is written");
+ value(com.demcha.compose.document.style.InlineBackground.class,
+ "cornerRadius:REPORTED:above zero, named, a run's shading being square",
+ "fill:REPORTED:as the run's shading; a translucent one, flattened against what Word paints under it, "
+ + "named; any other is written",
+ "padding:REPORTED:beside its letters, as space after a letter where one can take it; where none can "
+ + "(opening its line, after a picture, by an emoji, right to left), below zero, and above and below "
+ + "its letters, named");
+ value(com.demcha.compose.document.style.DocumentInsets.class,
+ "bottom:GAP:" + SPACE_BELOW_AT_THE_FOOT + "; " + PADDING_BELOW_ZERO
+ + "; below zero, the pull nothing under it gives back, on the next block (space above); "
+ + "a chart's or a spacer's no band or column measures from; a page field's beside other parts; a "
+ + "picture's padding drawn beside its text or over its badge; a chip's; a shape container's at its "
+ + "table cell's middle or foot; any other is written",
+ "left:GAP:" + PADDING_BELOW_ZERO
+ + "; a picture's, a barcode's and a chart's, and a page reference's on the side its "
+ + "alignment sets it from; a page field's at its sides; a picture's padding drawn beside its text or "
+ + "over its badge; a chip's its shading does not hold; any other is written",
+ "right:GAP:" + PADDING_BELOW_ZERO
+ + "; a page reference's on the side its alignment sets it from; a page field's at its "
+ + "sides; a picture's padding drawn beside its text or over its badge; a chip's its shading does not "
+ + "hold; any other is written",
+ "top:GAP:" + TOP_PULL + "; " + PADDING_BELOW_ZERO
+ + "; a chart's or a spacer's; a page field's beside other parts; a picture's padding drawn "
+ + "beside its text or over its badge; a chip's, above its letters; any other is written");
+ value(com.demcha.compose.document.style.DocumentTransform.class,
+ "rotationDegrees:REPORTED", "scaleX:REPORTED", "scaleY:REPORTED");
+ value(com.demcha.compose.document.style.ClipPolicy.class,
+ "CLIP_BOUNDS:REPORTED:where it cuts what its layers paint past its box, which is written whole; "
+ + "composed in a table cell, on its table; with no layout, wherever it is set; a canvas's, which the "
+ + "page does not apply, cuts nothing",
+ "CLIP_PATH:REPORTED:where it cuts what its layers paint past its outline, which is written whole, a "
+ + "picture filling the ellipse that clips it cropped to it in the file; composed in a table cell, on "
+ + "its table; with no layout, wherever it is set; a canvas's cuts nothing",
+ "OVERFLOW_VISIBLE:INERT:it cuts nothing, on the page or in the Word file");
+ value(com.demcha.compose.document.style.DocumentColor.class,
+ "color:REPORTED:its translucency where Word holds an opaque colour — a panel's or a table cell's "
+ + "fill or border, a rule, a chip, a page zone's separator — flattened against the colour under it; "
+ + "a chart's or a watermark's, with them; any other is written, alpha included");
+ value(com.demcha.compose.document.style.DocumentStroke.class,
+ "color:REPORTED:its translucency as a Word border — a panel's, a table cell's, a rule's — flattened "
+ + "against the panel's fill or the colour under it; a chart's, with its drawing; drawn, its alpha is "
+ + "written; any other is written",
+ "width:GAP:" + RIGHT_BORDER
+ + "; as a Word border, a width that, rounded to the eighth of a point, is below a quarter "
+ + "of a point or past 12pt, drawn at Word's thinnest or widest, and the room Word makes for a panel's "
+ + "or a cell's border past the page's; a chart's, with its drawing; any other is written");
+ value(com.demcha.compose.document.style.DocumentCornerRadius.class,
+ "bottomLeft:REPORTED:a panel's, square, as a Word table cell is; drawn, corners that differ once each "
+ + "is held to half the shorter side, at the largest of them; a chart's, with its drawing; any other "
+ + "is written",
+ "bottomRight:REPORTED:a panel's, square, as a Word table cell is; drawn, corners that differ once each "
+ + "is held to half the shorter side, at the largest of them; a chart's, with its drawing; any other "
+ + "is written",
+ "topLeft:REPORTED:a panel's, square, as a Word table cell is; drawn, corners that differ once each "
+ + "is held to half the shorter side, at the largest of them; a chart's, with its drawing; any other "
+ + "is written",
+ "topRight:REPORTED:a panel's, square, as a Word table cell is; drawn, corners that differ once each "
+ + "is held to half the shorter side, at the largest of them; a chart's, with its drawing; any other "
+ + "is written");
+ value(com.demcha.compose.document.style.DocumentBorders.class,
+ "bottom:REPORTED:as a panel's: its translucency, flattened against the panel's fill or the colour "
+ + "under it; a width outside Word's quarter of a point to 12pt; its reach past the panel's box the "
+ + "space below it and its padding do not take, on the block below; a row's where its paint is not "
+ + "written; any other is written",
+ "left:REPORTED:as a panel's: its translucency, flattened against the panel's fill or the colour "
+ + "under it, and a width outside Word's quarter of a point to 12pt; a row's where its paint is not "
+ + "written; any other is written",
+ "right:GAP:" + RIGHT_BORDER
+ + "; as a panel's: its translucency, flattened against the panel's fill or the colour "
+ + "under it, and a width outside Word's quarter of a point to 12pt; a row's where its paint is not "
+ + "written; any other is written",
+ "top:REPORTED:as a panel's: its translucency, flattened against the panel's fill or the colour "
+ + "under it; a width outside Word's quarter of a point to 12pt; its reach past the panel's box the "
+ + "space above it, its padding and the room over its first line do not take, on the panel; a row's "
+ + "where its paint is not written; any other is written");
+ value(com.demcha.compose.document.style.DocumentFlowWidth.class,
+ "points:REPORTED:of an unpainted block, a panel the layout gives no width — composed in a table "
+ + "cell, or with no layout — and a layer stack's column, where it holds text that wraps, which runs "
+ + "the width of its column; any other is written");
+ value(com.demcha.compose.document.style.DocumentDashPattern.class,
+ "segments:REPORTED:drawn, as a line's or a path's stroke, drawn solid; as a rule, Word's own dash or "
+ + "dots, not the pattern's lengths; in a table cell, on its table; an inline icon's is written in its "
+ + "picture");
+ value(com.demcha.compose.document.style.DocumentLineCap.class,
+ "BUTT:WRITTEN",
+ "ROUND:REPORTED:a rule's, a border ending flat; drawn, and in an inline icon's picture, it is written",
+ "SQUARE:REPORTED:a rule's, a border ending flat; drawn, and in an inline icon's picture, it is "
+ + "written");
+ value(com.demcha.compose.document.style.DocumentLineJoin.class,
+ "BEVEL:WRITTEN", "MITER:WRITTEN", "ROUND:WRITTEN");
+ value(com.demcha.compose.document.style.DocumentBleed.class,
+ "edges:REPORTED:of a panel in the flow the page pages, whose fill and borders stop at its box; "
+ + "elsewhere the page bleeds nothing");
+ value(com.demcha.compose.document.style.DocumentEdge.class,
+ "BOTTOM:REPORTED:as an edge a panel in the flow the page pages bleeds to: its fill and borders stop "
+ + "at its box",
+ "LEFT:REPORTED:as an edge a panel in the flow the page pages bleeds to: its fill and borders stop "
+ + "at its box",
+ "RIGHT:REPORTED:as an edge a panel in the flow the page pages bleeds to: its fill and borders stop "
+ + "at its box",
+ "TOP:REPORTED:as an edge a panel in the flow the page pages bleeds to: its fill and borders stop "
+ + "at its box");
+ value(com.demcha.compose.document.style.DocumentPaint.Solid.class,
+ "color:REPORTED:its translucency as a shape's fill written as a rule, flattened against the colour "
+ + "under it; a chart's, with its drawing; any other is written, alpha included");
+ value(com.demcha.compose.document.style.DocumentPaint.Linear.class,
+ "angleDegrees:REPORTED:" + GRADIENT,
+ "stops:REPORTED:" + GRADIENT);
+ value(com.demcha.compose.document.style.DocumentPaint.Radial.class,
+ "cx:REPORTED:" + GRADIENT,
+ "cy:REPORTED:" + GRADIENT,
+ "stops:REPORTED:" + GRADIENT);
+ value(com.demcha.compose.document.style.DocumentPaint.LinearAxis.class,
+ "stops:REPORTED:" + GRADIENT,
+ "x0:REPORTED:" + GRADIENT,
+ "x1:REPORTED:" + GRADIENT,
+ "y0:REPORTED:" + GRADIENT,
+ "y1:REPORTED:" + GRADIENT);
+ value(com.demcha.compose.document.style.DocumentPaint.RadialCircle.class,
+ "cx:REPORTED:" + GRADIENT,
+ "cy:REPORTED:" + GRADIENT,
+ "r:REPORTED:" + GRADIENT,
+ "stops:REPORTED:" + GRADIENT);
+ value(com.demcha.compose.document.style.DocumentPaint.Stop.class,
+ "color:REPORTED:as its gradient's: none is drawn on a shape or a path; in an inline icon the first "
+ + "stop's is drawn flat and the others are not; a chart's, with its drawing",
+ "offset:REPORTED:" + GRADIENT);
+ value(com.demcha.compose.document.node.RowVerticalAlign.class,
+ "BOTTOM:WRITTEN", "CENTER:WRITTEN",
+ "TOP:WRITTEN:Word's cell default, so nothing is written for it");
+ value(com.demcha.compose.document.node.RowArrangement.class,
+ "CENTER:REPORTED:where the layout does not show where the row's children start — composed in a "
+ + "table cell with a child its lines do not place, or inside a panel or another table in its cell — "
+ + "its columns are Word's, sized to their text; with no layout, in the section's note; any other is "
+ + "written",
+ "END:REPORTED:where the layout does not show where the row's children start — composed in a "
+ + "table cell with a child its lines do not place, or inside a panel or another table in its cell — "
+ + "its columns are Word's, sized to their text; with no layout, in the section's note; any other is "
+ + "written",
+ "SPACE_AROUND:REPORTED:where the layout does not show where the row's children start — composed in a "
+ + "table cell with a child its lines do not place, or inside a panel or another table in its cell — "
+ + "its columns are Word's, sized to their text; with no layout, in the section's note; any other is "
+ + "written",
+ "SPACE_BETWEEN:REPORTED:where the layout does not show where the row's children start — composed in a "
+ + "table cell with a child its lines do not place, or inside a panel or another table in its cell — "
+ + "its columns are Word's, sized to their text; with no layout, in the section's note; any other is "
+ + "written",
+ "SPACE_EVENLY:REPORTED:where the layout does not show where the row's children start — composed in a "
+ + "table cell with a child its lines do not place, or inside a panel or another table in its cell — "
+ + "its columns are Word's, sized to their text; with no layout, in the section's note; any other is "
+ + "written",
+ "START:WRITTEN");
+ value(com.demcha.compose.document.style.DocumentRowColumn.class,
+ "type:REPORTED:an auto column, with those beside it, where the layout does not show where the row's "
+ + "children start, Word's, sized to their text; with no layout, in the section's note; any other is "
+ + "written",
+ "value:GAP:" + ROW_COLUMNS_UNLAID
+ + "; a fixed width or a weight beside an auto column, where the layout does not show where the "
+ + "row's children start, Word's with the rest, named; in a row its arrangement or a growing spacer "
+ + "divides, the page reads none of them; any other is written");
+ value(com.demcha.compose.document.style.DocumentRowColumn.Type.class,
+ "AUTO:REPORTED:where the layout does not show where the row's children start — composed in a table "
+ + "cell with a child its lines do not place, or inside a panel or another table in its cell — Word's, "
+ + "sized to its text, with the columns beside it; with no layout, in the section's note; any other is "
+ + "written",
+ "FIXED:GAP:" + ROW_COLUMNS_UNLAID
+ + "; beside an auto column, where the layout does not show where the row's children start, "
+ + "Word's with the rest, named; any other is written",
+ "WEIGHT:GAP:" + ROW_COLUMNS_UNLAID
+ + "; beside an auto column, where the layout does not show where the row's children start, "
+ + "Word's with the rest, named; any other is written");
+ value(com.demcha.compose.document.layout.LayoutAnchorId.class,
+ "groupKey:INERT:an identity the layout resolves a wrapper's place by, which the export does not read",
+ "index:INERT:an identity the layout resolves a wrapper's place by, which the export does not read",
+ "kind:INERT:an identity the layout resolves a wrapper's place by, which the export does not read");
+ value(com.demcha.compose.document.node.DocumentBarcodeOptions.class,
+ "content:WRITTEN:encoded in the symbol's picture, and as its description",
+ "type:WRITTEN:as the symbology the picture's matrix is encoded in",
+ "foreground:WRITTEN:as the picture's dark cells, alpha included",
+ "background:WRITTEN:as the picture's light cells, alpha included",
+ "quietZoneMargin:WRITTEN:as the quiet zone round the picture's matrix");
+ value(com.demcha.compose.document.node.DocumentBarcodeType.class,
+ "QR_CODE:WRITTEN", "CODE_128:WRITTEN", "CODE_39:WRITTEN", "EAN_13:WRITTEN", "EAN_8:WRITTEN",
+ "UPC_A:WRITTEN", "PDF_417:WRITTEN", "DATA_MATRIX:WRITTEN");
+ value(com.demcha.compose.document.image.DocumentImageData.class,
+ "bytes:WRITTEN:embedded as the picture",
+ "path:WRITTEN:read when the document is exported, its bytes embedded as the picture; a file that "
+ + "does not read fails the export");
+ value(com.demcha.compose.document.image.DocumentImageFitMode.class,
+ "STRETCH:WRITTEN:at its box's size",
+ "CONTAIN:WRITTEN:at the size it fits its box at, the room it leaves held round it as the page "
+ + "centres it",
+ "COVER:WRITTEN:at its box's size, cropped to the box about its middle");
+ value(com.demcha.compose.document.node.CanvasChild.class,
+ "node:WRITTEN:as its own node's entries say",
+ "x:REPORTED:of a block the canvas writes, set in from its left edge, which is written at that "
+ + "edge, one block after another; a drawing is drawn where it is placed",
+ "y:REPORTED:of a block the canvas writes, set elsewhere than at the foot of the one before, which "
+ + "is written there; a drawing is drawn where it is placed");
+ value(com.demcha.compose.document.node.LayerStackNode.Layer.class,
+ "node:WRITTEN:as its own node's entries say",
+ "align:GAP:" + COMPOSED_STACK_LAYER
+ + "; with no layout, set in from the left, written from the left; among several layers "
+ + "written with no place of their own, not measured; in a shape's panel in a table cell, at "
+ + "another height than its first written layer, or across by a box the layout does not give; any "
+ + "other is written, where the layout places it",
+ "offsetX:GAP:" + COMPOSED_STACK_LAYER + "; " + LAYER_PAST_THE_EDGE
+ + "; of a stack's one layer, in a table cell or with no place of its own in the "
+ + "layout; left of its stack in a table cell further than the indents round it give; in a shape's "
+ + "panel in a table cell, past its right edge or by a box the layout does not give; with no "
+ + "layout; any other is written",
+ "offsetY:GAP:" + COMPOSED_STACK_LAYER + "; " + LAYER_UNLAID
+ + "; moving a stack's one layer down or up where no band or column sets the space "
+ + "above it, or with no place of its own in the layout; one the page starts above where the "
+ + "layers before it end, written under them; in a shape's panel in a table cell; among several "
+ + "layers written with no place of their own, not measured; any other is written",
+ "zIndex:GAP:" + DRAWING_OVER_TEXT
+ + "; as the order Word stacks the drawings in, among those behind the text and among "
+ + "those in front of it");
+ value(com.demcha.compose.document.node.LayerAlign.class,
+ "TOP_LEFT:REPORTED:on a layer after the first a shape's panel in a table cell writes, where that "
+ + "one is set lower: the cell sets all it holds at the first one's height; any other is written, "
+ + "where the layout places it",
+ "TOP_CENTER:GAP:" + COMPOSED_STACK_LAYER
+ + "; across, with no layout, written from the left, and in a shape's panel in a "
+ + "table cell where the layout gives no box to set it across by, from the shape's left; there, on "
+ + "a layer after the first written, where that one is set lower, at its height; any other is "
+ + "written",
+ "TOP_RIGHT:GAP:" + COMPOSED_STACK_LAYER
+ + "; across, with no layout, written from the left, and in a shape's panel in a "
+ + "table cell where the layout gives no box to set it across by, from the shape's left; there, on "
+ + "a layer after the first written, where that one is set lower, at its height; any other is "
+ + "written",
+ "CENTER_LEFT:GAP:" + COMPOSED_STACK_LAYER
+ + "; on a layer after the first a shape's panel in a table cell writes, where "
+ + "that one is set at another height: the cell sets all it holds at the first one's height; any "
+ + "other is written, where the layout places it",
+ "CENTER:GAP:" + COMPOSED_STACK_LAYER
+ + "; across, with no layout, written from the left, and in a shape's panel in a table "
+ + "cell where the layout gives no box to set it across by, from the shape's left; there, on a layer "
+ + "after the first written, where that one is set at another height, at its height; any other is "
+ + "written",
+ "CENTER_RIGHT:GAP:" + COMPOSED_STACK_LAYER
+ + "; across, with no layout, written from the left, and in a shape's panel in a "
+ + "table cell where the layout gives no box to set it across by, from the shape's left; there, on "
+ + "a layer after the first written, where that one is set at another height, at its height; any "
+ + "other is written",
+ "BOTTOM_LEFT:GAP:" + COMPOSED_STACK_LAYER
+ + "; on a layer after the first a shape's panel in a table cell writes, where "
+ + "that one is set higher: the cell sets all it holds at the first one's height; any other is "
+ + "written, where the layout places it",
+ "BOTTOM_CENTER:GAP:" + COMPOSED_STACK_LAYER
+ + "; across, with no layout, written from the left, and in a shape's panel in a "
+ + "table cell where the layout gives no box to set it across by, from the shape's left; there, on "
+ + "a layer after the first written, where that one is set higher, at its height; any other is "
+ + "written",
+ "BOTTOM_RIGHT:GAP:" + COMPOSED_STACK_LAYER
+ + "; across, with no layout, written from the left, and in a shape's panel in a "
+ + "table cell where the layout gives no box to set it across by, from the shape's left; there, on "
+ + "a layer after the first written, where that one is set higher, at its height; any other is "
+ + "written");
+ value(com.demcha.compose.document.style.ShapePoint.class,
+ "x:WRITTEN:as its share of the drawn shape's width, in its custom geometry or a picture's path",
+ "y:WRITTEN:as its share of the drawn shape's height, up from its foot, in its custom geometry or a "
+ + "picture's path");
+ value(com.demcha.compose.document.style.ShapeOutline.Rectangle.class,
+ "width:GAP:" + PANEL_WIDER_THAN_ITS_CELL
+ + "; as the drawn outline's, a panel's in a table cell, or an inline shape's picture's",
+ "height:WRITTEN:as the drawn outline's, a panel's in a table cell, or an inline shape's picture's");
+ value(com.demcha.compose.document.style.ShapeOutline.RoundedRectangle.class,
+ "width:GAP:" + PANEL_WIDER_THAN_ITS_CELL
+ + "; as the drawn outline's, a panel's in a table cell, or an inline shape's picture's",
+ "height:WRITTEN:as the drawn outline's, a panel's in a table cell, or an inline shape's picture's",
+ "cornerRadius:REPORTED:as a panel in a table cell, its corners square; any other is written, held "
+ + "to half the shorter side as the page holds it");
+ value(com.demcha.compose.document.style.ShapeOutline.RoundedRectanglePerCorner.class,
+ "width:GAP:" + PANEL_WIDER_THAN_ITS_CELL
+ + "; as the drawn outline's, a panel's in a table cell, or an inline shape's picture's",
+ "height:WRITTEN:as the drawn outline's, a panel's in a table cell, or an inline shape's picture's",
+ "corners:REPORTED:drawn as a shape, radii that differ once held to half its shorter side, drawn at "
+ + "the largest of them; as a panel in a table cell, square; any other is written, an inline "
+ + "shape's picture rounding each as the page does");
+ value(com.demcha.compose.document.style.ShapeOutline.Ellipse.class,
+ "width:WRITTEN:as the drawn ellipse's, or an inline shape's picture's",
+ "height:WRITTEN:as the drawn ellipse's, or an inline shape's picture's");
+ value(com.demcha.compose.document.style.ShapeOutline.Polygon.class,
+ "width:WRITTEN:as the drawn outline's, or an inline shape's picture's",
+ "height:WRITTEN:as the drawn outline's, or an inline shape's picture's",
+ "points:WRITTEN:as the drawn outline's custom geometry, or an inline shape's picture");
+ value(com.demcha.compose.document.style.ShapeOutline.Path.class,
+ "width:WRITTEN:as the drawn outline's, or an inline shape's picture's",
+ "height:WRITTEN:as the drawn outline's, or an inline shape's picture's",
+ "segments:WRITTEN:as the drawn outline's custom geometry, or an inline shape's picture");
+ value(com.demcha.compose.document.style.DocumentPathSegment.MoveTo.class,
+ "x:WRITTEN:as a move in the drawn custom geometry, or a picture's path",
+ "y:WRITTEN:as a move in the drawn custom geometry, or a picture's path");
+ value(com.demcha.compose.document.style.DocumentPathSegment.LineTo.class,
+ "x:WRITTEN:as a line in the drawn custom geometry, or a picture's path",
+ "y:WRITTEN:as a line in the drawn custom geometry, or a picture's path");
+ value(com.demcha.compose.document.style.DocumentPathSegment.CubicTo.class,
+ "control1X:WRITTEN:as a curve in the drawn custom geometry, or a picture's path",
+ "control1Y:WRITTEN:as a curve in the drawn custom geometry, or a picture's path",
+ "control2X:WRITTEN:as a curve in the drawn custom geometry, or a picture's path",
+ "control2Y:WRITTEN:as a curve in the drawn custom geometry, or a picture's path",
+ "x:WRITTEN:as a curve in the drawn custom geometry, or a picture's path",
+ "y:WRITTEN:as a curve in the drawn custom geometry, or a picture's path");
+ value(com.demcha.compose.document.style.DocumentPathSegment.Close.class);
+ value(com.demcha.compose.document.svg.SvgIcon.class,
+ "layers:REPORTED:drawn into the icon's picture, a gradient flat in its first colour, which the "
+ + "icon's note names; any other is written",
+ "sourceWidth:WRITTEN:as the scale its layers' stroke widths and dash lengths are drawn at",
+ "sourceHeight:INERT:the page draws an icon to its run's height, which a builder may size from "
+ + "this; the export reads the run's",
+ "text:REPORTED:written as the picture's description, not a character in the line, as the icon's "
+ + "note says");
+ value(com.demcha.compose.document.svg.SvgIcon.Layer.class,
+ "geometry:WRITTEN:drawn into the icon's picture",
+ "fill:WRITTEN:drawn into the icon's picture, alpha included",
+ "fillPaint:REPORTED:a gradient, drawn flat in its first colour, which the icon's note names; a "
+ + "solid one is written",
+ "stroke:WRITTEN:drawn into the icon's picture, its width scaled to the icon",
+ "strokePaint:REPORTED:a gradient, drawn flat in its first colour, which the icon's note names; a "
+ + "solid one is written",
+ "lineCap:WRITTEN:drawn into the icon's picture",
+ "lineJoin:WRITTEN:drawn into the icon's picture",
+ "dashArray:WRITTEN:drawn into the icon's picture, its lengths scaled to the icon",
+ "clip:WRITTEN:the layer is clipped to it in the icon's picture");
+ value(com.demcha.compose.document.svg.SvgPath.class,
+ "segments:WRITTEN:drawn into an icon's picture, as a layer's outline or its clip",
+ "sourceWidth:INERT:the size of the art the path was read from: a layer is drawn to its icon's box, "
+ + "and neither the page nor the export reads it",
+ "sourceHeight:INERT:the size of the art the path was read from: a layer is drawn to its icon's "
+ + "box, and neither the page nor the export reads it");
+ value(com.demcha.compose.document.node.ShapeLayer.class,
+ "outline:WRITTEN:drawn into the inline shape's picture, centred in its run's box",
+ "fill:WRITTEN:drawn into the inline shape's picture, alpha included",
+ "stroke:WRITTEN:drawn into the inline shape's picture at its width, mitred as on the page");
+ value(com.demcha.compose.document.chart.ChartStyle.class,
+ "palette:REPORTED:in the chart's note",
+ "seriesPaintOverrides:REPORTED:in the chart's note",
+ "lineWidth:REPORTED:in the chart's note",
+ "barCornerRadius:REPORTED:in the chart's note",
+ "barWidthRatio:REPORTED:in the chart's note",
+ "grid:REPORTED:in the chart's note",
+ "pointMarker:REPORTED:in the chart's note",
+ "valueLabelOffset:REPORTED:in the chart's note",
+ "axisTextStyle:REPORTED:in the chart's note",
+ "legendTextStyle:REPORTED:in the chart's note",
+ "valueLabelTextStyle:REPORTED:in the chart's note",
+ "valueLabelHalo:REPORTED:in the chart's note",
+ "areaOpacity:REPORTED:in the chart's note",
+ "sliceStroke:REPORTED:in the chart's note",
+ "sliceGapDegrees:REPORTED:in the chart's note",
+ "donutCenterTextStyle:REPORTED:in the chart's note");
+ value(com.demcha.compose.document.chart.ChartStyle.GridStyle.class,
+ "horizontal:REPORTED:in the chart's note",
+ "vertical:REPORTED:in the chart's note");
+ value(com.demcha.compose.document.chart.ChartSpec.Bar.class,
+ "data:WRITTEN:as the chart's data table, a row a category and a column a series",
+ "horizontal:REPORTED:in the chart's note",
+ "grouping:REPORTED:in the chart's note; each series' values are written either way",
+ "valueAxis:REPORTED:its format is written, as the data table's value format; its bounds, grid lines "
+ + "and tick labels, in the chart's note",
+ "legend:REPORTED:in the chart's note; the series' names are written as the data table's header",
+ "valueLabels:REPORTED:in the chart's note; the values are written in the data table",
+ "size:REPORTED:in the chart's note; the data table is as tall as its rows",
+ "showCategoryLabels:REPORTED:in the chart's note; the categories are written in the data table "
+ + "either way");
+ value(com.demcha.compose.document.chart.ChartSpec.Line.class,
+ "data:WRITTEN:as the chart's data table, a row a category and a column a series",
+ "interpolation:REPORTED:in the chart's note",
+ "area:REPORTED:in the chart's note",
+ "valueAxis:REPORTED:its format is written, as the data table's value format; its bounds, grid lines "
+ + "and tick labels, in the chart's note",
+ "legend:REPORTED:in the chart's note; the series' names are written as the data table's header",
+ "valueLabels:REPORTED:in the chart's note; the values are written in the data table",
+ "size:REPORTED:in the chart's note; the data table is as tall as its rows",
+ "showCategoryLabels:REPORTED:in the chart's note; the categories are written in the data table "
+ + "either way");
+ value(com.demcha.compose.document.chart.ChartSpec.Pie.class,
+ "data:WRITTEN:as the chart's data table, a row a category",
+ "donutRatio:REPORTED:in the chart's note",
+ "startAngleDegrees:REPORTED:in the chart's note",
+ "clockwise:REPORTED:in the chart's note",
+ "sliceLabels:REPORTED:in the chart's note; the values and categories are written in the data "
+ + "table, the shares not",
+ "valueFormat:WRITTEN:as the data table's value format",
+ "percentFormat:REPORTED:in the chart's note; the shares it formats are only drawn",
+ "centerText:REPORTED:in the chart's note; the text in the donut's hole is only drawn, and is not "
+ + "in the data table",
+ "legend:REPORTED:in the chart's note; the categories it lists are written in the data table",
+ "size:REPORTED:in the chart's note; the data table is as tall as its rows");
+ value(com.demcha.compose.document.chart.PointMarker.class,
+ "width:REPORTED:in the chart's note",
+ "height:REPORTED:in the chart's note",
+ "fill:REPORTED:in the chart's note",
+ "stroke:REPORTED:in the chart's note");
+ value(com.demcha.compose.document.table.DocumentTableStyle.class,
+ "padding:GAP:" + CELL_PADDING + "; " + PADDING_BELOW_ZERO
+ + "; a text cell's thinner than its rules' share, which Word makes room for, where its "
+ + "row then stands taller than the page's, named with the rules; any other is written, less "
+ + "that share",
+ "fillColor:REPORTED:its translucency, flattened against the colour under the cell; any other is "
+ + "written",
+ "stroke:REPORTED:its translucency, flattened against the cell's fill or the colour under it; a width "
+ + "that rounds below a quarter of a point or past 12pt, drawn at Word's thinnest or widest; the "
+ + "room Word makes for it past a text cell's thinner padding, where its row stands taller; any "
+ + "other is written, one of no width as no border",
+ "textStyle:WRITTEN:a text cell's; the page sets no cell's face on what a composed cell holds",
+ "textAnchor:WRITTEN:its height in every cell, its side in a text cell; the page sets what a "
+ + "composed cell holds by its height alone",
+ "lineSpacing:WRITTEN:a text cell's, as the space after each of its lines but the last; the page "
+ + "does not apply it to what a composed cell holds",
+ "direction:WRITTEN:a text cell's; the page does not apply it to what a composed cell holds");
+ value(com.demcha.compose.document.table.DocumentTableColumn.class,
+ "type:REPORTED:where a table has an auto one whose width the layout does not show — composed in a "
+ + "table cell, or with a column edge no row shows — named, its columns the editor's; with no "
+ + "layout, in the section's note; any other is written",
+ "fixedWidth:REPORTED:beside an auto column, where the layout does not show the widths, not "
+ + "written, named with the table; with no layout, in the section's note; any other is written");
+ value(com.demcha.compose.document.table.DocumentTableColumn.Type.class,
+ "AUTO:REPORTED:where the layout does not show its width — composed in a table cell, or with a "
+ + "column edge no row shows — the editor's, named; with no layout, in the section's note; any "
+ + "other is written, as the layout measured it",
+ "FIXED:REPORTED:beside an auto column, where the layout does not show the widths, its width not "
+ + "written, named with the table; with no layout, in the section's note; any other is written");
+ value(com.demcha.compose.document.table.DocumentTableTextAnchor.class,
+ "DEFAULT:WRITTEN:at the cell's foot and its left, where the page sets it",
+ "CENTER_LEFT:WRITTEN", "CENTER:WRITTEN", "CENTER_RIGHT:WRITTEN",
+ "TOP_LEFT:WRITTEN", "TOP_RIGHT:WRITTEN", "BOTTOM_LEFT:WRITTEN", "BOTTOM_RIGHT:WRITTEN");
+ value(com.demcha.compose.document.chart.ChartData.class,
+ "categories:WRITTEN:as the first column of the chart's data table",
+ "series:WRITTEN:as the chart's data table's other columns, one a series");
+ value(com.demcha.compose.document.chart.ChartData.Series.class,
+ "name:WRITTEN:as its column's header in the chart's data table",
+ "values:WRITTEN:in the chart's data table, in its value format, a gap as an empty cell");
+ value(com.demcha.compose.document.chart.BarGrouping.class,
+ "GROUPED:REPORTED:in the chart's note; each series' values are written",
+ "STACKED:REPORTED:in the chart's note; each series' values are written, not a stack's total");
+ value(com.demcha.compose.document.chart.AxisSpec.class,
+ "baselineAtZero:REPORTED:in the chart's note",
+ "min:REPORTED:in the chart's note",
+ "max:REPORTED:in the chart's note",
+ "format:WRITTEN:as the chart's data table's value format",
+ "showGridLines:REPORTED:in the chart's note",
+ "showTickLabels:REPORTED:in the chart's note");
+ value(com.demcha.compose.document.chart.LegendPosition.class,
+ "NONE:INERT:the page draws no legend; the names one lists are written in the data table all "
+ + "the same",
+ "BOTTOM:REPORTED:in the chart's note; the names it lists are written in the data table",
+ "RIGHT:REPORTED:in the chart's note; the names it lists are written in the data table",
+ "TOP:REPORTED:in the chart's note; the names it lists are written in the data table");
+ value(com.demcha.compose.document.chart.ValueLabelMode.class,
+ "NONE:INERT:the page draws no value labels",
+ "OUTSIDE:REPORTED:in the chart's note; the values are written in the data table",
+ "INSIDE:INERT:the page lays out no chart that asks for it");
+ value(com.demcha.compose.document.chart.LineInterpolation.class,
+ "LINEAR:REPORTED:in the chart's note",
+ "SMOOTH:REPORTED:in the chart's note",
+ "MONOTONE:REPORTED:in the chart's note");
+ value(com.demcha.compose.document.chart.SliceLabelMode.class,
+ "NONE:INERT:the page draws no slice labels",
+ "VALUE:REPORTED:in the chart's note; the values are written in the data table",
+ "PERCENT:REPORTED:in the chart's note; the shares are not written, only the values",
+ "CATEGORY:REPORTED:in the chart's note; the categories are written in the data table",
+ "CATEGORY_PERCENT:REPORTED:in the chart's note; the categories are written in the data table, "
+ + "the shares not");
+ value(com.demcha.compose.document.chart.NumberFormatSpec.class,
+ "pattern:REPORTED:as a pie's percent format, in the chart's note; as a chart's value format, an "
+ + "axis's or a pie's, written in its data table's values",
+ "locale:REPORTED:as a pie's percent format, in the chart's note; as a chart's value format, an "
+ + "axis's or a pie's, written in its data table's values",
+ "prefix:REPORTED:as a pie's percent format, in the chart's note; as a chart's value format, an "
+ + "axis's or a pie's, written in its data table's values",
+ "suffix:REPORTED:as a pie's percent format, in the chart's note; as a chart's value format, an "
+ + "axis's or a pie's, written in its data table's values",
+ "scaleDivisor:REPORTED:as a pie's percent format, in the chart's note; as a chart's value format, "
+ + "an axis's or a pie's, written in its data table's values");
+ value(com.demcha.compose.document.table.DocumentTableCell.class,
+ "lines:WRITTEN:a text cell's, a line each; a composed cell's are read neither by the page nor by "
+ + "the export",
+ "style:GAP:" + CELL_PADDING + "; " + PADDING_BELOW_ZERO
+ + "; as the cell style's parts say: the translucency of its fill and its rules, a rule's "
+ + "width Word does not draw, and the room Word makes for its rules past a text cell's thinner "
+ + "padding; any other is written",
+ "colSpan:WRITTEN",
+ "rowSpan:WRITTEN",
+ "content:WRITTEN:as its own node's entries say");
+ value(com.demcha.compose.document.chart.ChartSize.Fixed.class,
+ "points:REPORTED:in the chart's note; the data table is as tall as its rows");
+ value(com.demcha.compose.document.chart.ChartSize.AspectRatio.class,
+ "w:REPORTED:in the chart's note; the data table is as tall as its rows",
+ "h:REPORTED:in the chart's note; the data table is as tall as its rows");
+ value(com.demcha.compose.document.output.DocumentMetadata.class,
+ "author:WRITTEN:as the file's creator",
+ "creator:WRITTEN:as the application the file names as the one that made it",
+ "keywords:WRITTEN",
+ "producer:REPORTED:one of the author's own beside a creator other than it, which a Word file has "
+ + "no field for; with no creator, or the same as it, written as the application; the one every "
+ + "metadata starts with is not the author's, and not named",
+ "subject:WRITTEN",
+ "title:WRITTEN");
+ value(com.demcha.compose.document.output.DocumentWatermark.class,
+ "color:REPORTED", "fontSize:REPORTED", "imageBytes:REPORTED", "imagePath:REPORTED",
+ "layer:REPORTED", "opacity:REPORTED", "position:REPORTED", "rotation:REPORTED", "text:REPORTED");
+ value(com.demcha.compose.document.output.DocumentProtection.class,
+ "canAssemble:REPORTED", "canCopyContent:REPORTED", "canExtractForAccessibility:REPORTED",
+ "canFillForms:REPORTED", "canModify:REPORTED", "canPrint:REPORTED", "canPrintHighQuality:REPORTED",
+ "keyLength:REPORTED", "ownerPassword:REPORTED", "userPassword:REPORTED");
+ value(com.demcha.compose.document.output.DocumentViewerPreferences.class,
+ "centerWindow:REPORTED", "displayDocTitle:REPORTED", "fitWindow:REPORTED", "hideMenubar:REPORTED",
+ "hideToolbar:REPORTED", "pageLayout:REPORTED", "pageMode:REPORTED");
+ value(com.demcha.compose.document.output.DocumentWatermarkLayer.class,
+ "ABOVE_CONTENT:REPORTED", "BEHIND_CONTENT:REPORTED");
+ value(com.demcha.compose.document.output.DocumentWatermarkPosition.class,
+ "BOTTOM_LEFT:REPORTED", "BOTTOM_RIGHT:REPORTED", "CENTER:REPORTED", "TILE:REPORTED",
+ "TOP_LEFT:REPORTED", "TOP_RIGHT:REPORTED");
+ value(com.demcha.compose.document.style.DocumentPageMode.class,
+ "FULL_SCREEN:REPORTED", "USE_ATTACHMENTS:REPORTED", "USE_NONE:REPORTED", "USE_OUTLINES:REPORTED",
+ "USE_THUMBNAILS:REPORTED");
+ value(com.demcha.compose.document.style.DocumentPageLayout.class,
+ "ONE_COLUMN:REPORTED", "SINGLE_PAGE:REPORTED", "TWO_COLUMN_LEFT:REPORTED", "TWO_COLUMN_RIGHT:REPORTED",
+ "TWO_PAGE_LEFT:REPORTED", "TWO_PAGE_RIGHT:REPORTED");
+ value(com.demcha.compose.document.output.DocumentHeaderFooter.class,
+ "centerText:REPORTED:where it and another slot overlap where the page sets them, or it is wider "
+ + "than the line, which Word sets apart or wraps; any other is written, its page tokens as "
+ + "Word's page fields",
+ "fontName:WRITTEN",
+ "fontSize:WRITTEN",
+ "height:REPORTED:a band too shallow for its line, whose text and separator Word stands further in "
+ + "from the edge than the page; any other is written, as where its line and separator stand "
+ + "from the edge",
+ "leftText:REPORTED:where it and another slot overlap where the page sets them, or it is wider "
+ + "than the line, which Word sets apart or wraps; any other is written, its page tokens as "
+ + "Word's page fields",
+ "numbering:REPORTED:a count from past page 2, a start other than the page it counts from, and page "
+ + "numbers in letters from 28 on; any other is written",
+ "reserveSpace:WRITTEN:as the page margin the session widens to the band's height",
+ "rightText:REPORTED:where it and another slot overlap where the page sets them, or it is wider "
+ + "than the line, which Word sets apart or wraps; any other is written, its page tokens as "
+ + "Word's page fields",
+ "separatorColor:REPORTED:translucent, flattened against white; any other is written, a wholly "
+ + "transparent one as no separator",
+ "separatorThickness:REPORTED:a width that, rounded to the eighth of a point, is below a quarter of "
+ + "a point or past 12pt, drawn at Word's thinnest or widest; one of no width, at Word's thinnest; "
+ + "one below zero or not a number, not written; any other is written",
+ "showSeparator:REPORTED:in a band too shallow for its line, ruled further in from the edge than the "
+ + "page strokes it; any other is written, as its line's border",
+ "textColor:WRITTEN:its alpha as Word's text fill",
+ "zone:WRITTEN");
+ value(com.demcha.compose.document.output.DocumentPageZone.class,
+ "appliesTo:REPORTED:one answering otherwise for two pages of a kind Word has a part for — the "
+ + "first, even, odd — or refusing a page, written on every page; one telling even from odd, a "
+ + "blank page Word adds before a later section; any other is written",
+ "content:REPORTED:what a header or footer cannot hold of it, as the zones entry names; built "
+ + "otherwise for the last page than for those between, written there as for them; built as "
+ + "nothing where the layout draws it, not written; any other is written",
+ "height:REPORTED:where the layout shows none of the zone's text or pictures, or shows them built "
+ + "otherwise, its line Word's own, not measured; any other is written, through where the layout "
+ + "sets its content, and as the page margin where it reserves its height",
+ "padding:REPORTED:at its sides, where it stands a part off the place Word sets it on the zone's "
+ + "line, and where the line is not measured; any other is written, where its line stands",
+ "reserveSpace:WRITTEN:as the page margin the session widens to the zone's height",
+ "zone:WRITTEN");
+ value(com.demcha.compose.document.output.DocumentHeaderFooterZone.class,
+ "FOOTER:WRITTEN", "HEADER:WRITTEN");
+ value(com.demcha.compose.document.output.DocumentPageNumbering.class,
+ "countFrom:REPORTED:past page 2, which Word has no header or footer of its own for, written on "
+ + "every page but the first; any other is written, from page 2 as Word's pages after the first",
+ "showOnFirstPage:WRITTEN:off, as a first page whose header or footer holds nothing of the band",
+ "startAt:REPORTED:other than the page it counts from, Word's page fields numbering the pages from 1 "
+ + "and the total off by as much; any other is written",
+ "style:REPORTED:in letters, a page number or total of 28 or more, which Word letters by repeating "
+ + "one letter; any other is written, as its page fields' format");
+ value(com.demcha.compose.document.output.DocumentPageNumberStyle.class,
+ "DECIMAL:WRITTEN",
+ "LOWER_ALPHA:REPORTED:a page number or total of 28 or more, which Word letters by repeating one "
+ + "letter, 28 reading bb where the page reads ab; any other is written",
+ "LOWER_ROMAN:WRITTEN",
+ "UPPER_ALPHA:REPORTED:a page number or total of 28 or more, which Word letters by repeating one "
+ + "letter, 28 reading BB where the page reads AB; any other is written",
+ "UPPER_ROMAN:WRITTEN");
+ value(com.demcha.compose.document.output.PageContext.class,
+ "number:WRITTEN:asked of a zone's predicate as the page asks it, on the layout's pages and past "
+ + "them on a sample of six; read by a zone's content, it is refused, stopping the export, as on "
+ + "any semantic export: the content places a page field",
+ "paginated:WRITTEN:true where a zone's predicate is asked, as the page asks it; false for the "
+ + "content written, as any semantic export builds it, so content built otherwise for one is "
+ + "written as built for it",
+ "total:WRITTEN:asked of a zone's predicate as the page asks it, the layout's page count, six for "
+ + "the kinds of page a shorter layout lacks; read by a zone's content, it is refused, stopping "
+ + "the export, as on any semantic export: the content places a page total");
+ }
+
+ @Test
+ void everyValueTypeANodeOrAnOutputOptionHoldsHasAnEntry() throws Exception {
+ assertThat(names(reach().parts().keySet()))
+ .as("the value types a node's field or an output option holds: a type with no entry has "
+ + "parts nobody decided the DOCX export's answer for")
+ .containsExactlyInAnyOrderElementsOf(names(VALUES.keySet()));
+ }
+
+ @Test
+ void everyPartOfEveryValueTypeHasAnEntry() throws Exception {
+ Map> undecided = new TreeMap<>();
+ Map> stale = new TreeMap<>();
+ Map, Set> types = reach().parts();
+ VALUES.forEach((type, entries) -> {
+ Set parts = types.getOrDefault(type, Set.of());
+ Set missing = new TreeSet<>(parts);
+ missing.removeAll(entries.keySet());
+ Set extra = new TreeSet<>(entries.keySet());
+ extra.removeAll(parts);
+ if (!missing.isEmpty()) {
+ undecided.put(type.getName(), missing);
+ }
+ if (!extra.isEmpty()) {
+ stale.put(type.getName(), extra);
+ }
+ });
+ assertThat(undecided).as("parts with no entry: decide whether the DOCX export writes them, reports "
+ + "them, or has nothing to carry, and say which here").isEmpty();
+ assertThat(stale).as("entries for parts the type no longer has").isEmpty();
+ }
+
+ @Test
+ void noValueTypeIsOpenToKindsTheLedgerCannotList() throws Exception {
+ // An interface that is not sealed, or a class that is not final, may be implemented or
+ // extended anywhere, with parts of its own no entry here could name.
+ assertThat(reach().open()).as("value types any class may implement or extend").isEmpty();
+ }
+
+ @Test
+ void everyNodeAValueHoldsIsOneTheNodeLedgerLists() throws Exception {
+ // A node held in another package would be in neither ledger.
+ Set listed = names(DocxNodeFieldLedgerTest.nodeClasses());
+ listed.add(DocumentNode.class.getName());
+ assertThat(names(reach().nodes())).as("node types a field holds that the node ledger does not list")
+ .isSubsetOf(listed);
+ }
+
+ private static void value(Class> type, String... specs) {
+ if (VALUES.put(type, DocxNodeFieldLedgerTest.fields(specs)) != null) {
+ throw new IllegalStateException("two entries for " + type.getName());
+ }
+ }
+
+ private static Set names(java.util.Collection> types) {
+ return types.stream().map(Class::getName).collect(Collectors.toCollection(TreeSet::new));
+ }
+
+ /**
+ * What a node's fields and the output options hold, down to the last value.
+ *
+ * @param parts every GraphCompose value type held, with its parts: a record's components, a
+ * final class's instance fields and its superclasses', an enum's constants
+ * @param open the types held that any class may implement or extend
+ * @param nodes the node types held, which are the node ledger's
+ */
+ record Reach(Map, Set> parts, Set open, Set> nodes) {
+ }
+
+ /** Walks the types a node's fields and the output options hold. A sealed type is read as the kinds it permits. */
+ static Reach reach() throws Exception {
+ Deque queue = new ArrayDeque<>();
+ for (Class> kind : DocxNodeFieldLedgerTest.nodeClasses()) {
+ for (RecordComponent component : kind.getRecordComponents()) {
+ queue.add(component.getGenericType());
+ }
+ }
+ for (RecordComponent component : DocumentOutputOptions.class.getRecordComponents()) {
+ queue.add(component.getGenericType());
+ }
+ Map, Set> parts = new LinkedHashMap<>();
+ Set open = new TreeSet<>();
+ Set> nodes = new LinkedHashSet<>();
+ // A type read once: a bound that names its own variable would be read for ever.
+ Set seen = new HashSet<>();
+ while (!queue.isEmpty()) {
+ Type type = queue.poll();
+ if (!seen.add(type)) {
+ continue;
+ }
+ if (type instanceof ParameterizedType parameterized) {
+ queue.add(parameterized.getRawType());
+ queue.addAll(Arrays.asList(parameterized.getActualTypeArguments()));
+ continue;
+ }
+ if (type instanceof WildcardType wildcard) {
+ queue.addAll(Arrays.asList(wildcard.getUpperBounds()));
+ queue.addAll(Arrays.asList(wildcard.getLowerBounds()));
+ continue;
+ }
+ if (type instanceof GenericArrayType array) {
+ queue.add(array.getGenericComponentType());
+ continue;
+ }
+ if (type instanceof TypeVariable> variable) {
+ queue.addAll(Arrays.asList(variable.getBounds()));
+ continue;
+ }
+ if (!(type instanceof Class> raw)) {
+ continue;
+ }
+ if (raw.isArray()) {
+ queue.add(raw.getComponentType());
+ continue;
+ }
+ if (!raw.getName().startsWith("com.demcha.compose.")) {
+ continue;
+ }
+ if (DocumentNode.class.isAssignableFrom(raw)) {
+ nodes.add(raw);
+ continue;
+ }
+ Set own = new TreeSet<>();
+ if (raw.isEnum()) {
+ Arrays.stream(raw.getEnumConstants()).map(constant -> ((Enum>) constant).name()).forEach(own::add);
+ } else if (raw.isRecord()) {
+ for (RecordComponent component : raw.getRecordComponents()) {
+ own.add(component.getName());
+ queue.add(component.getGenericType());
+ }
+ } else if (raw.isSealed()) {
+ queue.addAll(Arrays.asList(raw.getPermittedSubclasses()));
+ continue;
+ } else if (raw.isInterface() || !Modifier.isFinal(raw.getModifiers())) {
+ open.add(raw.getName());
+ continue;
+ } else {
+ // A final class's state is its own fields and those it inherits.
+ for (Class> level = raw; level != null && level != Object.class; level = level.getSuperclass()) {
+ for (Field field : level.getDeclaredFields()) {
+ if (!Modifier.isStatic(field.getModifiers()) && !field.isSynthetic()) {
+ own.add(field.getName());
+ queue.add(field.getGenericType());
+ }
+ }
+ }
+ }
+ parts.put(raw, own);
+ }
+ return new Reach(parts, open, nodes);
+ }
+}
From ee8c0d053fa68f5b224c658877ab4cda10b76893 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Wed, 21 Oct 2026 21:26:48 +0100
Subject: [PATCH 2/2] test(docx): name a bottom below zero the export drops
owePendingSpacingAfter keeps only a positive amount, and a list, a
table, a row, a stack, a canvas, a shape container in the flow and a
block with no paint of its own hand it their bottom edge. A bottom below
zero then does not pull the next block up in Word, and the report says
nothing. It is now the gap BOTTOM_PULL, on DocumentInsets.bottom and on
the node entries it loses.
A panel's top below zero is dropped whole, as a table's is, and
TOP_PULL says so. A row's padding takes SPACE_BELOW_AT_THE_FOOT, as a
table's does, and a row's side padding below zero joins
PADDING_BELOW_ZERO. A page band's face is reported where it may not
travel, as a run's is. The node check asserts that DocumentNode is among
the nodes the walk reaches, so an empty walk cannot pass it.
---
CHANGELOG.md | 12 ++--
.../docx/DocxNodeFieldLedgerTest.java | 57 ++++++++++++-------
.../docx/DocxValueFieldLedgerTest.java | 7 ++-
3 files changed, 47 insertions(+), 29 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b24e55840..b47a197ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3653,15 +3653,17 @@ follow semantic versioning; release dates are ISO 8601.
and stopped at the field: a part added to a value one holds — a style's colour, a run's
alignment, a border's width, an enum's constant — passed it with no decision.
- `DocxValueFieldLedgerTest` reads the value types from the node classes and the output
- options, down to the last one they hold, and gives each of their 421 parts a fate: 150
- written, 220 reported, 12 with nothing for Word to carry and 39 gaps. It fails on a value
+ options, down to the last one they hold, and gives each of their 421 parts a fate: 149
+ written, 221 reported, 12 with nothing for Word to carry and 39 gaps. It fails on a value
type or a part with no entry, on an entry for a part that is gone, on a type any class may
implement or extend, and on a node type held from outside the node ledger's packages.
- Read against the export, some parts are lost in a case the report does not name. Each is a
gap, a fourth fate whose entry opens with the case, in 39 parts of values and 48 node fields:
- - a top below zero, not written on a picture, a barcode, a table or a row, and partly on
- another block;
- - a table cell's or a panel's padding below zero, written as none;
+ - a top below zero, not written on a picture, a barcode or a block written as a table, and
+ partly on another block;
+ - a bottom below zero on a list, a table, a row, a stack, a canvas, a shape container in the
+ flow or a block with no paint of its own, which does not pull the next block up;
+ - a table cell's or a panel's padding below zero, and a row's at its sides, written as none;
- the space below a table that ends a cell set at its middle or foot;
- a panel's right border drawn short of its table cell's edge;
- a cell's top or bottom margin that Word gives the whole row;
diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxNodeFieldLedgerTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxNodeFieldLedgerTest.java
index 95ea790d8..ff597dfc1 100644
--- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxNodeFieldLedgerTest.java
+++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxNodeFieldLedgerTest.java
@@ -141,11 +141,14 @@ record Entry(Fate fate, String note) {
+ "written, as a panel";
// The gaps: losses the export knows of and its report does not name yet.
- static final String TOP_PULL = "a top below zero: on a picture, a barcode, a table or a row, not written; on "
- + "another block, what the space above it and the lines round it cannot give, "
- + "not written; neither named";
- static final String PADDING_BELOW_ZERO = "a table cell's or a panel's padding below zero, written as none, not "
- + "named";
+ static final String TOP_PULL = "a top below zero: on a picture, a barcode or a block written as a table, a "
+ + "table, a row or a panel, not written; on another block, what the space above "
+ + "it and the lines round it cannot give, not written; neither named";
+ static final String BOTTOM_PULL = "a bottom below zero on a list, a table, a row, a stack, a canvas, a shape "
+ + "container in the flow or a block with no paint of its own: the next block "
+ + "not pulled up by it, set that much lower, not named";
+ static final String PADDING_BELOW_ZERO = "a table cell's or a panel's padding below zero, and a row's at its "
+ + "sides, written as none, not named";
static final String SPACE_BELOW_AT_THE_FOOT = "below a block written as a table that ends a table cell or a "
+ "row's column set at its middle or foot, which Word does not "
+ "count there, the block set that much lower, not named but for "
@@ -178,7 +181,7 @@ record Entry(Fate fate, String note) {
+ "cell; with no layout, in the section's note; any other is written";
/** The known cases: a gap's note opens with one of them, or the ledger fails. */
- private static final List GAPS = List.of(TOP_PULL, PADDING_BELOW_ZERO, SPACE_BELOW_AT_THE_FOOT,
+ private static final List GAPS = List.of(TOP_PULL, BOTTOM_PULL, PADDING_BELOW_ZERO, SPACE_BELOW_AT_THE_FOOT,
RIGHT_BORDER, CELL_PADDING, LIST_PICTURE, SEAT_UNREAD, ROW_COLUMNS_UNLAID, DRAWING_OVER_TEXT,
COMPOSED_STACK_LAYER, LAYER_PAST_THE_EDGE, LAYER_UNLAID, PANEL_WIDER_THAN_ITS_CELL);
@@ -210,7 +213,7 @@ record Entry(Fate fate, String note) {
node(AlignNode.class, "name:INERT", "child:WRITTEN",
"align:REPORTED:composed in a table cell where the layout does not say where its child stands, "
+ "written from the cell's left; with no layout, in the section's note; any other is written",
- "margin:GAP:" + TOP_PULL + "; any other is written");
+ "margin:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; any other is written");
node(BarcodeNode.class, "name:INERT", "barcodeOptions:WRITTEN", "width:WRITTEN", "height:WRITTEN",
"linkTarget:REPORTED", "bookmarkOptions:REPORTED",
"padding:GAP:" + TOP_PULL + "; its left side; its right moves nothing in a paragraph set from the "
@@ -227,14 +230,15 @@ record Entry(Fate fate, String note) {
+ "as taking no room, the room is theirs; any other is written, as the room under what it "
+ "writes, or as its whole box where it only draws",
"placements:REPORTED:where what it writes stands; its drawings stand where it places them",
- "clipPolicy:INERT:the page clips no canvas", "padding:GAP:" + TOP_PULL + "; any other is written",
- "margin:GAP:" + TOP_PULL + "; any other is written");
+ "clipPolicy:INERT:the page clips no canvas",
+ "padding:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; any other is written");
node(ChartNode.class, "name:INERT", "spec:REPORTED", "style:REPORTED:in the chart's note",
"margin:REPORTED:in the chart's note; above and below; below a block a band or a column measures from, written",
"padding:REPORTED:in the chart's note; above and below; below a block a band or a column measures from, written");
node(ContainerNode.class, "name:INERT", "children:WRITTEN", "spacing:WRITTEN",
- "padding:GAP:" + PADDING_BELOW_ZERO + "; any other is written",
- "margin:GAP:" + TOP_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; " + PULL,
+ "padding:GAP:" + PADDING_BELOW_ZERO + "; " + TOP_PULL + "; " + BOTTOM_PULL + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; " + PULL,
"fillColor:REPORTED:" + PANEL_TRANSLUCENCY,
"stroke:GAP:" + RIGHT_BORDER + "; " + PANEL_BORDERS,
"cornerRadius:REPORTED", "borders:GAP:" + RIGHT_BORDER + "; " + PANEL_BORDERS,
@@ -262,7 +266,10 @@ record Entry(Fate fate, String note) {
+ "and among those in front of it; the one of a stack of one layer its offset moves down or up, "
+ "or across in a table cell, or at all with no place of its own in the layout, and one set left "
+ "of the stack in a table cell further than the indents round it give",
- "padding:GAP:" + TOP_PULL + "; any other is written", "margin:GAP:" + TOP_PULL + "; any other is written",
+ "padding:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; " + SPACE_BELOW_AT_THE_FOOT
+ + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; " + SPACE_BELOW_AT_THE_FOOT
+ + "; any other is written",
"clipToBounds:REPORTED:where it cuts what its layers paint; composed in a table cell, on its table");
node(LineNode.class, "name:INERT", "width:WRITTEN", "height:WRITTEN", "startX:WRITTEN", "startY:WRITTEN",
"endX:WRITTEN", "endY:WRITTEN",
@@ -290,8 +297,8 @@ record Entry(Fate fate, String note) {
"itemSpacing:WRITTEN",
"continuationIndent:REPORTED:where an item wraps, or its lines are not read, in a markerless "
+ "list or a tree of items without hangingIndent, the only lists the page sets it in",
- "normalizeMarkers:WRITTEN", "padding:GAP:" + TOP_PULL + "; any other is written",
- "margin:GAP:" + TOP_PULL + "; any other is written",
+ "normalizeMarkers:WRITTEN", "padding:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; any other is written",
"hangingIndent:REPORTED:where an item stands at a stated column, a space past its marker or "
+ "two spaces a level in",
"markerGap:REPORTED:where an item stands at a stated column, a space past its marker or "
@@ -341,8 +348,10 @@ record Entry(Fate fate, String note) {
node(PolygonNode.class, "name:INERT", "width:WRITTEN", "height:WRITTEN", "points:WRITTEN",
"fillColor:WRITTEN", "stroke:WRITTEN", "padding:WRITTEN", "margin:WRITTEN");
node(RowNode.class, "name:INERT", "children:WRITTEN", "weights:WRITTEN", "gap:WRITTEN",
- "padding:GAP:" + TOP_PULL + "; any other is written",
- "margin:GAP:" + TOP_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; any other is written",
+ "padding:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; "
+ + PADDING_BELOW_ZERO + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; " + SPACE_BELOW_AT_THE_FOOT
+ + "; any other is written",
"fillColor:REPORTED:" + ROW_PAINT, "stroke:GAP:" + RIGHT_BORDER + "; " + ROW_PAINT,
"cornerRadius:REPORTED:a panel's corners, which a Word table cell does not round; with no paint "
+ "it paints nothing",
@@ -357,8 +366,8 @@ record Entry(Fate fate, String note) {
node(com.demcha.compose.document.layout.HorizontalBandContentNode.class, "name:INERT", "key:INERT",
"slot:WRITTEN", "child:WRITTEN");
node(SectionNode.class, "name:INERT", "children:WRITTEN", "spacing:WRITTEN",
- "padding:GAP:" + PADDING_BELOW_ZERO + "; any other is written",
- "margin:GAP:" + TOP_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; " + PULL,
+ "padding:GAP:" + PADDING_BELOW_ZERO + "; " + TOP_PULL + "; " + BOTTOM_PULL + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; " + PULL,
"fillColor:REPORTED:" + PANEL_TRANSLUCENCY,
"stroke:GAP:" + RIGHT_BORDER + "; " + PANEL_BORDERS,
"cornerRadius:REPORTED", "borders:GAP:" + RIGHT_BORDER + "; " + PANEL_BORDERS,
@@ -389,10 +398,12 @@ record Entry(Fate fate, String note) {
+ "panel's fill or "
+ "the colour under it, and as a panel's, " + BORDER_WIDTH + "; drawn, its alpha and its width "
+ "are written; any other is written",
- "padding:GAP:" + TOP_PULL + "; written as a panel's in a table cell, round its outline as a margin "
+ "padding:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; written as a panel's in a table cell, round its "
+ + "outline as a margin "
+ "is, and below it, where its table ends its cell and the cell sets what it holds at its middle or "
+ "foot, as far as Word does not count it there; any other is written",
- "margin:GAP:" + TOP_PULL + "; written as a panel's in a table cell, below it, where its table ends "
+ "margin:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; written as a panel's in a table cell, below it, "
+ + "where its table ends "
+ "its cell "
+ "and the cell sets what it holds at its middle or foot, as far as Word does not count it "
+ "there; any other is written",
@@ -418,8 +429,10 @@ record Entry(Fate fate, String note) {
"columnStyles:GAP:" + CELL_PADDING + "; " + PADDING_BELOW_ZERO + "; " + CELL_TRANSLUCENCY,
"width:WRITTEN", "linkTarget:REPORTED",
"bookmarkOptions:REPORTED",
- "padding:GAP:" + TOP_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; any other is written",
- "margin:GAP:" + TOP_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; any other is written",
+ "padding:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; " + SPACE_BELOW_AT_THE_FOOT
+ + "; any other is written",
+ "margin:GAP:" + TOP_PULL + "; " + BOTTOM_PULL + "; " + SPACE_BELOW_AT_THE_FOOT
+ + "; any other is written",
"repeatedHeaderRowCount:WRITTEN",
"anchor:WRITTEN");
}
diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxValueFieldLedgerTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxValueFieldLedgerTest.java
index 5641ba00c..714b153f7 100644
--- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxValueFieldLedgerTest.java
+++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxValueFieldLedgerTest.java
@@ -24,6 +24,7 @@
import java.util.TreeSet;
import java.util.stream.Collectors;
+import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.BOTTOM_PULL;
import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.CELL_PADDING;
import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.COMPOSED_STACK_LAYER;
import static com.demcha.compose.document.backend.semantic.docx.DocxNodeFieldLedgerTest.DRAWING_OVER_TEXT;
@@ -257,7 +258,7 @@ class DocxValueFieldLedgerTest {
+ "(opening its line, after a picture, by an emoji, right to left), below zero, and above and below "
+ "its letters, named");
value(com.demcha.compose.document.style.DocumentInsets.class,
- "bottom:GAP:" + SPACE_BELOW_AT_THE_FOOT + "; " + PADDING_BELOW_ZERO
+ "bottom:GAP:" + BOTTOM_PULL + "; " + SPACE_BELOW_AT_THE_FOOT + "; " + PADDING_BELOW_ZERO
+ "; below zero, the pull nothing under it gives back, on the next block (space above); "
+ "a chart's or a spacer's no band or column measures from; a page field's beside other parts; a "
+ "picture's padding drawn beside its text or over its badge; a chip's; a shape container's at its "
@@ -776,7 +777,8 @@ class DocxValueFieldLedgerTest {
"centerText:REPORTED:where it and another slot overlap where the page sets them, or it is wider "
+ "than the line, which Word sets apart or wraps; any other is written, its page tokens as "
+ "Word's page fields",
- "fontName:WRITTEN",
+ "fontName:REPORTED:a face that may not travel or cannot be read, named as an embedded font; any "
+ + "other is written",
"fontSize:WRITTEN",
"height:REPORTED:a band too shallow for its line, whose text and separator Word stands further in "
+ "from the edge than the page; any other is written, as where its line and separator stand "
@@ -887,6 +889,7 @@ void everyNodeAValueHoldsIsOneTheNodeLedgerLists() throws Exception {
Set listed = names(DocxNodeFieldLedgerTest.nodeClasses());
listed.add(DocumentNode.class.getName());
assertThat(names(reach().nodes())).as("node types a field holds that the node ledger does not list")
+ .contains(DocumentNode.class.getName())
.isSubsetOf(listed);
}