@docx4j/core-ts 0.6.0 → 0.7.0

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 (119) hide show
  1. package/README.md +60 -0
  2. package/dist/model/content/Body.mjs +6 -17
  3. package/dist/model/content/Comment.d.mts +2 -2
  4. package/dist/model/content/Comment.mjs +12 -7
  5. package/dist/model/content/ContentControl.d.mts +1 -1
  6. package/dist/model/content/ContentControl.mjs +8 -4
  7. package/dist/model/content/Font.d.mts +4 -1
  8. package/dist/model/content/Font.mjs +6 -2
  9. package/dist/model/content/List.d.mts +6 -3
  10. package/dist/model/content/List.mjs +15 -8
  11. package/dist/model/content/Paragraph.d.mts +12 -2
  12. package/dist/model/content/Paragraph.mjs +31 -11
  13. package/dist/model/content/Table.mjs +26 -16
  14. package/dist/model/content/TrackedChange.d.mts +2 -2
  15. package/dist/model/content/TrackedChange.mjs +6 -3
  16. package/dist/model/content/index.d.mts +3 -1
  17. package/dist/model/content/index.mjs +4 -1
  18. package/dist/model/content/ooxml.mjs +12 -1
  19. package/dist/model/content/paraIds.d.mts +5 -0
  20. package/dist/model/content/paraIds.mjs +54 -0
  21. package/dist/model/content/search.d.mts +2 -0
  22. package/dist/model/content/search.mjs +51 -7
  23. package/dist/model/content/story.d.mts +5 -2
  24. package/dist/model/content/story.mjs +6 -3
  25. package/dist/model/content/tableRules.mjs +2 -0
  26. package/dist/model/content/tracking.d.mts +35 -6
  27. package/dist/model/content/tracking.mjs +93 -12
  28. package/dist/model/content/tree.d.mts +7 -8
  29. package/dist/model/content/tree.mjs +7 -9
  30. package/dist/model/content/wordRange.d.mts +48 -0
  31. package/dist/model/content/wordRange.mjs +97 -0
  32. package/dist/model/customxml/CustomXmlPartCollection.d.mts +22 -1
  33. package/dist/model/customxml/CustomXmlPartCollection.mjs +48 -2
  34. package/dist/model/customxml/XmlMapping.d.mts +31 -7
  35. package/dist/model/customxml/XmlMapping.mjs +53 -10
  36. package/dist/model/customxml/bindings.d.mts +12 -2
  37. package/dist/model/customxml/bindings.mjs +98 -16
  38. package/dist/model/customxml/index.d.mts +1 -0
  39. package/dist/model/customxml/index.mjs +1 -0
  40. package/dist/model/customxml/opendope.d.mts +4 -1
  41. package/dist/model/customxml/opendope.mjs +9 -6
  42. package/dist/model/customxml/pictures.mjs +5 -3
  43. package/dist/model/customxml/storeItems.d.mts +57 -0
  44. package/dist/model/customxml/storeItems.mjs +122 -0
  45. package/dist/model/customxml/xhtml.mjs +15 -20
  46. package/dist/model/dateZone.d.mts +25 -0
  47. package/dist/model/dateZone.mjs +57 -0
  48. package/dist/model/index.d.mts +1 -0
  49. package/dist/model/index.mjs +1 -0
  50. package/dist/model/properties/PropertyResolver.d.mts +14 -1
  51. package/dist/model/properties/PropertyResolver.mjs +18 -3
  52. package/dist/model/properties/index.d.mts +1 -1
  53. package/dist/model/properties/log.mjs +2 -2
  54. package/dist/model/properties/styleUtil.d.mts +3 -1
  55. package/dist/model/properties/styleUtil.mjs +20 -24
  56. package/dist/model/text/CaseMapping.d.mts +38 -0
  57. package/dist/model/text/CaseMapping.mjs +86 -0
  58. package/dist/model/text/Extractor.d.mts +66 -6
  59. package/dist/model/text/Extractor.mjs +300 -56
  60. package/dist/model/text/PlainText.mjs +7 -4
  61. package/dist/model/text/TextContext.d.mts +103 -8
  62. package/dist/model/text/TextContext.mjs +228 -53
  63. package/dist/model/text/TextOptions.d.mts +88 -5
  64. package/dist/model/text/TextOptions.mjs +158 -17
  65. package/dist/model/text/TextResult.d.mts +126 -9
  66. package/dist/model/text/TextResult.mjs +115 -12
  67. package/dist/model/text/WmlToMarkdown.mjs +32 -7
  68. package/dist/model/text/caseMapping.generated.d.mts +7 -0
  69. package/dist/model/text/caseMapping.generated.mjs +214 -0
  70. package/dist/model/text/index.d.mts +3 -2
  71. package/dist/model/text/index.mjs +1 -0
  72. package/dist/model/wordIntegers.d.mts +29 -0
  73. package/dist/model/wordIntegers.mjs +78 -0
  74. package/dist/office-js/document.mjs +15 -1
  75. package/dist/office-js/toApiScript.mjs +8 -7
  76. package/dist/opc/DirectoryPartStore.d.mts +6 -2
  77. package/dist/opc/DirectoryPartStore.mjs +11 -4
  78. package/dist/opc/FlatOpcPartStore.d.mts +4 -2
  79. package/dist/opc/FlatOpcPartStore.mjs +11 -8
  80. package/dist/opc/Load.d.mts +32 -0
  81. package/dist/opc/Load.mjs +115 -5
  82. package/dist/opc/PartStore.d.mts +34 -1
  83. package/dist/opc/PartStore.mjs +71 -8
  84. package/dist/opc/Save.d.mts +17 -2
  85. package/dist/opc/Save.mjs +100 -4
  86. package/dist/opc/ZipPartStore.d.mts +4 -2
  87. package/dist/opc/ZipPartStore.mjs +12 -7
  88. package/dist/opc/exceptions.d.mts +140 -0
  89. package/dist/opc/exceptions.mjs +108 -0
  90. package/dist/opc/index.d.mts +7 -4
  91. package/dist/opc/index.mjs +5 -3
  92. package/dist/opc/mce/preprocessor.d.mts +121 -10
  93. package/dist/opc/mce/preprocessor.mjs +122 -21
  94. package/dist/opc/mce/understood.d.mts +3 -35
  95. package/dist/opc/mce/understood.mjs +27 -53
  96. package/dist/opc/repair/breaks.d.mts +23 -0
  97. package/dist/opc/repair/breaks.mjs +288 -0
  98. package/dist/opc/repair/hosts.d.mts +12 -0
  99. package/dist/opc/repair/hosts.mjs +33 -0
  100. package/dist/opc/repair/mainRoot.d.mts +19 -0
  101. package/dist/opc/repair/mainRoot.mjs +168 -0
  102. package/dist/opc/repair/unknownElements.d.mts +8 -0
  103. package/dist/opc/repair/unknownElements.mjs +180 -0
  104. package/dist/opc/repair/wmlNames.generated.d.mts +2 -0
  105. package/dist/opc/repair/wmlNames.generated.mjs +80 -0
  106. package/dist/opc/skipped.d.mts +50 -0
  107. package/dist/opc/skipped.mjs +140 -0
  108. package/dist/packages/OpcPackage.d.mts +98 -5
  109. package/dist/packages/OpcPackage.mjs +122 -7
  110. package/dist/packages/WordprocessingMLPackage.mjs +3 -1
  111. package/dist/packages/index.d.mts +1 -1
  112. package/dist/parts/Part.d.mts +5 -1
  113. package/dist/parts/RelationshipsPart.d.mts +15 -0
  114. package/dist/parts/RelationshipsPart.mjs +66 -1
  115. package/dist/parts/XmlPart.d.mts +54 -5
  116. package/dist/parts/XmlPart.mjs +203 -10
  117. package/dist/parts/wml/index.d.mts +12 -1
  118. package/dist/parts/wml/index.mjs +25 -1
  119. package/package.json +2 -2
package/dist/opc/Load.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { PartName, CONTENT_TYPES_NAME } from './PartName.mjs';
2
2
  import { ContentTypeManager } from './ContentTypeManager.mjs';
3
- import { Docx4JException, InvalidFormatException } from './exceptions.mjs';
3
+ import { Docx4JException, DuplicateEntryException, ExternalMainPartException, InvalidFormatException, MultipleMainPartsException } from './exceptions.mjs';
4
4
  import { Namespaces } from '../parts/Namespaces.mjs';
5
5
  import { RelationshipsPart, isExternal } from '../parts/RelationshipsPart.mjs';
6
6
  import { XmlPart } from '../parts/XmlPart.mjs';
@@ -8,6 +8,8 @@ import { CustomXmlDataStoragePart } from '../parts/DefaultXmlPart.mjs';
8
8
  import { CustomXmlDataStoragePropertiesPart, DS_NS } from '../parts/customXml/index.mjs';
9
9
  import { defaultPartRegistry } from '../parts/PartRegistry.mjs';
10
10
  import { decodeXmlText, parseXml } from '../xml/dom.mjs';
11
+ /** The values of `LoadOptions.repair`, in the WD's order. */
12
+ export const REPAIR_MODES = Object.freeze(['refuse', 'word', 'all']);
11
13
  /** Relationship types whose target is the main part (docx4j PackageRelsUtil.getNameOfMainPart). */
12
14
  const MAIN_PART_RELATIONSHIPS = [
13
15
  Namespaces.DOCUMENT,
@@ -15,23 +17,38 @@ const MAIN_PART_RELATIONSHIPS = [
15
17
  Namespaces.DRAWINGML_DIAGRAM_LAYOUT,
16
18
  Namespaces.DOCUMENT_STRICT,
17
19
  ];
20
+ /**
21
+ * What REQ-206's statement counts (docx4j PackageRelsUtil.isMainPart): a transitional and a strict relationship are two. An
22
+ * External one isn't counted (CR-024 section 9.2): it names no part, so it can't be what the package reads.
23
+ */
24
+ const OFFICE_DOCUMENT_RELATIONSHIPS = [Namespaces.DOCUMENT, Namespaces.DOCUMENT_STRICT];
18
25
  /**
19
26
  * Loads a package from a container (docx4j io3.Load3): content types, then the package
20
27
  * relationships, then every part reachable through relationships, recursively. Parts are not
21
28
  * unmarshalled. `createPackage` picks the package class from the main part's content type.
22
29
  */
23
30
  export async function loadPackage(store, createPackage, options = {}) {
24
- var _a;
31
+ var _a, _b, _c;
25
32
  const registry = (_a = options.registry) !== null && _a !== void 0 ? _a : defaultPartRegistry;
33
+ const repair = repairOf(options.repair);
34
+ const followsWord = repair !== 'refuse';
35
+ const duplicateEntries = duplicateEntriesOf(store, followsWord);
26
36
  if (!store.has(CONTENT_TYPES_NAME))
27
37
  throw new InvalidFormatException(`${CONTENT_TYPES_NAME} is missing from this package`);
28
38
  const ctm = ContentTypeManager.parse(decodeXmlText(await store.load(CONTENT_TYPES_NAME)));
29
39
  const packageRels = RelationshipsPart.createPackageRels();
30
40
  if (!store.has('_rels/.rels'))
31
41
  throw new InvalidFormatException('_rels/.rels appears to be missing from this package');
42
+ packageRels.readsByLocalName = followsWord;
32
43
  packageRels.setBytes(await store.load('_rels/.rels'));
33
44
  await packageRels.getContents();
34
- const mainRel = packageRels.list.find((r) => MAIN_PART_RELATIONSHIPS.includes(r.type));
45
+ // External first, as docx4j has it: refused at refuse and word whatever else the relationships hold, and dropped at all
46
+ // before Word's last-relationship repair
47
+ const external = externalMainPart(packageRels, repair);
48
+ const mainParts = oneMainPart(packageRels, followsWord);
49
+ // an External relationship names no part, so it gives way to one that does
50
+ const mainRels = packageRels.list.filter((r) => MAIN_PART_RELATIONSHIPS.includes(r.type));
51
+ const mainRel = (_b = mainRels.find((r) => !isExternal(r))) !== null && _b !== void 0 ? _b : mainRels[0];
35
52
  if (!mainRel)
36
53
  throw new InvalidFormatException('No relationship of type officeDocument');
37
54
  const mainPartName = PartName.resolve(PartName.ROOT, mainRel.target);
@@ -39,19 +56,111 @@ export async function loadPackage(store, createPackage, options = {}) {
39
56
  pkg.contentTypeManager = ctm;
40
57
  pkg.sourcePartStore = store;
41
58
  pkg.relationshipsPart = packageRels;
59
+ pkg.duplicateEntries = duplicateEntries;
60
+ pkg.multipleMainParts = mainParts === null || mainParts === void 0 ? void 0 : mainParts.warning;
61
+ pkg.externalMainPart = external;
42
62
  packageRels.sourceP = pkg;
43
63
  packageRels.package = pkg;
44
- const loader = new Loader(pkg, store, registry, options.preprocessor);
64
+ const loader = new Loader(pkg, store, registry, options.preprocessor, followsWord);
45
65
  await loader.addPartsFromRelationships(pkg, packageRels);
46
66
  await loader.registerCustomXmlDataStorageParts();
67
+ // after the walk, so a target the kept part reaches by a relationship of its own, or a second relationship names, keeps its type
68
+ for (const name of (_c = mainParts === null || mainParts === void 0 ? void 0 : mainParts.dropped) !== null && _c !== void 0 ? _c : [])
69
+ if (!pkg.getPart(name))
70
+ ctm.removeOverrideContentType(name);
47
71
  return pkg;
48
72
  }
73
+ /**
74
+ * More than one officeDocument relationship (WD REQ-206's statement; CR-024 section 9): refused under `repair: 'refuse'`, naming
75
+ * the targets. Under `'word'` and `'all'` the last stands and the others go here, before anything is read, so that the package's class, the
76
+ * walk, `getMainPart()` and `getMainDocumentPart()` all find one main part, and a save writes one. `dropped` is the part
77
+ * names they gave, whose Overrides `loadPackage` settles after the walk.
78
+ */
79
+ function oneMainPart(rels, followsWord) {
80
+ const mains = rels.list.filter((r) => OFFICE_DOCUMENT_RELATIONSHIPS.includes(r.type) && !isExternal(r));
81
+ if (mains.length < 2)
82
+ return undefined;
83
+ const names = mains.map((r) => PartName.resolve(PartName.ROOT, r.target));
84
+ if (!followsWord)
85
+ throw new MultipleMainPartsException(names.map((name) => name.name));
86
+ for (const r of mains.slice(0, -1))
87
+ rels.removeRelationship(r);
88
+ const partName = names[names.length - 1].name;
89
+ return {
90
+ warning: {
91
+ code: 'package.multiple_main_parts',
92
+ partName,
93
+ count: mains.length,
94
+ message: `${mains.length} officeDocument relationships in this package (REQ-206); the last's target read, ${partName}`,
95
+ },
96
+ dropped: names.slice(0, -1),
97
+ };
98
+ }
99
+ /** `LoadOptions.repair`, `'refuse'` when not given; any other value refused (WD REQ-207). */
100
+ function repairOf(repair) {
101
+ if (repair === undefined)
102
+ return 'refuse';
103
+ if (!REPAIR_MODES.includes(repair)) {
104
+ throw new Docx4JException(`repair: '${String(repair)}' is not ${REPAIR_MODES.map((m) => `'${m}'`).join(', ')} (WD REQ-207)`);
105
+ }
106
+ return repair;
107
+ }
108
+ /**
109
+ * An `External` officeDocument relationship (WD REQ-206's NOTE; CR-031 section 2.2). With no internal one beside it the package
110
+ * has no main document part, refused at every value (REQ-321), where the URL used to be taken for a part name. Beside an
111
+ * internal one it's refused at `'refuse'` and `'word'`, first or last alike, since Word refuses it even through its repair;
112
+ * at `'all'` every such relationship goes here, before anything is read, so a save writes none and the URL is never
113
+ * fetched, and the warning names the part read: the last internal relationship's target, as Word's repair of several reads.
114
+ */
115
+ function externalMainPart(rels, repair) {
116
+ const mains = rels.list.filter((r) => OFFICE_DOCUMENT_RELATIONSHIPS.includes(r.type));
117
+ const externals = mains.filter(isExternal);
118
+ if (externals.length === 0)
119
+ return undefined;
120
+ const urls = externals.map((r) => r.target);
121
+ const internals = mains.filter((r) => !isExternal(r)).map((r) => PartName.resolve(PartName.ROOT, r.target).name);
122
+ if (internals.length === 0) {
123
+ throw new InvalidFormatException(`This package's only officeDocument relationship is External, to ${urls.join(', ')}, so it has no `
124
+ + 'main document part (REQ-321); refused at every value of repair. The URL is not fetched');
125
+ }
126
+ if (repair !== 'all')
127
+ throw new ExternalMainPartException(urls, internals);
128
+ for (const r of externals)
129
+ rels.removeRelationship(r);
130
+ const partName = internals[internals.length - 1];
131
+ return {
132
+ code: 'package.external_main_part',
133
+ partName,
134
+ urls,
135
+ message: `${urls.length === 1 ? 'An External officeDocument relationship' : `${urls.length} External officeDocument relationships`}, `
136
+ + `to ${urls.join(', ')}, beside the internal one dropped, ${partName} read, the URL not fetched: a repair of this package's own (REQ-207)`,
137
+ };
138
+ }
139
+ /**
140
+ * The store's duplicated names (CR-024, WD REQ-206), refused under `repair: 'refuse'` before anything is read, else one warning
141
+ * each. A store without `duplicates()` is not checked.
142
+ */
143
+ function duplicateEntriesOf(store, followsWord) {
144
+ var _a;
145
+ const duplicates = (_a = store.duplicates) === null || _a === void 0 ? void 0 : _a.call(store);
146
+ if (!duplicates || duplicates.size === 0)
147
+ return [];
148
+ if (!followsWord)
149
+ throw new DuplicateEntryException(Object.fromEntries(duplicates));
150
+ return [...duplicates].map(([entry, count]) => ({
151
+ code: 'package.duplicate_entry',
152
+ entry,
153
+ count,
154
+ message: `${entry}: ${count} entries of one name in this package; the first read, the rest ignored (REQ-206)`,
155
+ }));
156
+ }
49
157
  class Loader {
50
- constructor(pkg, store, registry, preprocessor) {
158
+ constructor(pkg, store, registry, preprocessor, followsWord) {
51
159
  this.pkg = pkg;
52
160
  this.store = store;
53
161
  this.registry = registry;
54
162
  this.preprocessor = preprocessor;
163
+ this.followsWord = followsWord;
55
164
  this.handled = new Set();
56
165
  }
57
166
  async addPartsFromRelationships(source, rp) {
@@ -88,6 +197,7 @@ class Loader {
88
197
  const relsName = PartName.relsFor(partName).storeName;
89
198
  if (this.store.has(relsName)) {
90
199
  const rrp = new RelationshipsPart(part);
200
+ rrp.readsByLocalName = this.followsWord;
91
201
  rrp.setBytes(await this.store.load(relsName));
92
202
  await rrp.getContents();
93
203
  part.relationshipsPart = rrp;
@@ -10,6 +10,35 @@ export interface PartStore {
10
10
  load(partName: string): Promise<Uint8Array>;
11
11
  /** Size if known without loading, else undefined. */
12
12
  size?(partName: string): number | undefined;
13
+ /**
14
+ * The names the container holds more than one entry of, OPC part names compared without case (WD REQ-206; CR-024): each
15
+ * first entry's spelling, with the number of entries, in container order. The load refuses a package with any, unless
16
+ * `LoadOptions.duplicateEntries` is `'first'`. Every store here has it; a store without it is not checked.
17
+ */
18
+ duplicates?(): ReadonlyMap<string, number>;
19
+ }
20
+ /** An entry's key: its name without a leading '/', lower-cased, as `PartName`'s equality compares (CR-024 section 2.1). */
21
+ export declare function entryKey(name: string): string;
22
+ /**
23
+ * A container's entries by key (`entryKey`), the first entry of each key kept with its spelling and its value and the
24
+ * rest counted: what each store reads its container into, so that `has`, `load` and `size` agree and look up without
25
+ * case, and `duplicates()` reports a name held twice (CR-024, WD REQ-206).
26
+ */
27
+ export declare class EntryIndex<T> {
28
+ private readonly byKey;
29
+ /** Adds an entry; a second entry of a key is counted, its value not kept. */
30
+ add(name: string, value: T): void;
31
+ /** Sets an entry's value, replacing the one its key holds (a sink's write), its first spelling kept. */
32
+ set(name: string, value: T): void;
33
+ get(name: string): T | undefined;
34
+ has(name: string): boolean;
35
+ delete(name: string): boolean;
36
+ /** Each key's first spelling, in the order the entries were added. */
37
+ names(): string[];
38
+ /** Each key's value (its first entry's), in the same order. */
39
+ values(): T[];
40
+ /** The keys added more than once: the first spelling and the count. */
41
+ duplicates(): Map<string, number>;
13
42
  }
14
43
  export interface PutOptions {
15
44
  /** Deflate in a zip (default true); ignored by containers that do not compress. */
@@ -23,7 +52,10 @@ export interface PartSink<R = unknown> {
23
52
  /** Completes the container. */
24
53
  finish(): Promise<R>;
25
54
  }
26
- /** An in-memory container: new packages, tests, and the result of `MemoryPartSink`. */
55
+ /**
56
+ * An in-memory container: new packages, tests, and the result of `MemoryPartSink`. The constructor's entries are read as a
57
+ * container's (a second of one name counted, `duplicates()`); `put`, a sink's write, replaces.
58
+ */
27
59
  export declare class MemoryPartStore implements PartStore, PartSink<MemoryPartStore> {
28
60
  private readonly parts;
29
61
  /** Content types as put, for stores filled through the sink side. */
@@ -33,6 +65,7 @@ export declare class MemoryPartStore implements PartStore, PartSink<MemoryPartSt
33
65
  has(partName: string): boolean;
34
66
  load(partName: string): Promise<Uint8Array>;
35
67
  size(partName: string): number | undefined;
68
+ duplicates(): ReadonlyMap<string, number>;
36
69
  put(partName: string, bytes: Uint8Array, options?: PutOptions): void;
37
70
  delete(partName: string): boolean;
38
71
  finish(): Promise<MemoryPartStore>;
@@ -1,28 +1,91 @@
1
- /** An in-memory container: new packages, tests, and the result of `MemoryPartSink`. */
1
+ /** An entry's key: its name without a leading '/', lower-cased, as `PartName`'s equality compares (CR-024 section 2.1). */
2
+ export function entryKey(name) {
3
+ return (name.startsWith('/') ? name.substring(1) : name).toLowerCase();
4
+ }
5
+ /**
6
+ * A container's entries by key (`entryKey`), the first entry of each key kept with its spelling and its value and the
7
+ * rest counted: what each store reads its container into, so that `has`, `load` and `size` agree and look up without
8
+ * case, and `duplicates()` reports a name held twice (CR-024, WD REQ-206).
9
+ */
10
+ export class EntryIndex {
11
+ constructor() {
12
+ this.byKey = new Map();
13
+ }
14
+ /** Adds an entry; a second entry of a key is counted, its value not kept. */
15
+ add(name, value) {
16
+ const key = entryKey(name);
17
+ const entry = this.byKey.get(key);
18
+ if (entry)
19
+ entry.count++;
20
+ else
21
+ this.byKey.set(key, { name: name.startsWith('/') ? name.substring(1) : name, value, count: 1 });
22
+ }
23
+ /** Sets an entry's value, replacing the one its key holds (a sink's write), its first spelling kept. */
24
+ set(name, value) {
25
+ const entry = this.byKey.get(entryKey(name));
26
+ if (entry)
27
+ entry.value = value;
28
+ else
29
+ this.add(name, value);
30
+ }
31
+ get(name) {
32
+ var _a;
33
+ return (_a = this.byKey.get(entryKey(name))) === null || _a === void 0 ? void 0 : _a.value;
34
+ }
35
+ has(name) {
36
+ return this.byKey.has(entryKey(name));
37
+ }
38
+ delete(name) {
39
+ return this.byKey.delete(entryKey(name));
40
+ }
41
+ /** Each key's first spelling, in the order the entries were added. */
42
+ names() {
43
+ return [...this.byKey.values()].map((e) => e.name);
44
+ }
45
+ /** Each key's value (its first entry's), in the same order. */
46
+ values() {
47
+ return [...this.byKey.values()].map((e) => e.value);
48
+ }
49
+ /** The keys added more than once: the first spelling and the count. */
50
+ duplicates() {
51
+ const out = new Map();
52
+ for (const e of this.byKey.values())
53
+ if (e.count > 1)
54
+ out.set(e.name, e.count);
55
+ return out;
56
+ }
57
+ }
58
+ /**
59
+ * An in-memory container: new packages, tests, and the result of `MemoryPartSink`. The constructor's entries are read as a
60
+ * container's (a second of one name counted, `duplicates()`); `put`, a sink's write, replaces.
61
+ */
2
62
  export class MemoryPartStore {
3
63
  constructor(entries) {
4
- this.parts = new Map();
64
+ this.parts = new EntryIndex();
5
65
  /** Content types as put, for stores filled through the sink side. */
6
66
  this.contentTypes = new Map();
7
67
  if (entries)
8
68
  for (const [name, bytes] of entries)
9
- this.parts.set(normalize(name), bytes);
69
+ this.parts.add(normalize(name), bytes);
10
70
  }
11
71
  partNames() {
12
- return this.parts.keys();
72
+ return this.parts.names();
13
73
  }
14
74
  has(partName) {
15
- return this.parts.has(normalize(partName));
75
+ return this.parts.has(partName);
16
76
  }
17
77
  async load(partName) {
18
- const bytes = this.parts.get(normalize(partName));
78
+ const bytes = this.parts.get(partName);
19
79
  if (bytes === undefined)
20
80
  throw new Error(`No part ${partName}`);
21
81
  return bytes;
22
82
  }
23
83
  size(partName) {
24
84
  var _a;
25
- return (_a = this.parts.get(normalize(partName))) === null || _a === void 0 ? void 0 : _a.length;
85
+ return (_a = this.parts.get(partName)) === null || _a === void 0 ? void 0 : _a.length;
86
+ }
87
+ duplicates() {
88
+ return this.parts.duplicates();
26
89
  }
27
90
  put(partName, bytes, options) {
28
91
  const name = normalize(partName);
@@ -31,7 +94,7 @@ export class MemoryPartStore {
31
94
  this.contentTypes.set(name, options.contentType);
32
95
  }
33
96
  delete(partName) {
34
- return this.parts.delete(normalize(partName));
97
+ return this.parts.delete(partName);
35
98
  }
36
99
  async finish() {
37
100
  return this;
@@ -1,9 +1,12 @@
1
1
  import type { PartSink } from './PartStore.mjs';
2
+ import type { ValueOutOfRangeWarning, MisplacedContentWarning } from './mce/preprocessor.mjs';
3
+ import type { ContentSkippedWarning } from './skipped.mjs';
2
4
  import type { OpcPackage } from '../packages/OpcPackage.mjs';
3
5
  /**
4
6
  * A value a save wrote in its normal form, or left out (CR-015 D2 and D8; the objects facade's normaliser, its
5
7
  * CR-013 phase C, docx4j CR-061's rule): `path` from the part's root, `before` as read, `after` as written, null
6
- * where the attribute or the element was left out. The code is docx4j-python CR-018's.
8
+ * where the attribute or the element was left out. The code is this package's (CR-015 D8); docx4j-python CR-018 reports
9
+ * the same as `package.last_save.normalised`.
7
10
  */
8
11
  export interface ValueNormalised {
9
12
  code: 'value.normalised';
@@ -13,9 +16,21 @@ export interface ValueNormalised {
13
16
  after: string | null;
14
17
  message: string;
15
18
  }
16
- /** What the last save of a package did beyond writing it: each value it normalised (CR-015 D8). */
19
+ /**
20
+ * What the last save of a package did beyond writing it: each value it normalised (CR-015 D8), each integer
21
+ * attribute value it wrote that Word 365 reads differently, written as held (CR-017 P3, E3), with
22
+ * `checkContentOnSave` off, each child it wrote where its container never admits one (CR-016 D2), each with its path,
23
+ * each item the read dropped that a changed part was written without as Word's own save writes it (CR-025 section 10), and,
24
+ * with `checkLossOnSave` off, each it was written without that Word's save keeps or nobody has measured (CR-025 D8).
25
+ */
17
26
  export interface SaveReport {
18
27
  normalised: ValueNormalised[];
28
+ outOfRange: ValueOutOfRangeWarning[];
29
+ misplaced: MisplacedContentWarning[];
30
+ /** Items a changed part was written without, Word's own save dropping them too (`onSave: 'dropped'`, CR-025 D14): never refused. */
31
+ dropped: ContentSkippedWarning[];
32
+ /** Items a changed part was written without under `checkLossOnSave` off (`onSave: 'lost'`). */
33
+ lost: ContentSkippedWarning[];
19
34
  }
20
35
  /**
21
36
  * Writes a package to a sink (docx4j io3.Save): `[Content_Types].xml` first, then the package
package/dist/opc/Save.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  import { CONTENT_TYPES_NAME } from './PartName.mjs';
2
2
  import { ContentTypes } from './ContentTypes.mjs';
3
- import { Docx4JException } from './exceptions.mjs';
3
+ import { Docx4JException, MisplacedContentException, SaveLossException } from './exceptions.mjs';
4
4
  import { Namespaces } from '../parts/Namespaces.mjs';
5
5
  import { isExternal } from '../parts/RelationshipsPart.mjs';
6
+ import { XmlPart, valueOutOfRange, misplacedContent } from '../parts/XmlPart.mjs';
6
7
  import { encodeText } from '../xml/dom.mjs';
7
8
  function valueNormalised(partName, n) {
8
9
  const after = n.after === null ? 'left out' : `written ${JSON.stringify(n.after)}`;
@@ -11,6 +12,29 @@ function valueNormalised(partName, n) {
11
12
  message: `${partName}: ${n.path} ${n.before === undefined ? '(no value)' : JSON.stringify(n.before)} ${after}, as Word reads it`,
12
13
  };
13
14
  }
15
+ /**
16
+ * `pkg.writeTimesOnSave` (CR-018; docx4j `DocPropsCorePart.writeTimes` under `docx4j.dcterms.write`): `dcterms:modified`
17
+ * now, UTC to the second in the W3CDTF form Office writes, and `dcterms:created` too for a package created here (one with
18
+ * no source container) that has none, its core properties part made for it. A loaded package without the part is left
19
+ * without one, as docx4j leaves it. The part is unmarshalled for the write, so it is marshalled by this save.
20
+ */
21
+ async function writeTimes(pkg) {
22
+ const isNew = pkg.sourcePartStore === undefined;
23
+ let part = pkg.docPropsCorePart;
24
+ if (!part) {
25
+ if (!isNew)
26
+ return;
27
+ part = pkg.addDocPropsCorePart();
28
+ }
29
+ await part.getContents();
30
+ const props = part.contents;
31
+ const now = () => ({
32
+ TYPE_NAME: 'org_docx4j_docProps_core_dc_terms.W3CDTF', content: [new Date().toISOString().replace(/\.\d{3}Z$/, 'Z')],
33
+ });
34
+ if (props.created === undefined && isNew)
35
+ props.created = now();
36
+ props.modified = now();
37
+ }
14
38
  /**
15
39
  * Writes a package to a sink (docx4j io3.Save): `[Content_Types].xml` first, then the package
16
40
  * relationships, then every part reachable through relationships, each once. Relationship parts
@@ -18,11 +42,14 @@ function valueNormalised(partName, n) {
18
42
  * container unchanged.
19
43
  */
20
44
  export async function savePackage(pkg, sink) {
45
+ if (pkg.writeTimesOnSave)
46
+ await writeTimes(pkg);
21
47
  const saver = new Saver(pkg, sink);
48
+ await saver.settleLosses(); // before anything is written, whatever the sink
22
49
  sink.put(CONTENT_TYPES_NAME, encodeText(pkg.contentTypeManager.toXml()), { contentType: ContentTypes.CONTENT_TYPES_PART });
23
50
  await saver.saveRels(pkg.relationshipsPart);
24
51
  const result = await sink.finish();
25
- pkg.lastSave = { normalised: saver.normalised };
52
+ pkg.lastSave = { normalised: saver.normalised, outOfRange: saver.outOfRange, misplaced: saver.misplaced, dropped: saver.dropped, lost: saver.lost };
26
53
  return result;
27
54
  }
28
55
  class Saver {
@@ -31,6 +58,52 @@ class Saver {
31
58
  this.sink = sink;
32
59
  this.handled = new Set();
33
60
  this.normalised = [];
61
+ this.outOfRange = [];
62
+ this.misplaced = [];
63
+ this.dropped = [];
64
+ this.lost = [];
65
+ /** Parts holding skipped content, read and not changed: written from their source (CR-025 section 2.2). */
66
+ this.copied = new Set();
67
+ }
68
+ /**
69
+ * CR-025 sections 2.2, 2.3 and 10, over the parts this save reaches: one holding what its read dropped is copied if it hasn't
70
+ * changed. If it has, what Word's own save drops too is written without and listed (`dropped`), and for the rest the save is
71
+ * refused for all such parts at once, or, with `checkLossOnSave` off, says what went (`lost`).
72
+ */
73
+ async settleLosses() {
74
+ const losses = [];
75
+ const seen = new Set();
76
+ const visit = async (rp) => {
77
+ for (const r of rp.list) {
78
+ if (r.type === Namespaces.HYPERLINK || isExternal(r))
79
+ continue;
80
+ const part = this.pkg.getPart(rp.resolveTarget(r));
81
+ if (!part || seen.has(part.partName.key))
82
+ continue;
83
+ seen.add(part.partName.key);
84
+ if (part instanceof XmlPart) {
85
+ const skipped = part.skippedByRead;
86
+ if (skipped.length > 0) {
87
+ if (await part.isUnchangedSinceRead())
88
+ this.copied.add(part.partName.key);
89
+ else {
90
+ this.dropped.push(...skipped.filter((w) => w.onSave === 'dropped'));
91
+ const lost = skipped.filter((w) => w.onSave === 'lost');
92
+ if (lost.length > 0 && this.pkg.checkLossOnSave)
93
+ losses.push({ partName: part.partName.name, kind: 'skipped', count: lost.length });
94
+ else
95
+ this.lost.push(...lost);
96
+ }
97
+ }
98
+ }
99
+ const rrp = part.relationshipsPart;
100
+ if (rrp && rrp.size > 0)
101
+ await visit(rrp);
102
+ }
103
+ };
104
+ await visit(this.pkg.relationshipsPart);
105
+ if (losses.length > 0)
106
+ throw new SaveLossException(losses);
34
107
  }
35
108
  async saveRels(rp) {
36
109
  this.sink.put(rp.partName.storeName, await rp.getBytes(), { contentType: ContentTypes.RELATIONSHIPS_PART });
@@ -47,12 +120,35 @@ class Saver {
47
120
  if (this.handled.has(part.partName.key))
48
121
  return;
49
122
  this.handled.add(part.partName.key);
123
+ if (this.copied.has(part.partName.key)) {
124
+ this.sink.put(part.partName.storeName, (await part.bytesRead()), { compress: part.compress, contentType: part.contentType });
125
+ const held = part.relationshipsPart;
126
+ if (held && held.size > 0)
127
+ await this.saveRels(held);
128
+ return;
129
+ }
50
130
  let bytes;
131
+ // CR-016 D2: a child its container never admits refuses the save, every one in the part listed, unless
132
+ // pkg.checkContentOnSave is off, when each is written as held and reported; the runtime's own refusal (a string in a
133
+ // run) names what the walk reported either way, where before it said nothing of the cause
134
+ const misplaced = [];
51
135
  try {
52
- bytes = await part.getBytes({ onNormalised: (n) => this.normalised.push(valueNormalised(part.partName.name, n)) });
136
+ bytes = await part.getBytes({
137
+ onNormalised: (n) => this.normalised.push(valueNormalised(part.partName.name, n)),
138
+ onOutOfRange: (r) => this.outOfRange.push(valueOutOfRange(r, part.partName.name)),
139
+ onMisplaced: (r) => misplaced.push(misplacedContent(r, part.partName.name)),
140
+ });
53
141
  }
54
142
  catch (e) {
55
- throw new Docx4JException(`Problem saving part ${part.partName}`, { cause: e });
143
+ if (misplaced.length > 0 && this.pkg.checkContentOnSave)
144
+ throw new MisplacedContentException(part.partName.name, misplaced, { cause: e });
145
+ const because = misplaced.length > 0 ? `: ${misplaced.map((f) => f.message).join('; ')}` : '';
146
+ throw new Docx4JException(`Problem saving part ${part.partName}${because}`, { cause: e });
147
+ }
148
+ if (misplaced.length > 0) {
149
+ if (this.pkg.checkContentOnSave)
150
+ throw new MisplacedContentException(part.partName.name, misplaced);
151
+ this.misplaced.push(...misplaced);
56
152
  }
57
153
  this.sink.put(part.partName.storeName, bytes, { compress: part.compress, contentType: part.contentType });
58
154
  const rrp = part.relationshipsPart;
@@ -1,17 +1,19 @@
1
- import type { PartStore, PartSink, PutOptions } from './PartStore.mjs';
1
+ import { type PartStore, type PartSink, type PutOptions } from './PartStore.mjs';
2
2
  /**
3
3
  * A zip container read from bytes (docx4j io3.stores.ZipPartStore). The central directory is
4
4
  * read once; an entry is inflated on first `load` and then cached. ZIP64 archives are rejected.
5
+ * Entries are looked up as OPC part names, without case, and of two entries of one name the first
6
+ * in the central directory is the one read, the rest counted in `duplicates()` (CR-024).
5
7
  */
6
8
  export declare class ZipPartStore implements PartStore {
7
9
  private readonly data;
8
10
  private readonly entries;
9
- private readonly order;
10
11
  private readonly cache;
11
12
  constructor(source: Uint8Array | ArrayBuffer);
12
13
  partNames(): Iterable<string>;
13
14
  has(partName: string): boolean;
14
15
  size(partName: string): number | undefined;
16
+ duplicates(): ReadonlyMap<string, number>;
15
17
  load(partName: string): Promise<Uint8Array>;
16
18
  loadSync(partName: string): Uint8Array;
17
19
  private readCentralDirectory;
@@ -1,19 +1,21 @@
1
1
  import { inflateSync, zipSync } from 'fflate';
2
+ import { EntryIndex, entryKey } from './PartStore.mjs';
2
3
  import { InvalidFormatException } from './exceptions.mjs';
3
4
  /**
4
5
  * A zip container read from bytes (docx4j io3.stores.ZipPartStore). The central directory is
5
6
  * read once; an entry is inflated on first `load` and then cached. ZIP64 archives are rejected.
7
+ * Entries are looked up as OPC part names, without case, and of two entries of one name the first
8
+ * in the central directory is the one read, the rest counted in `duplicates()` (CR-024).
6
9
  */
7
10
  export class ZipPartStore {
8
11
  constructor(source) {
9
- this.entries = new Map();
10
- this.order = [];
12
+ this.entries = new EntryIndex();
11
13
  this.cache = new Map();
12
14
  this.data = source instanceof Uint8Array ? source : new Uint8Array(source);
13
15
  this.readCentralDirectory();
14
16
  }
15
17
  partNames() {
16
- return this.order;
18
+ return this.entries.names();
17
19
  }
18
20
  has(partName) {
19
21
  return this.entries.has(partName);
@@ -22,11 +24,15 @@ export class ZipPartStore {
22
24
  var _a;
23
25
  return (_a = this.entries.get(partName)) === null || _a === void 0 ? void 0 : _a.size;
24
26
  }
27
+ duplicates() {
28
+ return this.entries.duplicates();
29
+ }
25
30
  async load(partName) {
26
31
  return this.loadSync(partName);
27
32
  }
28
33
  loadSync(partName) {
29
- const cached = this.cache.get(partName);
34
+ const key = entryKey(partName);
35
+ const cached = this.cache.get(key);
30
36
  if (cached)
31
37
  return cached;
32
38
  const e = this.entries.get(partName);
@@ -47,7 +53,7 @@ export class ZipPartStore {
47
53
  bytes = inflateSync(raw);
48
54
  else
49
55
  throw new InvalidFormatException(`Unsupported compression method ${e.method} for ${partName}`);
50
- this.cache.set(partName, bytes);
56
+ this.cache.set(key, bytes);
51
57
  return bytes;
52
58
  }
53
59
  readCentralDirectory() {
@@ -85,8 +91,7 @@ export class ZipPartStore {
85
91
  p += 46 + nameLen + extraLen + commentLen;
86
92
  if (name.endsWith('/'))
87
93
  continue; // directory entry
88
- this.entries.set(name, { name, method, compressedSize, size, localHeaderOffset });
89
- this.order.push(name);
94
+ this.entries.add(name, { name, method, compressedSize, size, localHeaderOffset });
90
95
  }
91
96
  }
92
97
  }