@usejunior/docx-core 0.23.0 → 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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/index.js.map +1 -1
- package/dist/primitives/structural_validation.d.ts +54 -0
- package/dist/primitives/structural_validation.d.ts.map +1 -0
- package/dist/primitives/structural_validation.js +301 -0
- package/dist/primitives/structural_validation.js.map +1 -0
- package/dist/primitives/styles.d.ts +39 -17
- package/dist/primitives/styles.d.ts.map +1 -1
- package/dist/primitives/styles.js +412 -89
- package/dist/primitives/styles.js.map +1 -1
- 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,11 +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, docDefaultsRPr: null, tableStyleRPrs: [] };
|
|
124
|
+
return { byId, docDefaultsRPr: null, defaultTableStyleId: null, tableStyleRPrs: [] };
|
|
82
125
|
const docDefaults = stylesDoc.getElementsByTagNameNS(OOXML.W_NS, W.docDefaults).item(0);
|
|
83
126
|
const rPrDefault = docDefaults ? getFirstChild(docDefaults, OOXML.W_NS, W.rPrDefault) : null;
|
|
84
127
|
const docDefaultsRPr = rPrDefault ? getFirstChild(rPrDefault, OOXML.W_NS, W.rPr) : null;
|
|
85
128
|
const tableStyleRPrs = [];
|
|
129
|
+
let defaultTableStyleId = null;
|
|
86
130
|
const styles = Array.from(stylesDoc.getElementsByTagNameNS(OOXML.W_NS, W.style));
|
|
87
131
|
for (const st of styles) {
|
|
88
132
|
const id = getWAttr(st, 'styleId');
|
|
@@ -90,30 +134,48 @@ export function parseStylesXml(stylesDoc) {
|
|
|
90
134
|
continue;
|
|
91
135
|
const nameEl = getFirstChild(st, OOXML.W_NS, W.name);
|
|
92
136
|
const basedOnEl = getFirstChild(st, OOXML.W_NS, W.basedOn);
|
|
93
|
-
const
|
|
94
|
-
|
|
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);
|
|
95
145
|
const name = nameEl ? (getWAttr(nameEl, 'val') ?? id) : id;
|
|
96
146
|
const basedOn = basedOnEl ? (getWAttr(basedOnEl, 'val') ?? null) : null;
|
|
97
|
-
const
|
|
98
|
-
if (styleType === 'table') {
|
|
99
|
-
if (rPr)
|
|
100
|
-
tableStyleRPrs.push(rPr);
|
|
101
|
-
for (const conditional of Array.from(st.getElementsByTagNameNS(OOXML.W_NS, 'tblStylePr'))) {
|
|
102
|
-
const conditionalRPr = getFirstChild(conditional, OOXML.W_NS, W.rPr);
|
|
103
|
-
if (conditionalRPr)
|
|
104
|
-
tableStyleRPrs.push(conditionalRPr);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
byId.set(id, {
|
|
147
|
+
const def = {
|
|
108
148
|
styleId: id,
|
|
109
149
|
styleType,
|
|
110
150
|
name,
|
|
111
151
|
basedOn,
|
|
112
152
|
pPr: pPr ?? null,
|
|
113
|
-
rPr: rPr ?? null,
|
|
114
|
-
}
|
|
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);
|
|
115
177
|
}
|
|
116
|
-
return { byId, docDefaultsRPr: docDefaultsRPr ?? null, tableStyleRPrs };
|
|
178
|
+
return { byId, docDefaultsRPr: docDefaultsRPr ?? null, defaultTableStyleId, tableStyleRPrs };
|
|
117
179
|
}
|
|
118
180
|
function resolveStyleChain(model, styleId) {
|
|
119
181
|
const chain = [];
|
|
@@ -251,46 +313,310 @@ function parseBoolProp(parent, tagLocal) {
|
|
|
251
313
|
* @see https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oi29500/f7130225-2368-48f3-acae-a9d278d0fb25
|
|
252
314
|
* @see https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/f936abaf-a9cb-439b-923d-7f688d9202e9
|
|
253
315
|
*/
|
|
254
|
-
function resolveToggleProperty(steps, tagLocal
|
|
316
|
+
function resolveToggleProperty(steps, tagLocal) {
|
|
255
317
|
let effective = false;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
let absolute = false;
|
|
259
|
-
for (const { rPr, kind } of steps) {
|
|
260
|
-
const declaration = parseBoolProp(rPr, tagLocal);
|
|
318
|
+
for (const { rPr, declare, kind } of steps) {
|
|
319
|
+
const declaration = declare ? declare(tagLocal) : parseBoolProp(rPr ?? null, tagLocal);
|
|
261
320
|
if (declaration === null)
|
|
262
321
|
continue;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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;
|
|
267
327
|
}
|
|
268
|
-
else
|
|
328
|
+
else {
|
|
269
329
|
effective = declaration;
|
|
270
|
-
absolute = true;
|
|
271
|
-
}
|
|
272
|
-
else if (declaration) {
|
|
273
|
-
effective = !effective;
|
|
274
330
|
}
|
|
275
331
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
332
|
+
return effective;
|
|
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
|
+
};
|
|
283
374
|
}
|
|
284
|
-
|
|
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) {
|
|
285
408
|
for (let cur = node.parentNode; cur; cur = cur.parentNode) {
|
|
286
409
|
if (cur.nodeType !== 1)
|
|
287
410
|
continue;
|
|
288
411
|
const el = cur;
|
|
289
|
-
if (el.
|
|
290
|
-
|
|
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
|
+
}
|
|
291
445
|
}
|
|
292
446
|
return false;
|
|
293
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
|
+
}
|
|
294
620
|
function parseUnderline(parent) {
|
|
295
621
|
if (!parent)
|
|
296
622
|
return null;
|
|
@@ -425,8 +751,9 @@ function parseEffectiveHighlightVal(parent) {
|
|
|
425
751
|
* the run declares. Ordinary properties are taken from the first layer that
|
|
426
752
|
* specifies them: direct `w:rPr` on the run, then the `w:rStyle`
|
|
427
753
|
* character-style `basedOn` chain, then the paragraph mark's `w:rPr` inside
|
|
428
|
-
* `pPr`, then the paragraph style's `basedOn` chain,
|
|
429
|
-
* document defaults
|
|
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`).
|
|
430
757
|
*
|
|
431
758
|
* Each property is resolved independently down the chain — a style that
|
|
432
759
|
* specifies only color does not mask an ancestor's bold.
|
|
@@ -441,14 +768,21 @@ function parseEffectiveHighlightVal(parent) {
|
|
|
441
768
|
* Document defaults seed toggle evaluation as an absolute base value rather
|
|
442
769
|
* than acting as another parity level (see {@link resolveToggleProperty}).
|
|
443
770
|
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
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.
|
|
452
786
|
*
|
|
453
787
|
* Part of docx-core's public surface (see `src/index.ts`) so external
|
|
454
788
|
* diagnostics — `scripts/check_docx_formatting_loss.mjs` today, the planned
|
|
@@ -463,11 +797,14 @@ function parseEffectiveHighlightVal(parent) {
|
|
|
463
797
|
* @param params.theme the model produced by {@link parseThemeXml}; when
|
|
464
798
|
* omitted, direct font/color fallbacks retain their previous behavior
|
|
465
799
|
*
|
|
800
|
+
* @conformance ECMA-376 edition 5, Part 1 § 17.7.2
|
|
466
801
|
* @conformance ECMA-376 edition 5, Part 1 § 17.7.3
|
|
467
802
|
* @conformance ECMA-376 edition 5, Part 1 § 17.7.5.1
|
|
803
|
+
* @conformance ECMA-376 edition 5, Part 1 § 17.7.6
|
|
468
804
|
* @see https://github.com/UseJunior/safe-docx/issues/737
|
|
469
805
|
* @see https://github.com/UseJunior/safe-docx/issues/752
|
|
470
806
|
* @see https://github.com/UseJunior/safe-docx/issues/753
|
|
807
|
+
* @see https://github.com/UseJunior/safe-docx/issues/1159
|
|
471
808
|
*/
|
|
472
809
|
export function extractEffectiveRunFormatting(params) {
|
|
473
810
|
const { run, paragraphPPr, paragraphStyleId, styles, theme = null } = params;
|
|
@@ -480,11 +817,12 @@ export function extractEffectiveRunFormatting(params) {
|
|
|
480
817
|
const rStyleChain = resolveStyleChain(styles, rStyleId);
|
|
481
818
|
const paragraphStyleChain = resolveStyleChain(styles, paragraphStyleId);
|
|
482
819
|
// Priority: direct rPr → rStyle chain rPrs → paragraph mark rPr → paragraph
|
|
483
|
-
// style chain rPrs → document defaults.
|
|
484
|
-
// independently down this list: a chain member that
|
|
485
|
-
// must not mask an ancestor's bold, so the sources are
|
|
486
|
-
// containers, never "the first chain member that has an
|
|
487
|
-
// on #684; extractStyleRunFormatting above already
|
|
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).
|
|
488
826
|
const sourcesAboveDefaults = [
|
|
489
827
|
rPr,
|
|
490
828
|
...rStyleChain.map((s) => s.rPr),
|
|
@@ -492,48 +830,33 @@ export function extractEffectiveRunFormatting(params) {
|
|
|
492
830
|
...paragraphStyleChain.map((s) => s.rPr),
|
|
493
831
|
];
|
|
494
832
|
const docDefaultsRPr = styles.docDefaultsRPr ?? null;
|
|
495
|
-
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
//
|
|
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.
|
|
836
|
+
// Paragraph-mark rPr is direct formatting at its hierarchy level; a
|
|
837
|
+
// character style can still contribute above it before the run's own rPr
|
|
838
|
+
// supplies the final absolute override.
|
|
499
839
|
const toggleSteps = [
|
|
500
840
|
{ rPr: docDefaultsRPr, kind: 'default' },
|
|
841
|
+
{
|
|
842
|
+
declare: (tagLocal) => firstNonNull(tableLayers.map((layer) => parseBoolProp(layer, tagLocal))),
|
|
843
|
+
kind: 'table',
|
|
844
|
+
},
|
|
501
845
|
...[...paragraphStyleChain].reverse().map((style) => ({ rPr: style.rPr, kind: 'style' })),
|
|
502
846
|
{ rPr: pRPr, kind: 'direct' },
|
|
503
847
|
...[...rStyleChain].reverse().map((style) => ({ rPr: style.rPr, kind: 'style' })),
|
|
504
848
|
{ rPr, kind: 'direct' },
|
|
505
849
|
];
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
// declared nowhere above the document defaults, a table-style declaration
|
|
509
|
-
// that differs from the base below it makes the property unresolved. A
|
|
510
|
-
// declaration that restates that base cannot change the outcome.
|
|
511
|
-
const tableLayers = styles.tableStyleRPrs && styles.tableStyleRPrs.length > 0 && isInsideTable(run)
|
|
512
|
-
? styles.tableStyleRPrs
|
|
513
|
-
: [];
|
|
514
|
-
const toggle = (tagLocal) => resolveToggleProperty(toggleSteps, tagLocal, tableLayers.some((layer) => parseBoolProp(layer, tagLocal) === true));
|
|
850
|
+
const toggle = (tagLocal) => resolveToggleProperty(toggleSteps, tagLocal);
|
|
851
|
+
const sources = [...sourcesAboveDefaults, ...tableLayers];
|
|
515
852
|
/**
|
|
516
|
-
* Nearest declaration above the document defaults
|
|
517
|
-
* default, otherwise `ooxmlDefault`
|
|
518
|
-
*
|
|
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).
|
|
519
856
|
*/
|
|
520
|
-
// Hex colours and font names compare case-insensitively: `abcdef` restates
|
|
521
|
-
// `ABCDEF`.
|
|
522
|
-
const sameValue = (a, b) => typeof a === 'string' && typeof b === 'string' ? a.toUpperCase() === b.toUpperCase() : a === b;
|
|
523
857
|
const resolveLayered = (parse, ooxmlDefault) => {
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
return null;
|
|
527
|
-
if (above !== null)
|
|
528
|
-
return above;
|
|
529
|
-
const base = parse(docDefaultsRPr) ?? ooxmlDefault;
|
|
530
|
-
if (tableLayers.some((layer) => {
|
|
531
|
-
const declared = parse(layer);
|
|
532
|
-
return declared !== null && !sameValue(declared, base);
|
|
533
|
-
})) {
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
return base === DECLARED_UNRESOLVED ? null : base;
|
|
858
|
+
const value = firstNonNull(sources.map(parse)) ?? parse(docDefaultsRPr) ?? ooxmlDefault;
|
|
859
|
+
return value === DECLARED_UNRESOLVED ? null : value;
|
|
537
860
|
};
|
|
538
861
|
return {
|
|
539
862
|
bold: toggle(W.b),
|