@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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/src/core/commands/formatting-commands.ts +8 -7
  3. package/src/core/commands/paragraph-layout-commands.ts +11 -10
  4. package/src/core/commands/section-layout-commands.ts +7 -6
  5. package/src/core/commands/style-commands.ts +3 -2
  6. package/src/io/export/build-app-properties-xml.ts +24 -0
  7. package/src/io/normalize/normalize-text.ts +6 -5
  8. package/src/io/ooxml/docprops.ts +298 -0
  9. package/src/io/ooxml/parse-anchor.ts +15 -15
  10. package/src/io/ooxml/parse-drawing.ts +5 -5
  11. package/src/io/ooxml/parse-fields.ts +16 -15
  12. package/src/io/ooxml/parse-font-table.ts +2 -1
  13. package/src/io/ooxml/parse-footnotes.ts +3 -2
  14. package/src/io/ooxml/parse-headers-footers.ts +7 -6
  15. package/src/io/ooxml/parse-main-document.ts +41 -40
  16. package/src/io/ooxml/parse-numbering.ts +3 -2
  17. package/src/io/ooxml/parse-object.ts +6 -6
  18. package/src/io/ooxml/parse-paragraph-formatting.ts +12 -11
  19. package/src/io/ooxml/parse-picture.ts +16 -16
  20. package/src/io/ooxml/parse-run-formatting.ts +11 -10
  21. package/src/io/ooxml/parse-settings.ts +2 -1
  22. package/src/io/ooxml/parse-shapes.ts +18 -17
  23. package/src/io/ooxml/parse-styles.ts +16 -16
  24. package/src/io/ooxml/parse-theme.ts +5 -4
  25. package/src/model/canonical-document.ts +920 -815
  26. package/src/runtime/formatting/document-lookup.ts +3 -2
  27. package/src/runtime/formatting/formatting-context.ts +66 -25
  28. package/src/runtime/formatting/index.ts +18 -0
  29. package/src/runtime/formatting/layout-inputs.ts +256 -0
  30. package/src/runtime/formatting/numbering/geometry.ts +13 -12
  31. package/src/runtime/formatting/style-cascade.ts +2 -1
  32. package/src/runtime/formatting/table-style-resolver.ts +8 -7
  33. package/src/runtime/surface-projection.ts +31 -36
  34. package/src/session/export/stateful-export-pipeline.ts +9 -4
  35. package/src/session/export/stateful-export.ts +22 -6
  36. package/src/session/import/canonical-assembly.ts +2 -3
  37. package/src/session/import/loader-types.ts +3 -1
  38. package/src/session/import/loader.ts +12 -0
  39. package/src/session/import/normalize.ts +2 -1
  40. package/src/session/import/source-package-evidence.ts +1016 -0
  41. 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.101",
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, delta: -1 | 1): boolean {
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, surface: ParagraphSurfaceBlock) => void,
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, output: ParagraphNode[]): void {
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, output: ParagraphNode[]): void {
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, output: ParagraphNode[]): void {
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, surface: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>) => void,
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, surface: Extract<SurfaceBlockSnapshot, { kind: "table" }>) => void,
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
+ }