@usejunior/docx-core 0.22.1 → 0.23.1

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 (49) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/integration/generation-probes.d.ts.map +1 -1
  3. package/dist/integration/generation-probes.js +26 -21
  4. package/dist/integration/generation-probes.js.map +1 -1
  5. package/dist/integration/libreoffice-oracle.d.ts +6 -1
  6. package/dist/integration/libreoffice-oracle.d.ts.map +1 -1
  7. package/dist/integration/libreoffice-oracle.js +113 -44
  8. package/dist/integration/libreoffice-oracle.js.map +1 -1
  9. package/dist/integration/soffice-lock.d.ts +30 -0
  10. package/dist/integration/soffice-lock.d.ts.map +1 -0
  11. package/dist/integration/soffice-lock.js +308 -0
  12. package/dist/integration/soffice-lock.js.map +1 -0
  13. package/dist/primitives/accept_changes.d.ts +27 -1
  14. package/dist/primitives/accept_changes.d.ts.map +1 -1
  15. package/dist/primitives/accept_changes.js +57 -2
  16. package/dist/primitives/accept_changes.js.map +1 -1
  17. package/dist/primitives/comments.d.ts.map +1 -1
  18. package/dist/primitives/comments.js +2 -2
  19. package/dist/primitives/comments.js.map +1 -1
  20. package/dist/primitives/document_view-headings.d.ts.map +1 -1
  21. package/dist/primitives/document_view-headings.js +16 -6
  22. package/dist/primitives/document_view-headings.js.map +1 -1
  23. package/dist/primitives/footnotes.js +2 -2
  24. package/dist/primitives/footnotes.js.map +1 -1
  25. package/dist/primitives/formatting_tags.d.ts +6 -3
  26. package/dist/primitives/formatting_tags.d.ts.map +1 -1
  27. package/dist/primitives/formatting_tags.js +50 -44
  28. package/dist/primitives/formatting_tags.js.map +1 -1
  29. package/dist/primitives/index.d.ts +1 -0
  30. package/dist/primitives/index.d.ts.map +1 -1
  31. package/dist/primitives/index.js +1 -0
  32. package/dist/primitives/index.js.map +1 -1
  33. package/dist/primitives/paragraph-index.d.ts +28 -2
  34. package/dist/primitives/paragraph-index.d.ts.map +1 -1
  35. package/dist/primitives/paragraph-index.js +25 -6
  36. package/dist/primitives/paragraph-index.js.map +1 -1
  37. package/dist/primitives/structural_validation.d.ts +54 -0
  38. package/dist/primitives/structural_validation.d.ts.map +1 -0
  39. package/dist/primitives/structural_validation.js +301 -0
  40. package/dist/primitives/structural_validation.js.map +1 -0
  41. package/dist/primitives/styles.d.ts +110 -20
  42. package/dist/primitives/styles.d.ts.map +1 -1
  43. package/dist/primitives/styles.js +535 -45
  44. package/dist/primitives/styles.js.map +1 -1
  45. package/dist/primitives/zip.d.ts +6 -0
  46. package/dist/primitives/zip.d.ts.map +1 -1
  47. package/dist/primitives/zip.js +7 -1
  48. package/dist/primitives/zip.js.map +1 -1
  49. package/package.json +2 -2
@@ -5,6 +5,49 @@ function getWAttr(el, localName) {
5
5
  // when attributes were written without a real namespace binding.
6
6
  return getAttributeSafe(el, OOXML.W_NS, localName, 'w', { emptyIsMissing: true });
7
7
  }
8
+ function isOnValue(v) {
9
+ return v !== '0' && v !== 'false' && v !== 'off';
10
+ }
11
+ function childElements(parent, localName) {
12
+ const out = [];
13
+ for (let c = parent.firstChild; c; c = c.nextSibling) {
14
+ if (c.nodeType !== 1)
15
+ continue;
16
+ const el = c;
17
+ if (el.localName === localName && el.namespaceURI === OOXML.W_NS)
18
+ out.push(el);
19
+ }
20
+ return out;
21
+ }
22
+ /**
23
+ * The first direct `w:` child named `localName`. Unlike getFirstChild (a
24
+ * descendant search) it never reaches into a nested `w:tblPrChange` /
25
+ * `w:trPrChange` (the previous state) or a nested table.
26
+ */
27
+ function ownChild(parent, localName) {
28
+ for (let c = parent.firstChild; c; c = c.nextSibling) {
29
+ if (c.nodeType !== 1)
30
+ continue;
31
+ const el = c;
32
+ if (el.localName === localName && el.namespaceURI === OOXML.W_NS)
33
+ return el;
34
+ }
35
+ return null;
36
+ }
37
+ /**
38
+ * A table-style `w:rPr` without its `w:rPrChange`, which records the
39
+ * previous state of a tracked formatting change. The property readers search
40
+ * descendants, so the change record would otherwise read as current. The
41
+ * copy is detached and only ever read.
42
+ */
43
+ function currentRPr(rPr) {
44
+ if (!rPr || !ownChild(rPr, 'rPrChange'))
45
+ return rPr;
46
+ const copy = rPr.cloneNode(true);
47
+ for (const change of childElements(copy, 'rPrChange'))
48
+ copy.removeChild(change);
49
+ return copy;
50
+ }
8
51
  const THEME_COLOR_ELEMENT_BY_REFERENCE = {
9
52
  dark1: 'dk1',
10
53
  light1: 'lt1',
@@ -78,7 +121,12 @@ export function parseThemeXml(themeDoc) {
78
121
  export function parseStylesXml(stylesDoc) {
79
122
  const byId = new Map();
80
123
  if (!stylesDoc)
81
- return { byId };
124
+ return { byId, docDefaultsRPr: null, defaultTableStyleId: null, tableStyleRPrs: [] };
125
+ const docDefaults = stylesDoc.getElementsByTagNameNS(OOXML.W_NS, W.docDefaults).item(0);
126
+ const rPrDefault = docDefaults ? getFirstChild(docDefaults, OOXML.W_NS, W.rPrDefault) : null;
127
+ const docDefaultsRPr = rPrDefault ? getFirstChild(rPrDefault, OOXML.W_NS, W.rPr) : null;
128
+ const tableStyleRPrs = [];
129
+ let defaultTableStyleId = null;
82
130
  const styles = Array.from(stylesDoc.getElementsByTagNameNS(OOXML.W_NS, W.style));
83
131
  for (const st of styles) {
84
132
  const id = getWAttr(st, 'styleId');
@@ -86,20 +134,48 @@ export function parseStylesXml(stylesDoc) {
86
134
  continue;
87
135
  const nameEl = getFirstChild(st, OOXML.W_NS, W.name);
88
136
  const basedOnEl = getFirstChild(st, OOXML.W_NS, W.basedOn);
89
- const pPr = getFirstChild(st, OOXML.W_NS, W.pPr);
90
- const rPr = getFirstChild(st, OOXML.W_NS, W.rPr);
137
+ const styleType = getWAttr(st, 'type');
138
+ // getFirstChild searches descendants. A table style nests further pPr /
139
+ // rPr / tblPr inside each w:tblStylePr, so its own properties must be read
140
+ // from direct children, or a style without its own rPr would take the
141
+ // first conditional's as if it applied to the whole table.
142
+ const styleChild = (localName) => styleType === 'table' ? ownChild(st, localName) : getFirstChild(st, OOXML.W_NS, localName);
143
+ const pPr = styleChild(W.pPr);
144
+ const rPr = styleChild(W.rPr);
91
145
  const name = nameEl ? (getWAttr(nameEl, 'val') ?? id) : id;
92
146
  const basedOn = basedOnEl ? (getWAttr(basedOnEl, 'val') ?? null) : null;
93
- byId.set(id, {
147
+ const def = {
94
148
  styleId: id,
95
- styleType: getWAttr(st, 'type'),
149
+ styleType,
96
150
  name,
97
151
  basedOn,
98
152
  pPr: pPr ?? null,
99
- rPr: rPr ?? null,
100
- });
153
+ rPr: (styleType === 'table' ? currentRPr(rPr) : rPr) ?? null,
154
+ };
155
+ if (styleType === 'table') {
156
+ if (def.rPr)
157
+ tableStyleRPrs.push(def.rPr);
158
+ const conditionalRPrs = new Map();
159
+ for (const conditional of childElements(st, 'tblStylePr')) {
160
+ const type = getWAttr(conditional, 'type');
161
+ const conditionalRPr = currentRPr(ownChild(conditional, W.rPr));
162
+ if (!conditionalRPr)
163
+ continue;
164
+ tableStyleRPrs.push(conditionalRPr);
165
+ // The first block of a type wins, matching getFirstChild elsewhere.
166
+ if (type && !conditionalRPrs.has(type))
167
+ conditionalRPrs.set(type, conditionalRPr);
168
+ }
169
+ def.tblPr = styleChild(W.tblPr);
170
+ def.conditionalRPrs = conditionalRPrs;
171
+ const isDefault = getWAttr(st, 'default');
172
+ // The last default style of a type wins (§ 17.7.4.17).
173
+ if (isDefault !== null && isOnValue(isDefault))
174
+ defaultTableStyleId = id;
175
+ }
176
+ byId.set(id, def);
101
177
  }
102
- return { byId };
178
+ return { byId, docDefaultsRPr: docDefaultsRPr ?? null, defaultTableStyleId, tableStyleRPrs };
103
179
  }
104
180
  function resolveStyleChain(model, styleId) {
105
181
  const chain = [];
@@ -219,28 +295,328 @@ function parseBoolProp(parent, tagLocal) {
219
295
  return true;
220
296
  }
221
297
  /**
222
- * Evaluate a toggle property in hierarchy order. Style-level true values
223
- * invert the accumulated state while style-level false values preserve it;
224
- * direct formatting sets an absolute value.
298
+ * Evaluate a toggle property in hierarchy order. Document defaults seed the
299
+ * starting value; style-level true values invert the accumulated state while
300
+ * style-level false values preserve it; direct formatting sets an absolute
301
+ * value.
302
+ *
303
+ * Microsoft's ISO/IEC 29500 implementation note for §17.7.3 says Word falls
304
+ * back to the document defaults when a hierarchy level supplies no value and
305
+ * treats the document-default value as the base for subsequent parity.
306
+ * MS-OE376 §2.7.7 documents a separate Word deviation for paragraph styles but
307
+ * does not make `w:docDefaults` another toggling style level. So defaults seed
308
+ * the state rather than invert it.
225
309
  *
226
310
  * @conformance ECMA-376 edition 5, Part 1 § 17.7.3
227
311
  * @see https://github.com/UseJunior/safe-docx/issues/737
312
+ * @see https://github.com/UseJunior/safe-docx/issues/753
313
+ * @see https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oi29500/f7130225-2368-48f3-acae-a9d278d0fb25
314
+ * @see https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/f936abaf-a9cb-439b-923d-7f688d9202e9
228
315
  */
229
316
  function resolveToggleProperty(steps, tagLocal) {
230
317
  let effective = false;
231
- for (const { rPr, kind } of steps) {
232
- const declaration = parseBoolProp(rPr, tagLocal);
318
+ for (const { rPr, declare, kind } of steps) {
319
+ const declaration = declare ? declare(tagLocal) : parseBoolProp(rPr ?? null, tagLocal);
233
320
  if (declaration === null)
234
321
  continue;
235
- if (kind === 'direct') {
236
- effective = declaration;
322
+ // A table style resets the toggle to its declared value (Word; see the
323
+ // MS-OI29500 note on § 17.7.6), like the default and direct levels.
324
+ if (kind === 'style') {
325
+ if (declaration)
326
+ effective = !effective;
237
327
  }
238
- else if (declaration) {
239
- effective = !effective;
328
+ else {
329
+ effective = declaration;
240
330
  }
241
331
  }
242
332
  return effective;
243
333
  }
334
+ /**
335
+ * Conditional formatting types (ST_TblStyleOverrideType, § 17.18.89) in the
336
+ * order Word applies them; each later type overrides the ones before it.
337
+ * `wholeTable` is not among them: Word neither applies nor keeps a
338
+ * `wholeTable` conditional (MS-OI29500 note on § 17.18.89), and the style's
339
+ * own `w:rPr` is the whole-table formatting.
340
+ *
341
+ * ECMA-376 § 17.7.6 lists whole table, column bands, row bands, first/last
342
+ * row, first/last column, then the corner cells. Microsoft's implementation
343
+ * note for § 17.7.6.6 records that Office instead applies row bands, column
344
+ * bands, first/last column, first/last row, then the corners. This follows
345
+ * Office, the renderer whose output the resolver is meant to report.
346
+ *
347
+ * @conformance ECMA-376 edition 5, Part 1 § 17.7.6
348
+ * @see https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oi29500/2ac331d4-cf1e-4fa0-8bca-6da74411e284
349
+ */
350
+ const CONDITIONAL_ORDER = [
351
+ 'band1Horz',
352
+ 'band2Horz',
353
+ 'band1Vert',
354
+ 'band2Vert',
355
+ 'firstCol',
356
+ 'lastCol',
357
+ 'firstRow',
358
+ 'lastRow',
359
+ 'nwCell',
360
+ 'neCell',
361
+ 'swCell',
362
+ 'seCell',
363
+ ];
364
+ const WORD_DEFAULT_TABLE_LOOK = 0x04a0;
365
+ function tableLookFromBits(bits) {
366
+ return {
367
+ firstRow: (bits & 0x0020) !== 0,
368
+ lastRow: (bits & 0x0040) !== 0,
369
+ firstColumn: (bits & 0x0080) !== 0,
370
+ lastColumn: (bits & 0x0100) !== 0,
371
+ hBand: (bits & 0x0200) === 0,
372
+ vBand: (bits & 0x0400) === 0,
373
+ };
374
+ }
375
+ /**
376
+ * Read `w:tblLook`. The explicit attributes win; the transitional `w:val`
377
+ * hex bitmask is read only when none of them is present (0x0020 first row,
378
+ * 0x0040 last row, 0x0080 first column, 0x0100 last column, 0x0200 no
379
+ * horizontal banding, 0x0400 no vertical banding). An absent attribute is
380
+ * off.
381
+ *
382
+ * When the table has no `w:tblLook`, the standard assumes 0x0000 but Word
383
+ * assumes 0x04A0 (first row, first column, no vertical banding); this
384
+ * follows Word, the renderer whose output the resolver reports.
385
+ *
386
+ * @see https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oi29500/90f075ce-b16d-422a-b1b0-39c9777a594e
387
+ */
388
+ function parseTableLook(el) {
389
+ if (!el)
390
+ return tableLookFromBits(WORD_DEFAULT_TABLE_LOOK);
391
+ const named = ['firstRow', 'lastRow', 'firstColumn', 'lastColumn', 'noHBand', 'noVBand'].map((a) => getWAttr(el, a));
392
+ if (named.some((v) => v !== null)) {
393
+ const on = (v) => v !== null && isOnValue(v);
394
+ const [firstRow, lastRow, firstColumn, lastColumn, noHBand, noVBand] = named;
395
+ return {
396
+ firstRow: on(firstRow),
397
+ lastRow: on(lastRow),
398
+ firstColumn: on(firstColumn),
399
+ lastColumn: on(lastColumn),
400
+ hBand: !on(noHBand),
401
+ vBand: !on(noVBand),
402
+ };
403
+ }
404
+ const raw = getWAttr(el, 'val');
405
+ return tableLookFromBits(raw && /^[0-9A-Fa-f]{1,4}$/u.test(raw) ? Number.parseInt(raw, 16) : 0);
406
+ }
407
+ function nearestAncestor(node, localName, stopAt = null) {
408
+ for (let cur = node.parentNode; cur; cur = cur.parentNode) {
409
+ if (cur.nodeType !== 1)
410
+ continue;
411
+ const el = cur;
412
+ if (el.namespaceURI !== OOXML.W_NS)
413
+ continue;
414
+ if (el.localName === localName)
415
+ return el;
416
+ if (stopAt && el.localName === stopAt)
417
+ return null;
418
+ }
419
+ return null;
420
+ }
421
+ /**
422
+ * Visit the `w:tr` rows of a table (or `w:tc` cells of a row) in document
423
+ * order, or in reverse with `fromEnd`, looking through the content-control
424
+ * and custom-XML wrappers that may enclose them. Stops early when `visit`
425
+ * returns true.
426
+ */
427
+ function walkTableChildren(parent, localName, visit, fromEnd = false) {
428
+ for (let c = fromEnd ? parent.lastChild : parent.firstChild; c; c = fromEnd ? c.previousSibling : c.nextSibling) {
429
+ if (c.nodeType !== 1 || c.namespaceURI !== OOXML.W_NS)
430
+ continue;
431
+ const child = c;
432
+ if (child.localName === localName) {
433
+ if (visit(child))
434
+ return true;
435
+ }
436
+ else if (child.localName === 'sdt') {
437
+ const content = ownChild(child, 'sdtContent');
438
+ if (content && walkTableChildren(content, localName, visit, fromEnd))
439
+ return true;
440
+ }
441
+ else if (child.localName === 'customXml') {
442
+ if (walkTableChildren(child, localName, visit, fromEnd))
443
+ return true;
444
+ }
445
+ }
446
+ return false;
447
+ }
448
+ function edgeTableChild(parent, localName, fromEnd) {
449
+ let found = null;
450
+ walkTableChildren(parent, localName, (el) => {
451
+ found = el;
452
+ return true;
453
+ }, fromEnd);
454
+ return found;
455
+ }
456
+ function tableChildIndex(parent, localName, target) {
457
+ let index = 0;
458
+ let found = -1;
459
+ walkTableChildren(parent, localName, (el) => {
460
+ if (el === target) {
461
+ found = index;
462
+ return true;
463
+ }
464
+ index++;
465
+ return false;
466
+ });
467
+ return found;
468
+ }
469
+ function intVal(parent, localName, fallback) {
470
+ const el = parent ? ownChild(parent, localName) : null;
471
+ const v = el ? Number.parseInt(getWAttr(el, 'val') ?? '', 10) : Number.NaN;
472
+ return Number.isSafeInteger(v) && v >= 0 ? v : fallback;
473
+ }
474
+ function bandSize(sources, localName) {
475
+ for (const tblPr of sources) {
476
+ const v = intVal(tblPr, localName, 0);
477
+ if (v > 0)
478
+ return v;
479
+ }
480
+ return 1;
481
+ }
482
+ /**
483
+ * The cell's span of table grid columns. A row's `w:gridBefore` skips grid
484
+ * columns before its first cell (§ 17.4.15) and each cell covers
485
+ * `w:gridSpan` columns, so the first physical cell is not always the first
486
+ * column. The grid width is the table's `w:tblGrid`, or the row's own extent
487
+ * when the table has none.
488
+ */
489
+ function cellGridSpan(tbl, tr, tc) {
490
+ const trPr = ownChild(tr, W.trPr);
491
+ let col = intVal(trPr, 'gridBefore', 0);
492
+ let start = -1;
493
+ let end = -1;
494
+ walkTableChildren(tr, 'tc', (cell) => {
495
+ const span = Math.max(1, intVal(ownChild(cell, W.tcPr), 'gridSpan', 1));
496
+ if (cell === tc) {
497
+ start = col;
498
+ end = col + span;
499
+ }
500
+ col += span;
501
+ });
502
+ const grid = ownChild(tbl, W.tblGrid);
503
+ const gridCols = grid ? childElements(grid, 'gridCol').length : 0;
504
+ const gridCount = gridCols > 0 ? gridCols : col + intVal(trPr, 'gridAfter', 0);
505
+ return { start, end, gridCount };
506
+ }
507
+ /**
508
+ * Which conditional types apply to a cell, given the `w:tblLook` in force
509
+ * for its row. Corner types need both of their edges switched on (MS-OI29500
510
+ * note on § 17.7.6). Banding counts rows (grid columns) after the header row
511
+ * (first column) when that is switched on, in groups of
512
+ * `w:tblStyleRowBandSize` (`w:tblStyleColBandSize`). `rowBandsUsed` skips
513
+ * the row-index walk when the style has no row-band conditional.
514
+ */
515
+ function applicableConditionals(pos, look, rowBandsUsed) {
516
+ const out = new Set();
517
+ const isFirstCol = pos.colStart === 0;
518
+ if (look.hBand && rowBandsUsed) {
519
+ const idx = pos.rowIndex() - (look.firstRow ? 1 : 0);
520
+ if (idx >= 0)
521
+ out.add(Math.floor(idx / pos.rowBandSize) % 2 === 0 ? 'band1Horz' : 'band2Horz');
522
+ }
523
+ if (look.vBand) {
524
+ const idx = pos.colStart - (look.firstColumn ? 1 : 0);
525
+ if (idx >= 0)
526
+ out.add(Math.floor(idx / pos.colBandSize) % 2 === 0 ? 'band1Vert' : 'band2Vert');
527
+ }
528
+ if (look.firstColumn && isFirstCol)
529
+ out.add('firstCol');
530
+ if (look.lastColumn && pos.isLastCol)
531
+ out.add('lastCol');
532
+ if (look.firstRow && pos.isFirstRow)
533
+ out.add('firstRow');
534
+ if (look.lastRow && pos.isLastRow)
535
+ out.add('lastRow');
536
+ if (look.firstRow && look.firstColumn && pos.isFirstRow && isFirstCol)
537
+ out.add('nwCell');
538
+ if (look.firstRow && look.lastColumn && pos.isFirstRow && pos.isLastCol)
539
+ out.add('neCell');
540
+ if (look.lastRow && look.firstColumn && pos.isLastRow && isFirstCol)
541
+ out.add('swCell');
542
+ if (look.lastRow && look.lastColumn && pos.isLastRow && pos.isLastCol)
543
+ out.add('seCell');
544
+ return out;
545
+ }
546
+ /**
547
+ * The table-style `w:rPr` layers for one set of applicable conditionals,
548
+ * highest precedence first: the applied conditional types from last to first
549
+ * in {@link CONDITIONAL_ORDER}, then the style's own `w:rPr`. Within each,
550
+ * the derived style precedes its `basedOn` ancestors.
551
+ *
552
+ * @see https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oi29500/3359ff85-c423-4b9d-be78-d1cf96f79486
553
+ */
554
+ function tableStyleLayers(chain, applied) {
555
+ const layers = [];
556
+ const types = [...CONDITIONAL_ORDER].filter((t) => applied.has(t)).reverse();
557
+ for (const type of types) {
558
+ for (const style of chain) {
559
+ const rPr = style.conditionalRPrs?.get(type);
560
+ if (rPr)
561
+ layers.push(rPr);
562
+ }
563
+ }
564
+ for (const style of chain)
565
+ if (style.rPr)
566
+ layers.push(style.rPr);
567
+ return layers;
568
+ }
569
+ /**
570
+ * The table-style layers for a run, highest precedence first (see
571
+ * {@link tableStyleLayers}); empty when the run is not in a table or the
572
+ * table has no style.
573
+ *
574
+ * The table style is the table's `w:tblStyle`, or the default table style
575
+ * when it names none (or names one that does not exist). The innermost
576
+ * table governs a run in a nested table. Which conditionals apply is
577
+ * computed from the cell's position and the table's `w:tblLook`. The
578
+ * `w:cnfStyle` elements on paragraphs, rows and cells are not consulted: the
579
+ * standard describes them as an optimization that records the outcome, and
580
+ * the position and `w:tblLook` determine it.
581
+ *
582
+ * @conformance ECMA-376 edition 5, Part 1 § 17.7.2
583
+ * @conformance ECMA-376 edition 5, Part 1 § 17.7.6
584
+ * @see https://github.com/UseJunior/safe-docx/issues/1159
585
+ */
586
+ function tableStyleLayersForRun(run, styles) {
587
+ const tc = nearestAncestor(run, W.tc, W.tbl);
588
+ const tr = tc ? nearestAncestor(tc, W.tr, W.tbl) : null;
589
+ const tbl = tr ? nearestAncestor(tr, W.tbl) : null;
590
+ if (!tc || !tr || !tbl)
591
+ return [];
592
+ // Direct children only: a nested w:tblPrChange holds the previous state.
593
+ const tblPr = ownChild(tbl, W.tblPr);
594
+ const tblStyleEl = tblPr ? ownChild(tblPr, 'tblStyle') : null;
595
+ const named = tblStyleEl ? getWAttr(tblStyleEl, 'val') : null;
596
+ const styleId = named && styles.byId.get(named)?.styleType === 'table' ? named : (styles.defaultTableStyleId ?? null);
597
+ const chain = resolveStyleChain(styles, styleId).filter((st) => st.styleType === 'table');
598
+ if (chain.length === 0)
599
+ return [];
600
+ if (!chain.some((st) => st.conditionalRPrs && st.conditionalRPrs.size > 0)) {
601
+ return tableStyleLayers(chain, new Set());
602
+ }
603
+ // A row's w:tblPrEx/w:tblLook overrides the table's for that row (§ 17.4.54).
604
+ const tblPrEx = ownChild(tr, 'tblPrEx');
605
+ const lookEl = (tblPrEx ? ownChild(tblPrEx, 'tblLook') : null) ?? (tblPr ? ownChild(tblPr, 'tblLook') : null);
606
+ const bandSources = [tblPr, ...chain.map((st) => st.tblPr ?? null)];
607
+ const { start, end, gridCount } = cellGridSpan(tbl, tr, tc);
608
+ const pos = {
609
+ isFirstRow: edgeTableChild(tbl, 'tr', false) === tr,
610
+ isLastRow: edgeTableChild(tbl, 'tr', true) === tr,
611
+ rowIndex: () => tableChildIndex(tbl, 'tr', tr),
612
+ colStart: start,
613
+ isLastCol: end === gridCount,
614
+ rowBandSize: bandSize(bandSources, 'tblStyleRowBandSize'),
615
+ colBandSize: bandSize(bandSources, 'tblStyleColBandSize'),
616
+ };
617
+ const rowBandsUsed = chain.some((st) => st.conditionalRPrs?.has('band1Horz') || st.conditionalRPrs?.has('band2Horz'));
618
+ return tableStyleLayers(chain, applicableConditionals(pos, parseTableLook(lookEl), rowBandsUsed));
619
+ }
244
620
  function parseUnderline(parent) {
245
621
  if (!parent)
246
622
  return null;
@@ -324,13 +700,60 @@ function parseHighlightVal(parent) {
324
700
  return null;
325
701
  return v;
326
702
  }
703
+ /**
704
+ * Marks a layer that *does* declare a property whose value cannot be
705
+ * established — a theme colour or theme font with no theme to resolve it
706
+ * against. Resolution stops at that layer (a lower layer must not show
707
+ * through) and the property is reported as unresolved.
708
+ */
709
+ const DECLARED_UNRESOLVED = Symbol('declared-unresolved');
710
+ /**
711
+ * `'auto'` for a declared automatic colour, the hex otherwise, `null` if
712
+ * undeclared, {@link DECLARED_UNRESOLVED} for a theme colour reference that
713
+ * resolves neither through the theme nor through an explicit hex `w:val`.
714
+ */
715
+ function parseEffectiveColorHex(parent, theme) {
716
+ if (!parent)
717
+ return null;
718
+ const el = getFirstChild(parent, OOXML.W_NS, W.color);
719
+ if (!el)
720
+ return null;
721
+ const hex = parseColorHex(parent, theme);
722
+ if (hex !== null)
723
+ return hex;
724
+ return getWAttr(el, 'themeColor') ? DECLARED_UNRESOLVED : 'auto';
725
+ }
726
+ /**
727
+ * The font name, `null` if `w:rFonts` names none, or {@link DECLARED_UNRESOLVED}
728
+ * when it names the Latin font only through a theme reference that does not
729
+ * resolve and carries no explicit `w:ascii` / `w:hAnsi` fallback.
730
+ */
731
+ function parseEffectiveFontName(parent, theme) {
732
+ const name = parseFontName(parent, theme);
733
+ if (name !== null || !parent)
734
+ return name;
735
+ const el = getFirstChild(parent, OOXML.W_NS, W.rFonts);
736
+ if (!el)
737
+ return null;
738
+ return getWAttr(el, 'asciiTheme') || getWAttr(el, 'hAnsiTheme') ? DECLARED_UNRESOLVED : null;
739
+ }
740
+ /** `false` for a declared `none`, the value otherwise, `null` if undeclared. */
741
+ function parseEffectiveHighlightVal(parent) {
742
+ if (!parent)
743
+ return null;
744
+ const el = getFirstChild(parent, OOXML.W_NS, W.highlight);
745
+ if (!el)
746
+ return null;
747
+ return parseHighlightVal(parent) ?? false;
748
+ }
327
749
  /**
328
750
  * Resolve the run formatting a reader actually sees, not merely the formatting
329
751
  * the run declares. Ordinary properties are taken from the first layer that
330
752
  * specifies them: direct `w:rPr` on the run, then the `w:rStyle`
331
753
  * character-style `basedOn` chain, then the paragraph mark's `w:rPr` inside
332
- * `pPr`, then the paragraph style's `basedOn` chain. A property specified
333
- * nowhere resolves to the neutral value (`false`, `''`, `0`, or `null`).
754
+ * `pPr`, then the paragraph style's `basedOn` chain, then — for a run inside
755
+ * a table — the table style, and finally the document defaults
756
+ * (`w:docDefaults/w:rPrDefault/w:rPr`).
334
757
  *
335
758
  * Each property is resolved independently down the chain — a style that
336
759
  * specifies only color does not mask an ancestor's bold.
@@ -342,9 +765,24 @@ function parseHighlightVal(parent) {
342
765
  * `w:caps`, `w:smallCaps`, `w:strike`, `w:emboss`, `w:imprint`, `w:outline`,
343
766
  * `w:shadow`, and `w:vanish`.
344
767
  *
345
- * Not resolved: `w:docDefaults`, table-style run properties, and
346
- * numbering-level `rPr`. A formatting change confined to one of those layers
347
- * is invisible to this resolver.
768
+ * Document defaults seed toggle evaluation as an absolute base value rather
769
+ * than acting as another parity level (see {@link resolveToggleProperty}).
770
+ *
771
+ * Table styles (#1159): the table context is read from the run's ancestors —
772
+ * the innermost `w:tbl`, its `w:tblStyle` (or the default table style), the
773
+ * `basedOn` chain, `w:tblLook`, and the cell's row and column. The style's
774
+ * `w:rPr` and every conditional `w:tblStylePr/w:rPr` that applies to the
775
+ * cell merge into one layer between the document defaults and the paragraph
776
+ * style (see {@link tableStyleLayers} for the order). For toggles the
777
+ * merged layer's nearest declaration resets the value rather than toggling
778
+ * it: the standard says a table style toggles like any other style, but Word
779
+ * assigns the declared value (MS-OI29500 note on § 17.7.6), and this follows
780
+ * Word.
781
+ *
782
+ * @see https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oi29500/14452bbe-be4d-4dbb-90e6-3d23ae9361bc
783
+ *
784
+ * Numbering-level `rPr` formats only the list label (`w:lvlText`), never the
785
+ * paragraph's runs, so it is out of scope.
348
786
  *
349
787
  * Part of docx-core's public surface (see `src/index.ts`) so external
350
788
  * diagnostics — `scripts/check_docx_formatting_loss.mjs` today, the planned
@@ -359,8 +797,14 @@ function parseHighlightVal(parent) {
359
797
  * @param params.theme the model produced by {@link parseThemeXml}; when
360
798
  * omitted, direct font/color fallbacks retain their previous behavior
361
799
  *
800
+ * @conformance ECMA-376 edition 5, Part 1 § 17.7.2
362
801
  * @conformance ECMA-376 edition 5, Part 1 § 17.7.3
802
+ * @conformance ECMA-376 edition 5, Part 1 § 17.7.5.1
803
+ * @conformance ECMA-376 edition 5, Part 1 § 17.7.6
363
804
  * @see https://github.com/UseJunior/safe-docx/issues/737
805
+ * @see https://github.com/UseJunior/safe-docx/issues/752
806
+ * @see https://github.com/UseJunior/safe-docx/issues/753
807
+ * @see https://github.com/UseJunior/safe-docx/issues/1159
364
808
  */
365
809
  export function extractEffectiveRunFormatting(params) {
366
810
  const { run, paragraphPPr, paragraphStyleId, styles, theme = null } = params;
@@ -373,44 +817,90 @@ export function extractEffectiveRunFormatting(params) {
373
817
  const rStyleChain = resolveStyleChain(styles, rStyleId);
374
818
  const paragraphStyleChain = resolveStyleChain(styles, paragraphStyleId);
375
819
  // Priority: direct rPr → rStyle chain rPrs → paragraph mark rPr → paragraph
376
- // style chain rPrs. Each property resolves independently down this list: a
377
- // chain member that specifies only color must not mask an ancestor's bold,
378
- // so the sources are the individual rPr containers, never "the first chain
379
- // member that has an rPr" (peer review on #684; extractStyleRunFormatting
380
- // above already resolved per property).
381
- const sources = [
820
+ // style chain rPrs → table style (when in a table) → document defaults.
821
+ // Each property resolves independently down this list: a chain member that
822
+ // specifies only color must not mask an ancestor's bold, so the sources are
823
+ // the individual rPr containers, never "the first chain member that has an
824
+ // rPr" (peer review on #684; extractStyleRunFormatting above already
825
+ // resolved per property).
826
+ const sourcesAboveDefaults = [
382
827
  rPr,
383
828
  ...rStyleChain.map((s) => s.rPr),
384
829
  pRPr,
385
830
  ...paragraphStyleChain.map((s) => s.rPr),
386
831
  ];
387
- const resolve = (parse) => firstNonNull(sources.map(parse));
388
- // Apply from the least specific style ancestor to direct run formatting.
832
+ const docDefaultsRPr = styles.docDefaultsRPr ?? null;
833
+ const tableLayers = tableStyleLayersForRun(run, styles);
834
+ // Apply from the document defaults through the table style and the least
835
+ // specific paragraph-style ancestor to direct run formatting.
389
836
  // Paragraph-mark rPr is direct formatting at its hierarchy level; a
390
837
  // character style can still contribute above it before the run's own rPr
391
838
  // supplies the final absolute override.
392
839
  const toggleSteps = [
840
+ { rPr: docDefaultsRPr, kind: 'default' },
841
+ {
842
+ declare: (tagLocal) => firstNonNull(tableLayers.map((layer) => parseBoolProp(layer, tagLocal))),
843
+ kind: 'table',
844
+ },
393
845
  ...[...paragraphStyleChain].reverse().map((style) => ({ rPr: style.rPr, kind: 'style' })),
394
846
  { rPr: pRPr, kind: 'direct' },
395
847
  ...[...rStyleChain].reverse().map((style) => ({ rPr: style.rPr, kind: 'style' })),
396
848
  { rPr, kind: 'direct' },
397
849
  ];
850
+ const toggle = (tagLocal) => resolveToggleProperty(toggleSteps, tagLocal);
851
+ const sources = [...sourcesAboveDefaults, ...tableLayers];
852
+ /**
853
+ * Nearest declaration above the document defaults (table style
854
+ * included); otherwise the document default, otherwise `ooxmlDefault`
855
+ * (`null` for a property with no OOXML default).
856
+ */
857
+ const resolveLayered = (parse, ooxmlDefault) => {
858
+ const value = firstNonNull(sources.map(parse)) ?? parse(docDefaultsRPr) ?? ooxmlDefault;
859
+ return value === DECLARED_UNRESOLVED ? null : value;
860
+ };
861
+ return {
862
+ bold: toggle(W.b),
863
+ italic: toggle(W.i),
864
+ caps: toggle(W.caps),
865
+ smallCaps: toggle(W.smallCaps),
866
+ strike: toggle(W.strike),
867
+ emboss: toggle(W.emboss),
868
+ imprint: toggle(W.imprint),
869
+ outline: toggle(W.outline),
870
+ shadow: toggle(W.shadow),
871
+ vanish: toggle(W.vanish),
872
+ underline: resolveLayered(parseUnderline, false),
873
+ highlightVal: resolveLayered(parseEffectiveHighlightVal, false),
874
+ // No OOXML default: the rendered font and size are application-defined
875
+ // when nothing declares them, so an undeclared value stays unresolved.
876
+ fontName: resolveLayered((el) => parseEffectiveFontName(el, theme), null),
877
+ fontSizePt: resolveLayered(parseFontSizePt, null),
878
+ colorHex: resolveLayered((el) => parseEffectiveColorHex(el, theme), 'auto'),
879
+ };
880
+ }
881
+ /**
882
+ * Effective run formatting for annotation bodies (comments, footnotes), whose
883
+ * `tagged_text` is emitted in `full` mode and read back by docx-markdoc.
884
+ * Toggles, underline and highlight resolve through every layer, document
885
+ * defaults included. Colour, size and font are reported only when a layer
886
+ * above `w:docDefaults` declares them, otherwise as not declared (`'auto'` /
887
+ * `null`), so the inherited document font is not tagged as `face` on every run
888
+ * and a direct value that restates the document default is still emitted
889
+ * over a character style that would otherwise show through on import.
890
+ *
891
+ * @see https://github.com/UseJunior/safe-docx/issues/753
892
+ */
893
+ export function extractAnnotationRunFormatting(params) {
894
+ const effective = extractEffectiveRunFormatting(params);
895
+ const declared = extractEffectiveRunFormatting({
896
+ ...params,
897
+ styles: { ...params.styles, docDefaultsRPr: null },
898
+ });
398
899
  return {
399
- bold: resolveToggleProperty(toggleSteps, W.b),
400
- italic: resolveToggleProperty(toggleSteps, W.i),
401
- caps: resolveToggleProperty(toggleSteps, W.caps),
402
- smallCaps: resolveToggleProperty(toggleSteps, W.smallCaps),
403
- strike: resolveToggleProperty(toggleSteps, W.strike),
404
- emboss: resolveToggleProperty(toggleSteps, W.emboss),
405
- imprint: resolveToggleProperty(toggleSteps, W.imprint),
406
- outline: resolveToggleProperty(toggleSteps, W.outline),
407
- shadow: resolveToggleProperty(toggleSteps, W.shadow),
408
- vanish: resolveToggleProperty(toggleSteps, W.vanish),
409
- underline: resolve(parseUnderline) ?? false,
410
- highlightVal: resolve(parseHighlightVal),
411
- fontName: resolve((el) => parseFontName(el, theme)) ?? '',
412
- fontSizePt: resolve(parseFontSizePt) ?? 0,
413
- colorHex: resolve((el) => parseColorHex(el, theme)),
900
+ ...effective,
901
+ colorHex: declared.colorHex,
902
+ fontSizePt: declared.fontSizePt,
903
+ fontName: declared.fontName,
414
904
  };
415
905
  }
416
906
  //# sourceMappingURL=styles.js.map