diff --git a/CHANGELOG.md b/CHANGELOG.md index ff94681b0..b47a197ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3648,6 +3648,36 @@ 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: 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 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; + - 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..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 @@ -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,51 @@ 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 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 " + + "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, 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); + private static final Map, Map> NODES = new LinkedHashMap<>(); private static final Map OUTPUT_OPTIONS = fields( "metadata:WRITTEN", @@ -156,11 +210,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 + "; " + BOTTOM_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 +230,18 @@ 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 + "; " + 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: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 + "; " + 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, "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 +251,45 @@ 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 + "; " + 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", "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 +297,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 + "; " + 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 " @@ -240,8 +316,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 +328,36 @@ 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 + "; " + 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", - "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 +365,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 + "; " + 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, "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 +378,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 +394,26 @@ 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 + "; " + 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:REPORTED:written as a panel's in a table cell, below it, where its table ends its cell " + "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", "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 +423,18 @@ 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 + "; " + 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"); } @Test @@ -388,11 +489,14 @@ private static void node(Class 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..714b153f7 --- /dev/null +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxValueFieldLedgerTest.java @@ -0,0 +1,999 @@ +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.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; +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:" + 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 " + + "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: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 " + + "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") + .contains(DocumentNode.class.getName()) + .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); + } +}