@stll/folio-core 0.33.0 → 0.33.2
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.
- package/dist/ai-edits/apply.d.ts +15 -0
- package/dist/ai-edits/apply.js +135 -24
- package/dist/ai-edits/headless.d.ts +47 -3
- package/dist/ai-edits/headless.js +53 -3
- package/dist/ai-edits/index.d.ts +1 -1
- package/dist/ai-edits/snapshot.d.ts +19 -1
- package/dist/ai-edits/snapshot.js +25 -3
- package/dist/ai-edits/table-geometry.d.ts +56 -0
- package/dist/ai-edits/table-geometry.js +217 -0
- package/dist/ai-edits/table-row-column-mutations.d.ts +34 -16
- package/dist/ai-edits/table-row-column-mutations.js +12 -4
- package/dist/ai-edits/table-template.d.ts +60 -0
- package/dist/ai-edits/table-template.js +176 -0
- package/dist/ai-edits/types.d.ts +7 -0
- package/dist/compare/__fixtures__/body-sequence.d.ts +86 -4
- package/dist/compare/__fixtures__/body-sequence.js +169 -12
- package/dist/compare/compare.d.ts +15 -8
- package/dist/compare/compare.js +138 -34
- package/dist/compare/plan.d.ts +31 -1
- package/dist/compare/plan.js +277 -28
- package/dist/compare/reproducible-package.d.ts +7 -3
- package/dist/compare/reproducible-package.js +20 -3
- package/dist/compare/types.d.ts +20 -3
- package/dist/compare/types.js +13 -1
- package/dist/compare/verification.d.ts +49 -2
- package/dist/compare/verification.js +115 -1
- package/dist/compat/eigenpal.d.ts +5 -5
- package/dist/compat/eigenpal.js +3 -3
- package/dist/document-operations.d.ts +10 -1
- package/dist/document-operations.js +3 -2
- package/dist/docx/appVersionNormalization.d.ts +49 -0
- package/dist/docx/appVersionNormalization.js +74 -0
- package/dist/docx/blockContentParser.js +3 -2
- package/dist/docx/drawingUtils.js +4 -3
- package/dist/docx/hyperlinkParser.d.ts +9 -1
- package/dist/docx/hyperlinkParser.js +19 -13
- package/dist/docx/paraIdRangeNormalization.d.ts +40 -0
- package/dist/docx/paraIdRangeNormalization.js +64 -0
- package/dist/docx/paragraphParser.js +82 -6
- package/dist/docx/revisionIdNormalization.d.ts +15 -1
- package/dist/docx/revisionIdNormalization.js +22 -4
- package/dist/docx/rezip.d.ts +13 -1
- package/dist/docx/rezip.js +80 -21
- package/dist/docx/runParser.js +9 -8
- package/dist/docx/sdtProperties.js +4 -3
- package/dist/docx/selectiveSave.js +7 -6
- package/dist/docx/serializer/commentSerializer.d.ts +1 -1
- package/dist/docx/serializer/commentSerializer.js +43 -19
- package/dist/docx/serializer/documentSerializer.d.ts +3 -1
- package/dist/docx/serializer/documentSerializer.js +42 -78
- package/dist/docx/serializer/fontTableSerializer.js +8 -5
- package/dist/docx/serializer/headerFooterSerializer.d.ts +4 -1
- package/dist/docx/serializer/headerFooterSerializer.js +34 -29
- package/dist/docx/serializer/noteSerializer.js +34 -30
- package/dist/docx/serializer/numberingSerializer.js +8 -4
- package/dist/docx/serializer/paragraphSerializer.js +45 -21
- package/dist/docx/serializer/partNamespaces.d.ts +78 -0
- package/dist/docx/serializer/partNamespaces.js +324 -0
- package/dist/docx/serializer/runSerializer.js +1 -1
- package/dist/docx/serializer/settingsSerializer.js +8 -2
- package/dist/docx/serializer/stylesSerializer.js +8 -5
- package/dist/docx/serializer/tableSerializer.js +107 -27
- package/dist/docx/serializer/themeSerializer.js +10 -2
- package/dist/docx/server/build.d.ts +1 -1
- package/dist/docx/strictValueEncodings.gen.d.ts +15 -0
- package/dist/docx/strictValueEncodings.gen.js +275 -0
- package/dist/docx/tableParser.d.ts +0 -6
- package/dist/docx/tableParser.js +52 -7
- package/dist/docx/transitionalSpelling.d.ts +23 -0
- package/dist/docx/transitionalSpelling.js +36 -0
- package/dist/docx/universalMeasure.d.ts +20 -0
- package/dist/docx/universalMeasure.js +32 -0
- package/dist/docx/verbatimCapture.d.ts +20 -0
- package/dist/docx/verbatimCapture.js +131 -0
- package/dist/docx/vmlImageParser.js +4 -3
- package/dist/docx/watermarkParser.js +4 -3
- package/dist/docx/xmlParser.d.ts +23 -2
- package/dist/docx/xmlParser.js +43 -11
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/model.d.ts +3 -3
- package/dist/model.js +2 -2
- package/dist/prosemirror/commands/comments.js +8 -2
- package/dist/prosemirror/containerFinalParagraph.d.ts +47 -0
- package/dist/prosemirror/containerFinalParagraph.js +92 -0
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +15 -4
- package/dist/prosemirror/conversion/toProseDoc.js +12 -3
- package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -0
- package/dist/prosemirror/schema/marks.d.ts +1 -1
- package/dist/prosemirror/schema/nodes.d.ts +22 -0
- package/dist/server.d.ts +1 -1
- package/dist/types/block-id.d.ts +12 -1
- package/dist/types/block-id.js +17 -1
- package/dist/utils/canonicalJson.d.ts +14 -0
- package/dist/utils/canonicalJson.js +19 -0
- package/dist/utils/formatToStyle.d.ts +1 -1
- package/package.json +2 -2
|
@@ -13,14 +13,88 @@
|
|
|
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
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* One inline of a paragraph: plain text, or text carrying an external
|
|
73
|
+
* hyperlink. A link is the case where a revision wrapper and the linked runs
|
|
74
|
+
* have to nest one inside the other, so the fixture has to be able to author
|
|
75
|
+
* one.
|
|
76
|
+
*/
|
|
77
|
+
type ParagraphInline = string | {
|
|
78
|
+
text: string;
|
|
79
|
+
href: string;
|
|
80
|
+
};
|
|
16
81
|
/** One body-level item: a paragraph, or a table given row by row. */
|
|
17
82
|
type BodyItem = {
|
|
18
83
|
kind: "paragraph";
|
|
19
|
-
text: string;
|
|
84
|
+
text: string | readonly ParagraphInline[];
|
|
20
85
|
styleId?: string;
|
|
86
|
+
/**
|
|
87
|
+
* An authored `w14:paraId`, for a package whose ids a producer wrote
|
|
88
|
+
* without respecting the 31-bit bound the schema puts on them.
|
|
89
|
+
*/
|
|
90
|
+
paraId?: string;
|
|
21
91
|
} | {
|
|
22
92
|
kind: "table";
|
|
23
|
-
rows: readonly
|
|
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[];
|
|
24
98
|
/**
|
|
25
99
|
* Rows a package hides with `w:hidden`. The snapshot skips their whole
|
|
26
100
|
* subtree, so a document that has one is the case where the snapshot
|
|
@@ -28,6 +102,14 @@ type BodyItem = {
|
|
|
28
102
|
*/
|
|
29
103
|
hiddenRows?: readonly number[];
|
|
30
104
|
};
|
|
31
|
-
|
|
105
|
+
type BodySequenceOptions = {
|
|
106
|
+
/**
|
|
107
|
+
* A default header part, written as its own sequence. A header is a story of
|
|
108
|
+
* its own: it ends with its own paragraph, and a comparison writes it with
|
|
109
|
+
* its own revision ids.
|
|
110
|
+
*/
|
|
111
|
+
header?: readonly BodyItem[];
|
|
112
|
+
};
|
|
113
|
+
declare const buildBodySequenceDocx: (items: readonly BodyItem[], { header }?: BodySequenceOptions) => Promise<ArrayBuffer>;
|
|
32
114
|
//#endregion
|
|
33
|
-
export { BodyItem, CellContent, buildBodySequenceDocx };
|
|
115
|
+
export { BodyItem, BodySequenceOptions, Cell, CellContent, CellProperties, ParagraphInline, TableProperties, TableRow, buildBodySequenceDocx };
|
|
@@ -12,6 +12,12 @@ const NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
|
12
12
|
const RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
13
13
|
const OFFICE_RELATIONSHIPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
14
14
|
const WORDPROCESSING = "application/vnd.openxmlformats-officedocument.wordprocessingml";
|
|
15
|
+
const MARKUP_COMPATIBILITY = "http://schemas.openxmlformats.org/markup-compatibility/2006";
|
|
16
|
+
const PACKAGE_RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006";
|
|
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";
|
|
20
|
+
const WORDML_2010 = "http://schemas.microsoft.com/office/word/2010/wordml";
|
|
15
21
|
/**
|
|
16
22
|
* `createFolders: false` because JSZip stamps the folder entries it
|
|
17
23
|
* synthesizes with `new Date()`, which the fixed date above does not reach.
|
|
@@ -20,14 +26,62 @@ const ZIP_ENTRY_OPTIONS = {
|
|
|
20
26
|
date: new Date(Date.UTC(2e3, 0, 1)),
|
|
21
27
|
createFolders: false
|
|
22
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 : {};
|
|
35
|
+
/**
|
|
36
|
+
* A paragraph's id is derived from its own content, not from its position.
|
|
37
|
+
*
|
|
38
|
+
* Two packages authored from two descriptions are a base and a target, and a
|
|
39
|
+
* paragraph that appears in both is the same paragraph. Numbering the ids in
|
|
40
|
+
* document order would instead give the same id to the paragraph that happens
|
|
41
|
+
* to sit at the same index, which is how the fixture would tell a comparison
|
|
42
|
+
* that a removed paragraph was a rewrite of the one after it.
|
|
43
|
+
*/
|
|
44
|
+
const createParaIdAllocator = () => {
|
|
45
|
+
const taken = /* @__PURE__ */ new Set();
|
|
46
|
+
return (content) => {
|
|
47
|
+
let hash = 2166136261;
|
|
48
|
+
for (let index = 0; index < content.length; index += 1) hash = Math.imul(hash ^ content.charCodeAt(index), 16777619) >>> 0;
|
|
49
|
+
let candidate = hash % 2147483646;
|
|
50
|
+
while (taken.has((candidate + 1).toString(16).toUpperCase().padStart(8, "0"))) candidate = (candidate + 1) % 2147483646;
|
|
51
|
+
const paraId = (candidate + 1).toString(16).toUpperCase().padStart(8, "0");
|
|
52
|
+
taken.add(paraId);
|
|
53
|
+
return paraId;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const run = (text) => `<w:r><w:t xml:space="preserve">${text}</w:t></w:r>`;
|
|
57
|
+
const inlineXml = (inline, { links }) => typeof inline === "string" ? run(inline) : `<w:hyperlink r:id="${links.get(inline.href) ?? ""}">${run(inline.text)}</w:hyperlink>`;
|
|
23
58
|
/**
|
|
24
59
|
* An empty paragraph is a `w:p` with no run at all, which is what a package
|
|
25
60
|
* holds for a blank line or an empty cell. It is not the same thing as a
|
|
26
61
|
* paragraph whose run carries an empty string, and both shapes occur.
|
|
27
62
|
*/
|
|
28
|
-
|
|
63
|
+
/** A blank line carries no run at all, so an empty string is no inline. */
|
|
64
|
+
const nonEmptyInlines = (text) => text.length === 0 ? [] : [text];
|
|
65
|
+
const paragraph = (text, context, { styleId, paraId } = {}) => {
|
|
29
66
|
const properties = styleId === void 0 ? "" : `<w:pPr><w:pStyle w:val="${styleId}"/></w:pPr>`;
|
|
30
|
-
|
|
67
|
+
const inlines = typeof text === "string" ? nonEmptyInlines(text) : text;
|
|
68
|
+
const content = inlines.map((inline) => typeof inline === "string" ? inline : inline.text).join("");
|
|
69
|
+
const id = paraId ?? context.paraId(`${styleId ?? ""}|${content}`);
|
|
70
|
+
return `<w:p w14:paraId="${id}" w14:textId="${id}">${properties}${inlines.map((inline) => inlineXml(inline, context)).join("")}</w:p>`;
|
|
71
|
+
};
|
|
72
|
+
/** Every href the body carries, in document order, so the ids are stable. */
|
|
73
|
+
const collectHrefs = (items, hrefs) => {
|
|
74
|
+
for (const item of items) {
|
|
75
|
+
if (item.kind === "paragraph") {
|
|
76
|
+
if (typeof item.text === "string") continue;
|
|
77
|
+
for (const inline of item.text) if (typeof inline !== "string" && !hrefs.includes(inline.href)) hrefs.push(inline.href);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
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
|
+
}
|
|
84
|
+
}
|
|
31
85
|
};
|
|
32
86
|
const EMPTY_PARAGRAPH = {
|
|
33
87
|
kind: "paragraph",
|
|
@@ -44,20 +98,123 @@ const closedSequence = (items) => {
|
|
|
44
98
|
return last === void 0 || last.kind === "table" ? [...items, EMPTY_PARAGRAPH] : items;
|
|
45
99
|
};
|
|
46
100
|
/** A cell must also contain a paragraph, which the empty sequence supplies. */
|
|
47
|
-
const cellXml = (content) => typeof content === "string" ? paragraph(content) : itemsXml(closedSequence(content));
|
|
48
|
-
|
|
101
|
+
const cellXml = (content, context) => typeof content === "string" ? paragraph(content, context) : itemsXml(closedSequence(content), context);
|
|
102
|
+
/** `w:tbl` is `w:tblPr, w:tblGrid, rows`: a fixture without the grid is not one. */
|
|
103
|
+
const tableGrid = (item) => {
|
|
104
|
+
if (item.columnWidths) return `<w:tblGrid>${item.columnWidths.map((width) => `<w:gridCol w:w="${String(width)}"/>`).join("")}</w:tblGrid>`;
|
|
105
|
+
let columns = 0;
|
|
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
|
+
}
|
|
111
|
+
return `<w:tblGrid>${`<w:gridCol w:w="2000"/>`.repeat(columns)}</w:tblGrid>`;
|
|
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
|
+
};
|
|
184
|
+
const table = (item, context) => {
|
|
49
185
|
const hidden = new Set(item.hiddenRows ?? []);
|
|
50
|
-
return `<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>`;
|
|
51
187
|
};
|
|
52
|
-
const itemsXml = (items) => items.map((item) => item.kind === "paragraph" ? paragraph(item.text,
|
|
53
|
-
|
|
54
|
-
|
|
188
|
+
const itemsXml = (items, context) => items.map((item) => item.kind === "paragraph" ? paragraph(item.text, context, {
|
|
189
|
+
...item.styleId === void 0 ? {} : { styleId: item.styleId },
|
|
190
|
+
...item.paraId === void 0 ? {} : { paraId: item.paraId }
|
|
191
|
+
}) : table(item, context)).join("");
|
|
192
|
+
const bodyXml = (items, context) => itemsXml(closedSequence(items), context);
|
|
193
|
+
/** The relationship id the default header takes when a fixture asks for one. */
|
|
194
|
+
const HEADER_RELATIONSHIP_ID = "rId2";
|
|
195
|
+
const buildBodySequenceDocx = async (items, { header } = {}) => {
|
|
196
|
+
const hrefs = [];
|
|
197
|
+
collectHrefs(closedSequence(items), hrefs);
|
|
198
|
+
collectHrefs(closedSequence(header ?? []), hrefs);
|
|
199
|
+
const firstLinkRelationship = header ? 3 : 2;
|
|
200
|
+
const links = new Map(hrefs.map((href, index) => [href, `rId${index + firstLinkRelationship}`]));
|
|
201
|
+
const linkRelationships = hrefs.map((href, index) => `<Relationship Id="rId${index + firstLinkRelationship}" Type="${OFFICE_RELATIONSHIPS}/hyperlink" Target="${href}" TargetMode="External"/>`).join("");
|
|
202
|
+
const paraId = createParaIdAllocator();
|
|
55
203
|
const parts = {
|
|
56
|
-
"[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"/></Types>`,
|
|
57
|
-
"_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"/></Relationships>`,
|
|
58
|
-
"
|
|
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>`,
|
|
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>`,
|
|
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>`,
|
|
59
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>`,
|
|
60
|
-
"word/document.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="${NAMESPACE}"
|
|
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, {
|
|
211
|
+
links,
|
|
212
|
+
paraId
|
|
213
|
+
}) + `<w:sectPr>` + (header ? `<w:headerReference w:type="default" r:id="${HEADER_RELATIONSHIP_ID}"/>` : "") + "<w:pgSz w:w=\"12240\" w:h=\"15840\"/><w:pgMar w:top=\"1440\" w:right=\"1440\" w:bottom=\"1440\" w:left=\"1440\"/></w:sectPr></w:body></w:document>",
|
|
214
|
+
...header ? { "word/header1.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:hdr xmlns:w="${NAMESPACE}" xmlns:r="${OFFICE_RELATIONSHIPS}" xmlns:mc="${MARKUP_COMPATIBILITY}" xmlns:w14="${WORDML_2010}" mc:Ignorable="w14">${itemsXml(closedSequence(header), {
|
|
215
|
+
links,
|
|
216
|
+
paraId
|
|
217
|
+
})}</w:hdr>` } : {}
|
|
61
218
|
};
|
|
62
219
|
const zip = new JSZip();
|
|
63
220
|
for (const name of Object.keys(parts).toSorted()) zip.file(name, parts[name] ?? "", ZIP_ENTRY_OPTIONS);
|
|
@@ -3,7 +3,7 @@ import { WordDiffGranularity } from "../ai-edits/word-diff.js";
|
|
|
3
3
|
import { FolioRevisionStamp } from "../ai-edits/apply.js";
|
|
4
4
|
import { FolioDocumentStoryHandle, FolioDocxReviewer } from "../ai-edits/headless.js";
|
|
5
5
|
import { CompareVerification } from "./verification.js";
|
|
6
|
-
import { CompareChange, CompareDocxApplyError, CompareDocxError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxSerializeError, CompareResult, CompareUnsupportedPart, InvalidCompareDocxOptionsError } from "./types.js";
|
|
6
|
+
import { CompareChange, CompareDocxApplyError, CompareDocxError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxSerializeError, CompareResult, CompareUnsupportedPart, InvalidCompareDocxOptionsError } from "./types.js";
|
|
7
7
|
import { CompareStoryPlan } from "./plan.js";
|
|
8
8
|
import { Result } from "better-result";
|
|
9
9
|
//#region src/compare/compare.d.ts
|
|
@@ -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
|
|
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
|
|
63
|
-
* revisions must reproduce the target, and rejecting them
|
|
64
|
-
* base it was compared from. A difference the operation
|
|
65
|
-
* express would otherwise leave a redline that reads
|
|
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,
|
|
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
|
package/dist/compare/compare.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { FolioDocxReviewer } from "../ai-edits/headless.js";
|
|
2
|
+
import { projectTableGeometry } from "../ai-edits/table-geometry.js";
|
|
2
3
|
import "../document-operations.js";
|
|
3
4
|
import { pairFolioDocumentStories } from "../document-stories.js";
|
|
4
5
|
import { planStoryCompare } from "./plan.js";
|
|
5
6
|
import { withFixedPackageDates } from "./reproducible-package.js";
|
|
6
|
-
import { CompareDocxApplyError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError } from "./types.js";
|
|
7
|
-
import { classifyProjectionMismatch, projectSupportedInlineFormatting } from "./verification.js";
|
|
7
|
+
import { CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError } from "./types.js";
|
|
8
|
+
import { classifyGeometryMismatch, classifyProjectionMismatch, projectSupportedInlineFormatting, revisedFinalParagraphMarks } from "./verification.js";
|
|
8
9
|
import { Result, panic } from "better-result";
|
|
9
10
|
//#region src/compare/compare.ts
|
|
10
11
|
/**
|
|
@@ -151,6 +152,14 @@ const parseComparison = async (base, target, options) => {
|
|
|
151
152
|
const reviewer = baseParse.value;
|
|
152
153
|
const targetReviewer = targetParse.value;
|
|
153
154
|
const existing = existingRevisionsOf(reviewer);
|
|
155
|
+
for (const { handle } of reviewer.listStories()) reviewer.resolveReviewedStory({
|
|
156
|
+
story: handle,
|
|
157
|
+
view: "final"
|
|
158
|
+
});
|
|
159
|
+
for (const { handle } of targetReviewer.listStories()) targetReviewer.resolveReviewedStory({
|
|
160
|
+
story: handle,
|
|
161
|
+
view: "final"
|
|
162
|
+
});
|
|
154
163
|
const pairs = [];
|
|
155
164
|
const unsupported = [];
|
|
156
165
|
for (const { baseStory, revisedStory: targetStory } of pairFolioDocumentStories(reviewer.listStories().map(({ handle }) => handle), targetReviewer.listStories().map(({ handle }) => handle))) {
|
|
@@ -170,14 +179,6 @@ const parseComparison = async (base, target, options) => {
|
|
|
170
179
|
});
|
|
171
180
|
continue;
|
|
172
181
|
}
|
|
173
|
-
reviewer.resolveReviewedStory({
|
|
174
|
-
story: baseStory,
|
|
175
|
-
view: "final"
|
|
176
|
-
});
|
|
177
|
-
targetReviewer.resolveReviewedStory({
|
|
178
|
-
story: targetStory,
|
|
179
|
-
view: "final"
|
|
180
|
-
});
|
|
181
182
|
const baseSnapshot = reviewer.snapshotStory(baseStory);
|
|
182
183
|
const targetSnapshot = targetReviewer.snapshotStory(targetStory);
|
|
183
184
|
if (!baseSnapshot || !targetSnapshot) {
|
|
@@ -236,50 +237,126 @@ const planComparison = ({ pairs }) => {
|
|
|
236
237
|
return Result.ok(planned);
|
|
237
238
|
};
|
|
238
239
|
/**
|
|
240
|
+
* What a skipped operation says about the plan that derived it.
|
|
241
|
+
*
|
|
242
|
+
* Two different things go wrong at apply time, and only one of them leaves the
|
|
243
|
+
* result unusable. A reason that says the plan did not match the document it
|
|
244
|
+
* was planned against is an engine defect — the operations came from this very
|
|
245
|
+
* snapshot moments earlier, so nothing should have moved under them, and a
|
|
246
|
+
* redline built on the rest is built on a document the plan no longer
|
|
247
|
+
* describes. A reason that says the applier had nothing to write, or could not
|
|
248
|
+
* write that shape THERE, leaves the redline standing and turns the question
|
|
249
|
+
* into "was anything lost", which is what the round-trip check answers a few
|
|
250
|
+
* lines below. Refusing on those instead trades a partial answer for none, and
|
|
251
|
+
* refuses a whole document because one paragraph sits inside a structure the
|
|
252
|
+
* block snapshot does not model — a text box, a content control — where a
|
|
253
|
+
* paragraph mark has nowhere to go.
|
|
254
|
+
*/
|
|
255
|
+
const COMPARE_SKIP_DISPOSITION = {
|
|
256
|
+
missingBlock: "fatal",
|
|
257
|
+
changedBlock: "fatal",
|
|
258
|
+
ambiguousFind: "fatal",
|
|
259
|
+
missingFind: "fatal",
|
|
260
|
+
unsupportedBlock: "unwritable",
|
|
261
|
+
unsupportedMode: "fatal",
|
|
262
|
+
atomicBatchRejected: "fatal",
|
|
263
|
+
preconditionFailed: "fatal",
|
|
264
|
+
staleRange: "fatal",
|
|
265
|
+
emptyOperation: "unwritable",
|
|
266
|
+
noopOperation: "unwritable",
|
|
267
|
+
documentVersionMismatch: "fatal",
|
|
268
|
+
documentNotEditable: "fatal"
|
|
269
|
+
};
|
|
270
|
+
/**
|
|
271
|
+
* The table each `insertTable` / `insertTableRow` in the plan should place,
|
|
272
|
+
* resolved against the target document the plan named it in.
|
|
273
|
+
*
|
|
274
|
+
* The plan is pure and names a table by index; the node lives in the other
|
|
275
|
+
* package, which only this stage holds. A request naming a table the target
|
|
276
|
+
* does not have resolves to nothing and the operation falls back to its cell
|
|
277
|
+
* texts, which is the same redline the comparison produced before.
|
|
278
|
+
*/
|
|
279
|
+
const resolveTableTemplates = (targetTables, requests) => {
|
|
280
|
+
const templates = /* @__PURE__ */ new Map();
|
|
281
|
+
for (const { operationId, targetTableIndex, targetRowIndex } of requests) {
|
|
282
|
+
const table = targetTables.get(targetTableIndex);
|
|
283
|
+
if (!table) continue;
|
|
284
|
+
if (targetRowIndex === void 0) {
|
|
285
|
+
templates.set(operationId, table);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const row = table.maybeChild(targetRowIndex);
|
|
289
|
+
if (row) templates.set(operationId, row);
|
|
290
|
+
}
|
|
291
|
+
return templates;
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
239
294
|
* Stage 3: write the planned operations into the base document as tracked
|
|
240
295
|
* changes, then check the work rather than trust it. Both directions of the
|
|
241
|
-
* round trip are checked, structure included: accepting the
|
|
242
|
-
* revisions must reproduce the target, and rejecting them
|
|
243
|
-
* base it was compared from. A difference the operation
|
|
244
|
-
* express would otherwise leave a redline that reads
|
|
296
|
+
* round trip are checked, structure and table geometry included: accepting the
|
|
297
|
+
* story's generated revisions must reproduce the target, and rejecting them
|
|
298
|
+
* must reproduce the base it was compared from. A difference the operation
|
|
299
|
+
* vocabulary cannot express would otherwise leave a redline that reads
|
|
300
|
+
* plausibly and is wrong.
|
|
245
301
|
*
|
|
246
302
|
* The check reports rather than throws. {@link compareDocx} decides what to do
|
|
247
303
|
* with an unverified result, because "give me your best attempt and tell me
|
|
248
304
|
* what you could not represent" and "give me nothing unless you can prove it"
|
|
249
305
|
* are both legitimate asks and only the caller knows which one it is making.
|
|
250
306
|
*/
|
|
251
|
-
const applyComparison = ({ reviewer, revisionStamp, granularity, numberingChanges }, planned) => {
|
|
307
|
+
const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity, numberingChanges }, planned) => {
|
|
252
308
|
const changes = [...numberingChanges];
|
|
253
309
|
const failures = [];
|
|
254
310
|
let idSeed = revisionStamp.idSeed;
|
|
311
|
+
let documentChanged = false;
|
|
255
312
|
for (const { pair, plan } of planned) {
|
|
256
313
|
changes.push(...plan.changes);
|
|
257
|
-
if (plan.operations.length === 0) continue;
|
|
314
|
+
if (plan.operations.length === 0 && plan.tableGeometryPairings.length === 0) continue;
|
|
258
315
|
const baseBeforeBlocks = reviewer.readReviewedStory({
|
|
259
316
|
story: pair.baseStory,
|
|
260
317
|
view: "final"
|
|
261
318
|
})?.snapshot.blocks ?? [];
|
|
262
319
|
const baseBefore = projectBlocks(baseBeforeBlocks);
|
|
263
|
-
const
|
|
320
|
+
const baseBeforeGeometry = projectTableGeometry(reviewer.storyTables({ story: pair.baseStory }));
|
|
321
|
+
const targetTables = new Map(targetReviewer.storyTables({ story: pair.targetStory }).map(({ index, node }) => [index, node]));
|
|
322
|
+
const afterGeometry = reviewer.matchStoryTableGeometry({
|
|
264
323
|
story: pair.baseStory,
|
|
265
|
-
|
|
324
|
+
targetTables,
|
|
325
|
+
pairings: plan.tableGeometryPairings,
|
|
266
326
|
revisionStamp: {
|
|
267
327
|
date: revisionStamp.date,
|
|
268
328
|
idSeed
|
|
269
|
-
},
|
|
270
|
-
wordDiff: { granularity },
|
|
271
|
-
batch: {
|
|
272
|
-
version: 1,
|
|
273
|
-
mode: "tracked-changes",
|
|
274
|
-
operations: plan.operations
|
|
275
329
|
}
|
|
276
330
|
});
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
331
|
+
const geometryChanged = afterGeometry > idSeed;
|
|
332
|
+
documentChanged ||= geometryChanged;
|
|
333
|
+
idSeed = afterGeometry;
|
|
334
|
+
if (plan.operations.length === 0 && !geometryChanged) continue;
|
|
335
|
+
if (plan.operations.length > 0) {
|
|
336
|
+
const { skipped, nextRevisionId } = reviewer.applyDocumentOperationsToStory({
|
|
337
|
+
story: pair.baseStory,
|
|
338
|
+
snapshot: pair.baseSnapshot,
|
|
339
|
+
revisionStamp: {
|
|
340
|
+
date: revisionStamp.date,
|
|
341
|
+
idSeed
|
|
342
|
+
},
|
|
343
|
+
wordDiff: { granularity },
|
|
344
|
+
batch: {
|
|
345
|
+
version: 1,
|
|
346
|
+
mode: "tracked-changes",
|
|
347
|
+
operations: plan.operations
|
|
348
|
+
},
|
|
349
|
+
tableTemplates: resolveTableTemplates(targetTables, plan.tableTemplates)
|
|
350
|
+
});
|
|
351
|
+
if (nextRevisionId === void 0) panic("The applier did not report where it left the revision-id counter", { story: pair.baseStory });
|
|
352
|
+
idSeed = nextRevisionId;
|
|
353
|
+
documentChanged = true;
|
|
354
|
+
const refused = skipped.filter(({ reason }) => COMPARE_SKIP_DISPOSITION[reason] === "fatal");
|
|
355
|
+
if (refused.length > 0) return Result.err(new CompareDocxApplyError({
|
|
356
|
+
message: "Some derived operations were refused, so the result would not match the target.",
|
|
357
|
+
skipped: refused
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
283
360
|
const acceptedStory = reviewer.readReviewedStory({
|
|
284
361
|
story: pair.baseStory,
|
|
285
362
|
view: "final"
|
|
@@ -324,13 +401,34 @@ const applyComparison = ({ reviewer, revisionStamp, granularity, numberingChange
|
|
|
324
401
|
});
|
|
325
402
|
if (formattingFailure) failures.push(formattingFailure);
|
|
326
403
|
}
|
|
404
|
+
const geometryAcceptFailure = classifyGeometryMismatch({
|
|
405
|
+
invariant: "accept-reproduces-target",
|
|
406
|
+
story: pair.baseStory,
|
|
407
|
+
actual: projectTableGeometry(reviewer.storyTables({
|
|
408
|
+
story: pair.baseStory,
|
|
409
|
+
view: "final"
|
|
410
|
+
})),
|
|
411
|
+
expected: projectTableGeometry(targetReviewer.storyTables({ story: pair.targetStory }))
|
|
412
|
+
});
|
|
413
|
+
if (geometryAcceptFailure) failures.push(geometryAcceptFailure);
|
|
414
|
+
const geometryRejectFailure = classifyGeometryMismatch({
|
|
415
|
+
invariant: "reject-reproduces-base",
|
|
416
|
+
story: pair.baseStory,
|
|
417
|
+
actual: projectTableGeometry(reviewer.storyTables({
|
|
418
|
+
story: pair.baseStory,
|
|
419
|
+
view: "original"
|
|
420
|
+
})),
|
|
421
|
+
expected: baseBeforeGeometry
|
|
422
|
+
});
|
|
423
|
+
if (geometryRejectFailure) failures.push(geometryRejectFailure);
|
|
327
424
|
}
|
|
328
425
|
return Result.ok({
|
|
329
426
|
changes,
|
|
330
427
|
verification: failures.length === 0 ? { status: "verified" } : {
|
|
331
428
|
status: "unverified",
|
|
332
429
|
failures
|
|
333
|
-
}
|
|
430
|
+
},
|
|
431
|
+
documentChanged
|
|
334
432
|
});
|
|
335
433
|
};
|
|
336
434
|
/**
|
|
@@ -352,8 +450,14 @@ const applyComparison = ({ reviewer, revisionStamp, granularity, numberingChange
|
|
|
352
450
|
* composition let the benchmark's own composition disagree with the shipped
|
|
353
451
|
* one within a single run.
|
|
354
452
|
*/
|
|
355
|
-
const serializeComparison = async ({ baseBuffer, baseCarriedRevisions, reviewer, packageDate },
|
|
356
|
-
if (!baseCarriedRevisions &&
|
|
453
|
+
const serializeComparison = async ({ baseBuffer, baseCarriedRevisions, reviewer, packageDate, revisionStamp }, { documentChanged }) => {
|
|
454
|
+
if (!baseCarriedRevisions && !documentChanged) return Result.ok(baseBuffer);
|
|
455
|
+
const revisions = revisedFinalParagraphMarks(reviewer.toDocument(), { since: revisionStamp.idSeed });
|
|
456
|
+
const [first] = revisions;
|
|
457
|
+
if (first !== void 0) return Result.err(new CompareDocxFinalParagraphMarkError({
|
|
458
|
+
message: `A container's final paragraph mark carries a ${first.kind}, which no consumer can resolve: ${first.container} paragraph ${String(first.paragraphIndex)}.`,
|
|
459
|
+
revisions
|
|
460
|
+
}));
|
|
357
461
|
return await Result.tryPromise({
|
|
358
462
|
try: async () => await withFixedPackageDates(await reviewer.toBuffer(), packageDate),
|
|
359
463
|
catch: (cause) => new CompareDocxSerializeError({
|
|
@@ -393,7 +497,7 @@ const compareDocx = async (base, target, options) => {
|
|
|
393
497
|
failures: verification.failures
|
|
394
498
|
}));
|
|
395
499
|
}
|
|
396
|
-
const serialized = await serializeComparison(parsed.value,
|
|
500
|
+
const serialized = await serializeComparison(parsed.value, applied.value);
|
|
397
501
|
if (serialized.isErr()) return Result.err(serialized.error);
|
|
398
502
|
return Result.ok({
|
|
399
503
|
buffer: serialized.value,
|