@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
@@ -1,7 +1,9 @@
1
+ import { wallClockIn } from '../dateZone.mjs';
1
2
  import { deepCopy } from '@docx4j/generated-objects-ts';
2
3
  import * as el from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
3
4
  import { sdtProperty, textOf } from '@docx4j/generated-objects-ts/builders/wml';
4
5
  import { bindPicture } from './pictures.mjs';
6
+ import { isPropertiesStoreItem } from './storeItems.mjs';
5
7
  /**
6
8
  * The style Word gives placeholder text, and the text docx4j's placeholder.xml carries: Word 365's since docx4j's CR-063
7
9
  * D5 (CR-002 section 63.22), where it was Word 2010's "Click here to enter text.".
@@ -78,7 +80,9 @@ export function placeholderRun(rPr, text = PLACEHOLDER_TEXT) {
78
80
  * A date in Word's `w:dateFormat` pattern (the .NET forms Word writes: `d MMMM yyyy`,
79
81
  * `dddd, d MMMM yyyy`, `d/MM/yyyy`), in the control's `w:lid` locale. Month and day names come
80
82
  * from `Intl`; text inside single or double quotes is literal. docx4j uses Java's
81
- * `SimpleDateFormat` with `dddd` mapped to `EEEE`; this is the same vocabulary.
83
+ * `SimpleDateFormat` with `dddd` mapped to `EEEE`; this is the same vocabulary. `am/pm` and
84
+ * `AM/PM` (any mix of cases, as docx4j's `isAMPM`) are the locale's day-period names upper-cased,
85
+ * as Word shows both (CR-020 D5, docx4j CR-058's `wordAmPm`): `h:mm am/pm` at 15:05 is `3:05 PM`.
82
86
  *
83
87
  * The date's **UTC fields** are formatted: a bound date is its nominal date, the fields as written,
84
88
  * which `w:fullDate` keeps with a `Z` and `DatePickerContentControl.fullDate` returns as a `Date` in
@@ -96,9 +100,32 @@ export function formatDate(pattern, date, locale = 'en-US') {
96
100
  };
97
101
  const hours = date.getUTCHours();
98
102
  const hours12 = hours % 12 === 0 ? 12 : hours % 12;
99
- return pattern.replace(/d{1,4}|M{1,4}|y{2,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|tt|'[^']*'|"[^"]*"/g, (token) => {
103
+ const dayPeriod = () => {
104
+ const of = (tag) => {
105
+ var _a;
106
+ return (_a = new Intl.DateTimeFormat(tag, { hour: 'numeric', hour12: true, timeZone: 'UTC' })
107
+ .formatToParts(date).find((part) => part.type === 'dayPeriod')) === null || _a === void 0 ? void 0 : _a.value;
108
+ };
109
+ let value;
110
+ try {
111
+ value = of(locale);
112
+ }
113
+ catch {
114
+ value = of('en-US');
115
+ }
116
+ value !== null && value !== void 0 ? value : (value = hours < 12 ? 'AM' : 'PM');
117
+ try {
118
+ return value.toLocaleUpperCase(locale);
119
+ }
120
+ catch {
121
+ return value.toUpperCase();
122
+ }
123
+ };
124
+ return pattern.replace(/[aA][mM]\/[pP][mM]|d{1,4}|M{1,4}|y{2,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|tt|'[^']*'|"[^"]*"/g, (token) => {
100
125
  if (token.startsWith("'") || token.startsWith('"'))
101
126
  return token.slice(1, -1);
127
+ if (token.length === 5 && token[2] === '/')
128
+ return dayPeriod();
102
129
  switch (token) {
103
130
  case 'd': return String(date.getUTCDate());
104
131
  case 'dd': return pad(date.getUTCDate(), 2);
@@ -137,31 +164,84 @@ function isoOf(date) {
137
164
  * rule, as Word shows them; CR-002 section 56). A date that doesn't exist (`2026-02-30`) is not one.
138
165
  */
139
166
  function dateOf(value) {
140
- const m = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?)?(?:Z|[+-]\d{2}:?\d{2})?$/.exec(value);
167
+ var _a;
168
+ return (_a = parsedDateOf(value)) === null || _a === void 0 ? void 0 : _a.nominal;
169
+ }
170
+ const BOUND_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?)?(Z|[+-]\d{2}:?\d{2})?$/;
171
+ /** A bound value's date as its nominal fields (UTC fields of `nominal`), and its zone marker's offset in minutes, where it has one. */
172
+ function parsedDateOf(value) {
173
+ const m = BOUND_DATE.exec(value);
141
174
  if (!m)
142
175
  return undefined;
143
- const [year, month, day, hour, minute, second] = m.slice(1).map((f) => Number(f !== null && f !== void 0 ? f : 0));
176
+ const [year, month, day, hour, minute, second] = m.slice(1, 7).map((f) => Number(f !== null && f !== void 0 ? f : 0));
144
177
  const date = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
145
178
  // Date.UTC rolls an impossible field over (30 February is 2 March): such a value is not a date
146
179
  const exists = date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day
147
180
  && date.getUTCHours() === hour && date.getUTCMinutes() === minute && date.getUTCSeconds() === second;
148
- return exists ? date : undefined;
181
+ if (!exists)
182
+ return undefined;
183
+ const marker = m[7];
184
+ const offset = marker === undefined ? undefined : marker === 'Z' ? 0
185
+ : (marker.startsWith('-') ? -1 : 1) * (Number(marker.slice(1, 3)) * 60 + Number(marker.slice(-2)));
186
+ return { nominal: date, offset };
187
+ }
188
+ /**
189
+ * A bound value's date as Word shows it to a reader in `zone` (`pkg.boundDatesConvertedAsWord`, CR-018): a value carrying
190
+ * a `Z` or an offset is the instant it names, shown on `zone`'s wall clock (Word reads the picker's `Z` as UTC and shows
191
+ * the machine's local time: on a machine at UTC+11, `2026-10-22T15:05:00Z` shows 2:05 AM the next day; docx4j CR-058 D6's
192
+ * probe); a value with no marker, or a date alone, is nominal, as `dateOf` reads it (unmeasured in Word, the date
193
+ * rule's point 4). The result is a nominal date in UTC fields, as every bound date here is, so `formatDate` and
194
+ * `w:fullDate` take it unchanged.
195
+ */
196
+ function dateAsWordShows(value, zone) {
197
+ const parsed = parsedDateOf(value);
198
+ if (!parsed || parsed.offset === undefined)
199
+ return parsed === null || parsed === void 0 ? void 0 : parsed.nominal;
200
+ const instant = new Date(parsed.nominal.getTime() - parsed.offset * 60000);
201
+ const clock = wallClockIn(instant, zone);
202
+ return new Date(Date.UTC(clock.year, clock.month - 1, clock.day, clock.hour, clock.minute, clock.second));
149
203
  }
150
204
  /**
151
205
  * Pushes the custom XML value into one control (docx4j BindingHandler's per-sdt work). Returns
152
206
  * false when nothing was written: no binding, no part, an XPath that selects nothing, a kind
153
207
  * this phase does not handle (explicit rich text), a node holding a Flat OPC package, or a
154
208
  * picture control whose node holds no image (`notes` says which; CR-005 section 9).
209
+ *
210
+ * The part is the one the binding resolves to (CR-020): where its `w:storeItemID` names no part,
211
+ * the one its XPath fits, as Word repairs it on open; a control so bound gets that part's ID in
212
+ * its binding, as Word's save writes it (D2), and a note, as does a stale ID no part fits (D4).
155
213
  */
156
214
  export function applyBindingTo(control, notes) {
157
- var _a;
158
215
  const mapping = control.xmlMapping;
159
- if (!mapping.isMapped)
216
+ const binding = mapping.dataBinding;
217
+ if (!binding)
160
218
  return false;
161
- const node = mapping.customXmlNode;
219
+ const resolved = mapping.resolution;
220
+ const label = control.title || control.tag || `content control ${control.id}`;
221
+ if (!resolved) {
222
+ if (binding.storeItemID && !isPropertiesStoreItem(binding.storeItemID)) {
223
+ notes === null || notes === void 0 ? void 0 : notes.push(`${label}: store item ${binding.storeItemID} names no custom XML part, and none fits ${binding.xpath}; left as it is`);
224
+ }
225
+ return false;
226
+ }
227
+ const node = resolved.part.selectSingleNode(binding.xpath, binding.prefixMappings);
162
228
  if (!node)
163
229
  return false;
164
- const value = node.text.trim();
230
+ if (!bindValue(control, node.text.trim(), notes))
231
+ return false;
232
+ if (resolved.repaired) {
233
+ const stale = binding.storeItemID;
234
+ const id = resolved.part.id.toUpperCase();
235
+ mapping.repairStoreItemID(stale, id);
236
+ notes === null || notes === void 0 ? void 0 : notes.push(`${label}: store item ${stale} names no custom XML part; ${resolved.part.part.partName.name} ${id}`
237
+ + (resolved.fitting > 1 ? `, the first of the ${resolved.fitting} that fit ${binding.xpath} to select a node,` : ` fits ${binding.xpath} and`)
238
+ + ' is bound, its ID written into the control, as Word repairs it');
239
+ }
240
+ return true;
241
+ }
242
+ /** `applyBindingTo`'s write of a node's value into the control, by the control's kind. */
243
+ function bindValue(control, value, notes) {
244
+ var _a, _b, _c;
165
245
  // A picture control's node holds base64 image data: the picture's a:blip is pointed at a new
166
246
  // image part and the drawing kept, as docx4j's bind.xslt mode picture3 does (CR-005 section 9).
167
247
  if (control.type === 'Picture') {
@@ -198,12 +278,15 @@ export function applyBindingTo(control, notes) {
198
278
  }
199
279
  else if (control.type === 'DatePicker') {
200
280
  const date = control.datePickerContentControl;
201
- const parsed = dateOf(value);
281
+ const pkg = control.parentBody.package_;
282
+ const parsed = (pkg === null || pkg === void 0 ? void 0 : pkg.boundDatesConvertedAsWord) && pkg.effectiveDateTimeZone ? dateAsWordShows(value, pkg.effectiveDateTimeZone()) : dateOf(value);
202
283
  if (date && parsed) {
203
284
  date.fullDate = parsed;
204
- // Word shows the date in w:dateFormat, not the stored form (docx4j formats it the same way)
285
+ // Word shows the date in w:dateFormat, not the stored form (docx4j formats it the same way), in w:lid's language,
286
+ // else the document's default language (CR-020 D5, docx4j CR-058 D2 revised), else en-US
287
+ const language = date.dateDisplayLocale || ((_c = (_b = pkg === null || pkg === void 0 ? void 0 : pkg.propertyResolverOrUndefined) === null || _b === void 0 ? void 0 : _b.getDocumentDefaultRPr().lang) === null || _c === void 0 ? void 0 : _c.val) || undefined;
205
288
  if (date.dateFormat !== '')
206
- display = formatDate(date.dateFormat, parsed, date.dateDisplayLocale || undefined);
289
+ display = formatDate(date.dateFormat, parsed, language);
207
290
  }
208
291
  }
209
292
  // an empty value shows the control's placeholder in Word's form: the entry its w:placeholder names, else Word's default
@@ -221,7 +304,7 @@ export function applyBindingsTo(controls) {
221
304
  const result = { bound: 0, updated: 0, skipped: 0 };
222
305
  const notes = [];
223
306
  for (const control of controls) {
224
- if (!control.xmlMapping.isMapped)
307
+ if (!control.xmlMapping.dataBinding)
225
308
  continue;
226
309
  result.bound++;
227
310
  if (applyBindingTo(control, notes))
@@ -242,8 +325,7 @@ export function applyBindingsTo(controls) {
242
325
  export function updateFromControl(control) {
243
326
  var _a, _b;
244
327
  const mapping = control.xmlMapping;
245
- if (!mapping.isMapped)
246
- return false;
328
+ // the node in the part the binding resolves to, a stale ID's too, its ID left as it is (CR-020 D2, docx4j's UpdateXmlFromDocumentSurface)
247
329
  const node = mapping.customXmlNode;
248
330
  if (!node)
249
331
  return false;
@@ -290,7 +372,7 @@ export function updateFromControl(control) {
290
372
  export function updateFromControls(controls) {
291
373
  const result = { bound: 0, updated: 0, skipped: 0 };
292
374
  for (const control of controls) {
293
- if (!control.xmlMapping.isMapped)
375
+ if (!control.xmlMapping.dataBinding)
294
376
  continue;
295
377
  // counted by the picture pass, which reads the image part (CR-005 section 9)
296
378
  if (control.type === 'Picture')
@@ -2,6 +2,7 @@ export { type XPathEngine, type XPathValue, type BooleanConversionMode, booleanV
2
2
  export { CustomXmlPart, CustomXmlNode, CustomXmlPrefixMappingCollection, type CustomXmlNodeType, type CustomXmlPrefixMapping, type CustomXmlPartOwner } from './CustomXmlPart.mjs';
3
3
  export { CustomXmlPartCollection, type CustomXmlHost } from './CustomXmlPartCollection.mjs';
4
4
  export { XmlMapping, type CustomXmlPartLookup } from './XmlMapping.mjs';
5
+ export { StoreItemResolver, firstStep, isPropertiesStoreItem, normaliseStoreItemId, CORE_PROPERTIES_STOREITEMID, EXTENDED_PROPERTIES_STOREITEMID, type StoreItemCandidate, type StoreItemResolution } from './storeItems.mjs';
5
6
  export { CheckboxContentControl, DatePickerContentControl, ListContentControl, ContentControlListItem, PictureContentControl, RepeatingSectionContentControl, GroupContentControl, checkboxRun, CHECKED_SYMBOL, UNCHECKED_SYMBOL, CHECKBOX_FONT, } from './kinds.mjs';
6
7
  export { applyBindingsTo, applyBindingTo, updateFromControls, updateFromControl, runsForValue, formatDate, holdsFlatOpc, PLACEHOLDER_STYLE, PLACEHOLDER_TEXT, type BindingResult } from './bindings.mjs';
7
8
  export { controlIdScope, sdtKindFor, isRunLevel } from './insert.mjs';
@@ -4,6 +4,7 @@ export { booleanValue, DefaultXPathEngine, parsePrefixMappings, formatPrefixMapp
4
4
  export { CustomXmlPart, CustomXmlNode, CustomXmlPrefixMappingCollection } from './CustomXmlPart.mjs';
5
5
  export { CustomXmlPartCollection } from './CustomXmlPartCollection.mjs';
6
6
  export { XmlMapping } from './XmlMapping.mjs';
7
+ export { StoreItemResolver, firstStep, isPropertiesStoreItem, normaliseStoreItemId, CORE_PROPERTIES_STOREITEMID, EXTENDED_PROPERTIES_STOREITEMID } from './storeItems.mjs';
7
8
  export { CheckboxContentControl, DatePickerContentControl, ListContentControl, ContentControlListItem, PictureContentControl, RepeatingSectionContentControl, GroupContentControl, checkboxRun, CHECKED_SYMBOL, UNCHECKED_SYMBOL, CHECKBOX_FONT, } from './kinds.mjs';
8
9
  export { applyBindingsTo, applyBindingTo, updateFromControls, updateFromControl, runsForValue, formatDate, holdsFlatOpc, PLACEHOLDER_STYLE, PLACEHOLDER_TEXT } from './bindings.mjs';
9
10
  export { controlIdScope, sdtKindFor, isRunLevel } from './insert.mjs';
@@ -22,11 +22,14 @@ export interface XPathsEntry {
22
22
  export declare function xpathsEntriesOf(parts: CustomXmlPartLookup): Map<string, XPathsEntry>;
23
23
  /**
24
24
  * The node an `od:xpath` entry selects: the entry's XPath over the part its `storeItemID` names
25
- * (the id compared without braces or case, as `getItem` does). What a tag-bound control's pass
25
+ * (the id compared without braces or case, as `getItem` does), else, where it names none, the part
26
+ * its XPath fits, as a `w:dataBinding` is resolved (CR-020 D1, docx4j's `StoreItemResolver`; the
27
+ * entry is left as it is, and `repair` says which part was taken). What a tag-bound control's pass
26
28
  * starts from; the note says why there is no node.
27
29
  */
28
30
  export declare function selectEntryNode(entryId: string, parts: CustomXmlPartLookup): {
29
31
  node: CustomXmlNode;
32
+ repair?: string;
30
33
  } | {
31
34
  note: string;
32
35
  };
@@ -38,19 +38,22 @@ export function xpathsEntriesOf(parts) {
38
38
  }
39
39
  /**
40
40
  * The node an `od:xpath` entry selects: the entry's XPath over the part its `storeItemID` names
41
- * (the id compared without braces or case, as `getItem` does). What a tag-bound control's pass
41
+ * (the id compared without braces or case, as `getItem` does), else, where it names none, the part
42
+ * its XPath fits, as a `w:dataBinding` is resolved (CR-020 D1, docx4j's `StoreItemResolver`; the
43
+ * entry is left as it is, and `repair` says which part was taken). What a tag-bound control's pass
42
44
  * starts from; the note says why there is no node.
43
45
  */
44
46
  export function selectEntryNode(entryId, parts) {
45
- var _a;
46
47
  const entry = xpathsEntriesOf(parts).get(entryId);
47
48
  if (!entry)
48
49
  return { note: `od:xpath=${entryId}: no such entry in the XPaths part` };
49
- const part = (_a = parts.getItem(entry.storeItemID)) !== null && _a !== void 0 ? _a : parts.items.find((p) => p.id.replace(/[{}]/g, '').toLowerCase() === entry.storeItemID.replace(/[{}]/g, '').toLowerCase());
50
- if (!part)
50
+ const resolved = entry.storeItemID === '' ? undefined : parts.resolveStoreItem(entry.storeItemID, entry.xpath, entry.prefixMappings);
51
+ if (!resolved)
51
52
  return { note: `od:xpath=${entryId}: no custom XML part ${entry.storeItemID}` };
52
- const node = part.selectSingleNode(entry.xpath, entry.prefixMappings);
53
+ const node = resolved.part.selectSingleNode(entry.xpath, entry.prefixMappings);
53
54
  if (!node)
54
55
  return { note: `od:xpath=${entryId}: ${entry.xpath} selects nothing` };
55
- return { node };
56
+ if (!resolved.repaired)
57
+ return { node };
58
+ return { node, repair: `od:xpath=${entryId}: store item ${entry.storeItemID} names no custom XML part; ${resolved.part.part.partName.name} ${resolved.part.id} fits ${entry.xpath} and is used` };
56
59
  }
@@ -139,7 +139,9 @@ export function applyPictureHandlerTo(control, parts) {
139
139
  const selected = selectEntryNode(entryId, parts);
140
140
  if ('note' in selected)
141
141
  return { done: false, note: selected.note };
142
- return bindPicture(control, selected.node.text, pictureWidthOf(params), `od:xpath=${entryId}`);
142
+ const outcome = bindPicture(control, selected.node.text, pictureWidthOf(params), `od:xpath=${entryId}`);
143
+ // a stale storeItemID resolved as Word would (CR-020): said where the outcome has nothing else to say
144
+ return selected.repair && outcome.done && !outcome.note ? { ...outcome, note: selected.repair } : outcome;
143
145
  }
144
146
  /** The picture pass over the tag-bound controls, after the text bindings: counted into the result, what was left said in its notes. */
145
147
  export function applyPictureHandlersTo(controls, parts, result) {
@@ -170,7 +172,7 @@ export async function updateFromPictureControl(control) {
170
172
  var _a, _b;
171
173
  const mapping = control.xmlMapping;
172
174
  // a control showing its placeholder picture holds no picture of its node's (CR-005 section 10)
173
- if (!mapping.isMapped || control.type !== 'Picture' || control.isShowingPlaceholder)
175
+ if (!mapping.dataBinding || control.type !== 'Picture' || control.isShowingPlaceholder)
174
176
  return false;
175
177
  const node = mapping.customXmlNode;
176
178
  if (!node)
@@ -193,7 +195,7 @@ export async function updateFromPictureControl(control) {
193
195
  /** `updateFromContentControls`' picture pass: the mapped picture controls, counted into the result. */
194
196
  export async function updateFromPictureControls(controls, result) {
195
197
  for (const control of controls) {
196
- if (control.type !== 'Picture' || !control.xmlMapping.isMapped)
198
+ if (control.type !== 'Picture' || !control.xmlMapping.dataBinding)
197
199
  continue;
198
200
  result.bound++;
199
201
  if (await updateFromPictureControl(control))
@@ -0,0 +1,57 @@
1
+ /** The `w:storeItemID`s of the core and application properties (docx4j `BindingHandler`): not custom XML parts, never repaired. */
2
+ export declare const CORE_PROPERTIES_STOREITEMID = "{6C3C8BC8-F283-45AE-878A-BAB7291924A1}";
3
+ export declare const EXTENDED_PROPERTIES_STOREITEMID = "{6668398D-A668-4E3E-A5EB-62B293D839F1}";
4
+ /** An itemID without braces, lower-cased: how `getItem` compares ids, and so how a binding's names its part. */
5
+ export declare function normaliseStoreItemId(id: string): string;
6
+ /** Whether an ID is the core or the application properties'. */
7
+ export declare function isPropertiesStoreItem(id: string): boolean;
8
+ /** One custom XML part as the resolver sees it. */
9
+ export interface StoreItemCandidate<P> {
10
+ readonly part: P;
11
+ /** Its itemID, as its properties part spells it. */
12
+ readonly id: string;
13
+ /** Its part name: the order among several that fit. */
14
+ readonly partName: string;
15
+ /** Its document element; none where the part can't be read, which then fits nothing. */
16
+ readonly root: Element | null | undefined;
17
+ /** Whether the XPath, with these prefix mappings, selects a node in it. */
18
+ selects(xpath: string, prefixMappings: string | undefined): boolean;
19
+ }
20
+ /** The part a binding is evaluated against. */
21
+ export interface StoreItemResolution<P> {
22
+ readonly part: P;
23
+ /** True where the binding's ID names no part and this one fits its XPath: Word's repair. */
24
+ readonly repaired: boolean;
25
+ /** Where repaired, how many parts fit the XPath's first step; this one is the first, in part-name order, that selects a node. */
26
+ readonly fitting: number;
27
+ }
28
+ /**
29
+ * docx4j's `StoreItemResolver.resolve` rule over a package's custom XML parts:
30
+ * - the part the ID names, the comparison `getItem`'s (case and braces ignored); the core and application properties'
31
+ * IDs are left to the caller, and never repaired;
32
+ * - else the parts whose document element's namespace and local name match the XPath's first step, its prefix read with
33
+ * the binding's `w:prefixMappings`, else the objects package's `NAMESPACE_PREFIXES` (docx4j reads its own table); an
34
+ * XPath with no named first step (`//`, `*`, an axis, an attribute) fits every part. In part-name order: one fitting part
35
+ * is taken; of several, the first whose XPath selects a node, else the first (Word's choice in every fresh session,
36
+ * check 73b; CR-020 D7); none, and the binding is unresolved.
37
+ *
38
+ * What a stale ID resolves to is remembered per (ID, XPath, prefix mappings), since one stale ID fits different parts for
39
+ * different XPaths (the two-parts probe: B7 part 1, B11 part 2), and forgotten when the parts change (an add, a delete).
40
+ */
41
+ export declare class StoreItemResolver<P> {
42
+ private readonly candidates;
43
+ private readonly memo;
44
+ private signature;
45
+ constructor(candidates: () => readonly StoreItemCandidate<P>[]);
46
+ /** The part for a binding: named by its ID, else the one its XPath fits; undefined where none is named and none fits. */
47
+ resolve(storeItemId: string | undefined, xpath: string | undefined, prefixMappings: string | undefined): StoreItemResolution<P> | undefined;
48
+ }
49
+ /**
50
+ * The XPath's first step as its namespace and local name, with the binding's prefix mappings (`/p:root[1]/p:b7[1]` with
51
+ * `xmlns:p='urn:x'` is `urn:x` and `root`); undefined where the XPath does not start with a named step, when every part
52
+ * fits. docx4j's `StoreItemResolver.firstStep`.
53
+ */
54
+ export declare function firstStep(xpath: string | undefined, prefixMappings: string | undefined): {
55
+ namespaceURI: string;
56
+ localName: string;
57
+ } | undefined;
@@ -0,0 +1,122 @@
1
+ // CR-020: docx4j's StoreItemResolver (its CR-058 D4, `35cdeaee6`, released in 17.3.5): the custom XML part a binding is
2
+ // evaluated against. The part its `w:storeItemID` names, or, where that names no part, the one its XPath fits, as Word
3
+ // repairs such a binding on open (Word 365, 22 and 23 October 2026, and checks 73 and 73b: the control shows the part's
4
+ // value, Office JS's `customXmlPart` is that part, and the save writes its ID into `w:storeItemID`). Shared by every
5
+ // reader and the bind (CR-020 D1): the text model's bound values, `XmlMapping.customXmlPart` and `customXmlNode`,
6
+ // `applyBindings`, `updateFromContentControls` and the `od:xpath` passes, so they all name the part Word binds.
7
+ import { NAMESPACE_PREFIXES } from '@docx4j/generated-objects-ts';
8
+ import { parsePrefixMappings } from './xpath.mjs';
9
+ /** The `w:storeItemID`s of the core and application properties (docx4j `BindingHandler`): not custom XML parts, never repaired. */
10
+ export const CORE_PROPERTIES_STOREITEMID = '{6C3C8BC8-F283-45AE-878A-BAB7291924A1}';
11
+ export const EXTENDED_PROPERTIES_STOREITEMID = '{6668398D-A668-4E3E-A5EB-62B293D839F1}';
12
+ /** An itemID without braces, lower-cased: how `getItem` compares ids, and so how a binding's names its part. */
13
+ export function normaliseStoreItemId(id) {
14
+ return id.trim().replace(/[{}]/g, '').toLowerCase();
15
+ }
16
+ /** Whether an ID is the core or the application properties'. */
17
+ export function isPropertiesStoreItem(id) {
18
+ const key = normaliseStoreItemId(id);
19
+ return key === normaliseStoreItemId(CORE_PROPERTIES_STOREITEMID) || key === normaliseStoreItemId(EXTENDED_PROPERTIES_STOREITEMID);
20
+ }
21
+ /**
22
+ * docx4j's `StoreItemResolver.resolve` rule over a package's custom XML parts:
23
+ * - the part the ID names, the comparison `getItem`'s (case and braces ignored); the core and application properties'
24
+ * IDs are left to the caller, and never repaired;
25
+ * - else the parts whose document element's namespace and local name match the XPath's first step, its prefix read with
26
+ * the binding's `w:prefixMappings`, else the objects package's `NAMESPACE_PREFIXES` (docx4j reads its own table); an
27
+ * XPath with no named first step (`//`, `*`, an axis, an attribute) fits every part. In part-name order: one fitting part
28
+ * is taken; of several, the first whose XPath selects a node, else the first (Word's choice in every fresh session,
29
+ * check 73b; CR-020 D7); none, and the binding is unresolved.
30
+ *
31
+ * What a stale ID resolves to is remembered per (ID, XPath, prefix mappings), since one stale ID fits different parts for
32
+ * different XPaths (the two-parts probe: B7 part 1, B11 part 2), and forgotten when the parts change (an add, a delete).
33
+ */
34
+ export class StoreItemResolver {
35
+ constructor(candidates) {
36
+ this.candidates = candidates;
37
+ this.memo = new Map();
38
+ this.signature = '';
39
+ }
40
+ /** The part for a binding: named by its ID, else the one its XPath fits; undefined where none is named and none fits. */
41
+ resolve(storeItemId, xpath, prefixMappings) {
42
+ var _a;
43
+ if (!storeItemId)
44
+ return undefined;
45
+ const all = this.candidates();
46
+ const key = normaliseStoreItemId(storeItemId);
47
+ const named = all.find((candidate) => normaliseStoreItemId(candidate.id) === key);
48
+ if (named)
49
+ return { part: named.part, repaired: false, fitting: 0 };
50
+ if (isPropertiesStoreItem(storeItemId))
51
+ return undefined;
52
+ const signature = all.map((candidate) => `${normaliseStoreItemId(candidate.id)} ${candidate.partName}`).join('|');
53
+ if (signature !== this.signature) {
54
+ this.memo.clear();
55
+ this.signature = signature;
56
+ }
57
+ const memoKey = `${key}|${xpath !== null && xpath !== void 0 ? xpath : ''}|${prefixMappings !== null && prefixMappings !== void 0 ? prefixMappings : ''}`;
58
+ let resolved = this.memo.get(memoKey);
59
+ if (resolved === undefined) {
60
+ resolved = (_a = repair(all, xpath !== null && xpath !== void 0 ? xpath : '', prefixMappings)) !== null && _a !== void 0 ? _a : null;
61
+ this.memo.set(memoKey, resolved);
62
+ }
63
+ return resolved !== null && resolved !== void 0 ? resolved : undefined;
64
+ }
65
+ }
66
+ function repair(all, xpath, prefixMappings) {
67
+ const step = firstStep(xpath, prefixMappings);
68
+ const fitting = all.filter((candidate) => {
69
+ var _a, _b;
70
+ const root = candidate.root;
71
+ if (!root)
72
+ return false;
73
+ return step === undefined || (((_a = root.namespaceURI) !== null && _a !== void 0 ? _a : '') === step.namespaceURI && ((_b = root.localName) !== null && _b !== void 0 ? _b : root.nodeName) === step.localName);
74
+ }).sort((a, b) => (a.partName < b.partName ? -1 : a.partName > b.partName ? 1 : 0));
75
+ if (fitting.length === 0)
76
+ return undefined;
77
+ if (fitting.length === 1)
78
+ return { part: fitting[0].part, repaired: true, fitting: 1 };
79
+ const selecting = fitting.find((candidate) => {
80
+ try {
81
+ return candidate.selects(xpath, prefixMappings);
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ });
87
+ return { part: (selecting !== null && selecting !== void 0 ? selecting : fitting[0]).part, repaired: true, fitting: fitting.length };
88
+ }
89
+ /** The objects package's prefixes, prefix to namespace: what a prefix the binding doesn't map reads as. */
90
+ const NAMESPACES_BY_PREFIX = (() => {
91
+ const out = {};
92
+ for (const [uri, prefix] of Object.entries(NAMESPACE_PREFIXES))
93
+ if (prefix !== '' && !(prefix in out))
94
+ out[prefix] = uri;
95
+ return out;
96
+ })();
97
+ /**
98
+ * The XPath's first step as its namespace and local name, with the binding's prefix mappings (`/p:root[1]/p:b7[1]` with
99
+ * `xmlns:p='urn:x'` is `urn:x` and `root`); undefined where the XPath does not start with a named step, when every part
100
+ * fits. docx4j's `StoreItemResolver.firstStep`.
101
+ */
102
+ export function firstStep(xpath, prefixMappings) {
103
+ var _a, _b;
104
+ if (xpath === undefined)
105
+ return undefined;
106
+ let s = xpath.trim();
107
+ if (s.startsWith('//'))
108
+ return undefined;
109
+ if (s.startsWith('/'))
110
+ s = s.slice(1);
111
+ const end = s.search(/[/[(\s]/);
112
+ const step = end < 0 ? s : s.slice(0, end);
113
+ if (step === '' || step === '*' || step === '.' || step === '..' || step.includes('::') || step.startsWith('@'))
114
+ return undefined;
115
+ const colon = step.indexOf(':');
116
+ const prefix = colon < 0 ? '' : step.slice(0, colon);
117
+ const localName = colon < 0 ? step : step.slice(colon + 1);
118
+ if (localName === '' || localName === '*')
119
+ return undefined;
120
+ const namespaceURI = prefix === '' ? '' : ((_b = (_a = parsePrefixMappings(prefixMappings)[prefix]) !== null && _a !== void 0 ? _a : NAMESPACES_BY_PREFIX[prefix]) !== null && _b !== void 0 ? _b : '');
121
+ return { namespaceURI, localName };
122
+ }
@@ -5,7 +5,7 @@ import { contentOf } from '../content/ooxml.mjs';
5
5
  import { wrappingCellOf, endWithParagraph } from '../content/tableRules.mjs';
6
6
  import { convertHtml, styleLookupOf } from '../../html/convert.mjs';
7
7
  import { blocksToXml, needsOf, HYPERLINK_STYLE } from '../../html/elements.mjs';
8
- import { tagParamsOf, xpathsEntriesOf, XHTML_CONTENT_TYPE } from './opendope.mjs';
8
+ import { tagParamsOf, selectEntryNode, XHTML_CONTENT_TYPE } from './opendope.mjs';
9
9
  /** Whether a control is bound to XHTML through its tag. */
10
10
  export function isXhtmlBound(control) {
11
11
  const params = tagParamsOf(control.tag);
@@ -68,20 +68,15 @@ function inlineContentOf(paragraph) {
68
68
  * `dropped` (a run-level control's blocks after the first, and what the converter dropped).
69
69
  */
70
70
  export async function applyXhtmlBindingTo(control, parts, host, options = {}) {
71
- var _a, _b, _c, _d, _e, _f, _g, _h;
72
- var _j;
71
+ var _a, _b, _c, _d, _e, _f, _g;
72
+ var _h;
73
73
  const params = tagParamsOf(control.tag);
74
74
  const entryId = params.get('od:xpath');
75
- const entry = xpathsEntriesOf(parts).get(entryId);
76
- if (!entry)
77
- return { done: false, note: `od:xpath=${entryId}: no such entry in the XPaths part` };
78
- const part = (_a = parts.getItem(entry.storeItemID)) !== null && _a !== void 0 ? _a : parts.items.find((p) => p.id.replace(/[{}]/g, '').toLowerCase() === entry.storeItemID.replace(/[{}]/g, '').toLowerCase());
79
- if (!part)
80
- return { done: false, note: `od:xpath=${entryId}: no custom XML part ${entry.storeItemID}` };
81
- const node = part.selectSingleNode(entry.xpath, entry.prefixMappings);
82
- if (!node)
83
- return { done: false, note: `od:xpath=${entryId}: ${entry.xpath} selects nothing` };
84
- const parser = (_b = options.parser) !== null && _b !== void 0 ? _b : (typeof DOMParser === 'function' ? (h) => new DOMParser().parseFromString(h, 'text/html') : undefined);
75
+ const selected = selectEntryNode(entryId, parts);
76
+ if ('note' in selected)
77
+ return { done: false, note: selected.note };
78
+ const { node, repair } = selected;
79
+ const parser = (_a = options.parser) !== null && _a !== void 0 ? _a : (typeof DOMParser === 'function' ? (h) => new DOMParser().parseFromString(h, 'text/html') : undefined);
85
80
  if (!parser)
86
81
  return { done: false, note: `od:xpath=${entryId}: XHTML needs an HTML parser (applyBindings({ html: { parser } }))` };
87
82
  const converted = convertHtml(node.text, await lookupFor(host), { parser });
@@ -93,7 +88,7 @@ export async function applyXhtmlBindingTo(control, parts, host, options = {}) {
93
88
  await host.styles.ensure(converted.report.stylesWanted);
94
89
  }
95
90
  catch {
96
- dropped['styles the defaults lack'] = ((_c = dropped['styles the defaults lack']) !== null && _c !== void 0 ? _c : 0) + converted.report.stylesWanted.length;
91
+ dropped['styles the defaults lack'] = ((_b = dropped['styles the defaults lack']) !== null && _b !== void 0 ? _b : 0) + converted.report.stylesWanted.length;
97
92
  }
98
93
  }
99
94
  const needs = needsOf(blocks);
@@ -110,26 +105,26 @@ export async function applyXhtmlBindingTo(control, parts, host, options = {}) {
110
105
  lists.set(key, host.numbering.restart(firstNumbered));
111
106
  }
112
107
  const links = new Map();
113
- const rels = needs.links.length > 0 ? (_d = control.parentBody.part) === null || _d === void 0 ? void 0 : _d.getRelationshipsPart(true) : undefined;
108
+ const rels = needs.links.length > 0 ? (_c = control.parentBody.part) === null || _c === void 0 ? void 0 : _c.getRelationshipsPart(true) : undefined;
114
109
  if (rels) {
115
110
  for (const address of needs.links)
116
111
  links.set(address, rels.addExternalRelationship(Namespaces.HYPERLINK, address).id);
117
112
  host.requireStyles([HYPERLINK_STYLE]);
118
113
  }
119
114
  else if (needs.links.length > 0)
120
- dropped['links'] = ((_e = dropped['links']) !== null && _e !== void 0 ? _e : 0) + needs.links.length;
115
+ dropped['links'] = ((_d = dropped['links']) !== null && _d !== void 0 ? _d : 0) + needs.links.length;
121
116
  const elements = await contentOf(blocksToXml(blocks, { numId: (key) => lists.get(key), relId: (address) => links.get(address) }));
122
117
  applyRunProperties(elements, control.runProperties);
123
118
  const sdt = control.sdt;
124
- (_f = sdt.sdtContent) !== null && _f !== void 0 ? _f : (sdt.sdtContent = {});
125
- const content = ((_g = (_j = sdt.sdtContent).content) !== null && _g !== void 0 ? _g : (_j.content = []));
119
+ (_e = sdt.sdtContent) !== null && _e !== void 0 ? _e : (sdt.sdtContent = {});
120
+ const content = ((_f = (_h = sdt.sdtContent).content) !== null && _f !== void 0 ? _f : (_h.content = []));
126
121
  if (control.form === 'Run') {
127
122
  // A run-level control holds inline content: the first paragraph's, the rest reported.
128
123
  const first = elements.find((e) => typeNameOf(e) === 'org_docx4j_wml.P');
129
124
  const inlines = first ? inlineContentOf(first) : [];
130
125
  const rest = elements.length - (first ? 1 : 0);
131
126
  if (rest > 0)
132
- dropped['blocks after the first, in a run-level control'] = ((_h = dropped['blocks after the first, in a run-level control']) !== null && _h !== void 0 ? _h : 0) + rest;
127
+ dropped['blocks after the first, in a run-level control'] = ((_g = dropped['blocks after the first, in a run-level control']) !== null && _g !== void 0 ? _g : 0) + rest;
133
128
  content.length = 0;
134
129
  content.push(...inlines);
135
130
  linkParents(content, sdt.sdtContent);
@@ -145,7 +140,7 @@ export async function applyXhtmlBindingTo(control, parts, host, options = {}) {
145
140
  }
146
141
  // a control in a branch of a kept mc:AlternateContent too: the pass reaches every branch (CR-013 phase 3)
147
142
  writingEveryBranch(() => { control.isShowingPlaceholder = false; });
148
- return Object.keys(dropped).length > 0 ? { done: true, dropped } : { done: true };
143
+ return { done: true, ...(repair ? { note: repair } : {}), ...(Object.keys(dropped).length > 0 ? { dropped } : {}) };
149
144
  }
150
145
  /** The XHTML pass over the controls, after the text bindings: counted into the result, what was left said in its notes. */
151
146
  export async function applyXhtmlBindingsTo(controls, parts, host, result, options = {}) {
@@ -0,0 +1,25 @@
1
+ /** The fields a wall clock shows: `month` 1 to 12, `hour` 0 to 23. */
2
+ export interface WallClock {
3
+ readonly year: number;
4
+ readonly month: number;
5
+ readonly day: number;
6
+ readonly hour: number;
7
+ readonly minute: number;
8
+ readonly second: number;
9
+ }
10
+ /** The runtime's zone, an IANA id (`Australia/Sydney`; `UTC` where the runtime has no better answer). */
11
+ export declare function runtimeTimeZone(): string;
12
+ /**
13
+ * `id` when the runtime's `Intl` knows it as a zone, else a `RangeError` naming it: a package's
14
+ * `dateTimeZone` is refused at the set, never ignored, since a wrong zone shifts every stamp.
15
+ */
16
+ export declare function assertTimeZone(id: string): string;
17
+ /** The wall clock `date` shows in `zone`; the runtime's zone where `zone` is undefined. */
18
+ export declare function wallClockIn(date: Date, zone: string | undefined): WallClock;
19
+ /**
20
+ * The instant at which a wall clock in `zone` shows `clock`; the runtime's zone where `zone` is
21
+ * undefined. Found by offset: the clock taken as UTC, corrected by what the zone shows for that
22
+ * guess, twice, which settles across a daylight-saving step; a clock time that does not exist on a
23
+ * spring-forward day gives the instant an hour on, as `Date` does for the runtime's zone.
24
+ */
25
+ export declare function instantOf(clock: WallClock, zone: string | undefined): Date;