@beyondwork/docx-react-component 1.0.18 → 1.0.19
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/README.md +8 -2
- package/package.json +24 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +374 -4
- package/src/api/session-state.ts +58 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +5 -1
- package/src/core/commands/list-commands.ts +231 -36
- package/src/core/commands/paragraph-layout-commands.ts +339 -0
- package/src/core/commands/section-layout-commands.ts +680 -0
- package/src/core/commands/style-commands.ts +262 -0
- package/src/core/search/search-text.ts +329 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +1 -1
- package/src/index.ts +30 -0
- package/src/io/docx-session.ts +260 -39
- package/src/io/export/serialize-main-document.ts +202 -5
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/normalize/normalize-text.ts +63 -25
- package/src/io/ooxml/numbering-sentinels.ts +44 -0
- package/src/io/ooxml/parse-footnotes.ts +212 -20
- package/src/io/ooxml/parse-headers-footers.ts +229 -25
- package/src/io/ooxml/parse-inline-media.ts +16 -0
- package/src/io/ooxml/parse-main-document.ts +411 -6
- package/src/io/ooxml/parse-numbering.ts +7 -0
- package/src/io/ooxml/parse-settings.ts +184 -0
- package/src/io/ooxml/parse-shapes.ts +25 -0
- package/src/io/ooxml/parse-styles.ts +463 -0
- package/src/io/ooxml/parse-theme.ts +32 -0
- package/src/model/canonical-document.ts +133 -3
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +2 -1
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +564 -0
- package/src/runtime/document-runtime.ts +265 -35
- package/src/runtime/document-search.ts +145 -0
- package/src/runtime/numbering-prefix.ts +47 -26
- package/src/runtime/page-layout-estimation.ts +212 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +1 -0
- package/src/runtime/session-capabilities.ts +2 -0
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +239 -12
- package/src/runtime/table-schema.ts +87 -5
- package/src/runtime/view-state.ts +459 -0
- package/src/ui/WordReviewEditor.tsx +1902 -312
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- package/src/ui/headless/selection-helpers.ts +20 -0
- package/src/ui/headless/selection-toolbar-model.ts +22 -0
- package/src/ui/headless/use-editor-keyboard.ts +6 -1
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +125 -14
- package/src/ui-tailwind/editor-surface/perf-probe.ts +107 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +45 -6
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-schema.ts +47 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +95 -22
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +394 -77
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
- package/src/ui-tailwind/index.ts +2 -1
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-review-rail.tsx +6 -6
- package/src/ui-tailwind/theme/editor-theme.css +123 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +291 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +926 -27
- package/src/validation/compatibility-engine.ts +92 -20
- package/src/validation/diagnostics.ts +1 -0
- package/src/validation/docx-comment-proof.ts +487 -0
|
@@ -104,23 +104,13 @@ function collectContentFeatures(
|
|
|
104
104
|
lists: false,
|
|
105
105
|
hyperlinks: false,
|
|
106
106
|
images: false,
|
|
107
|
+
tables: false,
|
|
108
|
+
sections: false,
|
|
109
|
+
contentControls: false,
|
|
107
110
|
};
|
|
108
111
|
|
|
109
112
|
for (let index = 0; index < content.children.length; index += 1) {
|
|
110
|
-
|
|
111
|
-
if (block.type !== "paragraph") {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
flags.paragraphs = true;
|
|
116
|
-
if (block.styleId?.toLowerCase().startsWith("heading")) {
|
|
117
|
-
flags.headings = true;
|
|
118
|
-
}
|
|
119
|
-
if (block.numbering) {
|
|
120
|
-
flags.lists = true;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
measureParagraph(block, flags);
|
|
113
|
+
measureBlock(content.children[index], flags);
|
|
124
114
|
}
|
|
125
115
|
|
|
126
116
|
const entries: CompatibilityFeatureEntry[] = [];
|
|
@@ -145,16 +135,94 @@ function collectContentFeatures(
|
|
|
145
135
|
if (flags.images) {
|
|
146
136
|
entries.push(supportedEntry("inline-images", "Inline image placements stay attached to preserved media parts."));
|
|
147
137
|
}
|
|
138
|
+
if (flags.tables) {
|
|
139
|
+
entries.push(
|
|
140
|
+
supportedEntry(
|
|
141
|
+
"tables",
|
|
142
|
+
"Structured tables keep cell topology and common visual properties through runtime editing and export.",
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (flags.sections) {
|
|
147
|
+
entries.push(
|
|
148
|
+
supportedEntry(
|
|
149
|
+
"sections",
|
|
150
|
+
"Section boundaries and page-layout properties remain structured and export-safe.",
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (flags.contentControls) {
|
|
155
|
+
entries.push(
|
|
156
|
+
supportedEntry(
|
|
157
|
+
"content-controls",
|
|
158
|
+
"Common content controls render through structured SDT state and round-trip without flattening to opaque placeholders.",
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
148
162
|
return entries;
|
|
149
163
|
}
|
|
150
164
|
|
|
165
|
+
function measureBlock(
|
|
166
|
+
block: BlockNode,
|
|
167
|
+
flags: {
|
|
168
|
+
paragraphs: boolean;
|
|
169
|
+
runs: boolean;
|
|
170
|
+
whitespace: boolean;
|
|
171
|
+
headings: boolean;
|
|
172
|
+
lists: boolean;
|
|
173
|
+
hyperlinks: boolean;
|
|
174
|
+
images: boolean;
|
|
175
|
+
tables: boolean;
|
|
176
|
+
sections: boolean;
|
|
177
|
+
contentControls: boolean;
|
|
178
|
+
},
|
|
179
|
+
): number {
|
|
180
|
+
switch (block.type) {
|
|
181
|
+
case "paragraph":
|
|
182
|
+
flags.paragraphs = true;
|
|
183
|
+
if (block.styleId?.toLowerCase().startsWith("heading")) {
|
|
184
|
+
flags.headings = true;
|
|
185
|
+
}
|
|
186
|
+
if (block.numbering) {
|
|
187
|
+
flags.lists = true;
|
|
188
|
+
}
|
|
189
|
+
return measureParagraph(block, flags);
|
|
190
|
+
case "table":
|
|
191
|
+
flags.tables = true;
|
|
192
|
+
return block.rows.reduce(
|
|
193
|
+
(size, row) =>
|
|
194
|
+
size +
|
|
195
|
+
row.cells.reduce(
|
|
196
|
+
(cellSize, cell) =>
|
|
197
|
+
cellSize + cell.children.reduce((childSize, child) => childSize + measureBlock(child, flags), 0),
|
|
198
|
+
0,
|
|
199
|
+
),
|
|
200
|
+
0,
|
|
201
|
+
);
|
|
202
|
+
case "sdt":
|
|
203
|
+
flags.contentControls = true;
|
|
204
|
+
return block.children.reduce((size, child) => size + measureBlock(child, flags), 0);
|
|
205
|
+
case "custom_xml":
|
|
206
|
+
return block.children.reduce((size, child) => size + measureBlock(child, flags), 0);
|
|
207
|
+
case "section_break":
|
|
208
|
+
flags.sections = true;
|
|
209
|
+
return 1;
|
|
210
|
+
default:
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
151
215
|
function measureParagraph(
|
|
152
216
|
paragraph: ParagraphNode,
|
|
153
217
|
flags: {
|
|
218
|
+
paragraphs: boolean;
|
|
154
219
|
runs: boolean;
|
|
155
220
|
whitespace: boolean;
|
|
156
221
|
hyperlinks: boolean;
|
|
157
222
|
images: boolean;
|
|
223
|
+
tables: boolean;
|
|
224
|
+
sections: boolean;
|
|
225
|
+
contentControls: boolean;
|
|
158
226
|
},
|
|
159
227
|
): number {
|
|
160
228
|
let size = 0;
|
|
@@ -170,10 +238,14 @@ function measureParagraph(
|
|
|
170
238
|
function measureInlineNode(
|
|
171
239
|
node: InlineNode,
|
|
172
240
|
flags: {
|
|
241
|
+
paragraphs: boolean;
|
|
173
242
|
runs: boolean;
|
|
174
243
|
whitespace: boolean;
|
|
175
244
|
hyperlinks: boolean;
|
|
176
245
|
images: boolean;
|
|
246
|
+
tables: boolean;
|
|
247
|
+
sections: boolean;
|
|
248
|
+
contentControls: boolean;
|
|
177
249
|
},
|
|
178
250
|
): number {
|
|
179
251
|
switch (node.type) {
|
|
@@ -310,12 +382,12 @@ function collectSubPartFeatures(
|
|
|
310
382
|
Object.keys(subParts.footnoteCollection?.footnotes ?? {}).length +
|
|
311
383
|
Object.keys(subParts.footnoteCollection?.endnotes ?? {}).length;
|
|
312
384
|
|
|
313
|
-
if (hasHeaderFooterContent) {
|
|
385
|
+
if (hasHeaderFooterContent && !entries.some((entry) => entry.featureKey === "headers-footers-lossy")) {
|
|
314
386
|
entries.push({
|
|
315
387
|
featureEntryId: "feature:subparts:headers-footers",
|
|
316
388
|
featureKey: "headers-footers",
|
|
317
|
-
featureClass: "
|
|
318
|
-
message: "Headers and footers
|
|
389
|
+
featureClass: "supported-roundtrip",
|
|
390
|
+
message: "Headers and footers resolve as structured secondary stories with export-safe ownership.",
|
|
319
391
|
details: {
|
|
320
392
|
headerCount: subParts.headers?.length ?? 0,
|
|
321
393
|
footerCount: subParts.footers?.length ?? 0,
|
|
@@ -323,12 +395,12 @@ function collectSubPartFeatures(
|
|
|
323
395
|
});
|
|
324
396
|
}
|
|
325
397
|
|
|
326
|
-
if (noteCount > 0) {
|
|
398
|
+
if (noteCount > 0 && !entries.some((entry) => entry.featureKey === "notes-lossy")) {
|
|
327
399
|
entries.push({
|
|
328
400
|
featureEntryId: "feature:subparts:notes",
|
|
329
401
|
featureKey: "notes",
|
|
330
|
-
featureClass: "
|
|
331
|
-
message: "Footnotes and endnotes
|
|
402
|
+
featureClass: "supported-roundtrip",
|
|
403
|
+
message: "Footnotes and endnotes resolve as structured secondary stories with export-safe ownership.",
|
|
332
404
|
details: {
|
|
333
405
|
footnoteCount: Object.keys(subParts.footnoteCollection?.footnotes ?? {}).length,
|
|
334
406
|
endnoteCount: Object.keys(subParts.footnoteCollection?.endnotes ?? {}).length,
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CompatibilityReport,
|
|
3
|
+
DocumentNavigationSnapshot,
|
|
4
|
+
RuntimeRenderSnapshot,
|
|
5
|
+
} from "../api/public-types.ts";
|
|
6
|
+
import { parseCommentsFromOoxml } from "../io/ooxml/parse-comments.ts";
|
|
7
|
+
import { readOpcPackage } from "../io/opc/package-reader.ts";
|
|
8
|
+
|
|
9
|
+
export interface ClosureValidationContext {
|
|
10
|
+
renderSnapshot: Pick<
|
|
11
|
+
RuntimeRenderSnapshot,
|
|
12
|
+
| "isReady"
|
|
13
|
+
| "fatalError"
|
|
14
|
+
| "comments"
|
|
15
|
+
| "trackedChanges"
|
|
16
|
+
| "surface"
|
|
17
|
+
| "pageLayout"
|
|
18
|
+
>;
|
|
19
|
+
navigation?: Pick<DocumentNavigationSnapshot, "pages">;
|
|
20
|
+
compatibility: Pick<CompatibilityReport, "errors" | "featureEntries">;
|
|
21
|
+
surface: RuntimeRenderSnapshot["surface"];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ClosureValidationCheck =
|
|
25
|
+
| { type: "mustLoad" }
|
|
26
|
+
| { type: "compatibilityFeature"; featureKey: string; featureClass: string }
|
|
27
|
+
| { type: "minTrackedChanges"; count: number }
|
|
28
|
+
| { type: "maxTrackedChanges"; count: number }
|
|
29
|
+
| { type: "minCommentThreads"; count: number }
|
|
30
|
+
| { type: "lockedFragmentCountAtLeast"; count: number }
|
|
31
|
+
| { type: "surfaceBlockKind"; kind: string; count?: number }
|
|
32
|
+
| { type: "opaqueBlockLabelPrefix"; value: string }
|
|
33
|
+
| { type: "opaqueInlineLabelPrefix"; value: string }
|
|
34
|
+
| {
|
|
35
|
+
type: "secondaryStoryMatch";
|
|
36
|
+
kind: "header" | "footer" | "footnote" | "endnote";
|
|
37
|
+
relationshipId?: string;
|
|
38
|
+
variant?: "default" | "first" | "even";
|
|
39
|
+
noteId?: string;
|
|
40
|
+
minBlockCount?: number;
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
type: "pageLayoutMatch";
|
|
44
|
+
sectionIndex?: number;
|
|
45
|
+
orientation?: "portrait" | "landscape";
|
|
46
|
+
columns?: number;
|
|
47
|
+
differentFirstPage?: boolean;
|
|
48
|
+
differentOddEvenPages?: boolean;
|
|
49
|
+
headerVariantCountAtLeast?: number;
|
|
50
|
+
footerVariantCountAtLeast?: number;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
type: "commentThreadMatch";
|
|
54
|
+
commentId?: string;
|
|
55
|
+
status?: "open" | "resolved" | "detached";
|
|
56
|
+
entryCount?: number;
|
|
57
|
+
anchorKind?: "range" | "node" | "detached";
|
|
58
|
+
bodyIncludes?: string[];
|
|
59
|
+
entryBodies?: string[];
|
|
60
|
+
entryAuthorIds?: string[];
|
|
61
|
+
authorId?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export interface ClosureValidationResult {
|
|
65
|
+
type: string;
|
|
66
|
+
passed: boolean;
|
|
67
|
+
reason: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface DocxCommentProof {
|
|
71
|
+
partPresence: {
|
|
72
|
+
commentsXml: boolean;
|
|
73
|
+
commentsExtendedXml: boolean;
|
|
74
|
+
commentsIdsXml: boolean;
|
|
75
|
+
peopleXml: boolean;
|
|
76
|
+
};
|
|
77
|
+
serializedCommentIds: string[];
|
|
78
|
+
peopleAuthors: string[];
|
|
79
|
+
threads: Array<{
|
|
80
|
+
commentId: string;
|
|
81
|
+
rootCommentId?: string;
|
|
82
|
+
status: "open" | "resolved" | "detached";
|
|
83
|
+
entryCount: number;
|
|
84
|
+
bodies: string[];
|
|
85
|
+
authorIds: string[];
|
|
86
|
+
anchorKind: "range" | "node" | "detached";
|
|
87
|
+
}>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function evaluateClosureCheck(
|
|
91
|
+
check: ClosureValidationCheck,
|
|
92
|
+
context: ClosureValidationContext,
|
|
93
|
+
): ClosureValidationResult {
|
|
94
|
+
switch (check.type) {
|
|
95
|
+
case "mustLoad":
|
|
96
|
+
return {
|
|
97
|
+
type: check.type,
|
|
98
|
+
passed:
|
|
99
|
+
context.renderSnapshot.isReady &&
|
|
100
|
+
context.renderSnapshot.fatalError === undefined &&
|
|
101
|
+
context.compatibility.errors.length === 0,
|
|
102
|
+
reason: "exported artifact did not reload into a healthy runtime",
|
|
103
|
+
};
|
|
104
|
+
case "compatibilityFeature": {
|
|
105
|
+
const passed = context.compatibility.featureEntries.some(
|
|
106
|
+
(entry) =>
|
|
107
|
+
entry.featureKey === check.featureKey &&
|
|
108
|
+
entry.featureClass === check.featureClass,
|
|
109
|
+
);
|
|
110
|
+
return {
|
|
111
|
+
type: check.type,
|
|
112
|
+
passed,
|
|
113
|
+
reason: `missing compatibility feature ${check.featureKey}:${check.featureClass}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
case "minTrackedChanges":
|
|
117
|
+
return {
|
|
118
|
+
type: check.type,
|
|
119
|
+
passed: context.renderSnapshot.trackedChanges.totalCount >= check.count,
|
|
120
|
+
reason: `expected at least ${check.count} tracked changes`,
|
|
121
|
+
};
|
|
122
|
+
case "maxTrackedChanges":
|
|
123
|
+
return {
|
|
124
|
+
type: check.type,
|
|
125
|
+
passed: context.renderSnapshot.trackedChanges.totalCount <= check.count,
|
|
126
|
+
reason: `expected at most ${check.count} tracked changes`,
|
|
127
|
+
};
|
|
128
|
+
case "minCommentThreads":
|
|
129
|
+
return {
|
|
130
|
+
type: check.type,
|
|
131
|
+
passed: context.renderSnapshot.comments.totalCount >= check.count,
|
|
132
|
+
reason: `expected at least ${check.count} comment threads`,
|
|
133
|
+
};
|
|
134
|
+
case "lockedFragmentCountAtLeast":
|
|
135
|
+
return {
|
|
136
|
+
type: check.type,
|
|
137
|
+
passed: (context.surface?.lockedFragmentIds.length ?? 0) >= check.count,
|
|
138
|
+
reason: `expected at least ${check.count} locked fragments`,
|
|
139
|
+
};
|
|
140
|
+
case "surfaceBlockKind":
|
|
141
|
+
return {
|
|
142
|
+
type: check.type,
|
|
143
|
+
passed:
|
|
144
|
+
(context.surface?.blocks.filter((block) => block.kind === check.kind).length ?? 0) >=
|
|
145
|
+
(check.count ?? 1),
|
|
146
|
+
reason: `expected at least ${check.count ?? 1} surface blocks of kind ${check.kind}`,
|
|
147
|
+
};
|
|
148
|
+
case "opaqueBlockLabelPrefix":
|
|
149
|
+
return {
|
|
150
|
+
type: check.type,
|
|
151
|
+
passed:
|
|
152
|
+
context.surface?.blocks.some(
|
|
153
|
+
(block) =>
|
|
154
|
+
block.kind === "opaque_block" &&
|
|
155
|
+
typeof block.label === "string" &&
|
|
156
|
+
block.label.startsWith(check.value),
|
|
157
|
+
) ?? false,
|
|
158
|
+
reason: `expected an opaque block label starting with ${check.value}`,
|
|
159
|
+
};
|
|
160
|
+
case "opaqueInlineLabelPrefix":
|
|
161
|
+
return {
|
|
162
|
+
type: check.type,
|
|
163
|
+
passed: hasOpaqueInlineLabelPrefix(context.surface, check.value),
|
|
164
|
+
reason: `expected an opaque inline label starting with ${check.value}`,
|
|
165
|
+
};
|
|
166
|
+
case "secondaryStoryMatch": {
|
|
167
|
+
const passed =
|
|
168
|
+
context.surface?.secondaryStories.some((story) => {
|
|
169
|
+
if (story.target.kind !== check.kind) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (
|
|
173
|
+
"relationshipId" in story.target &&
|
|
174
|
+
check.relationshipId &&
|
|
175
|
+
story.target.relationshipId !== check.relationshipId
|
|
176
|
+
) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if ("variant" in story.target && check.variant && story.target.variant !== check.variant) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if ("noteId" in story.target && check.noteId && story.target.noteId !== check.noteId) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
if ((check.minBlockCount ?? 0) > story.blocks.length) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}) ?? false;
|
|
190
|
+
return {
|
|
191
|
+
type: check.type,
|
|
192
|
+
passed,
|
|
193
|
+
reason: `expected secondary story ${describeSecondaryStoryMatch(check)}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
case "pageLayoutMatch": {
|
|
197
|
+
const layouts = getLayoutsForCheck(context, check);
|
|
198
|
+
const headerVariantCount = countUniqueStoryVariants(layouts, "headerVariants");
|
|
199
|
+
const footerVariantCount = countUniqueStoryVariants(layouts, "footerVariants");
|
|
200
|
+
const passed = Boolean(
|
|
201
|
+
layouts.length > 0 &&
|
|
202
|
+
layouts.some(
|
|
203
|
+
(layout) =>
|
|
204
|
+
(check.orientation === undefined || layout.orientation === check.orientation) &&
|
|
205
|
+
(check.columns === undefined || layout.columns === check.columns) &&
|
|
206
|
+
(check.differentFirstPage === undefined ||
|
|
207
|
+
layout.differentFirstPage === check.differentFirstPage) &&
|
|
208
|
+
(check.differentOddEvenPages === undefined ||
|
|
209
|
+
layout.differentOddEvenPages === check.differentOddEvenPages),
|
|
210
|
+
) &&
|
|
211
|
+
(check.headerVariantCountAtLeast === undefined ||
|
|
212
|
+
headerVariantCount >= check.headerVariantCountAtLeast) &&
|
|
213
|
+
(check.footerVariantCountAtLeast === undefined ||
|
|
214
|
+
footerVariantCount >= check.footerVariantCountAtLeast)
|
|
215
|
+
);
|
|
216
|
+
return {
|
|
217
|
+
type: check.type,
|
|
218
|
+
passed,
|
|
219
|
+
reason: `expected page layout ${describePageLayoutMatch(check)}`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
case "commentThreadMatch": {
|
|
223
|
+
const passed = context.renderSnapshot.comments.threads.some((thread) =>
|
|
224
|
+
matchesCommentThread(thread, check),
|
|
225
|
+
);
|
|
226
|
+
return {
|
|
227
|
+
type: check.type,
|
|
228
|
+
passed,
|
|
229
|
+
reason: `expected a comment thread matching ${describeCommentThreadMatch(check)}`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
default:
|
|
233
|
+
return {
|
|
234
|
+
type: (check as { type: string }).type,
|
|
235
|
+
passed: false,
|
|
236
|
+
reason: `unknown closure validation check type ${(check as { type: string }).type}`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function extractDocxCommentProof(bytes: Uint8Array): DocxCommentProof {
|
|
242
|
+
const packageFile = readOpcPackage(bytes);
|
|
243
|
+
const documentXml = decodePartText(packageFile.parts.get("/word/document.xml")?.bytes);
|
|
244
|
+
const commentsXml = decodePartText(packageFile.parts.get("/word/comments.xml")?.bytes);
|
|
245
|
+
const commentsExtendedXml = decodePartText(
|
|
246
|
+
packageFile.parts.get("/word/commentsExtended.xml")?.bytes,
|
|
247
|
+
);
|
|
248
|
+
const commentsIdsXml = decodePartText(packageFile.parts.get("/word/commentsIds.xml")?.bytes);
|
|
249
|
+
const peopleXml = decodePartText(packageFile.parts.get("/word/people.xml")?.bytes);
|
|
250
|
+
const parsedComments =
|
|
251
|
+
documentXml && commentsXml
|
|
252
|
+
? parseCommentsFromOoxml(documentXml, {
|
|
253
|
+
commentsXml,
|
|
254
|
+
commentsExtendedXml,
|
|
255
|
+
commentsIdsXml,
|
|
256
|
+
peopleXml,
|
|
257
|
+
})
|
|
258
|
+
: {
|
|
259
|
+
threads: [],
|
|
260
|
+
definitions: [],
|
|
261
|
+
peopleAuthors: [],
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
partPresence: {
|
|
266
|
+
commentsXml: commentsXml.length > 0,
|
|
267
|
+
commentsExtendedXml: commentsExtendedXml.length > 0,
|
|
268
|
+
commentsIdsXml: commentsIdsXml.length > 0,
|
|
269
|
+
peopleXml: peopleXml.length > 0,
|
|
270
|
+
},
|
|
271
|
+
serializedCommentIds: parsedComments.definitions.map((definition) => definition.commentId),
|
|
272
|
+
peopleAuthors: [...parsedComments.peopleAuthors],
|
|
273
|
+
threads: parsedComments.threads.map((thread) => ({
|
|
274
|
+
commentId: thread.commentId,
|
|
275
|
+
rootCommentId: thread.metadata?.rootOoxmlCommentId,
|
|
276
|
+
status: thread.status,
|
|
277
|
+
entryCount: thread.entries.length,
|
|
278
|
+
bodies: thread.entries.map((entry) => entry.body),
|
|
279
|
+
authorIds: [...new Set(thread.entries.map((entry) => entry.authorId))],
|
|
280
|
+
anchorKind: thread.anchor.kind,
|
|
281
|
+
})),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function hasOpaqueInlineLabelPrefix(
|
|
286
|
+
surface: RuntimeRenderSnapshot["surface"],
|
|
287
|
+
prefix: string,
|
|
288
|
+
): boolean {
|
|
289
|
+
if (!surface) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const block of surface.blocks) {
|
|
294
|
+
if (block.kind !== "paragraph") {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (
|
|
299
|
+
block.segments.some(
|
|
300
|
+
(segment) =>
|
|
301
|
+
segment.kind === "opaque_inline" &&
|
|
302
|
+
typeof segment.label === "string" &&
|
|
303
|
+
segment.label.startsWith(prefix),
|
|
304
|
+
)
|
|
305
|
+
) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function matchesCommentThread(
|
|
314
|
+
thread: RuntimeRenderSnapshot["comments"]["threads"][number],
|
|
315
|
+
check: Extract<ClosureValidationCheck, { type: "commentThreadMatch" }>,
|
|
316
|
+
): boolean {
|
|
317
|
+
if (check.commentId && thread.commentId !== check.commentId) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
if (check.status && thread.status !== check.status) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
if (typeof check.entryCount === "number" && thread.entries.length !== check.entryCount) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
if (check.anchorKind && thread.anchor.kind !== check.anchorKind) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
if (check.authorId && thread.createdBy !== check.authorId) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
if (check.bodyIncludes?.length) {
|
|
333
|
+
const bodies = thread.entries.map((entry) => entry.body);
|
|
334
|
+
for (const expectedBody of check.bodyIncludes) {
|
|
335
|
+
if (!bodies.some((body) => body.includes(expectedBody))) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (check.entryBodies?.length) {
|
|
341
|
+
if (thread.entries.length < check.entryBodies.length) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
for (let index = 0; index < check.entryBodies.length; index += 1) {
|
|
345
|
+
const expectedBody = check.entryBodies[index];
|
|
346
|
+
const actualBody = thread.entries[index]?.body;
|
|
347
|
+
if (typeof expectedBody !== "string" || actualBody !== expectedBody) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (check.entryAuthorIds?.length) {
|
|
353
|
+
if (thread.entries.length < check.entryAuthorIds.length) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
for (let index = 0; index < check.entryAuthorIds.length; index += 1) {
|
|
357
|
+
const expectedAuthorId = check.entryAuthorIds[index];
|
|
358
|
+
const actualAuthorId = thread.entries[index]?.authorId;
|
|
359
|
+
if (typeof expectedAuthorId !== "string" || actualAuthorId !== expectedAuthorId) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function getLayoutsForCheck(
|
|
368
|
+
context: ClosureValidationContext,
|
|
369
|
+
check: Extract<ClosureValidationCheck, { type: "pageLayoutMatch" }>,
|
|
370
|
+
) {
|
|
371
|
+
const navigationLayouts =
|
|
372
|
+
context.navigation?.pages
|
|
373
|
+
.filter((page) => check.sectionIndex === undefined || page.sectionIndex === check.sectionIndex)
|
|
374
|
+
.map((page) => page.layout) ?? [];
|
|
375
|
+
if (navigationLayouts.length > 0) {
|
|
376
|
+
return navigationLayouts;
|
|
377
|
+
}
|
|
378
|
+
const activeLayout = context.renderSnapshot.pageLayout;
|
|
379
|
+
if (
|
|
380
|
+
activeLayout &&
|
|
381
|
+
(check.sectionIndex === undefined || activeLayout.sectionIndex === check.sectionIndex)
|
|
382
|
+
) {
|
|
383
|
+
return [activeLayout];
|
|
384
|
+
}
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function countUniqueStoryVariants(
|
|
389
|
+
layouts: Array<{
|
|
390
|
+
headerVariants: Array<{ variant: string; relationshipId: string }>;
|
|
391
|
+
footerVariants: Array<{ variant: string; relationshipId: string }>;
|
|
392
|
+
}>,
|
|
393
|
+
key: "headerVariants" | "footerVariants",
|
|
394
|
+
) {
|
|
395
|
+
const variants = new Set<string>();
|
|
396
|
+
for (const layout of layouts) {
|
|
397
|
+
for (const variant of layout[key]) {
|
|
398
|
+
variants.add(`${variant.variant}:${variant.relationshipId}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return variants.size;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function describeCommentThreadMatch(
|
|
405
|
+
check: Extract<ClosureValidationCheck, { type: "commentThreadMatch" }>,
|
|
406
|
+
): string {
|
|
407
|
+
const parts: string[] = [];
|
|
408
|
+
if (check.commentId) {
|
|
409
|
+
parts.push(`commentId=${check.commentId}`);
|
|
410
|
+
}
|
|
411
|
+
if (check.status) {
|
|
412
|
+
parts.push(`status=${check.status}`);
|
|
413
|
+
}
|
|
414
|
+
if (typeof check.entryCount === "number") {
|
|
415
|
+
parts.push(`entryCount=${check.entryCount}`);
|
|
416
|
+
}
|
|
417
|
+
if (check.anchorKind) {
|
|
418
|
+
parts.push(`anchorKind=${check.anchorKind}`);
|
|
419
|
+
}
|
|
420
|
+
if (check.authorId) {
|
|
421
|
+
parts.push(`authorId=${check.authorId}`);
|
|
422
|
+
}
|
|
423
|
+
if (check.bodyIncludes?.length) {
|
|
424
|
+
parts.push(`bodyIncludes=${check.bodyIncludes.join(" | ")}`);
|
|
425
|
+
}
|
|
426
|
+
if (check.entryBodies?.length) {
|
|
427
|
+
parts.push(`entryBodies=${check.entryBodies.join(" | ")}`);
|
|
428
|
+
}
|
|
429
|
+
if (check.entryAuthorIds?.length) {
|
|
430
|
+
parts.push(`entryAuthorIds=${check.entryAuthorIds.join(" | ")}`);
|
|
431
|
+
}
|
|
432
|
+
return parts.length > 0 ? parts.join(", ") : "any thread";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function describeSecondaryStoryMatch(
|
|
436
|
+
check: Extract<ClosureValidationCheck, { type: "secondaryStoryMatch" }>,
|
|
437
|
+
): string {
|
|
438
|
+
const parts = [`kind=${check.kind}`];
|
|
439
|
+
if (check.relationshipId) {
|
|
440
|
+
parts.push(`relationshipId=${check.relationshipId}`);
|
|
441
|
+
}
|
|
442
|
+
if (check.variant) {
|
|
443
|
+
parts.push(`variant=${check.variant}`);
|
|
444
|
+
}
|
|
445
|
+
if (check.noteId) {
|
|
446
|
+
parts.push(`noteId=${check.noteId}`);
|
|
447
|
+
}
|
|
448
|
+
if (typeof check.minBlockCount === "number") {
|
|
449
|
+
parts.push(`minBlockCount=${check.minBlockCount}`);
|
|
450
|
+
}
|
|
451
|
+
return parts.join(", ");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function describePageLayoutMatch(
|
|
455
|
+
check: Extract<ClosureValidationCheck, { type: "pageLayoutMatch" }>,
|
|
456
|
+
): string {
|
|
457
|
+
const parts: string[] = [];
|
|
458
|
+
if (typeof check.sectionIndex === "number") {
|
|
459
|
+
parts.push(`sectionIndex=${check.sectionIndex}`);
|
|
460
|
+
}
|
|
461
|
+
if (check.orientation) {
|
|
462
|
+
parts.push(`orientation=${check.orientation}`);
|
|
463
|
+
}
|
|
464
|
+
if (typeof check.columns === "number") {
|
|
465
|
+
parts.push(`columns=${check.columns}`);
|
|
466
|
+
}
|
|
467
|
+
if (typeof check.differentFirstPage === "boolean") {
|
|
468
|
+
parts.push(`differentFirstPage=${check.differentFirstPage}`);
|
|
469
|
+
}
|
|
470
|
+
if (typeof check.differentOddEvenPages === "boolean") {
|
|
471
|
+
parts.push(`differentOddEvenPages=${check.differentOddEvenPages}`);
|
|
472
|
+
}
|
|
473
|
+
if (typeof check.headerVariantCountAtLeast === "number") {
|
|
474
|
+
parts.push(`headerVariantCountAtLeast=${check.headerVariantCountAtLeast}`);
|
|
475
|
+
}
|
|
476
|
+
if (typeof check.footerVariantCountAtLeast === "number") {
|
|
477
|
+
parts.push(`footerVariantCountAtLeast=${check.footerVariantCountAtLeast}`);
|
|
478
|
+
}
|
|
479
|
+
return parts.length > 0 ? parts.join(", ") : "present";
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function decodePartText(bytes: Uint8Array | undefined): string {
|
|
483
|
+
if (!bytes) {
|
|
484
|
+
return "";
|
|
485
|
+
}
|
|
486
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
487
|
+
}
|