@beyondwork/docx-react-component 1.0.100 → 1.0.102
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/io/export/build-app-properties-xml.ts +24 -0
- package/src/io/ooxml/docprops.ts +298 -0
- package/src/model/canonical-document.ts +104 -1
- package/src/runtime/document-runtime.ts +53 -6
- 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/source-package-evidence.ts +405 -0
- package/src/session/shared/session-utils.ts +9 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.102",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
|
@@ -40,10 +40,22 @@ export interface AppPropertiesStats {
|
|
|
40
40
|
words?: number;
|
|
41
41
|
/** Character count. */
|
|
42
42
|
characters?: number;
|
|
43
|
+
/** Character count including spaces. */
|
|
44
|
+
charactersWithSpaces?: number;
|
|
43
45
|
/** Line count. */
|
|
44
46
|
lines?: number;
|
|
45
47
|
/** Paragraph count. */
|
|
46
48
|
paragraphs?: number;
|
|
49
|
+
/** Editing time in minutes. */
|
|
50
|
+
totalTime?: number;
|
|
51
|
+
/** Source template name. */
|
|
52
|
+
template?: string;
|
|
53
|
+
/** Source company property. */
|
|
54
|
+
company?: string;
|
|
55
|
+
/** Source manager property. */
|
|
56
|
+
manager?: string;
|
|
57
|
+
/** Document security flag. */
|
|
58
|
+
docSecurity?: number;
|
|
47
59
|
/** App-version string override; defaults to the package version. */
|
|
48
60
|
appVersion?: string;
|
|
49
61
|
/** Application identifier override; defaults to package.json name@version. */
|
|
@@ -58,19 +70,31 @@ export function buildAppPropertiesXml(
|
|
|
58
70
|
const pages = stats.pages ?? 0;
|
|
59
71
|
const words = stats.words ?? 0;
|
|
60
72
|
const characters = stats.characters ?? 0;
|
|
73
|
+
const charactersWithSpaces = stats.charactersWithSpaces;
|
|
61
74
|
const lines = stats.lines ?? 0;
|
|
62
75
|
const paragraphs = stats.paragraphs ?? 0;
|
|
76
|
+
const totalTime = stats.totalTime;
|
|
63
77
|
|
|
64
78
|
return [
|
|
65
79
|
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
|
|
66
80
|
`<Properties xmlns="${APP_PROPERTIES_NAMESPACE}" xmlns:vt="${APP_PROPERTIES_VT_NAMESPACE}">`,
|
|
67
81
|
` <Application>${escapeXml(application)}</Application>`,
|
|
68
82
|
` <AppVersion>${escapeXml(appVersion)}</AppVersion>`,
|
|
83
|
+
...(stats.template ? [` <Template>${escapeXml(stats.template)}</Template>`] : []),
|
|
69
84
|
` <Pages>${Math.max(0, Math.round(pages))}</Pages>`,
|
|
70
85
|
` <Words>${Math.max(0, Math.round(words))}</Words>`,
|
|
71
86
|
` <Characters>${Math.max(0, Math.round(characters))}</Characters>`,
|
|
87
|
+
...(charactersWithSpaces !== undefined
|
|
88
|
+
? [` <CharactersWithSpaces>${Math.max(0, Math.round(charactersWithSpaces))}</CharactersWithSpaces>`]
|
|
89
|
+
: []),
|
|
72
90
|
` <Lines>${Math.max(0, Math.round(lines))}</Lines>`,
|
|
73
91
|
` <Paragraphs>${Math.max(0, Math.round(paragraphs))}</Paragraphs>`,
|
|
92
|
+
...(totalTime !== undefined ? [` <TotalTime>${Math.max(0, Math.round(totalTime))}</TotalTime>`] : []),
|
|
93
|
+
...(stats.company ? [` <Company>${escapeXml(stats.company)}</Company>`] : []),
|
|
94
|
+
...(stats.manager ? [` <Manager>${escapeXml(stats.manager)}</Manager>`] : []),
|
|
95
|
+
...(stats.docSecurity !== undefined
|
|
96
|
+
? [` <DocSecurity>${Math.max(0, Math.round(stats.docSecurity))}</DocSecurity>`]
|
|
97
|
+
: []),
|
|
74
98
|
`</Properties>`,
|
|
75
99
|
].join("\n");
|
|
76
100
|
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import type { DocumentMetadata } from "../../model/canonical-document.ts";
|
|
2
|
+
import {
|
|
3
|
+
findFirstChild,
|
|
4
|
+
localName,
|
|
5
|
+
parseXml,
|
|
6
|
+
type XmlElementNode,
|
|
7
|
+
type XmlNode,
|
|
8
|
+
} from "./_mini-xml.ts";
|
|
9
|
+
|
|
10
|
+
export interface CorePropertiesView {
|
|
11
|
+
readonly title?: string;
|
|
12
|
+
readonly subject?: string;
|
|
13
|
+
readonly description?: string;
|
|
14
|
+
readonly creator?: string;
|
|
15
|
+
readonly language?: string;
|
|
16
|
+
readonly keywords?: string;
|
|
17
|
+
readonly category?: string;
|
|
18
|
+
readonly lastModifiedBy?: string;
|
|
19
|
+
readonly contentStatus?: string;
|
|
20
|
+
readonly revision?: string;
|
|
21
|
+
readonly version?: string;
|
|
22
|
+
readonly createdUtc?: string;
|
|
23
|
+
readonly modifiedUtc?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AppPropertiesView {
|
|
27
|
+
readonly application?: string;
|
|
28
|
+
readonly appVersion?: string;
|
|
29
|
+
readonly template?: string;
|
|
30
|
+
readonly pages?: number;
|
|
31
|
+
readonly words?: number;
|
|
32
|
+
readonly characters?: number;
|
|
33
|
+
readonly charactersWithSpaces?: number;
|
|
34
|
+
readonly totalTime?: number;
|
|
35
|
+
readonly company?: string;
|
|
36
|
+
readonly manager?: string;
|
|
37
|
+
readonly docSecurity?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CustomPropertyValue {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly value: string;
|
|
43
|
+
readonly valueKind: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DocpropsView {
|
|
47
|
+
readonly coreProperties: CorePropertiesView | null;
|
|
48
|
+
readonly appProperties: AppPropertiesView | null;
|
|
49
|
+
readonly customProperties: ReadonlyArray<CustomPropertyValue>;
|
|
50
|
+
readonly sourcePresence: {
|
|
51
|
+
readonly core: boolean;
|
|
52
|
+
readonly app: boolean;
|
|
53
|
+
readonly custom: boolean;
|
|
54
|
+
};
|
|
55
|
+
readonly parseErrors: ReadonlyArray<{
|
|
56
|
+
readonly part: "core" | "app" | "custom";
|
|
57
|
+
readonly message: string;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface DocpropsParts {
|
|
62
|
+
readonly core?: Uint8Array;
|
|
63
|
+
readonly app?: Uint8Array;
|
|
64
|
+
readonly custom?: Uint8Array;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface OpcPartMap {
|
|
68
|
+
get(path: string): { bytes: Uint8Array } | undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const CORE_PART = "/docProps/core.xml";
|
|
72
|
+
const APP_PART = "/docProps/app.xml";
|
|
73
|
+
const CUSTOM_PART = "/docProps/custom.xml";
|
|
74
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: false });
|
|
75
|
+
|
|
76
|
+
export function parseDocprops(parts: DocpropsParts): DocpropsView {
|
|
77
|
+
const parseErrors: { part: "core" | "app" | "custom"; message: string }[] = [];
|
|
78
|
+
|
|
79
|
+
let coreProperties: CorePropertiesView | null = null;
|
|
80
|
+
if (parts.core) {
|
|
81
|
+
try {
|
|
82
|
+
coreProperties = parseCoreXml(decodeXmlBytes(parts.core));
|
|
83
|
+
} catch (err) {
|
|
84
|
+
parseErrors.push({
|
|
85
|
+
part: "core",
|
|
86
|
+
message: err instanceof Error ? err.message : String(err),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let appProperties: AppPropertiesView | null = null;
|
|
92
|
+
if (parts.app) {
|
|
93
|
+
try {
|
|
94
|
+
appProperties = parseAppXml(decodeXmlBytes(parts.app));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
parseErrors.push({
|
|
97
|
+
part: "app",
|
|
98
|
+
message: err instanceof Error ? err.message : String(err),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let customProperties: ReadonlyArray<CustomPropertyValue> = [];
|
|
104
|
+
if (parts.custom) {
|
|
105
|
+
try {
|
|
106
|
+
customProperties = parseCustomXml(decodeXmlBytes(parts.custom));
|
|
107
|
+
} catch (err) {
|
|
108
|
+
parseErrors.push({
|
|
109
|
+
part: "custom",
|
|
110
|
+
message: err instanceof Error ? err.message : String(err),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
coreProperties,
|
|
117
|
+
appProperties,
|
|
118
|
+
customProperties,
|
|
119
|
+
sourcePresence: {
|
|
120
|
+
core: Boolean(parts.core),
|
|
121
|
+
app: Boolean(parts.app),
|
|
122
|
+
custom: Boolean(parts.custom),
|
|
123
|
+
},
|
|
124
|
+
parseErrors,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function parseDocpropsFromOpcParts(parts: OpcPartMap): DocpropsView {
|
|
129
|
+
const core = parts.get(CORE_PART);
|
|
130
|
+
const app = parts.get(APP_PART);
|
|
131
|
+
const custom = parts.get(CUSTOM_PART);
|
|
132
|
+
return parseDocprops({
|
|
133
|
+
...(core ? { core: core.bytes } : {}),
|
|
134
|
+
...(app ? { app: app.bytes } : {}),
|
|
135
|
+
...(custom ? { custom: custom.bytes } : {}),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function parseDocumentMetadataFromOpcParts(parts: OpcPartMap): DocumentMetadata {
|
|
140
|
+
return docpropsToDocumentMetadata(parseDocpropsFromOpcParts(parts));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function docpropsToDocumentMetadata(view: DocpropsView): DocumentMetadata {
|
|
144
|
+
const core = view.coreProperties;
|
|
145
|
+
const app = view.appProperties;
|
|
146
|
+
const appProperties = app ? compactObject({
|
|
147
|
+
application: app.application,
|
|
148
|
+
appVersion: app.appVersion,
|
|
149
|
+
template: app.template,
|
|
150
|
+
pages: app.pages,
|
|
151
|
+
words: app.words,
|
|
152
|
+
characters: app.characters,
|
|
153
|
+
charactersWithSpaces: app.charactersWithSpaces,
|
|
154
|
+
totalTime: app.totalTime,
|
|
155
|
+
company: app.company,
|
|
156
|
+
manager: app.manager,
|
|
157
|
+
docSecurity: app.docSecurity,
|
|
158
|
+
}) : undefined;
|
|
159
|
+
return {
|
|
160
|
+
...(core?.title ? { title: core.title } : {}),
|
|
161
|
+
...(core?.subject ? { subject: core.subject } : {}),
|
|
162
|
+
...(core?.description ? { description: core.description } : {}),
|
|
163
|
+
...(core?.creator ? { creator: core.creator } : {}),
|
|
164
|
+
...(core?.language ? { language: core.language } : {}),
|
|
165
|
+
...(parseKeywords(core?.keywords) ? { keywords: parseKeywords(core?.keywords) } : {}),
|
|
166
|
+
...(core?.category ? { category: core.category } : {}),
|
|
167
|
+
...(core?.lastModifiedBy ? { lastModifiedBy: core.lastModifiedBy } : {}),
|
|
168
|
+
...(core?.contentStatus ? { contentStatus: core.contentStatus } : {}),
|
|
169
|
+
...(core?.revision ? { revision: core.revision } : {}),
|
|
170
|
+
...(core?.version ? { version: core.version } : {}),
|
|
171
|
+
...(core?.createdUtc ? { createdUtc: core.createdUtc } : {}),
|
|
172
|
+
...(core?.modifiedUtc ? { modifiedUtc: core.modifiedUtc } : {}),
|
|
173
|
+
...(appProperties && Object.keys(appProperties).length > 0 ? { appProperties } : {}),
|
|
174
|
+
customProperties: Object.fromEntries(
|
|
175
|
+
view.customProperties.map((property) => [property.name, property.value]),
|
|
176
|
+
),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseCoreXml(xml: string): CorePropertiesView {
|
|
181
|
+
const parsed = parseXml(xml);
|
|
182
|
+
const root = documentElement(parsed) ?? parsed;
|
|
183
|
+
return {
|
|
184
|
+
title: textOfFirstChild(root, "title"),
|
|
185
|
+
subject: textOfFirstChild(root, "subject"),
|
|
186
|
+
description: textOfFirstChild(root, "description"),
|
|
187
|
+
creator: textOfFirstChild(root, "creator"),
|
|
188
|
+
language: textOfFirstChild(root, "language"),
|
|
189
|
+
keywords: textOfFirstChild(root, "keywords"),
|
|
190
|
+
category: textOfFirstChild(root, "category"),
|
|
191
|
+
lastModifiedBy: textOfFirstChild(root, "lastModifiedBy"),
|
|
192
|
+
contentStatus: textOfFirstChild(root, "contentStatus"),
|
|
193
|
+
revision: textOfFirstChild(root, "revision"),
|
|
194
|
+
version: textOfFirstChild(root, "version"),
|
|
195
|
+
createdUtc: textOfFirstChild(root, "created"),
|
|
196
|
+
modifiedUtc: textOfFirstChild(root, "modified"),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseAppXml(xml: string): AppPropertiesView {
|
|
201
|
+
const parsed = parseXml(xml);
|
|
202
|
+
const root = documentElement(parsed) ?? parsed;
|
|
203
|
+
return {
|
|
204
|
+
application: textOfFirstChild(root, "Application"),
|
|
205
|
+
appVersion: textOfFirstChild(root, "AppVersion"),
|
|
206
|
+
template: textOfFirstChild(root, "Template"),
|
|
207
|
+
pages: parseNonNegativeInt(textOfFirstChild(root, "Pages")),
|
|
208
|
+
words: parseNonNegativeInt(textOfFirstChild(root, "Words")),
|
|
209
|
+
characters: parseNonNegativeInt(textOfFirstChild(root, "Characters")),
|
|
210
|
+
charactersWithSpaces: parseNonNegativeInt(textOfFirstChild(root, "CharactersWithSpaces")),
|
|
211
|
+
totalTime: parseNonNegativeInt(textOfFirstChild(root, "TotalTime")),
|
|
212
|
+
company: textOfFirstChild(root, "Company"),
|
|
213
|
+
manager: textOfFirstChild(root, "Manager"),
|
|
214
|
+
docSecurity: parseNonNegativeInt(textOfFirstChild(root, "DocSecurity")),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function parseCustomXml(xml: string): CustomPropertyValue[] {
|
|
219
|
+
const parsed = parseXml(xml);
|
|
220
|
+
const root = documentElement(parsed) ?? parsed;
|
|
221
|
+
const out: CustomPropertyValue[] = [];
|
|
222
|
+
for (const child of root.children as XmlNode[]) {
|
|
223
|
+
if (child.type !== "element" || localName(child.name) !== "property") {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const name = child.attributes.name;
|
|
227
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const valueNode = firstElementChild(child);
|
|
231
|
+
if (!valueNode) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
out.push({
|
|
235
|
+
name,
|
|
236
|
+
value: extractChildText(valueNode).trim(),
|
|
237
|
+
valueKind: localName(valueNode.name),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function decodeXmlBytes(bytes: Uint8Array): string {
|
|
244
|
+
return UTF8_DECODER.decode(bytes);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function documentElement(root: XmlElementNode): XmlElementNode | undefined {
|
|
248
|
+
for (const child of root.children) {
|
|
249
|
+
if (child.type === "element") return child;
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function textOfFirstChild(node: XmlElementNode, name: string): string | undefined {
|
|
255
|
+
const child = findFirstChild(node, name);
|
|
256
|
+
if (!child) return undefined;
|
|
257
|
+
const raw = extractChildText(child).trim();
|
|
258
|
+
return raw.length === 0 ? undefined : raw;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function extractChildText(node: XmlElementNode): string {
|
|
262
|
+
let out = "";
|
|
263
|
+
for (const child of node.children) {
|
|
264
|
+
if (child.type === "text") {
|
|
265
|
+
out += child.text;
|
|
266
|
+
} else {
|
|
267
|
+
out += extractChildText(child);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function firstElementChild(node: XmlElementNode): XmlElementNode | undefined {
|
|
274
|
+
for (const child of node.children) {
|
|
275
|
+
if (child.type === "element") return child;
|
|
276
|
+
}
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parseNonNegativeInt(raw: string | undefined): number | undefined {
|
|
281
|
+
if (raw === undefined) return undefined;
|
|
282
|
+
const n = Number.parseInt(raw, 10);
|
|
283
|
+
return Number.isFinite(n) && n >= 0 ? n : undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parseKeywords(raw: string | undefined): string[] | undefined {
|
|
287
|
+
if (!raw) return undefined;
|
|
288
|
+
const keywords = raw.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
289
|
+
return keywords.length > 0 ? keywords : undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function compactObject<T extends Record<string, string | number | undefined>>(
|
|
293
|
+
value: T,
|
|
294
|
+
): { [K in keyof T]?: Exclude<T[K], undefined> } {
|
|
295
|
+
return Object.fromEntries(
|
|
296
|
+
Object.entries(value).filter(([, entry]) => entry !== undefined),
|
|
297
|
+
) as { [K in keyof T]?: Exclude<T[K], undefined> };
|
|
298
|
+
}
|
|
@@ -189,13 +189,35 @@ export interface DocumentMetadata {
|
|
|
189
189
|
title?: string;
|
|
190
190
|
subject?: string;
|
|
191
191
|
description?: string;
|
|
192
|
+
creator?: string;
|
|
192
193
|
language?: string;
|
|
193
194
|
keywords?: string[];
|
|
194
195
|
category?: string;
|
|
196
|
+
lastModifiedBy?: string;
|
|
197
|
+
contentStatus?: string;
|
|
198
|
+
revision?: string;
|
|
199
|
+
version?: string;
|
|
200
|
+
createdUtc?: string;
|
|
201
|
+
modifiedUtc?: string;
|
|
195
202
|
importMode?: string;
|
|
203
|
+
appProperties?: DocumentAppProperties;
|
|
196
204
|
customProperties: Record<string, string>;
|
|
197
205
|
}
|
|
198
206
|
|
|
207
|
+
export interface DocumentAppProperties {
|
|
208
|
+
application?: string;
|
|
209
|
+
appVersion?: string;
|
|
210
|
+
template?: string;
|
|
211
|
+
pages?: number;
|
|
212
|
+
words?: number;
|
|
213
|
+
characters?: number;
|
|
214
|
+
charactersWithSpaces?: number;
|
|
215
|
+
totalTime?: number;
|
|
216
|
+
company?: string;
|
|
217
|
+
manager?: string;
|
|
218
|
+
docSecurity?: number;
|
|
219
|
+
}
|
|
220
|
+
|
|
199
221
|
export interface StylesCatalog {
|
|
200
222
|
paragraphs: Record<string, ParagraphStyleDefinition>;
|
|
201
223
|
characters: Record<string, CharacterStyleDefinition>;
|
|
@@ -2408,6 +2430,9 @@ export function repairCanonicalDocumentEnvelope(
|
|
|
2408
2430
|
});
|
|
2409
2431
|
const record = isPlainRecord(value) ? value : {};
|
|
2410
2432
|
const metadata = isPlainRecord(record.metadata) ? record.metadata : {};
|
|
2433
|
+
const metadataRest = { ...metadata };
|
|
2434
|
+
delete metadataRest.customProperties;
|
|
2435
|
+
delete metadataRest.appProperties;
|
|
2411
2436
|
const customProperties = isPlainRecord(metadata.customProperties)
|
|
2412
2437
|
? (Object.fromEntries(
|
|
2413
2438
|
Object.entries(metadata.customProperties).filter(
|
|
@@ -2415,6 +2440,15 @@ export function repairCanonicalDocumentEnvelope(
|
|
|
2415
2440
|
),
|
|
2416
2441
|
) as Record<string, string>)
|
|
2417
2442
|
: {};
|
|
2443
|
+
const appProperties = isPlainRecord(metadata.appProperties)
|
|
2444
|
+
? (Object.fromEntries(
|
|
2445
|
+
Object.entries(metadata.appProperties).filter(
|
|
2446
|
+
([, entry]) =>
|
|
2447
|
+
typeof entry === "string" ||
|
|
2448
|
+
(typeof entry === "number" && Number.isFinite(entry) && entry >= 0),
|
|
2449
|
+
),
|
|
2450
|
+
) as DocumentAppProperties)
|
|
2451
|
+
: undefined;
|
|
2418
2452
|
|
|
2419
2453
|
const envelope: Mutable<CanonicalDocument> = {
|
|
2420
2454
|
...base,
|
|
@@ -2430,7 +2464,8 @@ export function repairCanonicalDocumentEnvelope(
|
|
|
2430
2464
|
: base.updatedAt,
|
|
2431
2465
|
metadata: {
|
|
2432
2466
|
...base.metadata,
|
|
2433
|
-
...
|
|
2467
|
+
...metadataRest,
|
|
2468
|
+
...(appProperties !== undefined ? { appProperties } : {}),
|
|
2434
2469
|
customProperties,
|
|
2435
2470
|
} as CanonicalDocument["metadata"],
|
|
2436
2471
|
styles:
|
|
@@ -2614,6 +2649,74 @@ function validateMetadata(
|
|
|
2614
2649
|
return;
|
|
2615
2650
|
}
|
|
2616
2651
|
|
|
2652
|
+
for (const field of [
|
|
2653
|
+
"title",
|
|
2654
|
+
"subject",
|
|
2655
|
+
"description",
|
|
2656
|
+
"creator",
|
|
2657
|
+
"language",
|
|
2658
|
+
"category",
|
|
2659
|
+
"lastModifiedBy",
|
|
2660
|
+
"contentStatus",
|
|
2661
|
+
"revision",
|
|
2662
|
+
"version",
|
|
2663
|
+
"createdUtc",
|
|
2664
|
+
"modifiedUtc",
|
|
2665
|
+
"importMode",
|
|
2666
|
+
]) {
|
|
2667
|
+
if (record[field] !== undefined && typeof record[field] !== "string") {
|
|
2668
|
+
issues.push({
|
|
2669
|
+
path: `${path}.${field}`,
|
|
2670
|
+
message: "metadata string fields must be strings when present.",
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
if (
|
|
2676
|
+
record.keywords !== undefined &&
|
|
2677
|
+
(!Array.isArray(record.keywords) ||
|
|
2678
|
+
record.keywords.some((entry) => typeof entry !== "string"))
|
|
2679
|
+
) {
|
|
2680
|
+
issues.push({
|
|
2681
|
+
path: `${path}.keywords`,
|
|
2682
|
+
message: "metadata keywords must be an array of strings when present.",
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
if (record.appProperties !== undefined) {
|
|
2687
|
+
const appProperties = asPlainObject(record.appProperties, `${path}.appProperties`, issues);
|
|
2688
|
+
if (appProperties) {
|
|
2689
|
+
for (const field of ["application", "appVersion", "template", "company", "manager"]) {
|
|
2690
|
+
if (appProperties[field] !== undefined && typeof appProperties[field] !== "string") {
|
|
2691
|
+
issues.push({
|
|
2692
|
+
path: `${path}.appProperties.${field}`,
|
|
2693
|
+
message: "appProperties string fields must be strings when present.",
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
for (const field of [
|
|
2698
|
+
"pages",
|
|
2699
|
+
"words",
|
|
2700
|
+
"characters",
|
|
2701
|
+
"charactersWithSpaces",
|
|
2702
|
+
"totalTime",
|
|
2703
|
+
"docSecurity",
|
|
2704
|
+
]) {
|
|
2705
|
+
if (
|
|
2706
|
+
appProperties[field] !== undefined &&
|
|
2707
|
+
(typeof appProperties[field] !== "number" ||
|
|
2708
|
+
!Number.isFinite(appProperties[field]) ||
|
|
2709
|
+
appProperties[field] < 0)
|
|
2710
|
+
) {
|
|
2711
|
+
issues.push({
|
|
2712
|
+
path: `${path}.appProperties.${field}`,
|
|
2713
|
+
message: "appProperties numeric fields must be finite non-negative numbers when present.",
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2617
2720
|
const customProperties = asPlainObject(record.customProperties, `${path}.customProperties`, issues);
|
|
2618
2721
|
if (!customProperties) {
|
|
2619
2722
|
return;
|
|
@@ -294,6 +294,7 @@ import type {
|
|
|
294
294
|
ParagraphNode,
|
|
295
295
|
SectionProperties,
|
|
296
296
|
SubPartsCatalog,
|
|
297
|
+
TocCachedEntry,
|
|
297
298
|
TocRegion,
|
|
298
299
|
} from "../model/canonical-document.ts";
|
|
299
300
|
import {
|
|
@@ -7550,14 +7551,16 @@ function refreshDocumentTableOfContents(
|
|
|
7550
7551
|
: parseTocLevelRange(field.instruction);
|
|
7551
7552
|
const entries = navigation.headings
|
|
7552
7553
|
.filter((heading) => heading.level >= levelRange.from && heading.level <= levelRange.to)
|
|
7553
|
-
.map((heading) => {
|
|
7554
|
+
.map((heading, index) => {
|
|
7554
7555
|
const bookmarkName = bookmarkNameByOffset.get(heading.offset);
|
|
7555
|
-
|
|
7556
|
+
const entry: RuntimeTocEntry = {
|
|
7556
7557
|
level: heading.level,
|
|
7557
7558
|
text: heading.text,
|
|
7558
7559
|
pageIndex: heading.pageIndex,
|
|
7559
7560
|
...(bookmarkName ? { bookmarkName } : {}),
|
|
7560
7561
|
};
|
|
7562
|
+
const cachedPageText = resolveCachedTocPageText(selectedRegion, entry, index);
|
|
7563
|
+
return cachedPageText ? { ...entry, pageText: cachedPageText } : entry;
|
|
7561
7564
|
});
|
|
7562
7565
|
if (resultEntries.length === 0) {
|
|
7563
7566
|
resultEntries = entries;
|
|
@@ -7657,6 +7660,7 @@ type RuntimeTocEntry = {
|
|
|
7657
7660
|
level: number;
|
|
7658
7661
|
text: string;
|
|
7659
7662
|
pageIndex: number;
|
|
7663
|
+
pageText?: string;
|
|
7660
7664
|
bookmarkName?: string;
|
|
7661
7665
|
};
|
|
7662
7666
|
|
|
@@ -7688,11 +7692,54 @@ function buildTocRefreshResult(
|
|
|
7688
7692
|
level: entry.level,
|
|
7689
7693
|
text: entry.text,
|
|
7690
7694
|
pageIndex: entry.pageIndex,
|
|
7695
|
+
...(entry.pageText ? { pageText: entry.pageText } : {}),
|
|
7691
7696
|
source: "generated",
|
|
7692
7697
|
})),
|
|
7693
7698
|
};
|
|
7694
7699
|
}
|
|
7695
7700
|
|
|
7701
|
+
function resolveCachedTocPageText(
|
|
7702
|
+
region: TocRegion | undefined,
|
|
7703
|
+
entry: RuntimeTocEntry,
|
|
7704
|
+
index: number,
|
|
7705
|
+
): string | undefined {
|
|
7706
|
+
if (!region || region.cachedEntries.length === 0) {
|
|
7707
|
+
return undefined;
|
|
7708
|
+
}
|
|
7709
|
+
const indexed = region.cachedEntries[index];
|
|
7710
|
+
if (indexed?.pageText && cachedTocEntryMatchesRuntimeEntry(indexed, entry)) {
|
|
7711
|
+
return indexed.pageText;
|
|
7712
|
+
}
|
|
7713
|
+
if (entry.bookmarkName) {
|
|
7714
|
+
const bookmarkMatch = region.cachedEntries.find(
|
|
7715
|
+
(cached) =>
|
|
7716
|
+
cached.bookmarkName === entry.bookmarkName &&
|
|
7717
|
+
cached.pageText &&
|
|
7718
|
+
cached.level === entry.level,
|
|
7719
|
+
);
|
|
7720
|
+
if (bookmarkMatch?.pageText) {
|
|
7721
|
+
return bookmarkMatch.pageText;
|
|
7722
|
+
}
|
|
7723
|
+
}
|
|
7724
|
+
return undefined;
|
|
7725
|
+
}
|
|
7726
|
+
|
|
7727
|
+
function cachedTocEntryMatchesRuntimeEntry(
|
|
7728
|
+
cached: TocCachedEntry,
|
|
7729
|
+
entry: RuntimeTocEntry,
|
|
7730
|
+
): boolean {
|
|
7731
|
+
return cached.level === entry.level &&
|
|
7732
|
+
normalizeTocEntryPageReuseText(cached.text) === normalizeTocEntryPageReuseText(entry.text);
|
|
7733
|
+
}
|
|
7734
|
+
|
|
7735
|
+
function normalizeTocEntryPageReuseText(text: string): string {
|
|
7736
|
+
return text
|
|
7737
|
+
.replace(/\s+/gu, " ")
|
|
7738
|
+
.trim()
|
|
7739
|
+
.replace(/^(\d+(?:\.\d+)*\.?)\s+/u, "$1")
|
|
7740
|
+
.toLowerCase();
|
|
7741
|
+
}
|
|
7742
|
+
|
|
7696
7743
|
function tocPageTextToPageIndex(pageText: string | undefined): number {
|
|
7697
7744
|
const value = Number.parseInt(pageText ?? "", 10);
|
|
7698
7745
|
return Number.isFinite(value) && value > 0 ? value - 1 : 0;
|
|
@@ -7873,7 +7920,7 @@ function buildTocEntryInlineNodes(
|
|
|
7873
7920
|
children.push({ type: "text", text: entry.text });
|
|
7874
7921
|
}
|
|
7875
7922
|
children.push({ type: "tab" });
|
|
7876
|
-
const displayed = resolveDisplayPageNumber?.(entry.pageIndex);
|
|
7923
|
+
const displayed = entry.pageText ?? resolveDisplayPageNumber?.(entry.pageIndex);
|
|
7877
7924
|
children.push({
|
|
7878
7925
|
type: "text",
|
|
7879
7926
|
text: String(displayed ?? entry.pageIndex + 1),
|
|
@@ -8056,7 +8103,7 @@ function buildInlineNodesFromDisplayText(text: string): InlineNode[] {
|
|
|
8056
8103
|
* resolver, falls back to the raw `pageIndex + 1` (pre-P5 behavior).
|
|
8057
8104
|
*/
|
|
8058
8105
|
function buildTocInlineNodes(
|
|
8059
|
-
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number; bookmarkName?: string }>,
|
|
8106
|
+
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number; pageText?: string; bookmarkName?: string }>,
|
|
8060
8107
|
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
8061
8108
|
): InlineNode[] {
|
|
8062
8109
|
const children: InlineNode[] = [];
|
|
@@ -8071,7 +8118,7 @@ function buildTocInlineNodes(
|
|
|
8071
8118
|
children.push({ type: "text", text: entry.text });
|
|
8072
8119
|
}
|
|
8073
8120
|
children.push({ type: "tab" });
|
|
8074
|
-
const displayed = resolveDisplayPageNumber?.(entry.pageIndex);
|
|
8121
|
+
const displayed = entry.pageText ?? resolveDisplayPageNumber?.(entry.pageIndex);
|
|
8075
8122
|
children.push({
|
|
8076
8123
|
type: "text",
|
|
8077
8124
|
text: String(displayed ?? entry.pageIndex + 1),
|
|
@@ -8085,7 +8132,7 @@ function buildTocInlineNodes(
|
|
|
8085
8132
|
|
|
8086
8133
|
/** Test-only export of `buildTocInlineNodes` (P5 unit tests). */
|
|
8087
8134
|
export function __buildTocInlineNodes(
|
|
8088
|
-
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number; bookmarkName?: string }>,
|
|
8135
|
+
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number; pageText?: string; bookmarkName?: string }>,
|
|
8089
8136
|
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
8090
8137
|
): InlineNode[] {
|
|
8091
8138
|
return buildTocInlineNodes(entries, resolveDisplayPageNumber);
|
|
@@ -136,7 +136,7 @@ export function ensureHostMetadataParts(
|
|
|
136
136
|
if (!corePropertiesPart || corePropertiesPart.contentType !== CORE_PROPERTIES_CONTENT_TYPE) {
|
|
137
137
|
exportSession.replaceOwnedPart({
|
|
138
138
|
path: CORE_PROPERTIES_PART_PATH,
|
|
139
|
-
bytes:
|
|
139
|
+
bytes: new TextEncoder().encode(buildCorePropertiesXml(document)),
|
|
140
140
|
contentType: CORE_PROPERTIES_CONTENT_TYPE,
|
|
141
141
|
compression: corePropertiesPart?.compression,
|
|
142
142
|
});
|
|
@@ -146,7 +146,7 @@ export function ensureHostMetadataParts(
|
|
|
146
146
|
if (!appPropertiesPart || appPropertiesPart.contentType !== APP_PROPERTIES_CONTENT_TYPE) {
|
|
147
147
|
exportSession.replaceOwnedPart({
|
|
148
148
|
path: APP_PROPERTIES_PART_PATH,
|
|
149
|
-
bytes:
|
|
149
|
+
bytes: new TextEncoder().encode(buildAppPropertiesXml(document.metadata.appProperties)),
|
|
150
150
|
contentType: APP_PROPERTIES_CONTENT_TYPE,
|
|
151
151
|
compression: appPropertiesPart?.compression,
|
|
152
152
|
});
|
|
@@ -305,11 +305,16 @@ export function buildCorePropertiesXml(document: CanonicalDocumentEnvelope): str
|
|
|
305
305
|
xmlNode("dc:title", metadata.title),
|
|
306
306
|
xmlNode("dc:subject", metadata.subject),
|
|
307
307
|
xmlNode("dc:description", metadata.description),
|
|
308
|
+
xmlNode("dc:creator", metadata.creator),
|
|
308
309
|
xmlNode("dc:language", metadata.language),
|
|
309
310
|
xmlNode("cp:keywords", keywords),
|
|
310
311
|
xmlNode("cp:category", metadata.category),
|
|
311
|
-
xmlNode(
|
|
312
|
-
xmlNode(
|
|
312
|
+
xmlNode("cp:lastModifiedBy", metadata.lastModifiedBy),
|
|
313
|
+
xmlNode("cp:contentStatus", metadata.contentStatus),
|
|
314
|
+
xmlNode("cp:revision", metadata.revision),
|
|
315
|
+
xmlNode("cp:version", metadata.version),
|
|
316
|
+
xmlNode('dcterms:created xsi:type="dcterms:W3CDTF"', metadata.createdUtc ?? document.createdAt),
|
|
317
|
+
xmlNode('dcterms:modified xsi:type="dcterms:W3CDTF"', metadata.modifiedUtc ?? document.updatedAt),
|
|
313
318
|
].filter((line): line is string => Boolean(line));
|
|
314
319
|
|
|
315
320
|
return [
|
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
import { toEditorSessionState } from "../import/canonical-assembly.ts";
|
|
84
84
|
import {
|
|
85
85
|
serializeCanonicalDocumentForExport,
|
|
86
|
+
serializeNumberingCatalogForExport,
|
|
86
87
|
} from "../shared/session-utils.ts";
|
|
87
88
|
import { withDocumentRelatedParts } from "../import/package-parts.ts";
|
|
88
89
|
import {
|
|
@@ -290,6 +291,14 @@ export async function runStatefulExport(
|
|
|
290
291
|
state.sourcePeoplePartPath ?? PEOPLE_PART_PATH;
|
|
291
292
|
const numberingPartPath =
|
|
292
293
|
state.sourceNumberingPartPath ?? NUMBERING_PART_PATH;
|
|
294
|
+
const sourceNumberingPart = state.sourceNumberingPartPath
|
|
295
|
+
? state.sourcePackage.parts.get(numberingPartPath)
|
|
296
|
+
: undefined;
|
|
297
|
+
const numberingSignatureMatch =
|
|
298
|
+
serializeNumberingCatalogForExport(currentDocument.numbering as NumberingCatalog) ===
|
|
299
|
+
state.initialNumberingSignature;
|
|
300
|
+
const canReuseSourceNumberingPart =
|
|
301
|
+
numberingSignatureMatch && sourceNumberingPart !== undefined;
|
|
293
302
|
const serializedNumberingXml = hasSerializableNumberingEntries(
|
|
294
303
|
currentDocument.numbering as NumberingCatalog,
|
|
295
304
|
)
|
|
@@ -303,6 +312,7 @@ export async function runStatefulExport(
|
|
|
303
312
|
partPath: numberingPartPath,
|
|
304
313
|
existingRelationshipId: state.sourceNumberingRelationshipId,
|
|
305
314
|
include:
|
|
315
|
+
canReuseSourceNumberingPart ||
|
|
306
316
|
Boolean(serializedNumberingXml) ||
|
|
307
317
|
Boolean(state.sourceNumberingPartPath),
|
|
308
318
|
},
|
|
@@ -423,16 +433,22 @@ export async function runStatefulExport(
|
|
|
423
433
|
relationships: nextRelationships,
|
|
424
434
|
});
|
|
425
435
|
|
|
426
|
-
if (serializedNumberingXml || state.sourceNumberingPartPath) {
|
|
436
|
+
if (canReuseSourceNumberingPart || serializedNumberingXml || state.sourceNumberingPartPath) {
|
|
437
|
+
const numberingBytes =
|
|
438
|
+
canReuseSourceNumberingPart && sourceNumberingPart
|
|
439
|
+
? sourceNumberingPart.bytes
|
|
440
|
+
: new TextEncoder().encode(
|
|
441
|
+
serializedNumberingXml ??
|
|
442
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"></w:numbering>`,
|
|
443
|
+
);
|
|
444
|
+
|
|
427
445
|
exportSession.replaceOwnedPart({
|
|
428
446
|
path: numberingPartPath,
|
|
429
|
-
bytes:
|
|
430
|
-
serializedNumberingXml ??
|
|
431
|
-
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"></w:numbering>`,
|
|
432
|
-
),
|
|
447
|
+
bytes: numberingBytes,
|
|
433
448
|
contentType:
|
|
434
|
-
|
|
449
|
+
sourceNumberingPart?.contentType ??
|
|
435
450
|
WORD_NUMBERING_CONTENT_TYPE,
|
|
451
|
+
compression: sourceNumberingPart?.compression,
|
|
436
452
|
});
|
|
437
453
|
}
|
|
438
454
|
|
|
@@ -133,6 +133,7 @@ export function createImportedCanonicalDocument(input: {
|
|
|
133
133
|
subParts?: SubPartsCatalog;
|
|
134
134
|
parsedStyles?: ParseStylesResult;
|
|
135
135
|
fontTable?: CanonicalDocument["fontTable"];
|
|
136
|
+
metadata?: CanonicalDocument["metadata"];
|
|
136
137
|
preservation: CanonicalDocument["preservation"];
|
|
137
138
|
diagnostics: CanonicalDocument["diagnostics"];
|
|
138
139
|
review: CanonicalDocument["review"];
|
|
@@ -166,9 +167,7 @@ export function createImportedCanonicalDocument(input: {
|
|
|
166
167
|
docId: createCanonicalDocumentId(input.documentId),
|
|
167
168
|
createdAt: input.timestamp,
|
|
168
169
|
updatedAt: input.timestamp,
|
|
169
|
-
metadata: {
|
|
170
|
-
customProperties: {},
|
|
171
|
-
},
|
|
170
|
+
metadata: input.metadata ?? { customProperties: {} },
|
|
172
171
|
styles,
|
|
173
172
|
numbering,
|
|
174
173
|
media: input.media,
|
|
@@ -252,7 +252,8 @@ export interface LoadedDocxEditorSession {
|
|
|
252
252
|
* State captured at import time that the export pipeline consumes to
|
|
253
253
|
* round-trip the package. Carries source bytes, the OPC package
|
|
254
254
|
* snapshot, per-part relationships, preserved-part manifest, and the
|
|
255
|
-
* initial canonical
|
|
255
|
+
* initial canonical signatures used for fast-path byte reuse and
|
|
256
|
+
* preserve-first per-part export decisions.
|
|
256
257
|
*
|
|
257
258
|
* Load path builds this; `exportDocxEditorSession` consumes it via
|
|
258
259
|
* the closure captured at open time.
|
|
@@ -299,6 +300,7 @@ export interface ImportedDocxState {
|
|
|
299
300
|
preservedCommentDefinitions: readonly ImportedCommentDefinition[];
|
|
300
301
|
blockingCommentDiagnostics: readonly CommentImportDiagnostic[];
|
|
301
302
|
initialCanonicalSignature: string;
|
|
303
|
+
initialNumberingSignature: string;
|
|
302
304
|
sourceSubPartPaths: {
|
|
303
305
|
headers: Array<{ partPath: string; relationshipId: string }>;
|
|
304
306
|
footers: Array<{ partPath: string; relationshipId: string }>;
|
|
@@ -97,6 +97,7 @@ import {
|
|
|
97
97
|
} from "../../io/ooxml/parse-revisions.ts";
|
|
98
98
|
import { parseCommentsFromOoxml } from "../../io/ooxml/parse-comments.ts";
|
|
99
99
|
import { parseNumberingXml } from "../../io/ooxml/parse-numbering.ts";
|
|
100
|
+
import { parseDocumentMetadataFromOpcParts } from "../../io/ooxml/docprops.ts";
|
|
100
101
|
import {
|
|
101
102
|
parseHeaderFooterReferences,
|
|
102
103
|
parseHeaderXml,
|
|
@@ -131,6 +132,7 @@ import {
|
|
|
131
132
|
decodeUtf8,
|
|
132
133
|
extractDocumentRootAttributes,
|
|
133
134
|
serializeCanonicalDocumentForExport,
|
|
135
|
+
serializeNumberingCatalogForExport,
|
|
134
136
|
toUint8Array,
|
|
135
137
|
} from "../shared/session-utils.ts";
|
|
136
138
|
import {
|
|
@@ -285,6 +287,7 @@ export async function loadDocxSessionAsync(
|
|
|
285
287
|
}),
|
|
286
288
|
);
|
|
287
289
|
}
|
|
290
|
+
const packageMetadata = parseDocumentMetadataFromOpcParts(sourcePackage.parts);
|
|
288
291
|
stages.emit("opc");
|
|
289
292
|
await scheduler.yield();
|
|
290
293
|
const embeddedWorkflowPayload = parseWorkflowPayloadEnvelopeFromPackage(sourcePackage);
|
|
@@ -1018,6 +1021,7 @@ export async function loadDocxSessionAsync(
|
|
|
1018
1021
|
subParts,
|
|
1019
1022
|
parsedStyles,
|
|
1020
1023
|
fontTable: parsedFontTable,
|
|
1024
|
+
metadata: packageMetadata,
|
|
1021
1025
|
preservation: buildImportPreservation(normalizedDocument.preservation, sourcePackage, [
|
|
1022
1026
|
mainDocumentPath,
|
|
1023
1027
|
numberingPartPath,
|
|
@@ -1152,6 +1156,9 @@ export async function loadDocxSessionAsync(
|
|
|
1152
1156
|
BLOCKING_COMMENT_DIAGNOSTIC_CODES.has(diagnostic.code),
|
|
1153
1157
|
),
|
|
1154
1158
|
initialCanonicalSignature: serializeCanonicalDocumentForExport(document),
|
|
1159
|
+
initialNumberingSignature: serializeNumberingCatalogForExport(
|
|
1160
|
+
document.numbering as _NumberingCatalog,
|
|
1161
|
+
),
|
|
1155
1162
|
sourceSubPartPaths: {
|
|
1156
1163
|
headers: sourceHeaderPaths,
|
|
1157
1164
|
footers: sourceFooterPaths,
|
|
@@ -1229,6 +1236,7 @@ export function loadDocxSessionSync(
|
|
|
1229
1236
|
}),
|
|
1230
1237
|
);
|
|
1231
1238
|
}
|
|
1239
|
+
const packageMetadata = parseDocumentMetadataFromOpcParts(sourcePackage.parts);
|
|
1232
1240
|
stages.emit("opc");
|
|
1233
1241
|
const embeddedWorkflowPayload = parseWorkflowPayloadEnvelopeFromPackage(sourcePackage);
|
|
1234
1242
|
const embeddedWorkflowMetadata = embeddedWorkflowPayload?.workflowMetadata;
|
|
@@ -1697,6 +1705,7 @@ export function loadDocxSessionSync(
|
|
|
1697
1705
|
subParts,
|
|
1698
1706
|
parsedStyles,
|
|
1699
1707
|
fontTable: parsedFontTable,
|
|
1708
|
+
metadata: packageMetadata,
|
|
1700
1709
|
preservation: buildImportPreservation(normalizedDocument.preservation, sourcePackage, [
|
|
1701
1710
|
mainDocumentPath,
|
|
1702
1711
|
numberingPartPath,
|
|
@@ -1808,6 +1817,9 @@ export function loadDocxSessionSync(
|
|
|
1808
1817
|
BLOCKING_COMMENT_DIAGNOSTIC_CODES.has(diagnostic.code),
|
|
1809
1818
|
),
|
|
1810
1819
|
initialCanonicalSignature: serializeCanonicalDocumentForExport(document),
|
|
1820
|
+
initialNumberingSignature: serializeNumberingCatalogForExport(
|
|
1821
|
+
document.numbering as _NumberingCatalog,
|
|
1822
|
+
),
|
|
1811
1823
|
sourceSubPartPaths: {
|
|
1812
1824
|
headers: sourceHeaderPaths,
|
|
1813
1825
|
footers: sourceFooterPaths,
|
|
@@ -0,0 +1,405 @@
|
|
|
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 } 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 SourcePackageEvidence {
|
|
54
|
+
schemaVersion: 1;
|
|
55
|
+
docId: string;
|
|
56
|
+
sourceLabel?: string;
|
|
57
|
+
sha256Hex: string;
|
|
58
|
+
sourceByteLength: number;
|
|
59
|
+
mainDocumentPartPath?: string;
|
|
60
|
+
partCount: number;
|
|
61
|
+
contentPartCount: number;
|
|
62
|
+
relationshipCount: number;
|
|
63
|
+
internalRelationshipCount: number;
|
|
64
|
+
externalRelationshipCount: number;
|
|
65
|
+
brokenInternalRelationshipCount: number;
|
|
66
|
+
counts: SourcePackageElementCounts & {
|
|
67
|
+
headers: number;
|
|
68
|
+
footers: number;
|
|
69
|
+
mediaParts: number;
|
|
70
|
+
imageParts: number;
|
|
71
|
+
chartParts: number;
|
|
72
|
+
embeddedParts: number;
|
|
73
|
+
customXmlParts: number;
|
|
74
|
+
commentsParts: number;
|
|
75
|
+
numberingParts: number;
|
|
76
|
+
settingsParts: number;
|
|
77
|
+
themeParts: number;
|
|
78
|
+
};
|
|
79
|
+
parts: SourcePackagePartEvidence[];
|
|
80
|
+
relationships: SourcePackageRelationshipEvidence[];
|
|
81
|
+
parseErrors: Array<{ partPath: string; message: string }>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type SourcePackageStoryKind =
|
|
85
|
+
| "main-document"
|
|
86
|
+
| "header"
|
|
87
|
+
| "footer"
|
|
88
|
+
| "footnotes"
|
|
89
|
+
| "endnotes"
|
|
90
|
+
| "comments"
|
|
91
|
+
| "numbering"
|
|
92
|
+
| "settings"
|
|
93
|
+
| "styles"
|
|
94
|
+
| "font-table"
|
|
95
|
+
| "theme"
|
|
96
|
+
| "chart"
|
|
97
|
+
| "media"
|
|
98
|
+
| "embedded"
|
|
99
|
+
| "custom-xml"
|
|
100
|
+
| "relationships"
|
|
101
|
+
| "content-types"
|
|
102
|
+
| "other";
|
|
103
|
+
|
|
104
|
+
const ZERO_COUNTS: SourcePackageElementCounts = {
|
|
105
|
+
tables: 0,
|
|
106
|
+
tableRows: 0,
|
|
107
|
+
tableCells: 0,
|
|
108
|
+
anchors: 0,
|
|
109
|
+
inlineDrawings: 0,
|
|
110
|
+
drawings: 0,
|
|
111
|
+
vmlShapes: 0,
|
|
112
|
+
contentControls: 0,
|
|
113
|
+
fieldMarkers: 0,
|
|
114
|
+
fieldInstructions: 0,
|
|
115
|
+
simpleFields: 0,
|
|
116
|
+
bookmarks: 0,
|
|
117
|
+
footnotes: 0,
|
|
118
|
+
endnotes: 0,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export function createSourcePackageEvidenceFromBytes(
|
|
122
|
+
input: {
|
|
123
|
+
bytes: Uint8Array | ArrayBuffer;
|
|
124
|
+
docId: string;
|
|
125
|
+
sourceLabel?: string;
|
|
126
|
+
},
|
|
127
|
+
): SourcePackageEvidence {
|
|
128
|
+
const bytes = input.bytes instanceof Uint8Array
|
|
129
|
+
? input.bytes
|
|
130
|
+
: new Uint8Array(input.bytes);
|
|
131
|
+
return createSourcePackageEvidence(readOpcPackage(bytes), {
|
|
132
|
+
docId: input.docId,
|
|
133
|
+
sourceLabel: input.sourceLabel,
|
|
134
|
+
sha256Hex: sha256Hex(bytes),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createSourcePackageEvidence(
|
|
139
|
+
sourcePackage: OpcPackage,
|
|
140
|
+
input: {
|
|
141
|
+
docId: string;
|
|
142
|
+
sourceLabel?: string;
|
|
143
|
+
sha256Hex?: string;
|
|
144
|
+
},
|
|
145
|
+
): SourcePackageEvidence {
|
|
146
|
+
const mainDocumentPartPath = resolveMainDocumentPartPath(sourcePackage);
|
|
147
|
+
const parseErrors: SourcePackageEvidence["parseErrors"] = [];
|
|
148
|
+
const parts = [...sourcePackage.parts.values()]
|
|
149
|
+
.sort((left, right) => left.path.localeCompare(right.path))
|
|
150
|
+
.map((part) => summarizePart(part, mainDocumentPartPath, parseErrors));
|
|
151
|
+
const relationships = collectRelationships(sourcePackage);
|
|
152
|
+
const relationshipCount = relationships.length;
|
|
153
|
+
const internalRelationshipCount = relationships.filter(
|
|
154
|
+
(relationship) => relationship.targetMode === "internal",
|
|
155
|
+
).length;
|
|
156
|
+
const externalRelationshipCount = relationshipCount - internalRelationshipCount;
|
|
157
|
+
const brokenInternalRelationshipCount = relationships.filter(
|
|
158
|
+
(relationship) =>
|
|
159
|
+
relationship.targetMode === "internal" && !relationship.targetExists,
|
|
160
|
+
).length;
|
|
161
|
+
const contentParts = parts.filter(
|
|
162
|
+
(part) => part.storyKind !== "relationships" && part.storyKind !== "content-types",
|
|
163
|
+
);
|
|
164
|
+
const aggregateCounts = aggregateElementCounts(parts.map((part) => part.counts));
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
schemaVersion: 1,
|
|
168
|
+
docId: input.docId,
|
|
169
|
+
...(input.sourceLabel !== undefined ? { sourceLabel: input.sourceLabel } : {}),
|
|
170
|
+
sha256Hex: input.sha256Hex ?? "",
|
|
171
|
+
sourceByteLength: sourcePackage.sourceByteLength,
|
|
172
|
+
...(mainDocumentPartPath !== undefined ? { mainDocumentPartPath } : {}),
|
|
173
|
+
partCount: sourcePackage.parts.size,
|
|
174
|
+
contentPartCount: contentParts.length,
|
|
175
|
+
relationshipCount,
|
|
176
|
+
internalRelationshipCount,
|
|
177
|
+
externalRelationshipCount,
|
|
178
|
+
brokenInternalRelationshipCount,
|
|
179
|
+
counts: {
|
|
180
|
+
...aggregateCounts,
|
|
181
|
+
headers: countParts(parts, "header"),
|
|
182
|
+
footers: countParts(parts, "footer"),
|
|
183
|
+
mediaParts: countParts(parts, "media"),
|
|
184
|
+
imageParts: parts.filter((part) => part.contentType?.startsWith("image/")).length,
|
|
185
|
+
chartParts: countParts(parts, "chart"),
|
|
186
|
+
embeddedParts: countParts(parts, "embedded"),
|
|
187
|
+
customXmlParts: countParts(parts, "custom-xml"),
|
|
188
|
+
commentsParts: countParts(parts, "comments"),
|
|
189
|
+
numberingParts: countParts(parts, "numbering"),
|
|
190
|
+
settingsParts: countParts(parts, "settings"),
|
|
191
|
+
themeParts: countParts(parts, "theme"),
|
|
192
|
+
},
|
|
193
|
+
parts,
|
|
194
|
+
relationships,
|
|
195
|
+
parseErrors,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function summarizePart(
|
|
200
|
+
part: OpcPackagePart,
|
|
201
|
+
mainDocumentPartPath: string | undefined,
|
|
202
|
+
parseErrors: SourcePackageEvidence["parseErrors"],
|
|
203
|
+
): SourcePackagePartEvidence {
|
|
204
|
+
const storyKind = classifyPart(part, mainDocumentPartPath);
|
|
205
|
+
return {
|
|
206
|
+
path: part.path,
|
|
207
|
+
storyKind,
|
|
208
|
+
contentType: part.contentType,
|
|
209
|
+
byteLength: part.bytes.byteLength,
|
|
210
|
+
relationshipCount: part.relationships.length,
|
|
211
|
+
counts: countPartElements(part, storyKind, parseErrors),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function countPartElements(
|
|
216
|
+
part: OpcPackagePart,
|
|
217
|
+
storyKind: SourcePackageStoryKind,
|
|
218
|
+
parseErrors: SourcePackageEvidence["parseErrors"],
|
|
219
|
+
): SourcePackageElementCounts {
|
|
220
|
+
if (!shouldParseXmlPart(part, storyKind)) {
|
|
221
|
+
return { ...ZERO_COUNTS };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
return countElements(parseXml(new TextDecoder("utf-8").decode(part.bytes)));
|
|
226
|
+
} catch (error) {
|
|
227
|
+
parseErrors.push({
|
|
228
|
+
partPath: part.path,
|
|
229
|
+
message: error instanceof Error ? error.message : String(error),
|
|
230
|
+
});
|
|
231
|
+
return { ...ZERO_COUNTS };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function shouldParseXmlPart(
|
|
236
|
+
part: OpcPackagePart,
|
|
237
|
+
storyKind: SourcePackageStoryKind,
|
|
238
|
+
): boolean {
|
|
239
|
+
if (part.surfaceKind !== "content") {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
if (storyKind === "media" || storyKind === "embedded") {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
return (
|
|
246
|
+
part.contentType?.includes("xml") === true ||
|
|
247
|
+
part.path.endsWith(".xml")
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function countElements(root: XmlElementNode): SourcePackageElementCounts {
|
|
252
|
+
const counts = { ...ZERO_COUNTS };
|
|
253
|
+
const visit = (node: XmlElementNode): void => {
|
|
254
|
+
const name = localName(node.name);
|
|
255
|
+
switch (name) {
|
|
256
|
+
case "tbl":
|
|
257
|
+
counts.tables += 1;
|
|
258
|
+
break;
|
|
259
|
+
case "tr":
|
|
260
|
+
counts.tableRows += 1;
|
|
261
|
+
break;
|
|
262
|
+
case "tc":
|
|
263
|
+
counts.tableCells += 1;
|
|
264
|
+
break;
|
|
265
|
+
case "anchor":
|
|
266
|
+
counts.anchors += 1;
|
|
267
|
+
break;
|
|
268
|
+
case "inline":
|
|
269
|
+
counts.inlineDrawings += 1;
|
|
270
|
+
break;
|
|
271
|
+
case "drawing":
|
|
272
|
+
counts.drawings += 1;
|
|
273
|
+
break;
|
|
274
|
+
case "shape":
|
|
275
|
+
counts.vmlShapes += 1;
|
|
276
|
+
break;
|
|
277
|
+
case "sdt":
|
|
278
|
+
counts.contentControls += 1;
|
|
279
|
+
break;
|
|
280
|
+
case "fldChar":
|
|
281
|
+
counts.fieldMarkers += 1;
|
|
282
|
+
break;
|
|
283
|
+
case "instrText":
|
|
284
|
+
counts.fieldInstructions += 1;
|
|
285
|
+
break;
|
|
286
|
+
case "fldSimple":
|
|
287
|
+
counts.simpleFields += 1;
|
|
288
|
+
break;
|
|
289
|
+
case "bookmarkStart":
|
|
290
|
+
counts.bookmarks += 1;
|
|
291
|
+
break;
|
|
292
|
+
case "footnote":
|
|
293
|
+
counts.footnotes += 1;
|
|
294
|
+
break;
|
|
295
|
+
case "endnote":
|
|
296
|
+
counts.endnotes += 1;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const child of node.children) {
|
|
301
|
+
if (child.type === "element") {
|
|
302
|
+
visit(child);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
visit(root);
|
|
308
|
+
return counts;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function collectRelationships(
|
|
312
|
+
sourcePackage: OpcPackage,
|
|
313
|
+
): SourcePackageRelationshipEvidence[] {
|
|
314
|
+
const relationships: SourcePackageRelationshipEvidence[] = [];
|
|
315
|
+
const pushRelationship = (
|
|
316
|
+
sourcePartPath: string | null,
|
|
317
|
+
relationship: OpcRelationship,
|
|
318
|
+
): void => {
|
|
319
|
+
const resolvedTarget =
|
|
320
|
+
relationship.targetMode === "internal"
|
|
321
|
+
? resolveRelationshipTarget(sourcePartPath, relationship)
|
|
322
|
+
: relationship.target;
|
|
323
|
+
relationships.push({
|
|
324
|
+
sourcePartPath,
|
|
325
|
+
relationshipId: relationship.id,
|
|
326
|
+
relationshipType: relationship.type,
|
|
327
|
+
targetMode: relationship.targetMode,
|
|
328
|
+
target: relationship.target,
|
|
329
|
+
resolvedTarget,
|
|
330
|
+
targetExists:
|
|
331
|
+
relationship.targetMode === "external" ||
|
|
332
|
+
sourcePackage.parts.has(normalizePartPath(resolvedTarget)),
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
for (const relationship of sourcePackage.manifest.packageRelationships) {
|
|
337
|
+
pushRelationship(null, relationship);
|
|
338
|
+
}
|
|
339
|
+
for (const part of sourcePackage.parts.values()) {
|
|
340
|
+
for (const relationship of part.relationships) {
|
|
341
|
+
pushRelationship(part.path, relationship);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return relationships.sort((left, right) => {
|
|
346
|
+
const leftKey = `${left.sourcePartPath ?? ""}:${left.relationshipId}`;
|
|
347
|
+
const rightKey = `${right.sourcePartPath ?? ""}:${right.relationshipId}`;
|
|
348
|
+
return leftKey.localeCompare(rightKey);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function classifyPart(
|
|
353
|
+
part: OpcPackagePart,
|
|
354
|
+
mainDocumentPartPath: string | undefined,
|
|
355
|
+
): SourcePackageStoryKind {
|
|
356
|
+
const path = part.path;
|
|
357
|
+
if (part.surfaceKind === "relationships") return "relationships";
|
|
358
|
+
if (part.surfaceKind === "content-types") return "content-types";
|
|
359
|
+
if (mainDocumentPartPath && path === mainDocumentPartPath) return "main-document";
|
|
360
|
+
if (/^\/word\/header\d*\.xml$/u.test(path)) return "header";
|
|
361
|
+
if (/^\/word\/footer\d*\.xml$/u.test(path)) return "footer";
|
|
362
|
+
if (path === "/word/footnotes.xml") return "footnotes";
|
|
363
|
+
if (path === "/word/endnotes.xml") return "endnotes";
|
|
364
|
+
if (path.startsWith("/word/comments")) return "comments";
|
|
365
|
+
if (path === "/word/numbering.xml") return "numbering";
|
|
366
|
+
if (path === "/word/settings.xml") return "settings";
|
|
367
|
+
if (path === "/word/styles.xml") return "styles";
|
|
368
|
+
if (path === "/word/fontTable.xml") return "font-table";
|
|
369
|
+
if (path.startsWith("/word/theme/")) return "theme";
|
|
370
|
+
if (path.startsWith("/word/charts/")) return "chart";
|
|
371
|
+
if (path.startsWith("/word/media/")) return "media";
|
|
372
|
+
if (path.startsWith("/word/embeddings/")) return "embedded";
|
|
373
|
+
if (path.startsWith("/customXml/") && path.endsWith(".xml")) return "custom-xml";
|
|
374
|
+
return "other";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function aggregateElementCounts(
|
|
378
|
+
countsList: SourcePackageElementCounts[],
|
|
379
|
+
): SourcePackageElementCounts {
|
|
380
|
+
const aggregate = { ...ZERO_COUNTS };
|
|
381
|
+
for (const counts of countsList) {
|
|
382
|
+
aggregate.tables += counts.tables;
|
|
383
|
+
aggregate.tableRows += counts.tableRows;
|
|
384
|
+
aggregate.tableCells += counts.tableCells;
|
|
385
|
+
aggregate.anchors += counts.anchors;
|
|
386
|
+
aggregate.inlineDrawings += counts.inlineDrawings;
|
|
387
|
+
aggregate.drawings += counts.drawings;
|
|
388
|
+
aggregate.vmlShapes += counts.vmlShapes;
|
|
389
|
+
aggregate.contentControls += counts.contentControls;
|
|
390
|
+
aggregate.fieldMarkers += counts.fieldMarkers;
|
|
391
|
+
aggregate.fieldInstructions += counts.fieldInstructions;
|
|
392
|
+
aggregate.simpleFields += counts.simpleFields;
|
|
393
|
+
aggregate.bookmarks += counts.bookmarks;
|
|
394
|
+
aggregate.footnotes += counts.footnotes;
|
|
395
|
+
aggregate.endnotes += counts.endnotes;
|
|
396
|
+
}
|
|
397
|
+
return aggregate;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function countParts(
|
|
401
|
+
parts: SourcePackagePartEvidence[],
|
|
402
|
+
storyKind: SourcePackageStoryKind,
|
|
403
|
+
): number {
|
|
404
|
+
return parts.filter((part) => part.storyKind === storyKind).length;
|
|
405
|
+
}
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* signature wrapper that zeroes out `docId` / `createdAt` /
|
|
17
17
|
* `updatedAt` so structural comparison doesn't falsely register
|
|
18
18
|
* semantic drift.
|
|
19
|
+
* - `serializeNumberingCatalogForExport` — numbering-only signature
|
|
20
|
+
* used by export to preserve source numbering bytes when unrelated
|
|
21
|
+
* document edits force a package rewrite.
|
|
19
22
|
*
|
|
20
23
|
* P6-clean: imports only from `src/model/**` + `src/api/**`.
|
|
21
24
|
*/
|
|
@@ -80,3 +83,9 @@ export function serializeCanonicalDocumentForExport(
|
|
|
80
83
|
updatedAt: "__export__",
|
|
81
84
|
});
|
|
82
85
|
}
|
|
86
|
+
|
|
87
|
+
export function serializeNumberingCatalogForExport(
|
|
88
|
+
numbering: NumberingCatalog,
|
|
89
|
+
): string {
|
|
90
|
+
return createCanonicalDocumentSignature(numbering);
|
|
91
|
+
}
|