@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/integration/generation-probes.d.ts.map +1 -1
- package/dist/integration/generation-probes.js +26 -21
- package/dist/integration/generation-probes.js.map +1 -1
- package/dist/integration/libreoffice-oracle.d.ts +6 -1
- package/dist/integration/libreoffice-oracle.d.ts.map +1 -1
- package/dist/integration/libreoffice-oracle.js +113 -44
- package/dist/integration/libreoffice-oracle.js.map +1 -1
- package/dist/integration/soffice-lock.d.ts +30 -0
- package/dist/integration/soffice-lock.d.ts.map +1 -0
- package/dist/integration/soffice-lock.js +308 -0
- package/dist/integration/soffice-lock.js.map +1 -0
- package/dist/primitives/accept_changes.d.ts +27 -1
- package/dist/primitives/accept_changes.d.ts.map +1 -1
- package/dist/primitives/accept_changes.js +57 -2
- package/dist/primitives/accept_changes.js.map +1 -1
- package/dist/primitives/comments.d.ts.map +1 -1
- package/dist/primitives/comments.js +2 -2
- package/dist/primitives/comments.js.map +1 -1
- package/dist/primitives/document_view-headings.d.ts.map +1 -1
- package/dist/primitives/document_view-headings.js +16 -6
- package/dist/primitives/document_view-headings.js.map +1 -1
- package/dist/primitives/footnotes.js +2 -2
- package/dist/primitives/footnotes.js.map +1 -1
- package/dist/primitives/formatting_tags.d.ts +6 -3
- package/dist/primitives/formatting_tags.d.ts.map +1 -1
- package/dist/primitives/formatting_tags.js +50 -44
- package/dist/primitives/formatting_tags.js.map +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/paragraph-index.d.ts +28 -2
- package/dist/primitives/paragraph-index.d.ts.map +1 -1
- package/dist/primitives/paragraph-index.js +25 -6
- package/dist/primitives/paragraph-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 +110 -20
- package/dist/primitives/styles.d.ts.map +1 -1
- package/dist/primitives/styles.js +535 -45
- package/dist/primitives/styles.js.map +1 -1
- package/dist/primitives/zip.d.ts +6 -0
- package/dist/primitives/zip.d.ts.map +1 -1
- package/dist/primitives/zip.js +7 -1
- package/dist/primitives/zip.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,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
|
|
90
|
-
|
|
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
|
-
|
|
147
|
+
const def = {
|
|
94
148
|
styleId: id,
|
|
95
|
-
styleType
|
|
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.
|
|
223
|
-
*
|
|
224
|
-
* direct formatting sets an absolute
|
|
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
|
-
|
|
236
|
-
|
|
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
|
|
239
|
-
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
|
|
333
|
-
*
|
|
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
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
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
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
|
|
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
|
|
388
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|