@stll/folio-core 0.33.1 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/ai-edits/apply.d.ts +15 -0
  2. package/dist/ai-edits/apply.js +271 -48
  3. package/dist/ai-edits/headless.d.ts +47 -3
  4. package/dist/ai-edits/headless.js +53 -3
  5. package/dist/ai-edits/index.d.ts +1 -1
  6. package/dist/ai-edits/snapshot.d.ts +19 -1
  7. package/dist/ai-edits/snapshot.js +49 -5
  8. package/dist/ai-edits/table-geometry.d.ts +56 -0
  9. package/dist/ai-edits/table-geometry.js +217 -0
  10. package/dist/ai-edits/table-row-column-mutations.d.ts +34 -16
  11. package/dist/ai-edits/table-row-column-mutations.js +12 -4
  12. package/dist/ai-edits/table-template.d.ts +60 -0
  13. package/dist/ai-edits/table-template.js +176 -0
  14. package/dist/ai-edits/types.d.ts +13 -6
  15. package/dist/compare/__fixtures__/body-sequence.d.ts +61 -2
  16. package/dist/compare/__fixtures__/body-sequence.js +94 -6
  17. package/dist/compare/compare.d.ts +14 -7
  18. package/dist/compare/compare.js +136 -38
  19. package/dist/compare/formatting.d.ts +1 -1
  20. package/dist/compare/formatting.js +38 -9
  21. package/dist/compare/plan.d.ts +31 -1
  22. package/dist/compare/plan.js +233 -66
  23. package/dist/compare/types.d.ts +9 -7
  24. package/dist/compare/types.js +7 -5
  25. package/dist/compare/verification.d.ts +31 -12
  26. package/dist/compare/verification.js +106 -18
  27. package/dist/compat/eigenpal.d.ts +4 -4
  28. package/dist/compat/eigenpal.js +2 -2
  29. package/dist/controller/headerFooterEditorManager.js +11 -9
  30. package/dist/controller/layoutPipeline.js +24 -3
  31. package/dist/display-list/build/watermarkPrimitives.js +15 -3
  32. package/dist/display-list/primitives.d.ts +1 -1
  33. package/dist/document-operations.d.ts +10 -1
  34. package/dist/document-operations.js +33 -5
  35. package/dist/docx/appVersionNormalization.d.ts +49 -0
  36. package/dist/docx/appVersionNormalization.js +74 -0
  37. package/dist/docx/blockContentParser.js +3 -2
  38. package/dist/docx/drawingUtils.js +4 -3
  39. package/dist/docx/headerFooterParser.js +8 -14
  40. package/dist/docx/paragraphParser.js +48 -2
  41. package/dist/docx/rezip.d.ts +10 -4
  42. package/dist/docx/rezip.js +54 -18
  43. package/dist/docx/runParser.js +9 -8
  44. package/dist/docx/sdtProperties.js +4 -3
  45. package/dist/docx/selectiveSave.js +7 -6
  46. package/dist/docx/serializer/commentSerializer.d.ts +1 -1
  47. package/dist/docx/serializer/commentSerializer.js +43 -19
  48. package/dist/docx/serializer/documentSerializer.d.ts +3 -1
  49. package/dist/docx/serializer/documentSerializer.js +42 -78
  50. package/dist/docx/serializer/fontTableSerializer.js +8 -5
  51. package/dist/docx/serializer/headerFooterSerializer.d.ts +4 -1
  52. package/dist/docx/serializer/headerFooterSerializer.js +55 -31
  53. package/dist/docx/serializer/noteSerializer.js +34 -30
  54. package/dist/docx/serializer/numberingSerializer.js +8 -4
  55. package/dist/docx/serializer/paragraphSerializer.d.ts +1 -1
  56. package/dist/docx/serializer/paragraphSerializer.js +22 -12
  57. package/dist/docx/serializer/partNamespaces.d.ts +78 -0
  58. package/dist/docx/serializer/partNamespaces.js +324 -0
  59. package/dist/docx/serializer/runSerializer.js +1 -1
  60. package/dist/docx/serializer/settingsSerializer.js +8 -2
  61. package/dist/docx/serializer/stylesSerializer.js +8 -5
  62. package/dist/docx/serializer/tableSerializer.js +83 -18
  63. package/dist/docx/serializer/themeSerializer.js +10 -2
  64. package/dist/docx/server/build.d.ts +1 -1
  65. package/dist/docx/settingsParser.js +3 -0
  66. package/dist/docx/strictValueEncodings.gen.d.ts +15 -0
  67. package/dist/docx/strictValueEncodings.gen.js +275 -0
  68. package/dist/docx/tableParser.d.ts +0 -6
  69. package/dist/docx/tableParser.js +52 -7
  70. package/dist/docx/transitionalSpelling.d.ts +23 -0
  71. package/dist/docx/transitionalSpelling.js +36 -0
  72. package/dist/docx/universalMeasure.d.ts +20 -0
  73. package/dist/docx/universalMeasure.js +32 -0
  74. package/dist/docx/verbatimCapture.d.ts +20 -0
  75. package/dist/docx/verbatimCapture.js +131 -0
  76. package/dist/docx/vmlImageParser.js +4 -3
  77. package/dist/docx/watermarkParser.d.ts +2 -4
  78. package/dist/docx/watermarkParser.js +8 -9
  79. package/dist/docx/xmlParser.d.ts +23 -2
  80. package/dist/docx/xmlParser.js +43 -11
  81. package/dist/headless-layout.js +14 -2
  82. package/dist/index.d.ts +4 -4
  83. package/dist/index.js +2 -2
  84. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -2
  85. package/dist/layout-bridge/convert/footnoteLayout.js +23 -10
  86. package/dist/layout-bridge/convert/headerFooterLayout.js +13 -2
  87. package/dist/layout-bridge/convert/toFlowBlocks.js +96 -13
  88. package/dist/layout-engine/index.js +11 -4
  89. package/dist/layout-engine/justifiedLineFit.d.ts +4 -4
  90. package/dist/layout-engine/justifiedLineFit.js +4 -4
  91. package/dist/layout-engine/measure/lineBreakProvider.js +1 -0
  92. package/dist/layout-engine/measure/measureBlocks.js +1 -1
  93. package/dist/layout-engine/measure/measureParagraph.js +29 -29
  94. package/dist/layout-painter/renderPage.js +6 -1
  95. package/dist/layout-painter/renderParagraph.js +18 -8
  96. package/dist/layout-painter/renderWatermark.js +11 -4
  97. package/dist/model.d.ts +3 -3
  98. package/dist/model.js +2 -2
  99. package/dist/prosemirror/attrs/index.js +11 -0
  100. package/dist/prosemirror/commands/comments.js +24 -3
  101. package/dist/prosemirror/containerFinalParagraph.d.ts +47 -0
  102. package/dist/prosemirror/containerFinalParagraph.js +92 -0
  103. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
  104. package/dist/prosemirror/conversion/fromProseDoc.js +47 -9
  105. package/dist/prosemirror/conversion/toProseDoc.js +61 -14
  106. package/dist/prosemirror/extensions/core/DocExtension.js +7 -1
  107. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  108. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +1 -0
  109. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -0
  110. package/dist/prosemirror/schema/marks.d.ts +3 -9
  111. package/dist/prosemirror/schema/nodes.d.ts +22 -0
  112. package/dist/server.d.ts +1 -1
  113. package/dist/types/block-id.d.ts +12 -1
  114. package/dist/types/block-id.js +17 -1
  115. package/dist/utils/canonicalJson.d.ts +14 -0
  116. package/dist/utils/canonicalJson.js +19 -0
  117. package/dist/utils/fontResolver.js +51 -0
  118. package/dist/utils/formatToStyle.d.ts +1 -1
  119. package/dist/utils/formatToStyle.js +41 -1
  120. package/dist/watermark/index.js +7 -0
  121. package/package.json +2 -2
@@ -0,0 +1,176 @@
1
+ import { stripBlockIdentityAttrs } from "./block-identity.js";
2
+ import { Fragment } from "prosemirror-model";
3
+ //#region src/ai-edits/table-template.ts
4
+ /**
5
+ * Building a table from another document's table rather than from a grid of
6
+ * strings.
7
+ *
8
+ * `insertTable` and `insertTableRow` describe their content as cell texts,
9
+ * which is the right shape for a caller that writes a table from nothing. A
10
+ * comparison is not that caller: the table it adds already exists in the
11
+ * document it is comparing against, complete with its `w:tblPr`, its
12
+ * `w:tblGrid` widths, per-row `w:trPr`, per-cell `w:tcPr` — spans, vertical
13
+ * merges, shading, borders, margins, alignment — and cell paragraphs carrying
14
+ * their own properties. Rebuilding that from text loses every one of them, and
15
+ * a consumer accepting the redline then gets a table that is not the one it
16
+ * was compared to.
17
+ *
18
+ * So the operation may be handed the node it should place. The copy is
19
+ * structural and keeps everything by default; what it cannot bring is what
20
+ * only the package it came from can resolve — a relationship id (a drawing, a
21
+ * hyperlink), a note or comment id, a bookmark, a paragraph identity, or a
22
+ * tracked change belonging to the other document's history. Everything that
23
+ * describes the table itself travels.
24
+ */
25
+ /**
26
+ * Nodes that name something the other package owns: a drawing's relationship,
27
+ * an anchored frame's, a bookmark's id. They resolve to nothing once the table
28
+ * has crossed, so they are dropped rather than copied into a dangling
29
+ * reference.
30
+ */
31
+ const PACKAGE_BOUND_NODE_NAMES = /* @__PURE__ */ new Set([
32
+ "image",
33
+ "textBox",
34
+ "textBoxAnchor",
35
+ "shape",
36
+ "bookmarkBoundary"
37
+ ]);
38
+ /**
39
+ * Marks that name something the other package owns. Run properties are the
40
+ * point of copying the runs at all, so the denial is narrow: a relationship, a
41
+ * note, a comment, and the other document's revisions.
42
+ */
43
+ const PACKAGE_BOUND_MARK_NAMES = /* @__PURE__ */ new Set([
44
+ "hyperlink",
45
+ "footnoteRef",
46
+ "comment",
47
+ "insertion",
48
+ "deletion",
49
+ "runPropertyChange"
50
+ ]);
51
+ /**
52
+ * Paragraph attrs cleared on a copied paragraph: its identity, the bookmarks
53
+ * and empty hyperlinks that name package-scoped ids, the section it ended, and
54
+ * the tracked-change history of the document it came from. Its formatting —
55
+ * style, alignment, spacing, indentation, borders, shading, tabs — is what the
56
+ * copy exists to carry, and stays.
57
+ */
58
+ const CLEARED_PARAGRAPH_ATTRS = [
59
+ "bookmarks",
60
+ "_emptyHyperlinks",
61
+ "_propertyChanges",
62
+ "pPrMark",
63
+ "_suggestedInsert",
64
+ "_sectionProperties",
65
+ "sectionBreakType",
66
+ "renderedPageBreakBefore"
67
+ ];
68
+ /** Table, row and cell attrs that record the other document's revisions. */
69
+ const CLEARED_TABLE_ATTRS = ["tblPrChange", "_suggestedInsert"];
70
+ const CLEARED_ROW_ATTRS = [
71
+ "trIns",
72
+ "trDel",
73
+ "trPrChange"
74
+ ];
75
+ const CLEARED_CELL_ATTRS = ["cellMarker", "tcPrChange"];
76
+ const withoutAttrs = (attrs, cleared) => {
77
+ const next = { ...attrs };
78
+ for (const key of cleared) next[key] = null;
79
+ return next;
80
+ };
81
+ const insertionMarkOf = (schema, revision) => {
82
+ const markType = schema.marks["insertion"];
83
+ if (!revision || !markType) return null;
84
+ const { revisionId, author, date, initials, provenance, suggestionId } = revision;
85
+ return markType.create({
86
+ revisionId,
87
+ author,
88
+ date,
89
+ ...initials !== void 0 && { initials },
90
+ ...provenance !== void 0 && { provenance },
91
+ ...suggestionId !== void 0 && { suggestionId }
92
+ });
93
+ };
94
+ const copiedAttrs = (node, context) => {
95
+ switch (node.type.spec["tableRole"]) {
96
+ case "table": return withoutAttrs(node.attrs, CLEARED_TABLE_ATTRS);
97
+ case "row": {
98
+ const attrs = withoutAttrs(node.attrs, CLEARED_ROW_ATTRS);
99
+ return context.revision ? {
100
+ ...attrs,
101
+ trIns: context.revision
102
+ } : attrs;
103
+ }
104
+ case "cell":
105
+ case "header_cell": {
106
+ const attrs = withoutAttrs(node.attrs, CLEARED_CELL_ATTRS);
107
+ return context.clampRowSpan ? {
108
+ ...attrs,
109
+ rowspan: 1
110
+ } : attrs;
111
+ }
112
+ default: return node.isTextblock ? withoutAttrs(stripBlockIdentityAttrs(node.attrs), CLEARED_PARAGRAPH_ATTRS) : node.attrs;
113
+ }
114
+ };
115
+ /**
116
+ * One node of the template, or `null` when it names something the package it
117
+ * came from owns. Inline content carries the insertion mark; a nested table is
118
+ * copied by the same rules as the one holding it.
119
+ */
120
+ const copyNode = (node, context) => {
121
+ if (PACKAGE_BOUND_NODE_NAMES.has(node.type.name)) return null;
122
+ const marks = node.marks.filter(({ type }) => !PACKAGE_BOUND_MARK_NAMES.has(type.name));
123
+ if (node.isLeaf) return node.mark(node.isInline && context.insertion ? [...marks, context.insertion] : marks);
124
+ const inner = node.type.spec["tableRole"] === "table" ? {
125
+ ...context,
126
+ clampRowSpan: false
127
+ } : context;
128
+ const content = [];
129
+ node.forEach((child) => {
130
+ const copied = copyNode(child, inner);
131
+ if (copied) content.push(copied);
132
+ });
133
+ return node.type.create(copiedAttrs(node, context), Fragment.fromArray(content), marks);
134
+ };
135
+ /**
136
+ * The template table, ready to insert: its properties, grid, rows and cells
137
+ * carried verbatim, nested tables included, and every row plus every run
138
+ * stamped as an insertion when the caller is tracking changes.
139
+ *
140
+ * `null` when the template is not a table or holds no rows, so the caller can
141
+ * fall back to the operation's own cell texts rather than place something that
142
+ * is not one.
143
+ */
144
+ const tableFromTemplate = ({ schema, template, revision }) => {
145
+ if (template.type.spec["tableRole"] !== "table" || template.childCount === 0) return null;
146
+ return copyNode(template, {
147
+ revision: revision ?? null,
148
+ insertion: insertionMarkOf(schema, revision ?? null),
149
+ clampRowSpan: false
150
+ });
151
+ };
152
+ /**
153
+ * The template row, ready to insert into an existing table.
154
+ *
155
+ * Refused — `null`, so the caller falls back to the operation's cell texts —
156
+ * when the row's cells do not span exactly the receiving table's grid: a row
157
+ * of a different width would leave the table's map inconsistent with its own
158
+ * grid. A cell that merged vertically in the document it came from is placed
159
+ * unmerged, because the rows it reached into are not this table's.
160
+ */
161
+ const tableRowFromTemplate = ({ template, columnCount }) => {
162
+ if (template.type.spec["tableRole"] !== "row" || template.childCount === 0) return null;
163
+ let spanned = 0;
164
+ template.forEach((cell) => {
165
+ const colspan = cell.attrs["colspan"];
166
+ spanned += typeof colspan === "number" ? colspan : 1;
167
+ });
168
+ if (spanned !== columnCount) return null;
169
+ return copyNode(template, {
170
+ revision: null,
171
+ insertion: null,
172
+ clampRowSpan: true
173
+ });
174
+ };
175
+ //#endregion
176
+ export { tableFromTemplate, tableRowFromTemplate };
@@ -1,5 +1,10 @@
1
1
  //#region src/ai-edits/types.d.ts
2
2
  type FolioAIBlockKind = "heading" | "listItem" | "paragraph";
3
+ type FolioAIInlineFormatting = Partial<Record<"bold" | "italic" | "underline" | "strike", boolean>> & {
4
+ fontFamily?: string | null;
5
+ fontSizePt?: number | null;
6
+ color?: string | null;
7
+ };
3
8
  type FolioAIBlockPreviewRun = {
4
9
  text: string;
5
10
  bold?: boolean;
@@ -9,6 +14,7 @@ type FolioAIBlockPreviewRun = {
9
14
  fontFamily?: string;
10
15
  fontSizePt?: number;
11
16
  color?: string;
17
+ directFormatting?: FolioAIInlineFormatting;
12
18
  };
13
19
  /**
14
20
  * Where a block sits inside its innermost enclosing table. Every index is
@@ -165,12 +171,6 @@ type FolioDocumentNavigationTarget = {
165
171
  story: "main";
166
172
  blockId: string;
167
173
  } | FolioAITextRangeHandle;
168
- type FolioAIInlineFormatting = {
169
- bold?: boolean;
170
- italic?: boolean;
171
- underline?: boolean;
172
- strike?: boolean;
173
- };
174
174
  /**
175
175
  * A party in an `insertSignatureTable` op. Mirrors the
176
176
  * `signatureTable` helper in `docx-core/legal-source/compile.ts`:
@@ -276,6 +276,13 @@ type FolioAIEditOperation = FolioAIEditReviewMeta & {
276
276
  * Delete the whole block. A block with words loses them and its paragraph
277
277
  * mark; a BLANK block has only a paragraph mark to lose, and loses it, so
278
278
  * the empty line goes away rather than the operation doing nothing.
279
+ *
280
+ * The block that ENDS its container is the exception, in both modes: a
281
+ * body, a cell, a header or footer, a note and a text box each end with a
282
+ * paragraph, and a deleted mark there would say "join with the paragraph
283
+ * after this one" where there is none. It loses its words and keeps its
284
+ * place. To remove it, delete the mark of the block BEFORE it, which
285
+ * merges forward into it.
279
286
  */
280
287
  {
281
288
  id: string;
@@ -13,6 +13,61 @@
13
13
  * gets written.
14
14
  */
15
15
  type CellContent = string | readonly BodyItem[];
16
+ /** `w:tcPr` children a fixture can author, each named after the element. */
17
+ type CellProperties = {
18
+ /** `w:gridSpan`: grid columns this cell occupies. */
19
+ gridSpan?: number;
20
+ /** `w:vMerge`: the cell starts a vertical merge, or continues one. */
21
+ verticalMerge?: "restart" | "continue";
22
+ /** `w:tcW` in twips. */
23
+ width?: number;
24
+ /** `w:shd` fill colour, as six hex digits. */
25
+ shadingFill?: string;
26
+ /** `w:vAlign`. */
27
+ verticalAlign?: "top" | "center" | "bottom";
28
+ /** `w:tcBorders`, single style on all four sides, in eighths of a point. */
29
+ borderSize?: number;
30
+ /** `w:tcMar`, the same margin on all four sides, in twips. */
31
+ margin?: number;
32
+ };
33
+ /** A cell: its content alone, or its content and its own `w:tcPr`. */
34
+ type Cell = CellContent | ({
35
+ content: CellContent;
36
+ } & CellProperties);
37
+ /** A row: its cells alone, or its cells and its own `w:trPr`. */
38
+ type TableRow = readonly Cell[] | {
39
+ cells: readonly Cell[];
40
+ /** `w:trHeight` in twips. */
41
+ height?: number;
42
+ /** `w:tblHeader`: the row repeats at the top of every page. */
43
+ header?: boolean;
44
+ /** `w:jc` on the row. */
45
+ justification?: "left" | "center" | "right";
46
+ };
47
+ /** `w:tblPr` children a fixture can author, each named after the element. */
48
+ type TableProperties = {
49
+ /** `w:tblStyle`. */
50
+ styleId?: string;
51
+ /** `w:tblW`. */
52
+ width?: {
53
+ value: number;
54
+ type: "auto" | "dxa" | "pct";
55
+ };
56
+ /** `w:jc` on the table. */
57
+ justification?: "left" | "center" | "right";
58
+ /** `w:tblInd` in twips. */
59
+ indent?: number;
60
+ /** `w:tblBorders`, single style on every side, in eighths of a point. */
61
+ borderSize?: number;
62
+ /** `w:shd` fill colour on the table, as six hex digits. */
63
+ shadingFill?: string;
64
+ /** `w:tblLayout`. */
65
+ layout?: "fixed" | "autofit";
66
+ /** `w:tblCellMar`, the same margin on all four sides, in twips. */
67
+ cellMargin?: number;
68
+ /** `w:tblLook` value, as four hex digits. */
69
+ look?: string;
70
+ };
16
71
  /**
17
72
  * One inline of a paragraph: plain text, or text carrying an external
18
73
  * hyperlink. A link is the case where a revision wrapper and the linked runs
@@ -35,7 +90,11 @@ type BodyItem = {
35
90
  paraId?: string;
36
91
  } | {
37
92
  kind: "table";
38
- rows: readonly (readonly CellContent[])[];
93
+ rows: readonly TableRow[];
94
+ /** `w:tblPr` children, written in the order the schema requires. */
95
+ properties?: TableProperties;
96
+ /** `w:tblGrid` column widths, in twips. One per grid column. */
97
+ columnWidths?: readonly number[];
39
98
  /**
40
99
  * Rows a package hides with `w:hidden`. The snapshot skips their whole
41
100
  * subtree, so a document that has one is the case where the snapshot
@@ -53,4 +112,4 @@ type BodySequenceOptions = {
53
112
  };
54
113
  declare const buildBodySequenceDocx: (items: readonly BodyItem[], { header }?: BodySequenceOptions) => Promise<ArrayBuffer>;
55
114
  //#endregion
56
- export { BodyItem, BodySequenceOptions, CellContent, ParagraphInline, buildBodySequenceDocx };
115
+ export { BodyItem, BodySequenceOptions, Cell, CellContent, CellProperties, ParagraphInline, TableProperties, TableRow, buildBodySequenceDocx };
@@ -15,6 +15,8 @@ const WORDPROCESSING = "application/vnd.openxmlformats-officedocument.wordproces
15
15
  const MARKUP_COMPATIBILITY = "http://schemas.openxmlformats.org/markup-compatibility/2006";
16
16
  const PACKAGE_RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006";
17
17
  const CORE_PROPERTIES_TYPE = "application/vnd.openxmlformats-package.core-properties+xml";
18
+ const EXTENDED_PROPERTIES = "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties";
19
+ const EXTENDED_PROPERTIES_TYPE = "application/vnd.openxmlformats-officedocument.extended-properties+xml";
18
20
  const WORDML_2010 = "http://schemas.microsoft.com/office/word/2010/wordml";
19
21
  /**
20
22
  * `createFolders: false` because JSZip stamps the folder entries it
@@ -24,6 +26,12 @@ const ZIP_ENTRY_OPTIONS = {
24
26
  date: new Date(Date.UTC(2e3, 0, 1)),
25
27
  createFolders: false
26
28
  };
29
+ const isRowOptions = (row) => "cells" in row;
30
+ const isCellSpec = (cell) => typeof cell === "object" && "content" in cell;
31
+ const rowCells = (row) => isRowOptions(row) ? row.cells : row;
32
+ const rowOptions = (row) => isRowOptions(row) ? row : {};
33
+ const cellContent = (cell) => isCellSpec(cell) ? cell.content : cell;
34
+ const cellProperties = (cell) => isCellSpec(cell) ? cell : {};
27
35
  /**
28
36
  * A paragraph's id is derived from its own content, not from its position.
29
37
  *
@@ -69,7 +77,10 @@ const collectHrefs = (items, hrefs) => {
69
77
  for (const inline of item.text) if (typeof inline !== "string" && !hrefs.includes(inline.href)) hrefs.push(inline.href);
70
78
  continue;
71
79
  }
72
- for (const row of item.rows) for (const cell of row) if (typeof cell !== "string") collectHrefs(cell, hrefs);
80
+ for (const row of item.rows) for (const cell of rowCells(row)) {
81
+ const content = cellContent(cell);
82
+ if (typeof content !== "string") collectHrefs(content, hrefs);
83
+ }
73
84
  }
74
85
  };
75
86
  const EMPTY_PARAGRAPH = {
@@ -89,14 +100,90 @@ const closedSequence = (items) => {
89
100
  /** A cell must also contain a paragraph, which the empty sequence supplies. */
90
101
  const cellXml = (content, context) => typeof content === "string" ? paragraph(content, context) : itemsXml(closedSequence(content), context);
91
102
  /** `w:tbl` is `w:tblPr, w:tblGrid, rows`: a fixture without the grid is not one. */
92
- const tableGrid = (rows) => {
103
+ const tableGrid = (item) => {
104
+ if (item.columnWidths) return `<w:tblGrid>${item.columnWidths.map((width) => `<w:gridCol w:w="${String(width)}"/>`).join("")}</w:tblGrid>`;
93
105
  let columns = 0;
94
- for (const cells of rows) columns = Math.max(columns, cells.length);
106
+ for (const row of item.rows) {
107
+ let spanned = 0;
108
+ for (const cell of rowCells(row)) spanned += cellProperties(cell).gridSpan ?? 1;
109
+ columns = Math.max(columns, spanned);
110
+ }
95
111
  return `<w:tblGrid>${`<w:gridCol w:w="2000"/>`.repeat(columns)}</w:tblGrid>`;
96
112
  };
113
+ const BORDER_SIDES = [
114
+ "top",
115
+ "left",
116
+ "bottom",
117
+ "right",
118
+ "insideH",
119
+ "insideV"
120
+ ];
121
+ const CELL_BORDER_SIDES = [
122
+ "top",
123
+ "left",
124
+ "bottom",
125
+ "right"
126
+ ];
127
+ const MARGIN_SIDES = [
128
+ "top",
129
+ "left",
130
+ "bottom",
131
+ "right"
132
+ ];
133
+ const borders = (element, sides, size) => `<w:${element}>${sides.map((side) => `<w:${side} w:val="single" w:sz="${String(size)}" w:space="0" w:color="000000"/>`).join("")}</w:${element}>`;
134
+ const margins = (element, value) => `<w:${element}>${MARGIN_SIDES.map((side) => `<w:${side} w:w="${String(value)}" w:type="dxa"/>`).join("")}</w:${element}>`;
135
+ /**
136
+ * `w:tblPr` children in the order CT_TblPrBase declares: tblStyle, tblW, jc,
137
+ * tblInd, tblBorders, shd, tblLayout, tblCellMar, tblLook. `w:tblW` is always
138
+ * written.
139
+ */
140
+ const tableProperties = ({ properties }) => {
141
+ const width = properties?.width ?? {
142
+ value: 0,
143
+ type: "auto"
144
+ };
145
+ return `<w:tblPr>${[
146
+ ...properties?.styleId === void 0 ? [] : [`<w:tblStyle w:val="${properties.styleId}"/>`],
147
+ `<w:tblW w:w="${String(width.value)}" w:type="${width.type}"/>`,
148
+ ...properties?.justification === void 0 ? [] : [`<w:jc w:val="${properties.justification}"/>`],
149
+ ...properties?.indent === void 0 ? [] : [`<w:tblInd w:w="${String(properties.indent)}" w:type="dxa"/>`],
150
+ ...properties?.borderSize === void 0 ? [] : [borders("tblBorders", BORDER_SIDES, properties.borderSize)],
151
+ ...properties?.shadingFill === void 0 ? [] : [`<w:shd w:val="clear" w:color="auto" w:fill="${properties.shadingFill}"/>`],
152
+ ...properties?.layout === void 0 ? [] : [`<w:tblLayout w:type="${properties.layout}"/>`],
153
+ ...properties?.cellMargin === void 0 ? [] : [margins("tblCellMar", properties.cellMargin)],
154
+ ...properties?.look === void 0 ? [] : [`<w:tblLook w:val="${properties.look}"/>`]
155
+ ].join("")}</w:tblPr>`;
156
+ };
157
+ /**
158
+ * `w:trPr` children. `CT_TrPrBase` is a repeated choice rather than a
159
+ * sequence, so the order here is the readable one rather than a required one.
160
+ */
161
+ const rowProperties = (row, hidden) => {
162
+ const { header, height, justification } = rowOptions(row);
163
+ const parts = [
164
+ ...height === void 0 ? [] : [`<w:trHeight w:val="${String(height)}"/>`],
165
+ ...header === true ? ["<w:tblHeader/>"] : [],
166
+ ...justification === void 0 ? [] : [`<w:jc w:val="${justification}"/>`],
167
+ ...hidden ? ["<w:hidden/>"] : []
168
+ ];
169
+ return parts.length === 0 ? "" : `<w:trPr>${parts.join("")}</w:trPr>`;
170
+ };
171
+ /** `w:tcPr` children in schema order; `w:tcW` is always written. */
172
+ const cellPropertiesXml = (cell) => {
173
+ const properties = cellProperties(cell);
174
+ return `<w:tcPr>${[
175
+ `<w:tcW w:w="${String(properties.width ?? 2e3)}" w:type="dxa"/>`,
176
+ ...properties.gridSpan === void 0 ? [] : [`<w:gridSpan w:val="${String(properties.gridSpan)}"/>`],
177
+ ...properties.verticalMerge === void 0 ? [] : [properties.verticalMerge === "restart" ? `<w:vMerge w:val="restart"/>` : `<w:vMerge/>`],
178
+ ...properties.borderSize === void 0 ? [] : [borders("tcBorders", CELL_BORDER_SIDES, properties.borderSize)],
179
+ ...properties.shadingFill === void 0 ? [] : [`<w:shd w:val="clear" w:color="auto" w:fill="${properties.shadingFill}"/>`],
180
+ ...properties.margin === void 0 ? [] : [margins("tcMar", properties.margin)],
181
+ ...properties.verticalAlign === void 0 ? [] : [`<w:vAlign w:val="${properties.verticalAlign}"/>`]
182
+ ].join("")}</w:tcPr>`;
183
+ };
97
184
  const table = (item, context) => {
98
185
  const hidden = new Set(item.hiddenRows ?? []);
99
- return `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr>` + tableGrid(item.rows) + item.rows.map((cells, rowIndex) => `<w:tr>${hidden.has(rowIndex) ? `<w:trPr><w:hidden/></w:trPr>` : ""}${cells.map((content) => `<w:tc><w:tcPr><w:tcW w:w="2000" w:type="dxa"/></w:tcPr>${cellXml(content, context)}</w:tc>`).join("")}</w:tr>`).join("") + `</w:tbl>`;
186
+ return `<w:tbl>${tableProperties(item)}` + tableGrid(item) + item.rows.map((row, rowIndex) => `<w:tr>${rowProperties(row, hidden.has(rowIndex))}${rowCells(row).map((cell) => `<w:tc>${cellPropertiesXml(cell)}${cellXml(cellContent(cell), context)}</w:tc>`).join("")}</w:tr>`).join("") + `</w:tbl>`;
100
187
  };
101
188
  const itemsXml = (items, context) => items.map((item) => item.kind === "paragraph" ? paragraph(item.text, context, {
102
189
  ...item.styleId === void 0 ? {} : { styleId: item.styleId },
@@ -114,9 +201,10 @@ const buildBodySequenceDocx = async (items, { header } = {}) => {
114
201
  const linkRelationships = hrefs.map((href, index) => `<Relationship Id="rId${index + firstLinkRelationship}" Type="${OFFICE_RELATIONSHIPS}/hyperlink" Target="${href}" TargetMode="External"/>`).join("");
115
202
  const paraId = createParaIdAllocator();
116
203
  const parts = {
117
- "[Content_Types].xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="${WORDPROCESSING}.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="${WORDPROCESSING}.styles+xml"/>` + (header ? `<Override PartName="/word/header1.xml" ContentType="${WORDPROCESSING}.header+xml"/>` : "") + `<Override PartName="/docProps/core.xml" ContentType="${CORE_PROPERTIES_TYPE}"/></Types>`,
118
- "_rels/.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="${RELATIONSHIPS}"><Relationship Id="rId1" Type="${OFFICE_RELATIONSHIPS}/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="${PACKAGE_RELATIONSHIPS}/metadata/core-properties" Target="docProps/core.xml"/></Relationships>`,
204
+ "[Content_Types].xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="${WORDPROCESSING}.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="${WORDPROCESSING}.styles+xml"/>` + (header ? `<Override PartName="/word/header1.xml" ContentType="${WORDPROCESSING}.header+xml"/>` : "") + `<Override PartName="/docProps/core.xml" ContentType="${CORE_PROPERTIES_TYPE}"/><Override PartName="/docProps/app.xml" ContentType="${EXTENDED_PROPERTIES_TYPE}"/></Types>`,
205
+ "_rels/.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="${RELATIONSHIPS}"><Relationship Id="rId1" Type="${OFFICE_RELATIONSHIPS}/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="${PACKAGE_RELATIONSHIPS}/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId3" Type="${OFFICE_RELATIONSHIPS}/extended-properties" Target="docProps/app.xml"/></Relationships>`,
119
206
  "docProps/core.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="${PACKAGE_RELATIONSHIPS}/metadata/core-properties" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dcterms:created xsi:type="dcterms:W3CDTF">2000-01-01T00:00:00Z</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">2000-01-01T00:00:00Z</dcterms:modified></cp:coreProperties>`,
207
+ "docProps/app.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="${EXTENDED_PROPERTIES}"><AppVersion>1.0.0</AppVersion></Properties>`,
120
208
  "word/_rels/document.xml.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="${RELATIONSHIPS}"><Relationship Id="rId1" Type="${OFFICE_RELATIONSHIPS}/styles" Target="styles.xml"/>` + (header ? `<Relationship Id="${HEADER_RELATIONSHIP_ID}" Type="${OFFICE_RELATIONSHIPS}/header" Target="header1.xml"/>` : "") + linkRelationships + `</Relationships>`,
121
209
  "word/styles.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="${NAMESPACE}"><w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/></w:style><w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/></w:style></w:styles>`,
122
210
  "word/document.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="${NAMESPACE}" xmlns:r="${OFFICE_RELATIONSHIPS}" xmlns:mc="${MARKUP_COMPATIBILITY}" xmlns:w14="${WORDML_2010}" mc:Ignorable="w14"><w:body>` + bodyXml(items, {
@@ -51,25 +51,32 @@ type PlannedStoryComparison = {
51
51
  * parsing, no serialization, no clock.
52
52
  */
53
53
  declare const planComparison: ({ pairs }: ParsedComparison) => Result<readonly PlannedStoryComparison[], CompareDocxOperationLimitError>;
54
- /** What stage 3 produced: the change list, and whether it was proven. */
54
+ /** What stage 3 produced: the change list, whether it was proven, and whether it wrote anything. */
55
55
  type AppliedComparison = {
56
56
  changes: readonly CompareChange[];
57
57
  verification: CompareVerification;
58
+ /**
59
+ * Whether the stage wrote into the base document at all. A comparison that
60
+ * found nothing writes nothing, and the serialize stage then hands the
61
+ * arriving bytes back rather than reproducing them.
62
+ */
63
+ documentChanged: boolean;
58
64
  };
59
65
  /**
60
66
  * Stage 3: write the planned operations into the base document as tracked
61
67
  * changes, then check the work rather than trust it. Both directions of the
62
- * round trip are checked, structure included: accepting the story's generated
63
- * revisions must reproduce the target, and rejecting them must reproduce the
64
- * base it was compared from. A difference the operation vocabulary cannot
65
- * express would otherwise leave a redline that reads plausibly and is wrong.
68
+ * round trip are checked, structure and table geometry included: accepting the
69
+ * story's generated revisions must reproduce the target, and rejecting them
70
+ * must reproduce the base it was compared from. A difference the operation
71
+ * vocabulary cannot express would otherwise leave a redline that reads
72
+ * plausibly and is wrong.
66
73
  *
67
74
  * The check reports rather than throws. {@link compareDocx} decides what to do
68
75
  * with an unverified result, because "give me your best attempt and tell me
69
76
  * what you could not represent" and "give me nothing unless you can prove it"
70
77
  * are both legitimate asks and only the caller knows which one it is making.
71
78
  */
72
- declare const applyComparison: ({ reviewer, revisionStamp, granularity, numberingChanges }: ParsedComparison, planned: readonly PlannedStoryComparison[]) => Result<AppliedComparison, CompareDocxApplyError>;
79
+ declare const applyComparison: ({ reviewer, targetReviewer, revisionStamp, granularity, numberingChanges }: ParsedComparison, planned: readonly PlannedStoryComparison[]) => Result<AppliedComparison, CompareDocxApplyError>;
73
80
  /**
74
81
  * Stage 4: the result package, with every ZIP entry date pinned.
75
82
  *
@@ -89,7 +96,7 @@ declare const applyComparison: ({ reviewer, revisionStamp, granularity, numberin
89
96
  * composition let the benchmark's own composition disagree with the shipped
90
97
  * one within a single run.
91
98
  */
92
- declare const serializeComparison: ({ baseBuffer, baseCarriedRevisions, reviewer, packageDate }: ParsedComparison, planned: readonly PlannedStoryComparison[]) => Promise<Result<ArrayBuffer, CompareDocxSerializeError | CompareDocxFinalParagraphMarkError>>;
99
+ declare const serializeComparison: ({ baseBuffer, baseCarriedRevisions, reviewer, packageDate, revisionStamp }: ParsedComparison, { documentChanged }: AppliedComparison) => Promise<Result<ArrayBuffer, CompareDocxSerializeError | CompareDocxFinalParagraphMarkError>>;
93
100
  /**
94
101
  * Compare `base` against `target` and return `base` carrying the tracked
95
102
  * changes that turn it into `target`, alongside the change list describing