@beyondwork/docx-react-component 1.0.101 → 1.0.103
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/package.json +1 -1
- package/src/core/commands/formatting-commands.ts +8 -7
- package/src/core/commands/paragraph-layout-commands.ts +11 -10
- package/src/core/commands/section-layout-commands.ts +7 -6
- package/src/core/commands/style-commands.ts +3 -2
- package/src/io/export/build-app-properties-xml.ts +24 -0
- package/src/io/normalize/normalize-text.ts +6 -5
- package/src/io/ooxml/docprops.ts +298 -0
- package/src/io/ooxml/parse-anchor.ts +15 -15
- package/src/io/ooxml/parse-drawing.ts +5 -5
- package/src/io/ooxml/parse-fields.ts +16 -15
- package/src/io/ooxml/parse-font-table.ts +2 -1
- package/src/io/ooxml/parse-footnotes.ts +3 -2
- package/src/io/ooxml/parse-headers-footers.ts +7 -6
- package/src/io/ooxml/parse-main-document.ts +41 -40
- package/src/io/ooxml/parse-numbering.ts +3 -2
- package/src/io/ooxml/parse-object.ts +6 -6
- package/src/io/ooxml/parse-paragraph-formatting.ts +12 -11
- package/src/io/ooxml/parse-picture.ts +16 -16
- package/src/io/ooxml/parse-run-formatting.ts +11 -10
- package/src/io/ooxml/parse-settings.ts +2 -1
- package/src/io/ooxml/parse-shapes.ts +18 -17
- package/src/io/ooxml/parse-styles.ts +16 -16
- package/src/io/ooxml/parse-theme.ts +5 -4
- package/src/model/canonical-document.ts +920 -815
- package/src/runtime/formatting/document-lookup.ts +3 -2
- package/src/runtime/formatting/formatting-context.ts +66 -25
- package/src/runtime/formatting/index.ts +18 -0
- package/src/runtime/formatting/layout-inputs.ts +256 -0
- package/src/runtime/formatting/numbering/geometry.ts +13 -12
- package/src/runtime/formatting/style-cascade.ts +2 -1
- package/src/runtime/formatting/table-style-resolver.ts +8 -7
- package/src/runtime/surface-projection.ts +31 -36
- package/src/session/export/stateful-export-pipeline.ts +9 -4
- package/src/session/export/stateful-export.ts +22 -6
- package/src/session/import/canonical-assembly.ts +2 -3
- package/src/session/import/loader-types.ts +3 -1
- package/src/session/import/loader.ts +12 -0
- package/src/session/import/normalize.ts +2 -1
- package/src/session/import/source-package-evidence.ts +1016 -0
- package/src/session/shared/session-utils.ts +9 -0
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
import type { OpcPackage } from "../../io/opc/package-reader.ts";
|
|
2
|
+
import { readOpcPackage } from "../../io/opc/package-reader.ts";
|
|
3
|
+
import { sha256Hex } from "../../io/source-package-provenance.ts";
|
|
4
|
+
import {
|
|
5
|
+
normalizePartPath,
|
|
6
|
+
resolveRelationshipTarget,
|
|
7
|
+
type OpcPackagePart,
|
|
8
|
+
type OpcRelationship,
|
|
9
|
+
} from "../../io/ooxml/part-manifest.ts";
|
|
10
|
+
import { parseXml, parseXmlWithOffsets } from "../../io/ooxml/xml-parser.ts";
|
|
11
|
+
import type { XmlElementNode } from "../../io/ooxml/xml-element.ts";
|
|
12
|
+
import { localName } from "../../io/ooxml/xml-attr-helpers.ts";
|
|
13
|
+
import {
|
|
14
|
+
resolveMainDocumentPartPath,
|
|
15
|
+
} from "./part-discovery.ts";
|
|
16
|
+
|
|
17
|
+
export interface SourcePackageElementCounts {
|
|
18
|
+
tables: number;
|
|
19
|
+
tableRows: number;
|
|
20
|
+
tableCells: number;
|
|
21
|
+
anchors: number;
|
|
22
|
+
inlineDrawings: number;
|
|
23
|
+
drawings: number;
|
|
24
|
+
vmlShapes: number;
|
|
25
|
+
contentControls: number;
|
|
26
|
+
fieldMarkers: number;
|
|
27
|
+
fieldInstructions: number;
|
|
28
|
+
simpleFields: number;
|
|
29
|
+
bookmarks: number;
|
|
30
|
+
footnotes: number;
|
|
31
|
+
endnotes: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SourcePackagePartEvidence {
|
|
35
|
+
path: string;
|
|
36
|
+
storyKind: SourcePackageStoryKind;
|
|
37
|
+
contentType: string | null;
|
|
38
|
+
byteLength: number;
|
|
39
|
+
relationshipCount: number;
|
|
40
|
+
counts: SourcePackageElementCounts;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SourcePackageRelationshipEvidence {
|
|
44
|
+
sourcePartPath: string | null;
|
|
45
|
+
relationshipId: string;
|
|
46
|
+
relationshipType: string;
|
|
47
|
+
targetMode: OpcRelationship["targetMode"];
|
|
48
|
+
target: string;
|
|
49
|
+
resolvedTarget: string;
|
|
50
|
+
targetExists: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SourcePackageLocationEvidence {
|
|
54
|
+
sourceId: string;
|
|
55
|
+
partPath: string;
|
|
56
|
+
storyKind: SourcePackageStoryKind;
|
|
57
|
+
element: string;
|
|
58
|
+
ordinal: number;
|
|
59
|
+
startOffset?: number;
|
|
60
|
+
endOffset?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SourcePackageStoryPartDescriptor {
|
|
64
|
+
sourceId: string;
|
|
65
|
+
partPath: string;
|
|
66
|
+
storyKind: SourcePackageStoryKind;
|
|
67
|
+
relationshipId?: string;
|
|
68
|
+
relationshipType?: string;
|
|
69
|
+
relationshipSourcePartPath?: string | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SourcePackageHeaderFooterReferenceEvidence {
|
|
73
|
+
kind: "header" | "footer";
|
|
74
|
+
relationshipId: string;
|
|
75
|
+
type?: string;
|
|
76
|
+
resolvedTarget?: string;
|
|
77
|
+
targetExists?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SourcePackageSectionDescriptorEvidence
|
|
81
|
+
extends SourcePackageLocationEvidence {
|
|
82
|
+
headerFooterReferences: SourcePackageHeaderFooterReferenceEvidence[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SourcePackageObjectExtentEvidence
|
|
86
|
+
extends SourcePackageLocationEvidence {
|
|
87
|
+
kind: "drawing" | "vml-shape" | "object";
|
|
88
|
+
display: "inline" | "floating" | "unknown";
|
|
89
|
+
preserveOnly: true;
|
|
90
|
+
fallbackHint:
|
|
91
|
+
| "drawing-inline"
|
|
92
|
+
| "drawing-floating"
|
|
93
|
+
| "vml-shape"
|
|
94
|
+
| "ole-object"
|
|
95
|
+
| "opaque-object";
|
|
96
|
+
extentEmu?: { width: number; height: number };
|
|
97
|
+
extentTwips?: { width: number; height: number };
|
|
98
|
+
relationshipIds: string[];
|
|
99
|
+
shapeId?: string;
|
|
100
|
+
shapeType?: string;
|
|
101
|
+
vmlStyle?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface SourcePackageRelationshipGroupEvidence {
|
|
105
|
+
headerFooters: SourcePackageRelationshipEvidence[];
|
|
106
|
+
media: SourcePackageRelationshipEvidence[];
|
|
107
|
+
charts: SourcePackageRelationshipEvidence[];
|
|
108
|
+
embedded: SourcePackageRelationshipEvidence[];
|
|
109
|
+
customXml: SourcePackageRelationshipEvidence[];
|
|
110
|
+
settings: SourcePackageRelationshipEvidence[];
|
|
111
|
+
numbering: SourcePackageRelationshipEvidence[];
|
|
112
|
+
comments: SourcePackageRelationshipEvidence[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface SourcePackagePartGroupEvidence {
|
|
116
|
+
media: SourcePackageStoryPartDescriptor[];
|
|
117
|
+
images: SourcePackageStoryPartDescriptor[];
|
|
118
|
+
charts: SourcePackageStoryPartDescriptor[];
|
|
119
|
+
embedded: SourcePackageStoryPartDescriptor[];
|
|
120
|
+
customXml: SourcePackageStoryPartDescriptor[];
|
|
121
|
+
settings: SourcePackageStoryPartDescriptor[];
|
|
122
|
+
numbering: SourcePackageStoryPartDescriptor[];
|
|
123
|
+
comments: SourcePackageStoryPartDescriptor[];
|
|
124
|
+
themes: SourcePackageStoryPartDescriptor[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface SourcePackageLayoutInventoryEvidence {
|
|
128
|
+
storyParts: SourcePackageStoryPartDescriptor[];
|
|
129
|
+
partGroups: SourcePackagePartGroupEvidence;
|
|
130
|
+
relationships: SourcePackageRelationshipGroupEvidence;
|
|
131
|
+
sections: SourcePackageSectionDescriptorEvidence[];
|
|
132
|
+
tables: SourcePackageLocationEvidence[];
|
|
133
|
+
fields: SourcePackageLocationEvidence[];
|
|
134
|
+
drawings: SourcePackageLocationEvidence[];
|
|
135
|
+
vmlShapes: SourcePackageLocationEvidence[];
|
|
136
|
+
contentControls: SourcePackageLocationEvidence[];
|
|
137
|
+
bookmarks: SourcePackageLocationEvidence[];
|
|
138
|
+
notes: SourcePackageLocationEvidence[];
|
|
139
|
+
objects: SourcePackageObjectExtentEvidence[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface SourcePackageEvidence {
|
|
143
|
+
schemaVersion: 1;
|
|
144
|
+
docId: string;
|
|
145
|
+
sourceLabel?: string;
|
|
146
|
+
sha256Hex: string;
|
|
147
|
+
sourceByteLength: number;
|
|
148
|
+
mainDocumentPartPath?: string;
|
|
149
|
+
partCount: number;
|
|
150
|
+
contentPartCount: number;
|
|
151
|
+
relationshipCount: number;
|
|
152
|
+
internalRelationshipCount: number;
|
|
153
|
+
externalRelationshipCount: number;
|
|
154
|
+
brokenInternalRelationshipCount: number;
|
|
155
|
+
counts: SourcePackageElementCounts & {
|
|
156
|
+
headers: number;
|
|
157
|
+
footers: number;
|
|
158
|
+
mediaParts: number;
|
|
159
|
+
imageParts: number;
|
|
160
|
+
chartParts: number;
|
|
161
|
+
embeddedParts: number;
|
|
162
|
+
customXmlParts: number;
|
|
163
|
+
commentsParts: number;
|
|
164
|
+
numberingParts: number;
|
|
165
|
+
settingsParts: number;
|
|
166
|
+
themeParts: number;
|
|
167
|
+
};
|
|
168
|
+
parts: SourcePackagePartEvidence[];
|
|
169
|
+
relationships: SourcePackageRelationshipEvidence[];
|
|
170
|
+
layoutInventory: SourcePackageLayoutInventoryEvidence;
|
|
171
|
+
parseErrors: Array<{ partPath: string; message: string }>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type SourcePackageStoryKind =
|
|
175
|
+
| "main-document"
|
|
176
|
+
| "header"
|
|
177
|
+
| "footer"
|
|
178
|
+
| "footnotes"
|
|
179
|
+
| "endnotes"
|
|
180
|
+
| "comments"
|
|
181
|
+
| "numbering"
|
|
182
|
+
| "settings"
|
|
183
|
+
| "styles"
|
|
184
|
+
| "font-table"
|
|
185
|
+
| "theme"
|
|
186
|
+
| "chart"
|
|
187
|
+
| "media"
|
|
188
|
+
| "embedded"
|
|
189
|
+
| "custom-xml"
|
|
190
|
+
| "relationships"
|
|
191
|
+
| "content-types"
|
|
192
|
+
| "other";
|
|
193
|
+
|
|
194
|
+
const ZERO_COUNTS: SourcePackageElementCounts = {
|
|
195
|
+
tables: 0,
|
|
196
|
+
tableRows: 0,
|
|
197
|
+
tableCells: 0,
|
|
198
|
+
anchors: 0,
|
|
199
|
+
inlineDrawings: 0,
|
|
200
|
+
drawings: 0,
|
|
201
|
+
vmlShapes: 0,
|
|
202
|
+
contentControls: 0,
|
|
203
|
+
fieldMarkers: 0,
|
|
204
|
+
fieldInstructions: 0,
|
|
205
|
+
simpleFields: 0,
|
|
206
|
+
bookmarks: 0,
|
|
207
|
+
footnotes: 0,
|
|
208
|
+
endnotes: 0,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export function createSourcePackageEvidenceFromBytes(
|
|
212
|
+
input: {
|
|
213
|
+
bytes: Uint8Array | ArrayBuffer;
|
|
214
|
+
docId: string;
|
|
215
|
+
sourceLabel?: string;
|
|
216
|
+
},
|
|
217
|
+
): SourcePackageEvidence {
|
|
218
|
+
const bytes = input.bytes instanceof Uint8Array
|
|
219
|
+
? input.bytes
|
|
220
|
+
: new Uint8Array(input.bytes);
|
|
221
|
+
return createSourcePackageEvidence(readOpcPackage(bytes), {
|
|
222
|
+
docId: input.docId,
|
|
223
|
+
sourceLabel: input.sourceLabel,
|
|
224
|
+
sha256Hex: sha256Hex(bytes),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function createSourcePackageEvidence(
|
|
229
|
+
sourcePackage: OpcPackage,
|
|
230
|
+
input: {
|
|
231
|
+
docId: string;
|
|
232
|
+
sourceLabel?: string;
|
|
233
|
+
sha256Hex?: string;
|
|
234
|
+
},
|
|
235
|
+
): SourcePackageEvidence {
|
|
236
|
+
const mainDocumentPartPath = resolveMainDocumentPartPath(sourcePackage);
|
|
237
|
+
const parseErrors: SourcePackageEvidence["parseErrors"] = [];
|
|
238
|
+
const parts = [...sourcePackage.parts.values()]
|
|
239
|
+
.sort((left, right) => left.path.localeCompare(right.path))
|
|
240
|
+
.map((part) => summarizePart(part, mainDocumentPartPath, parseErrors));
|
|
241
|
+
const relationships = collectRelationships(sourcePackage);
|
|
242
|
+
const relationshipCount = relationships.length;
|
|
243
|
+
const internalRelationshipCount = relationships.filter(
|
|
244
|
+
(relationship) => relationship.targetMode === "internal",
|
|
245
|
+
).length;
|
|
246
|
+
const externalRelationshipCount = relationshipCount - internalRelationshipCount;
|
|
247
|
+
const brokenInternalRelationshipCount = relationships.filter(
|
|
248
|
+
(relationship) =>
|
|
249
|
+
relationship.targetMode === "internal" && !relationship.targetExists,
|
|
250
|
+
).length;
|
|
251
|
+
const contentParts = parts.filter(
|
|
252
|
+
(part) => part.storyKind !== "relationships" && part.storyKind !== "content-types",
|
|
253
|
+
);
|
|
254
|
+
const aggregateCounts = aggregateElementCounts(parts.map((part) => part.counts));
|
|
255
|
+
const layoutInventory = collectLayoutInventory(
|
|
256
|
+
sourcePackage,
|
|
257
|
+
parts,
|
|
258
|
+
relationships,
|
|
259
|
+
mainDocumentPartPath,
|
|
260
|
+
parseErrors,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
schemaVersion: 1,
|
|
265
|
+
docId: input.docId,
|
|
266
|
+
...(input.sourceLabel !== undefined ? { sourceLabel: input.sourceLabel } : {}),
|
|
267
|
+
sha256Hex: input.sha256Hex ?? "",
|
|
268
|
+
sourceByteLength: sourcePackage.sourceByteLength,
|
|
269
|
+
...(mainDocumentPartPath !== undefined ? { mainDocumentPartPath } : {}),
|
|
270
|
+
partCount: sourcePackage.parts.size,
|
|
271
|
+
contentPartCount: contentParts.length,
|
|
272
|
+
relationshipCount,
|
|
273
|
+
internalRelationshipCount,
|
|
274
|
+
externalRelationshipCount,
|
|
275
|
+
brokenInternalRelationshipCount,
|
|
276
|
+
counts: {
|
|
277
|
+
...aggregateCounts,
|
|
278
|
+
headers: countParts(parts, "header"),
|
|
279
|
+
footers: countParts(parts, "footer"),
|
|
280
|
+
mediaParts: countParts(parts, "media"),
|
|
281
|
+
imageParts: parts.filter((part) => part.contentType?.startsWith("image/")).length,
|
|
282
|
+
chartParts: countParts(parts, "chart"),
|
|
283
|
+
embeddedParts: countParts(parts, "embedded"),
|
|
284
|
+
customXmlParts: countParts(parts, "custom-xml"),
|
|
285
|
+
commentsParts: countParts(parts, "comments"),
|
|
286
|
+
numberingParts: countParts(parts, "numbering"),
|
|
287
|
+
settingsParts: countParts(parts, "settings"),
|
|
288
|
+
themeParts: countParts(parts, "theme"),
|
|
289
|
+
},
|
|
290
|
+
parts,
|
|
291
|
+
relationships,
|
|
292
|
+
layoutInventory,
|
|
293
|
+
parseErrors,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function summarizePart(
|
|
298
|
+
part: OpcPackagePart,
|
|
299
|
+
mainDocumentPartPath: string | undefined,
|
|
300
|
+
parseErrors: SourcePackageEvidence["parseErrors"],
|
|
301
|
+
): SourcePackagePartEvidence {
|
|
302
|
+
const storyKind = classifyPart(part, mainDocumentPartPath);
|
|
303
|
+
return {
|
|
304
|
+
path: part.path,
|
|
305
|
+
storyKind,
|
|
306
|
+
contentType: part.contentType,
|
|
307
|
+
byteLength: part.bytes.byteLength,
|
|
308
|
+
relationshipCount: part.relationships.length,
|
|
309
|
+
counts: countPartElements(part, storyKind, parseErrors),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function countPartElements(
|
|
314
|
+
part: OpcPackagePart,
|
|
315
|
+
storyKind: SourcePackageStoryKind,
|
|
316
|
+
parseErrors: SourcePackageEvidence["parseErrors"],
|
|
317
|
+
): SourcePackageElementCounts {
|
|
318
|
+
if (!shouldParseXmlPart(part, storyKind)) {
|
|
319
|
+
return { ...ZERO_COUNTS };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
return countElements(parseXml(new TextDecoder("utf-8").decode(part.bytes)));
|
|
324
|
+
} catch (error) {
|
|
325
|
+
parseErrors.push({
|
|
326
|
+
partPath: part.path,
|
|
327
|
+
message: error instanceof Error ? error.message : String(error),
|
|
328
|
+
});
|
|
329
|
+
return { ...ZERO_COUNTS };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function shouldParseXmlPart(
|
|
334
|
+
part: OpcPackagePart,
|
|
335
|
+
storyKind: SourcePackageStoryKind,
|
|
336
|
+
): boolean {
|
|
337
|
+
if (part.surfaceKind !== "content") {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
if (storyKind === "media" || storyKind === "embedded") {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
return (
|
|
344
|
+
part.contentType?.includes("xml") === true ||
|
|
345
|
+
part.path.endsWith(".xml")
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function countElements(root: XmlElementNode): SourcePackageElementCounts {
|
|
350
|
+
const counts = { ...ZERO_COUNTS };
|
|
351
|
+
const visit = (node: XmlElementNode): void => {
|
|
352
|
+
const name = localName(node.name);
|
|
353
|
+
switch (name) {
|
|
354
|
+
case "tbl":
|
|
355
|
+
counts.tables += 1;
|
|
356
|
+
break;
|
|
357
|
+
case "tr":
|
|
358
|
+
counts.tableRows += 1;
|
|
359
|
+
break;
|
|
360
|
+
case "tc":
|
|
361
|
+
counts.tableCells += 1;
|
|
362
|
+
break;
|
|
363
|
+
case "anchor":
|
|
364
|
+
counts.anchors += 1;
|
|
365
|
+
break;
|
|
366
|
+
case "inline":
|
|
367
|
+
counts.inlineDrawings += 1;
|
|
368
|
+
break;
|
|
369
|
+
case "drawing":
|
|
370
|
+
counts.drawings += 1;
|
|
371
|
+
break;
|
|
372
|
+
case "shape":
|
|
373
|
+
counts.vmlShapes += 1;
|
|
374
|
+
break;
|
|
375
|
+
case "sdt":
|
|
376
|
+
counts.contentControls += 1;
|
|
377
|
+
break;
|
|
378
|
+
case "fldChar":
|
|
379
|
+
counts.fieldMarkers += 1;
|
|
380
|
+
break;
|
|
381
|
+
case "instrText":
|
|
382
|
+
counts.fieldInstructions += 1;
|
|
383
|
+
break;
|
|
384
|
+
case "fldSimple":
|
|
385
|
+
counts.simpleFields += 1;
|
|
386
|
+
break;
|
|
387
|
+
case "bookmarkStart":
|
|
388
|
+
counts.bookmarks += 1;
|
|
389
|
+
break;
|
|
390
|
+
case "footnote":
|
|
391
|
+
counts.footnotes += 1;
|
|
392
|
+
break;
|
|
393
|
+
case "endnote":
|
|
394
|
+
counts.endnotes += 1;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for (const child of node.children) {
|
|
399
|
+
if (child.type === "element") {
|
|
400
|
+
visit(child);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
visit(root);
|
|
406
|
+
return counts;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function collectLayoutInventory(
|
|
410
|
+
sourcePackage: OpcPackage,
|
|
411
|
+
parts: SourcePackagePartEvidence[],
|
|
412
|
+
relationships: SourcePackageRelationshipEvidence[],
|
|
413
|
+
mainDocumentPartPath: string | undefined,
|
|
414
|
+
parseErrors: SourcePackageEvidence["parseErrors"],
|
|
415
|
+
): SourcePackageLayoutInventoryEvidence {
|
|
416
|
+
const storyParts = collectStoryPartDescriptors(parts, relationships);
|
|
417
|
+
const partGroups = groupParts(storyParts, parts);
|
|
418
|
+
const relationshipGroups = groupRelationships(relationships);
|
|
419
|
+
const locations: Pick<
|
|
420
|
+
SourcePackageLayoutInventoryEvidence,
|
|
421
|
+
| "sections"
|
|
422
|
+
| "tables"
|
|
423
|
+
| "fields"
|
|
424
|
+
| "drawings"
|
|
425
|
+
| "vmlShapes"
|
|
426
|
+
| "contentControls"
|
|
427
|
+
| "bookmarks"
|
|
428
|
+
| "notes"
|
|
429
|
+
| "objects"
|
|
430
|
+
> = {
|
|
431
|
+
sections: [],
|
|
432
|
+
tables: [],
|
|
433
|
+
fields: [],
|
|
434
|
+
drawings: [],
|
|
435
|
+
vmlShapes: [],
|
|
436
|
+
contentControls: [],
|
|
437
|
+
bookmarks: [],
|
|
438
|
+
notes: [],
|
|
439
|
+
objects: [],
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const partEvidenceByPath = new Map(parts.map((part) => [part.path, part]));
|
|
443
|
+
const sourceRelationshipByPart = relationships.reduce((map, relationship) => {
|
|
444
|
+
if (!relationship.sourcePartPath) return map;
|
|
445
|
+
const list = map.get(relationship.sourcePartPath) ?? [];
|
|
446
|
+
list.push(relationship);
|
|
447
|
+
map.set(relationship.sourcePartPath, list);
|
|
448
|
+
return map;
|
|
449
|
+
}, new Map<string, SourcePackageRelationshipEvidence[]>());
|
|
450
|
+
|
|
451
|
+
for (const part of sourcePackage.parts.values()) {
|
|
452
|
+
const partEvidence = partEvidenceByPath.get(part.path);
|
|
453
|
+
const storyKind = partEvidence?.storyKind ?? classifyPart(part, mainDocumentPartPath);
|
|
454
|
+
if (!shouldParseXmlPart(part, storyKind)) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
let root: XmlElementNode;
|
|
459
|
+
try {
|
|
460
|
+
root = parseXmlWithOffsets(new TextDecoder("utf-8").decode(part.bytes));
|
|
461
|
+
} catch (error) {
|
|
462
|
+
parseErrors.push({
|
|
463
|
+
partPath: part.path,
|
|
464
|
+
message: error instanceof Error ? error.message : String(error),
|
|
465
|
+
});
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
collectPartLocations(
|
|
470
|
+
root,
|
|
471
|
+
{
|
|
472
|
+
partPath: part.path,
|
|
473
|
+
storyKind,
|
|
474
|
+
relationships: sourceRelationshipByPart.get(part.path) ?? [],
|
|
475
|
+
},
|
|
476
|
+
locations,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
storyParts,
|
|
482
|
+
partGroups,
|
|
483
|
+
relationships: relationshipGroups,
|
|
484
|
+
...locations,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function collectStoryPartDescriptors(
|
|
489
|
+
parts: SourcePackagePartEvidence[],
|
|
490
|
+
relationships: SourcePackageRelationshipEvidence[],
|
|
491
|
+
): SourcePackageStoryPartDescriptor[] {
|
|
492
|
+
const descriptorByPath = new Map<string, SourcePackageStoryPartDescriptor>();
|
|
493
|
+
for (const part of parts) {
|
|
494
|
+
if (!isLayoutRelevantStoryKind(part.storyKind)) continue;
|
|
495
|
+
descriptorByPath.set(part.path, {
|
|
496
|
+
sourceId: partSourceId(part.path),
|
|
497
|
+
partPath: part.path,
|
|
498
|
+
storyKind: part.storyKind,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
for (const relationship of relationships) {
|
|
503
|
+
if (relationship.targetMode !== "internal") continue;
|
|
504
|
+
const resolvedPath = normalizePartPath(relationship.resolvedTarget);
|
|
505
|
+
const descriptor = descriptorByPath.get(resolvedPath);
|
|
506
|
+
if (!descriptor) continue;
|
|
507
|
+
descriptorByPath.set(resolvedPath, {
|
|
508
|
+
...descriptor,
|
|
509
|
+
relationshipId: relationship.relationshipId,
|
|
510
|
+
relationshipType: relationship.relationshipType,
|
|
511
|
+
relationshipSourcePartPath: relationship.sourcePartPath,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return [...descriptorByPath.values()].sort((left, right) =>
|
|
516
|
+
left.partPath.localeCompare(right.partPath),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function groupParts(
|
|
521
|
+
descriptors: SourcePackageStoryPartDescriptor[],
|
|
522
|
+
parts: SourcePackagePartEvidence[],
|
|
523
|
+
): SourcePackagePartGroupEvidence {
|
|
524
|
+
const contentTypeByPath = new Map(parts.map((part) => [part.path, part.contentType]));
|
|
525
|
+
const byKind = (kind: SourcePackageStoryKind): SourcePackageStoryPartDescriptor[] =>
|
|
526
|
+
descriptors.filter((part) => part.storyKind === kind);
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
media: byKind("media"),
|
|
530
|
+
images: byKind("media").filter((part) =>
|
|
531
|
+
contentTypeByPath.get(part.partPath)?.startsWith("image/") === true,
|
|
532
|
+
),
|
|
533
|
+
charts: byKind("chart"),
|
|
534
|
+
embedded: byKind("embedded"),
|
|
535
|
+
customXml: byKind("custom-xml"),
|
|
536
|
+
settings: byKind("settings"),
|
|
537
|
+
numbering: byKind("numbering"),
|
|
538
|
+
comments: byKind("comments"),
|
|
539
|
+
themes: byKind("theme"),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function groupRelationships(
|
|
544
|
+
relationships: SourcePackageRelationshipEvidence[],
|
|
545
|
+
): SourcePackageRelationshipGroupEvidence {
|
|
546
|
+
const byRole = (role: string): SourcePackageRelationshipEvidence[] =>
|
|
547
|
+
relationships.filter((relationship) => relationshipRole(relationship) === role);
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
headerFooters: relationships.filter((relationship) => {
|
|
551
|
+
const role = relationshipRole(relationship);
|
|
552
|
+
return role === "header" || role === "footer";
|
|
553
|
+
}),
|
|
554
|
+
media: byRole("image"),
|
|
555
|
+
charts: byRole("chart"),
|
|
556
|
+
embedded: relationships.filter((relationship) => {
|
|
557
|
+
const role = relationshipRole(relationship);
|
|
558
|
+
return role === "oleObject" || role === "package";
|
|
559
|
+
}),
|
|
560
|
+
customXml: byRole("customXml"),
|
|
561
|
+
settings: byRole("settings"),
|
|
562
|
+
numbering: byRole("numbering"),
|
|
563
|
+
comments: byRole("comments"),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function collectPartLocations(
|
|
568
|
+
root: XmlElementNode,
|
|
569
|
+
context: {
|
|
570
|
+
partPath: string;
|
|
571
|
+
storyKind: SourcePackageStoryKind;
|
|
572
|
+
relationships: SourcePackageRelationshipEvidence[];
|
|
573
|
+
},
|
|
574
|
+
locations: Pick<
|
|
575
|
+
SourcePackageLayoutInventoryEvidence,
|
|
576
|
+
| "sections"
|
|
577
|
+
| "tables"
|
|
578
|
+
| "fields"
|
|
579
|
+
| "drawings"
|
|
580
|
+
| "vmlShapes"
|
|
581
|
+
| "contentControls"
|
|
582
|
+
| "bookmarks"
|
|
583
|
+
| "notes"
|
|
584
|
+
| "objects"
|
|
585
|
+
>,
|
|
586
|
+
): void {
|
|
587
|
+
const ordinals = new Map<string, number>();
|
|
588
|
+
const nextOrdinal = (element: string): number => {
|
|
589
|
+
const next = (ordinals.get(element) ?? 0) + 1;
|
|
590
|
+
ordinals.set(element, next);
|
|
591
|
+
return next;
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const visit = (node: XmlElementNode): void => {
|
|
595
|
+
const name = localName(node.name);
|
|
596
|
+
switch (name) {
|
|
597
|
+
case "sectPr":
|
|
598
|
+
locations.sections.push({
|
|
599
|
+
...locationFor(context, node, name, nextOrdinal(name)),
|
|
600
|
+
headerFooterReferences: collectHeaderFooterReferences(node, context.relationships),
|
|
601
|
+
});
|
|
602
|
+
break;
|
|
603
|
+
case "tbl":
|
|
604
|
+
locations.tables.push(locationFor(context, node, name, nextOrdinal(name)));
|
|
605
|
+
break;
|
|
606
|
+
case "fldSimple":
|
|
607
|
+
case "fldChar":
|
|
608
|
+
case "instrText":
|
|
609
|
+
locations.fields.push(locationFor(context, node, name, nextOrdinal(name)));
|
|
610
|
+
break;
|
|
611
|
+
case "drawing": {
|
|
612
|
+
const ordinal = nextOrdinal(name);
|
|
613
|
+
locations.drawings.push(locationFor(context, node, name, ordinal));
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
case "inline":
|
|
617
|
+
case "anchor": {
|
|
618
|
+
const ordinal = nextOrdinal(name);
|
|
619
|
+
locations.objects.push(drawingObjectFor(context, node, name, ordinal));
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
case "shape": {
|
|
623
|
+
const ordinal = nextOrdinal(name);
|
|
624
|
+
locations.vmlShapes.push(locationFor(context, node, name, ordinal));
|
|
625
|
+
locations.objects.push(vmlObjectFor(context, node, ordinal));
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
case "object": {
|
|
629
|
+
const ordinal = nextOrdinal(name);
|
|
630
|
+
locations.objects.push(objectFor(context, node, ordinal));
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
case "sdt":
|
|
634
|
+
locations.contentControls.push(locationFor(context, node, name, nextOrdinal(name)));
|
|
635
|
+
break;
|
|
636
|
+
case "bookmarkStart":
|
|
637
|
+
locations.bookmarks.push(locationFor(context, node, name, nextOrdinal(name)));
|
|
638
|
+
break;
|
|
639
|
+
case "footnote":
|
|
640
|
+
case "endnote":
|
|
641
|
+
locations.notes.push(locationFor(context, node, name, nextOrdinal(name)));
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
for (const child of node.children) {
|
|
646
|
+
if (child.type === "element") {
|
|
647
|
+
visit(child);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
visit(root);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function locationFor(
|
|
656
|
+
context: {
|
|
657
|
+
partPath: string;
|
|
658
|
+
storyKind: SourcePackageStoryKind;
|
|
659
|
+
},
|
|
660
|
+
node: XmlElementNode,
|
|
661
|
+
element: string,
|
|
662
|
+
ordinal: number,
|
|
663
|
+
): SourcePackageLocationEvidence {
|
|
664
|
+
return {
|
|
665
|
+
sourceId: elementSourceId(context.partPath, element, ordinal),
|
|
666
|
+
partPath: context.partPath,
|
|
667
|
+
storyKind: context.storyKind,
|
|
668
|
+
element,
|
|
669
|
+
ordinal,
|
|
670
|
+
...(node.start !== undefined ? { startOffset: node.start } : {}),
|
|
671
|
+
...(node.end !== undefined ? { endOffset: node.end } : {}),
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function drawingObjectFor(
|
|
676
|
+
context: {
|
|
677
|
+
partPath: string;
|
|
678
|
+
storyKind: SourcePackageStoryKind;
|
|
679
|
+
},
|
|
680
|
+
node: XmlElementNode,
|
|
681
|
+
element: string,
|
|
682
|
+
ordinal: number,
|
|
683
|
+
): SourcePackageObjectExtentEvidence {
|
|
684
|
+
const extentEmu = readDrawingExtentEmu(node);
|
|
685
|
+
return {
|
|
686
|
+
...locationFor(context, node, element, ordinal),
|
|
687
|
+
kind: "drawing",
|
|
688
|
+
display: element === "anchor" ? "floating" : "inline",
|
|
689
|
+
preserveOnly: true,
|
|
690
|
+
fallbackHint: element === "anchor" ? "drawing-floating" : "drawing-inline",
|
|
691
|
+
...(extentEmu ? { extentEmu, extentTwips: extentEmuToTwips(extentEmu) } : {}),
|
|
692
|
+
relationshipIds: collectRelationshipIds(node),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function vmlObjectFor(
|
|
697
|
+
context: {
|
|
698
|
+
partPath: string;
|
|
699
|
+
storyKind: SourcePackageStoryKind;
|
|
700
|
+
},
|
|
701
|
+
node: XmlElementNode,
|
|
702
|
+
ordinal: number,
|
|
703
|
+
): SourcePackageObjectExtentEvidence {
|
|
704
|
+
const extentTwips = readVmlExtentTwips(node.attributes.style);
|
|
705
|
+
return {
|
|
706
|
+
...locationFor(context, node, "shape", ordinal),
|
|
707
|
+
kind: "vml-shape",
|
|
708
|
+
display: "unknown",
|
|
709
|
+
preserveOnly: true,
|
|
710
|
+
fallbackHint: "vml-shape",
|
|
711
|
+
...(extentTwips ? { extentTwips, extentEmu: extentTwipsToEmu(extentTwips) } : {}),
|
|
712
|
+
relationshipIds: collectRelationshipIds(node),
|
|
713
|
+
...(node.attributes.id ? { shapeId: node.attributes.id } : {}),
|
|
714
|
+
...(node.attributes.type ? { shapeType: node.attributes.type } : {}),
|
|
715
|
+
...(node.attributes.style ? { vmlStyle: node.attributes.style } : {}),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function objectFor(
|
|
720
|
+
context: {
|
|
721
|
+
partPath: string;
|
|
722
|
+
storyKind: SourcePackageStoryKind;
|
|
723
|
+
},
|
|
724
|
+
node: XmlElementNode,
|
|
725
|
+
ordinal: number,
|
|
726
|
+
): SourcePackageObjectExtentEvidence {
|
|
727
|
+
return {
|
|
728
|
+
...locationFor(context, node, "object", ordinal),
|
|
729
|
+
kind: "object",
|
|
730
|
+
display: "unknown",
|
|
731
|
+
preserveOnly: true,
|
|
732
|
+
fallbackHint: hasDescendant(node, "OLEObject") ? "ole-object" : "opaque-object",
|
|
733
|
+
relationshipIds: collectRelationshipIds(node),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function collectHeaderFooterReferences(
|
|
738
|
+
node: XmlElementNode,
|
|
739
|
+
relationships: SourcePackageRelationshipEvidence[],
|
|
740
|
+
): SourcePackageHeaderFooterReferenceEvidence[] {
|
|
741
|
+
const byId = new Map(relationships.map((relationship) => [relationship.relationshipId, relationship]));
|
|
742
|
+
const refs: SourcePackageHeaderFooterReferenceEvidence[] = [];
|
|
743
|
+
const visit = (current: XmlElementNode): void => {
|
|
744
|
+
const name = localName(current.name);
|
|
745
|
+
if (name === "headerReference" || name === "footerReference") {
|
|
746
|
+
const relationshipId = readRelationshipId(current);
|
|
747
|
+
if (relationshipId) {
|
|
748
|
+
const relationship = byId.get(relationshipId);
|
|
749
|
+
refs.push({
|
|
750
|
+
kind: name === "headerReference" ? "header" : "footer",
|
|
751
|
+
relationshipId,
|
|
752
|
+
...(current.attributes["w:type"] ?? current.attributes.type
|
|
753
|
+
? { type: current.attributes["w:type"] ?? current.attributes.type }
|
|
754
|
+
: {}),
|
|
755
|
+
...(relationship ? { resolvedTarget: relationship.resolvedTarget } : {}),
|
|
756
|
+
...(relationship ? { targetExists: relationship.targetExists } : {}),
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
for (const child of current.children) {
|
|
761
|
+
if (child.type === "element") visit(child);
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
visit(node);
|
|
765
|
+
return refs;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function collectRelationships(
|
|
769
|
+
sourcePackage: OpcPackage,
|
|
770
|
+
): SourcePackageRelationshipEvidence[] {
|
|
771
|
+
const relationships: SourcePackageRelationshipEvidence[] = [];
|
|
772
|
+
const pushRelationship = (
|
|
773
|
+
sourcePartPath: string | null,
|
|
774
|
+
relationship: OpcRelationship,
|
|
775
|
+
): void => {
|
|
776
|
+
const resolvedTarget =
|
|
777
|
+
relationship.targetMode === "internal"
|
|
778
|
+
? resolveRelationshipTarget(sourcePartPath, relationship)
|
|
779
|
+
: relationship.target;
|
|
780
|
+
relationships.push({
|
|
781
|
+
sourcePartPath,
|
|
782
|
+
relationshipId: relationship.id,
|
|
783
|
+
relationshipType: relationship.type,
|
|
784
|
+
targetMode: relationship.targetMode,
|
|
785
|
+
target: relationship.target,
|
|
786
|
+
resolvedTarget,
|
|
787
|
+
targetExists:
|
|
788
|
+
relationship.targetMode === "external" ||
|
|
789
|
+
sourcePackage.parts.has(normalizePartPath(resolvedTarget)),
|
|
790
|
+
});
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
for (const relationship of sourcePackage.manifest.packageRelationships) {
|
|
794
|
+
pushRelationship(null, relationship);
|
|
795
|
+
}
|
|
796
|
+
for (const part of sourcePackage.parts.values()) {
|
|
797
|
+
for (const relationship of part.relationships) {
|
|
798
|
+
pushRelationship(part.path, relationship);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return relationships.sort((left, right) => {
|
|
803
|
+
const leftKey = `${left.sourcePartPath ?? ""}:${left.relationshipId}`;
|
|
804
|
+
const rightKey = `${right.sourcePartPath ?? ""}:${right.relationshipId}`;
|
|
805
|
+
return leftKey.localeCompare(rightKey);
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function classifyPart(
|
|
810
|
+
part: OpcPackagePart,
|
|
811
|
+
mainDocumentPartPath: string | undefined,
|
|
812
|
+
): SourcePackageStoryKind {
|
|
813
|
+
const path = part.path;
|
|
814
|
+
if (part.surfaceKind === "relationships") return "relationships";
|
|
815
|
+
if (part.surfaceKind === "content-types") return "content-types";
|
|
816
|
+
if (mainDocumentPartPath && path === mainDocumentPartPath) return "main-document";
|
|
817
|
+
if (/^\/word\/header\d*\.xml$/u.test(path)) return "header";
|
|
818
|
+
if (/^\/word\/footer\d*\.xml$/u.test(path)) return "footer";
|
|
819
|
+
if (path === "/word/footnotes.xml") return "footnotes";
|
|
820
|
+
if (path === "/word/endnotes.xml") return "endnotes";
|
|
821
|
+
if (path.startsWith("/word/comments")) return "comments";
|
|
822
|
+
if (path === "/word/numbering.xml") return "numbering";
|
|
823
|
+
if (path === "/word/settings.xml") return "settings";
|
|
824
|
+
if (path === "/word/styles.xml") return "styles";
|
|
825
|
+
if (path === "/word/fontTable.xml") return "font-table";
|
|
826
|
+
if (path.startsWith("/word/theme/")) return "theme";
|
|
827
|
+
if (path.startsWith("/word/charts/")) return "chart";
|
|
828
|
+
if (path.startsWith("/word/media/")) return "media";
|
|
829
|
+
if (path.startsWith("/word/embeddings/")) return "embedded";
|
|
830
|
+
if (path.startsWith("/customXml/") && path.endsWith(".xml")) return "custom-xml";
|
|
831
|
+
return "other";
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function aggregateElementCounts(
|
|
835
|
+
countsList: SourcePackageElementCounts[],
|
|
836
|
+
): SourcePackageElementCounts {
|
|
837
|
+
const aggregate = { ...ZERO_COUNTS };
|
|
838
|
+
for (const counts of countsList) {
|
|
839
|
+
aggregate.tables += counts.tables;
|
|
840
|
+
aggregate.tableRows += counts.tableRows;
|
|
841
|
+
aggregate.tableCells += counts.tableCells;
|
|
842
|
+
aggregate.anchors += counts.anchors;
|
|
843
|
+
aggregate.inlineDrawings += counts.inlineDrawings;
|
|
844
|
+
aggregate.drawings += counts.drawings;
|
|
845
|
+
aggregate.vmlShapes += counts.vmlShapes;
|
|
846
|
+
aggregate.contentControls += counts.contentControls;
|
|
847
|
+
aggregate.fieldMarkers += counts.fieldMarkers;
|
|
848
|
+
aggregate.fieldInstructions += counts.fieldInstructions;
|
|
849
|
+
aggregate.simpleFields += counts.simpleFields;
|
|
850
|
+
aggregate.bookmarks += counts.bookmarks;
|
|
851
|
+
aggregate.footnotes += counts.footnotes;
|
|
852
|
+
aggregate.endnotes += counts.endnotes;
|
|
853
|
+
}
|
|
854
|
+
return aggregate;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function countParts(
|
|
858
|
+
parts: SourcePackagePartEvidence[],
|
|
859
|
+
storyKind: SourcePackageStoryKind,
|
|
860
|
+
): number {
|
|
861
|
+
return parts.filter((part) => part.storyKind === storyKind).length;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function isLayoutRelevantStoryKind(storyKind: SourcePackageStoryKind): boolean {
|
|
865
|
+
return (
|
|
866
|
+
storyKind === "main-document" ||
|
|
867
|
+
storyKind === "header" ||
|
|
868
|
+
storyKind === "footer" ||
|
|
869
|
+
storyKind === "footnotes" ||
|
|
870
|
+
storyKind === "endnotes" ||
|
|
871
|
+
storyKind === "comments" ||
|
|
872
|
+
storyKind === "numbering" ||
|
|
873
|
+
storyKind === "settings" ||
|
|
874
|
+
storyKind === "theme" ||
|
|
875
|
+
storyKind === "chart" ||
|
|
876
|
+
storyKind === "media" ||
|
|
877
|
+
storyKind === "embedded" ||
|
|
878
|
+
storyKind === "custom-xml"
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function relationshipRole(relationship: SourcePackageRelationshipEvidence): string {
|
|
883
|
+
const slashIndex = relationship.relationshipType.lastIndexOf("/");
|
|
884
|
+
return slashIndex >= 0
|
|
885
|
+
? relationship.relationshipType.slice(slashIndex + 1)
|
|
886
|
+
: relationship.relationshipType;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function partSourceId(partPath: string): string {
|
|
890
|
+
return `part:${partPath}`;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function elementSourceId(partPath: string, element: string, ordinal: number): string {
|
|
894
|
+
return `${partSourceId(partPath)}#${element}:${ordinal}`;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function readDrawingExtentEmu(
|
|
898
|
+
node: XmlElementNode,
|
|
899
|
+
): { width: number; height: number } | undefined {
|
|
900
|
+
const extent = findFirstDescendantByLocalName(node, "extent");
|
|
901
|
+
const width = extent ? readIntAttribute(extent, "cx") : undefined;
|
|
902
|
+
const height = extent ? readIntAttribute(extent, "cy") : undefined;
|
|
903
|
+
if (width !== undefined && height !== undefined) {
|
|
904
|
+
return { width, height };
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const transform = findFirstDescendantByLocalName(node, "xfrm");
|
|
908
|
+
const ext = transform ? findFirstDescendantByLocalName(transform, "ext") : undefined;
|
|
909
|
+
const fallbackWidth = ext ? readIntAttribute(ext, "cx") : undefined;
|
|
910
|
+
const fallbackHeight = ext ? readIntAttribute(ext, "cy") : undefined;
|
|
911
|
+
if (fallbackWidth !== undefined && fallbackHeight !== undefined) {
|
|
912
|
+
return { width: fallbackWidth, height: fallbackHeight };
|
|
913
|
+
}
|
|
914
|
+
return undefined;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function readVmlExtentTwips(
|
|
918
|
+
style: string | undefined,
|
|
919
|
+
): { width: number; height: number } | undefined {
|
|
920
|
+
if (!style) return undefined;
|
|
921
|
+
const width = readCssLengthTwips(style, "width");
|
|
922
|
+
const height = readCssLengthTwips(style, "height");
|
|
923
|
+
if (width !== undefined && height !== undefined) {
|
|
924
|
+
return { width, height };
|
|
925
|
+
}
|
|
926
|
+
return undefined;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function readCssLengthTwips(style: string, property: "width" | "height"): number | undefined {
|
|
930
|
+
const match = new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([0-9.]+)\\s*(pt|in|cm|mm|px)?`, "iu")
|
|
931
|
+
.exec(style);
|
|
932
|
+
if (!match) return undefined;
|
|
933
|
+
const value = Number.parseFloat(match[1] ?? "");
|
|
934
|
+
if (!Number.isFinite(value)) return undefined;
|
|
935
|
+
const unit = (match[2] ?? "pt").toLowerCase();
|
|
936
|
+
switch (unit) {
|
|
937
|
+
case "pt":
|
|
938
|
+
return Math.round(value * 20);
|
|
939
|
+
case "in":
|
|
940
|
+
return Math.round(value * 1440);
|
|
941
|
+
case "cm":
|
|
942
|
+
return Math.round(value * 567);
|
|
943
|
+
case "mm":
|
|
944
|
+
return Math.round(value * 56.7);
|
|
945
|
+
case "px":
|
|
946
|
+
return Math.round(value * 15);
|
|
947
|
+
default:
|
|
948
|
+
return undefined;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function extentEmuToTwips(
|
|
953
|
+
extent: { width: number; height: number },
|
|
954
|
+
): { width: number; height: number } {
|
|
955
|
+
return {
|
|
956
|
+
width: Math.round(extent.width / 635),
|
|
957
|
+
height: Math.round(extent.height / 635),
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function extentTwipsToEmu(
|
|
962
|
+
extent: { width: number; height: number },
|
|
963
|
+
): { width: number; height: number } {
|
|
964
|
+
return {
|
|
965
|
+
width: Math.round(extent.width * 635),
|
|
966
|
+
height: Math.round(extent.height * 635),
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function collectRelationshipIds(node: XmlElementNode): string[] {
|
|
971
|
+
const ids = new Set<string>();
|
|
972
|
+
const visit = (current: XmlElementNode): void => {
|
|
973
|
+
const id = readRelationshipId(current);
|
|
974
|
+
if (id) ids.add(id);
|
|
975
|
+
for (const child of current.children) {
|
|
976
|
+
if (child.type === "element") visit(child);
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
visit(node);
|
|
980
|
+
return [...ids].sort();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function readRelationshipId(node: XmlElementNode): string | undefined {
|
|
984
|
+
return (
|
|
985
|
+
node.attributes["r:id"] ??
|
|
986
|
+
node.attributes.id ??
|
|
987
|
+
node.attributes["r:embed"] ??
|
|
988
|
+
node.attributes.embed ??
|
|
989
|
+
node.attributes["r:link"] ??
|
|
990
|
+
node.attributes.link
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function findFirstDescendantByLocalName(
|
|
995
|
+
node: XmlElementNode,
|
|
996
|
+
name: string,
|
|
997
|
+
): XmlElementNode | undefined {
|
|
998
|
+
for (const child of node.children) {
|
|
999
|
+
if (child.type !== "element") continue;
|
|
1000
|
+
if (localName(child.name) === name) return child;
|
|
1001
|
+
const nested = findFirstDescendantByLocalName(child, name);
|
|
1002
|
+
if (nested) return nested;
|
|
1003
|
+
}
|
|
1004
|
+
return undefined;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function hasDescendant(node: XmlElementNode, name: string): boolean {
|
|
1008
|
+
return findFirstDescendantByLocalName(node, name) !== undefined;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function readIntAttribute(node: XmlElementNode, name: string): number | undefined {
|
|
1012
|
+
const raw = node.attributes[name];
|
|
1013
|
+
if (raw === undefined) return undefined;
|
|
1014
|
+
const value = Number.parseInt(raw, 10);
|
|
1015
|
+
return Number.isFinite(value) ? value : undefined;
|
|
1016
|
+
}
|