@stll/folio-core 0.33.1 → 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 +134 -25
- 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 +61 -2
- package/dist/compare/__fixtures__/body-sequence.js +94 -6
- package/dist/compare/compare.d.ts +14 -7
- package/dist/compare/compare.js +136 -38
- package/dist/compare/plan.d.ts +31 -1
- package/dist/compare/plan.js +233 -66
- package/dist/compare/types.d.ts +9 -7
- package/dist/compare/types.js +7 -5
- package/dist/compare/verification.d.ts +31 -12
- package/dist/compare/verification.js +88 -17
- package/dist/compat/eigenpal.d.ts +4 -4
- package/dist/compat/eigenpal.js +2 -2
- package/dist/display-list/primitives.d.ts +1 -1
- 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/paragraphParser.js +3 -2
- package/dist/docx/rezip.d.ts +10 -4
- package/dist/docx/rezip.js +54 -18
- 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 +3 -3
- 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 +83 -18
- 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 +4 -4
- package/dist/index.js +2 -2
- package/dist/model.d.ts +3 -3
- package/dist/model.js +2 -2
- package/dist/prosemirror/commands/comments.js +2 -1
- 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
|
@@ -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)
|
|
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 = (
|
|
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
|
|
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
|
|
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
|
|
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
7
|
import { CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError } from "./types.js";
|
|
7
|
-
import {
|
|
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,13 +450,13 @@ 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 &&
|
|
357
|
-
const
|
|
358
|
-
const [
|
|
359
|
-
if (
|
|
360
|
-
message: `A container's final paragraph mark carries a ${
|
|
361
|
-
|
|
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
|
|
362
460
|
}));
|
|
363
461
|
return await Result.tryPromise({
|
|
364
462
|
try: async () => await withFixedPackageDates(await reviewer.toBuffer(), packageDate),
|
|
@@ -399,7 +497,7 @@ const compareDocx = async (base, target, options) => {
|
|
|
399
497
|
failures: verification.failures
|
|
400
498
|
}));
|
|
401
499
|
}
|
|
402
|
-
const serialized = await serializeComparison(parsed.value,
|
|
500
|
+
const serialized = await serializeComparison(parsed.value, applied.value);
|
|
403
501
|
if (serialized.isErr()) return Result.err(serialized.error);
|
|
404
502
|
return Result.ok({
|
|
405
503
|
buffer: serialized.value,
|
package/dist/compare/plan.d.ts
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
import { FolioAIEditOperation, FolioAIEditSnapshot } from "../ai-edits/types.js";
|
|
2
|
+
import { TableGeometryPairing } from "../ai-edits/table-geometry.js";
|
|
2
3
|
import { FolioDocumentStoryHandle } from "../ai-edits/headless.js";
|
|
3
4
|
import { CompareChange } from "./types.js";
|
|
4
5
|
//#region src/compare/plan.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* A table the target document already holds, which the operation naming it
|
|
8
|
+
* should place verbatim instead of rebuilding from its cell texts.
|
|
9
|
+
*
|
|
10
|
+
* The plan is pure and sees only block snapshots, so it names the table by the
|
|
11
|
+
* index the snapshot numbers tables with; the caller, which has both
|
|
12
|
+
* documents, resolves the index to the node.
|
|
13
|
+
*/
|
|
14
|
+
type CompareTableTemplateRequest = {
|
|
15
|
+
/** The `insertTable` or `insertTableRow` operation this table belongs to. */
|
|
16
|
+
operationId: string;
|
|
17
|
+
/** Index of the table in the TARGET story. */
|
|
18
|
+
targetTableIndex: number;
|
|
19
|
+
/** Set for a row insertion: which of that table's rows to place. */
|
|
20
|
+
targetRowIndex?: number;
|
|
21
|
+
};
|
|
5
22
|
type CompareStoryPlan = {
|
|
6
23
|
changes: CompareChange[];
|
|
7
24
|
operations: FolioAIEditOperation[];
|
|
25
|
+
/**
|
|
26
|
+
* Where an operation's table comes from. Kept beside the operations rather
|
|
27
|
+
* than inside them because a table node is not JSON, and the serialized
|
|
28
|
+
* operation contract describes a table by its cell texts.
|
|
29
|
+
*/
|
|
30
|
+
tableTemplates: CompareTableTemplateRequest[];
|
|
31
|
+
/**
|
|
32
|
+
* Base cells the alignment put opposite a target cell. Their tables, rows
|
|
33
|
+
* and cells are the ones whose `w:tblPr` / `w:trPr` / `w:tcPr` the caller
|
|
34
|
+
* matches: a table that stayed in place while its widths, shading or header
|
|
35
|
+
* row changed moves no block, so no operation carries the difference.
|
|
36
|
+
*/
|
|
37
|
+
tableGeometryPairings: TableGeometryPairing[];
|
|
8
38
|
};
|
|
9
39
|
type PlanStoryCompareOptions = {
|
|
10
40
|
story: FolioDocumentStoryHandle;
|
|
@@ -19,4 +49,4 @@ type PlanStoryCompareOptions = {
|
|
|
19
49
|
*/
|
|
20
50
|
declare const planStoryCompare: ({ story, baseSnapshot, targetSnapshot, maxOperations }: PlanStoryCompareOptions) => CompareStoryPlan | null;
|
|
21
51
|
//#endregion
|
|
22
|
-
export { CompareStoryPlan, PlanStoryCompareOptions, planStoryCompare };
|
|
52
|
+
export { CompareStoryPlan, CompareTableTemplateRequest, PlanStoryCompareOptions, planStoryCompare };
|