@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
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.103",
|
|
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": [
|
|
@@ -37,6 +37,7 @@ import type {
|
|
|
37
37
|
ParagraphNode,
|
|
38
38
|
TextMark,
|
|
39
39
|
TextNode,
|
|
40
|
+
Mutable,
|
|
40
41
|
} from "../../model/canonical-document.ts";
|
|
41
42
|
|
|
42
43
|
// ---------------------------------------------------------------------------
|
|
@@ -659,7 +660,7 @@ export function applyTextMarkOperationToDocumentRange(
|
|
|
659
660
|
updateMarks,
|
|
660
661
|
);
|
|
661
662
|
if (transformed.changed) {
|
|
662
|
-
block.children = transformed.nodes;
|
|
663
|
+
(block as Mutable<typeof block>).children = transformed.nodes;
|
|
663
664
|
changed = true;
|
|
664
665
|
}
|
|
665
666
|
}
|
|
@@ -862,7 +863,7 @@ function resolveMarkUpdater(
|
|
|
862
863
|
}
|
|
863
864
|
|
|
864
865
|
function applyAlignment(
|
|
865
|
-
paragraph: ParagraphNode
|
|
866
|
+
paragraph: Mutable<ParagraphNode>,
|
|
866
867
|
alignment: FormattingAlignment,
|
|
867
868
|
): boolean {
|
|
868
869
|
const nextAlignment = alignment === "justify" ? "both" : alignment;
|
|
@@ -880,7 +881,7 @@ function applyAlignment(
|
|
|
880
881
|
* must clone first if the source is shared. Returns `false` when no change
|
|
881
882
|
* occurred (already at the 0 / 8 bound, or no-op).
|
|
882
883
|
*/
|
|
883
|
-
export function applyIndentation(paragraph: ParagraphNode
|
|
884
|
+
export function applyIndentation(paragraph: Mutable<ParagraphNode>, delta: -1 | 1): boolean {
|
|
884
885
|
if (paragraph.numbering) {
|
|
885
886
|
const nextLevel = clamp(paragraph.numbering.level + delta, 0, 8);
|
|
886
887
|
if (nextLevel === paragraph.numbering.level) {
|
|
@@ -904,10 +905,10 @@ export function applyIndentation(paragraph: ParagraphNode, delta: -1 | 1): boole
|
|
|
904
905
|
...(paragraph.indentation ?? {}),
|
|
905
906
|
};
|
|
906
907
|
if (nextLeft > 0) {
|
|
907
|
-
nextIndentation.left = nextLeft;
|
|
908
|
+
(nextIndentation as Mutable<typeof nextIndentation>).left = nextLeft;
|
|
908
909
|
paragraph.indentation = nextIndentation;
|
|
909
910
|
} else if (paragraph.indentation) {
|
|
910
|
-
delete nextIndentation.left;
|
|
911
|
+
delete (nextIndentation as Mutable<typeof nextIndentation>).left;
|
|
911
912
|
paragraph.indentation =
|
|
912
913
|
Object.keys(nextIndentation).length > 0 ? nextIndentation : undefined;
|
|
913
914
|
}
|
|
@@ -1042,7 +1043,7 @@ function getConsistentValue<TItem, TValue>(
|
|
|
1042
1043
|
function visitParagraphBindings(
|
|
1043
1044
|
blocks: BlockNode[],
|
|
1044
1045
|
surfaceBlocks: SurfaceBlockSnapshot[],
|
|
1045
|
-
visitor: (paragraph: ParagraphNode
|
|
1046
|
+
visitor: (paragraph: Mutable<ParagraphNode>, surface: ParagraphSurfaceBlock) => void,
|
|
1046
1047
|
): void {
|
|
1047
1048
|
for (let index = 0; index < Math.min(blocks.length, surfaceBlocks.length); index += 1) {
|
|
1048
1049
|
const block = blocks[index];
|
|
@@ -1146,7 +1147,7 @@ function transformInlineNodes(
|
|
|
1146
1147
|
}
|
|
1147
1148
|
|
|
1148
1149
|
function transformTextNode(
|
|
1149
|
-
node: TextNode
|
|
1150
|
+
node: Mutable<TextNode>,
|
|
1150
1151
|
start: number,
|
|
1151
1152
|
selectionFrom: number,
|
|
1152
1153
|
selectionTo: number,
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
TableCellNode,
|
|
12
12
|
TableNode,
|
|
13
13
|
TableRowNode,
|
|
14
|
+
Mutable,
|
|
14
15
|
} from "../../model/canonical-document.ts";
|
|
15
16
|
|
|
16
17
|
export interface ParagraphLayoutCommandContext {
|
|
@@ -26,7 +27,7 @@ export interface ParagraphLayoutMutationResult {
|
|
|
26
27
|
export function setActiveParagraphIndentation(
|
|
27
28
|
document: CanonicalDocumentEnvelope,
|
|
28
29
|
snapshot: RuntimeRenderSnapshot,
|
|
29
|
-
indentation: ParagraphIndentation
|
|
30
|
+
indentation: Mutable<ParagraphIndentation>,
|
|
30
31
|
_context: ParagraphLayoutCommandContext,
|
|
31
32
|
): ParagraphLayoutMutationResult {
|
|
32
33
|
return mutateActiveParagraph(document, snapshot, (paragraph) => {
|
|
@@ -66,7 +67,7 @@ export function setActiveParagraphTabStops(
|
|
|
66
67
|
function mutateActiveParagraph(
|
|
67
68
|
document: CanonicalDocumentEnvelope,
|
|
68
69
|
snapshot: RuntimeRenderSnapshot,
|
|
69
|
-
mutate: (paragraph: ParagraphNode) => boolean,
|
|
70
|
+
mutate: (paragraph: Mutable<ParagraphNode>) => boolean,
|
|
70
71
|
): ParagraphLayoutMutationResult {
|
|
71
72
|
const surface = snapshot.surface;
|
|
72
73
|
if (!surface) {
|
|
@@ -190,27 +191,27 @@ function collectCanonicalParagraphs(
|
|
|
190
191
|
return output;
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
function collectParagraphsFromTable(table: TableNode
|
|
194
|
+
function collectParagraphsFromTable(table: Mutable<TableNode>, output: ParagraphNode[]): void {
|
|
194
195
|
for (const row of table.rows) {
|
|
195
196
|
collectParagraphsFromRow(row, output);
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
|
|
199
|
-
function collectParagraphsFromRow(row: TableRowNode
|
|
200
|
+
function collectParagraphsFromRow(row: Mutable<TableRowNode>, output: ParagraphNode[]): void {
|
|
200
201
|
for (const cell of row.cells) {
|
|
201
202
|
collectParagraphsFromCell(cell, output);
|
|
202
203
|
}
|
|
203
204
|
}
|
|
204
205
|
|
|
205
|
-
function collectParagraphsFromCell(cell: TableCellNode
|
|
206
|
+
function collectParagraphsFromCell(cell: Mutable<TableCellNode>, output: ParagraphNode[]): void {
|
|
206
207
|
collectCanonicalParagraphs(cell.children, output);
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
function mergeIndentationPatch(
|
|
210
211
|
current: ParagraphIndentation | undefined,
|
|
211
|
-
patch: ParagraphIndentation
|
|
212
|
+
patch: Mutable<ParagraphIndentation>,
|
|
212
213
|
): ParagraphIndentation | undefined {
|
|
213
|
-
const merged: ParagraphIndentation = {
|
|
214
|
+
const merged: Mutable<ParagraphIndentation> = {
|
|
214
215
|
...(current ?? {}),
|
|
215
216
|
};
|
|
216
217
|
|
|
@@ -237,9 +238,9 @@ function mergeIndentationPatch(
|
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
function normalizeIndentation(
|
|
240
|
-
indentation: ParagraphIndentation
|
|
241
|
+
indentation: Mutable<ParagraphIndentation>,
|
|
241
242
|
): ParagraphIndentation | undefined {
|
|
242
|
-
const normalized: ParagraphIndentation = {};
|
|
243
|
+
const normalized: Mutable<ParagraphIndentation> = {};
|
|
243
244
|
if (indentation.left !== undefined && indentation.left > 0) {
|
|
244
245
|
normalized.left = Math.round(indentation.left);
|
|
245
246
|
}
|
|
@@ -250,7 +251,7 @@ function normalizeIndentation(
|
|
|
250
251
|
normalized.firstLine = Math.round(indentation.firstLine);
|
|
251
252
|
}
|
|
252
253
|
if (indentation.hanging !== undefined && indentation.hanging > 0) {
|
|
253
|
-
normalized.hanging = Math.round(indentation.hanging);
|
|
254
|
+
(normalized as Mutable<typeof normalized>).hanging = Math.round(indentation.hanging);
|
|
254
255
|
delete normalized.firstLine;
|
|
255
256
|
}
|
|
256
257
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
PageSize,
|
|
22
22
|
SectionBreakNode,
|
|
23
23
|
SectionProperties,
|
|
24
|
+
Mutable,
|
|
24
25
|
} from "../../model/canonical-document.ts";
|
|
25
26
|
import type {
|
|
26
27
|
MarginPresetDefinition,
|
|
@@ -145,7 +146,7 @@ export function insertSectionBreak(
|
|
|
145
146
|
|
|
146
147
|
const sectionTarget = resolveSectionTarget(cloned, surface.blocks, position);
|
|
147
148
|
const inheritedProperties = cloneSectionProperties(sectionTarget?.properties);
|
|
148
|
-
const sectionBreak: SectionBreakNode = {
|
|
149
|
+
const sectionBreak: Mutable<SectionBreakNode> = {
|
|
149
150
|
type: "section_break",
|
|
150
151
|
sectionProperties: {
|
|
151
152
|
...inheritedProperties,
|
|
@@ -178,7 +179,7 @@ export function insertSectionBreakAfterSectionIndex(
|
|
|
178
179
|
const inheritedProperties = cloneSectionProperties(
|
|
179
180
|
getSectionPropertiesAtIndex(cloned, sectionIndex),
|
|
180
181
|
);
|
|
181
|
-
const sectionBreak: SectionBreakNode = {
|
|
182
|
+
const sectionBreak: Mutable<SectionBreakNode> = {
|
|
182
183
|
type: "section_break",
|
|
183
184
|
sectionProperties: {
|
|
184
185
|
...inheritedProperties,
|
|
@@ -364,9 +365,9 @@ export function setSectionPageNumberingAtSectionIndex(
|
|
|
364
365
|
|
|
365
366
|
const nextProperties = cloneSectionProperties(target.properties);
|
|
366
367
|
if (pageNumbering === null) {
|
|
367
|
-
delete nextProperties.pageNumbering;
|
|
368
|
+
delete (nextProperties as Mutable<typeof nextProperties>).pageNumbering;
|
|
368
369
|
} else {
|
|
369
|
-
nextProperties.pageNumbering = {
|
|
370
|
+
(nextProperties as Mutable<typeof nextProperties>).pageNumbering = {
|
|
370
371
|
...(target.properties?.pageNumbering ?? {}),
|
|
371
372
|
...pageNumbering,
|
|
372
373
|
};
|
|
@@ -683,10 +684,10 @@ function findNearestSectionBreak(
|
|
|
683
684
|
}
|
|
684
685
|
|
|
685
686
|
function applySectionLayoutPatch(
|
|
686
|
-
existing: SectionProperties
|
|
687
|
+
existing: Mutable<SectionProperties>,
|
|
687
688
|
patch: SectionLayoutPatch,
|
|
688
689
|
): SectionProperties {
|
|
689
|
-
const result: SectionProperties = { ...existing };
|
|
690
|
+
const result: Mutable<SectionProperties> = { ...existing };
|
|
690
691
|
|
|
691
692
|
if (patch.pageSize) {
|
|
692
693
|
result.pageSize = {
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
DocumentRootNode,
|
|
9
9
|
ParagraphNode,
|
|
10
10
|
TableNode,
|
|
11
|
+
Mutable,
|
|
11
12
|
} from "../../model/canonical-document.ts";
|
|
12
13
|
|
|
13
14
|
type CanonicalDocumentEnvelope = PersistedEditorSnapshot["canonicalDocument"];
|
|
@@ -161,7 +162,7 @@ function isValidTableStyleId(
|
|
|
161
162
|
function visitParagraphBindings(
|
|
162
163
|
blocks: BlockNode[],
|
|
163
164
|
surfaceBlocks: SurfaceBlockSnapshot[],
|
|
164
|
-
visitor: (paragraph: ParagraphNode
|
|
165
|
+
visitor: (paragraph: Mutable<ParagraphNode>, surface: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>) => void,
|
|
165
166
|
): void {
|
|
166
167
|
for (let index = 0; index < Math.min(blocks.length, surfaceBlocks.length); index += 1) {
|
|
167
168
|
const block = blocks[index];
|
|
@@ -203,7 +204,7 @@ function visitParagraphBindings(
|
|
|
203
204
|
function visitTableBindings(
|
|
204
205
|
blocks: BlockNode[],
|
|
205
206
|
surfaceBlocks: SurfaceBlockSnapshot[],
|
|
206
|
-
visitor: (table: TableNode
|
|
207
|
+
visitor: (table: Mutable<TableNode>, surface: Extract<SurfaceBlockSnapshot, { kind: "table" }>) => void,
|
|
207
208
|
): void {
|
|
208
209
|
for (let index = 0; index < Math.min(blocks.length, surfaceBlocks.length); index += 1) {
|
|
209
210
|
const block = blocks[index];
|
|
@@ -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
|
}
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
TextMark,
|
|
19
19
|
TextNode,
|
|
20
20
|
SdtNode,
|
|
21
|
+
Mutable,
|
|
21
22
|
} from "../../model/canonical-document.ts";
|
|
22
23
|
import type {
|
|
23
24
|
ParsedAltChunkNode,
|
|
@@ -108,7 +109,7 @@ export function normalizeParsedTextDocument(
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
const content: DocumentRootNode = { type: "doc", children };
|
|
112
|
+
const content: Mutable<DocumentRootNode> = { type: "doc", children };
|
|
112
113
|
|
|
113
114
|
return {
|
|
114
115
|
content,
|
|
@@ -180,7 +181,7 @@ export async function normalizeParsedTextDocumentAsync(
|
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
183
|
-
const content: DocumentRootNode = { type: "doc", children };
|
|
184
|
+
const content: Mutable<DocumentRootNode> = { type: "doc", children };
|
|
184
185
|
|
|
185
186
|
return {
|
|
186
187
|
content,
|
|
@@ -432,7 +433,7 @@ function normalizeInlineChildren(
|
|
|
432
433
|
|
|
433
434
|
const previous = normalized[normalized.length - 1];
|
|
434
435
|
if (previous?.type === "text" && sameMarks(previous.marks, node.marks)) {
|
|
435
|
-
previous.text += node.text;
|
|
436
|
+
(previous as Mutable<typeof previous>).text += node.text;
|
|
436
437
|
} else {
|
|
437
438
|
normalized.push({
|
|
438
439
|
type: "text",
|
|
@@ -752,7 +753,7 @@ function normalizeHyperlink(node: ParsedHyperlinkNode): {
|
|
|
752
753
|
}
|
|
753
754
|
const previous = children[children.length - 1];
|
|
754
755
|
if (previous?.type === "text" && sameMarks(previous.marks, child.marks)) {
|
|
755
|
-
previous.text += child.text;
|
|
756
|
+
(previous as Mutable<typeof previous>).text += child.text;
|
|
756
757
|
} else {
|
|
757
758
|
children.push({
|
|
758
759
|
type: "text",
|
|
@@ -835,7 +836,7 @@ function recordOpaqueFragment(
|
|
|
835
836
|
const rangeStart = state.cursor;
|
|
836
837
|
const rangeEnd = state.cursor + 1;
|
|
837
838
|
|
|
838
|
-
const record: OpaqueFragmentRecord = {
|
|
839
|
+
const record: Mutable<OpaqueFragmentRecord> = {
|
|
839
840
|
fragmentId,
|
|
840
841
|
payloadKind: "xml-subtree",
|
|
841
842
|
payloadReference: rawXml,
|
|
@@ -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
|
+
}
|