@docx4j/core-ts 0.2.0 → 0.2.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/README.md +5 -0
- package/dist/model/content/Paragraph.mjs +19 -14
- package/dist/model/content/Range.d.mts +29 -3
- package/dist/model/content/Range.mjs +76 -18
- package/dist/model/content/binder.d.mts +1 -1
- package/dist/model/content/binder.mjs +54 -21
- package/dist/packages/WordprocessingMLPackage.d.mts +13 -1
- package/dist/packages/WordprocessingMLPackage.mjs +21 -1
- package/dist/xml/dom.d.mts +10 -1
- package/dist/xml/dom.mjs +46 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,6 +113,11 @@ body.replaceText('(\\d+) pages', '$1 pp.', { matchRegExp: true });
|
|
|
113
113
|
body.replaceText('(quick) (fox)', '\\2 \\1', { matchWildcards: true });
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
`range.hyperlink = 'https://example.com/'` (or `'#bookmark'` for a place in the document) makes the
|
|
117
|
+
range a link, styled as Word styles one: its runs take the `Hyperlink` character style, over any
|
|
118
|
+
character style they had, and lose their direct colour; `''` removes the links the range touches,
|
|
119
|
+
style and all. A document that lacks the `Hyperlink` definition gets it when it is saved.
|
|
120
|
+
|
|
116
121
|
A style the document lacks is spliced in from docx4j's defaults, with the styles it is based on and
|
|
117
122
|
linked to: `await pkg.styles.ensure(['FootnoteText', 'FootnoteReference'])` returns the ids it
|
|
118
123
|
added, and a document that already has them keeps its styles part byte for byte.
|
|
@@ -527,11 +527,8 @@ export class Paragraph {
|
|
|
527
527
|
const value = seg.text.substring(0, from) + middle + seg.text.substring(to);
|
|
528
528
|
if (value === '')
|
|
529
529
|
toRemove.push(seg);
|
|
530
|
-
else
|
|
531
|
-
t
|
|
532
|
-
if (/^\s|\s$|\s\s/.test(value))
|
|
533
|
-
t.space = 'preserve';
|
|
534
|
-
}
|
|
530
|
+
else
|
|
531
|
+
setText(t, value);
|
|
535
532
|
}
|
|
536
533
|
else
|
|
537
534
|
toRemove.push(seg);
|
|
@@ -554,9 +551,7 @@ export class Paragraph {
|
|
|
554
551
|
if (target) {
|
|
555
552
|
const t = target.item.value;
|
|
556
553
|
const at = start - target.start;
|
|
557
|
-
t
|
|
558
|
-
if (/^\s|\s$|\s\s/.test(t.value))
|
|
559
|
-
t.space = 'preserve';
|
|
554
|
+
setText(t, target.text.substring(0, at) + text + target.text.substring(at));
|
|
560
555
|
}
|
|
561
556
|
else {
|
|
562
557
|
// no w:t to extend: a new run next to the segment at `start`, with that run's formatting
|
|
@@ -613,9 +608,7 @@ export class Paragraph {
|
|
|
613
608
|
const t = seg.item.value;
|
|
614
609
|
const head = seg.text.substring(0, at);
|
|
615
610
|
const tail = seg.text.substring(at);
|
|
616
|
-
t
|
|
617
|
-
if (/^\s|\s$|\s\s/.test(head))
|
|
618
|
-
t.space = 'preserve';
|
|
611
|
+
setText(t, head);
|
|
619
612
|
const rest = seg.owner.splice(seg.index + 1);
|
|
620
613
|
const second = runOf([textItem(tail), ...rest], seg.run.rPr ? deepCopy(seg.run.rPr) : undefined);
|
|
621
614
|
seg.runOwner.splice(seg.runIndex + 1, 0, second);
|
|
@@ -807,9 +800,7 @@ export class Paragraph {
|
|
|
807
800
|
continue;
|
|
808
801
|
if (seg.editable) {
|
|
809
802
|
const t = seg.item.value;
|
|
810
|
-
t
|
|
811
|
-
if (/^\s|\s$|\s\s/.test(t.value))
|
|
812
|
-
t.space = 'preserve';
|
|
803
|
+
setText(t, side === 'after' ? seg.text + text : text + seg.text);
|
|
813
804
|
}
|
|
814
805
|
else {
|
|
815
806
|
const run = runOf([textItem(text)], copyRPr(seg.run.rPr));
|
|
@@ -927,3 +918,17 @@ function outlineLevelOf(pPr) {
|
|
|
927
918
|
const level = (_a = pPr === null || pPr === void 0 ? void 0 : pPr.outlineLvl) === null || _a === void 0 ? void 0 : _a.val;
|
|
928
919
|
return level === undefined ? 10 : level + 1;
|
|
929
920
|
}
|
|
921
|
+
/**
|
|
922
|
+
* Sets a `w:t`'s text with `xml:space="preserve"` exactly when the text needs it - whitespace at
|
|
923
|
+
* either end, or two whitespace characters together - which is how the builders' `t()` and Word
|
|
924
|
+
* write it. Setting the attribute and never taking it off left a text shortened from "Font styles "
|
|
925
|
+
* to "Font X" still preserving, the same text but not the same bytes as a `w:t` made afresh
|
|
926
|
+
* (CR-002 section 32 item 2).
|
|
927
|
+
*/
|
|
928
|
+
function setText(t, value) {
|
|
929
|
+
t.value = value;
|
|
930
|
+
if (/^\s|\s$|\s\s/.test(value))
|
|
931
|
+
t.space = 'preserve';
|
|
932
|
+
else
|
|
933
|
+
delete t.space;
|
|
934
|
+
}
|
|
@@ -90,14 +90,40 @@ export declare class Range {
|
|
|
90
90
|
* Setting removes the hyperlinks the range already carries and wraps its runs in one
|
|
91
91
|
* `w:hyperlink`, splitting runs at the boundaries as `font` and `insertContentControl` do;
|
|
92
92
|
* setting `""` removes them and wraps nothing. An external address becomes a relationship of
|
|
93
|
-
* this range's part, which is why the part must exist (CR-002 section 20).
|
|
93
|
+
* this range's part, which is why the part must exist (CR-002 section 20). Only the hyperlinks
|
|
94
|
+
* the range touches are removed; before 0.2.1 every one in the paragraph was (section 25).
|
|
95
|
+
*
|
|
96
|
+
* The runs are styled as Word styles them, measured through Office JS (`test/README.md` check
|
|
97
|
+
* 15, CR-002 section 25): each run a link wraps takes the `Hyperlink` character style, over any
|
|
98
|
+
* character style it had, and loses its direct colour, keeping the rest of its direct formatting;
|
|
99
|
+
* a link removed takes the style off its runs again. The `Hyperlink` definition is added to the
|
|
100
|
+
* styles part when the document lacks it, at the latest when the package is saved - a setter
|
|
101
|
+
* cannot await it - so `await pkg.styles.ensure('Hyperlink')` first where an effective read
|
|
102
|
+
* before the save must see it. Under change tracking the restyle is a formatting revision, as a
|
|
103
|
+
* `font` write is.
|
|
94
104
|
*/
|
|
95
105
|
get hyperlink(): string;
|
|
96
106
|
set hyperlink(value: string);
|
|
97
|
-
/** The `w:hyperlink` this range
|
|
107
|
+
/** The first `w:hyperlink` this range touches, if any. */
|
|
98
108
|
private hyperlinkHolder;
|
|
99
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Unwraps every `w:hyperlink` the range touches, keeping its runs where they are and taking the
|
|
111
|
+
* `Hyperlink` style off them. Until 0.2.1 this unwrapped every hyperlink of the paragraph, so
|
|
112
|
+
* linking one word unlinked the others (CR-002 section 25).
|
|
113
|
+
*/
|
|
100
114
|
private removeHyperlinks;
|
|
115
|
+
/**
|
|
116
|
+
* Whether this range touches a hyperlink's text: overlaps it, or for an empty range lies in it
|
|
117
|
+
* (from its first character up to, not including, its end). A hyperlink holding no text touches
|
|
118
|
+
* nothing.
|
|
119
|
+
*/
|
|
120
|
+
private touches;
|
|
121
|
+
/**
|
|
122
|
+
* The `Hyperlink` character style on every run of a hyperlink, in place of any character style
|
|
123
|
+
* and with the direct colour removed, or off again when the link goes: what Word does (CR-002
|
|
124
|
+
* section 25). A run whose properties are then empty loses its `w:rPr`, as in Word's output.
|
|
125
|
+
*/
|
|
126
|
+
private restyle;
|
|
101
127
|
/** The target of a relationship of this range's part, for the getter. */
|
|
102
128
|
private relationshipTarget;
|
|
103
129
|
/** An external relationship of this range's part for the address, and its id. */
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Font } from './Font.mjs';
|
|
2
2
|
import { runFontSelectorOf } from '../fonts/lookup.mjs';
|
|
3
|
-
import { typeNameOf, linkParents, runItemsOf } from './tree.mjs';
|
|
3
|
+
import { typeNameOf, linkParents, runItemsOf, runsOf } from './tree.mjs';
|
|
4
4
|
import { Docx4JException } from '../../opc/exceptions.mjs';
|
|
5
5
|
import { ContentControl } from './ContentControl.mjs';
|
|
6
|
-
import { sdt as sdtOf, nextSdtId } from '@docx4j/generated-objects-ts/builders/wml';
|
|
6
|
+
import { sdt as sdtOf, nextSdtId, applyRunOptions } from '@docx4j/generated-objects-ts/builders/wml';
|
|
7
7
|
import { hyperlink as hyperlinkOf } from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
|
|
8
8
|
import { Namespaces } from '../../parts/Namespaces.mjs';
|
|
9
9
|
import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
|
|
@@ -247,7 +247,17 @@ export class Range {
|
|
|
247
247
|
* Setting removes the hyperlinks the range already carries and wraps its runs in one
|
|
248
248
|
* `w:hyperlink`, splitting runs at the boundaries as `font` and `insertContentControl` do;
|
|
249
249
|
* setting `""` removes them and wraps nothing. An external address becomes a relationship of
|
|
250
|
-
* this range's part, which is why the part must exist (CR-002 section 20).
|
|
250
|
+
* this range's part, which is why the part must exist (CR-002 section 20). Only the hyperlinks
|
|
251
|
+
* the range touches are removed; before 0.2.1 every one in the paragraph was (section 25).
|
|
252
|
+
*
|
|
253
|
+
* The runs are styled as Word styles them, measured through Office JS (`test/README.md` check
|
|
254
|
+
* 15, CR-002 section 25): each run a link wraps takes the `Hyperlink` character style, over any
|
|
255
|
+
* character style it had, and loses its direct colour, keeping the rest of its direct formatting;
|
|
256
|
+
* a link removed takes the style off its runs again. The `Hyperlink` definition is added to the
|
|
257
|
+
* styles part when the document lacks it, at the latest when the package is saved - a setter
|
|
258
|
+
* cannot await it - so `await pkg.styles.ensure('Hyperlink')` first where an effective read
|
|
259
|
+
* before the save must see it. Under change tracking the restyle is a formatting revision, as a
|
|
260
|
+
* `font` write is.
|
|
251
261
|
*/
|
|
252
262
|
get hyperlink() {
|
|
253
263
|
var _a;
|
|
@@ -259,7 +269,7 @@ export class Range {
|
|
|
259
269
|
return anchor === '' ? address : `${address}#${anchor}`;
|
|
260
270
|
}
|
|
261
271
|
set hyperlink(value) {
|
|
262
|
-
var _a;
|
|
272
|
+
var _a, _b, _c;
|
|
263
273
|
const paragraph = this.paragraph;
|
|
264
274
|
this.removeHyperlinks();
|
|
265
275
|
if (value === '')
|
|
@@ -300,35 +310,73 @@ export class Range {
|
|
|
300
310
|
holder.value.content = items;
|
|
301
311
|
owner.splice(at, items.length, holder);
|
|
302
312
|
linkParents(holder, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
|
|
313
|
+
this.restyle(holder.value, true);
|
|
314
|
+
(_c = (_b = paragraph.parentBody.package_) === null || _b === void 0 ? void 0 : _b.requireStyles) === null || _c === void 0 ? void 0 : _c.call(_b, [HYPERLINK_STYLE]);
|
|
303
315
|
}
|
|
304
|
-
/** The `w:hyperlink` this range
|
|
316
|
+
/** The first `w:hyperlink` this range touches, if any. */
|
|
305
317
|
hyperlinkHolder() {
|
|
306
|
-
|
|
307
|
-
if (segment.end <= this.start && this.start !== this.end)
|
|
308
|
-
continue;
|
|
309
|
-
if (segment.start >= this.end && this.start !== this.end)
|
|
310
|
-
break;
|
|
311
|
-
const parent = segment.run.PARENT;
|
|
312
|
-
const found = hyperlinkHolders(this.paragraph).find((h) => h.value === parent);
|
|
313
|
-
if (found)
|
|
314
|
-
return found;
|
|
315
|
-
}
|
|
316
|
-
return undefined;
|
|
318
|
+
return hyperlinkHolders(this.paragraph).find((holder) => this.touches(holder));
|
|
317
319
|
}
|
|
318
|
-
/**
|
|
320
|
+
/**
|
|
321
|
+
* Unwraps every `w:hyperlink` the range touches, keeping its runs where they are and taking the
|
|
322
|
+
* `Hyperlink` style off them. Until 0.2.1 this unwrapped every hyperlink of the paragraph, so
|
|
323
|
+
* linking one word unlinked the others (CR-002 section 25).
|
|
324
|
+
*/
|
|
319
325
|
removeHyperlinks() {
|
|
320
326
|
var _a, _b, _c;
|
|
321
|
-
for (const holder of hyperlinkHolders(this.paragraph)) {
|
|
327
|
+
for (const holder of hyperlinkHolders(this.paragraph).filter((h) => this.touches(h))) {
|
|
322
328
|
const owner = containerOf(holder, this.paragraph);
|
|
323
329
|
const at = owner.indexOf(holder);
|
|
324
330
|
if (at === -1)
|
|
325
331
|
continue;
|
|
332
|
+
this.restyle(holder.value, false);
|
|
326
333
|
owner.splice(at, 1, ...((_a = holder.value.content) !== null && _a !== void 0 ? _a : []));
|
|
327
334
|
for (const item of ((_b = holder.value.content) !== null && _b !== void 0 ? _b : [])) {
|
|
328
335
|
linkParents(item, (_c = holder.PARENT) !== null && _c !== void 0 ? _c : this.paragraph.p);
|
|
329
336
|
}
|
|
330
337
|
}
|
|
331
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Whether this range touches a hyperlink's text: overlaps it, or for an empty range lies in it
|
|
341
|
+
* (from its first character up to, not including, its end). A hyperlink holding no text touches
|
|
342
|
+
* nothing.
|
|
343
|
+
*/
|
|
344
|
+
touches(holder) {
|
|
345
|
+
let start;
|
|
346
|
+
let end = 0;
|
|
347
|
+
for (const segment of this.paragraph.segments()) {
|
|
348
|
+
if (!within(segment.run, holder.value, this.paragraph.p))
|
|
349
|
+
continue;
|
|
350
|
+
start !== null && start !== void 0 ? start : (start = segment.start);
|
|
351
|
+
end = segment.end;
|
|
352
|
+
}
|
|
353
|
+
if (start === undefined)
|
|
354
|
+
return false;
|
|
355
|
+
return this.start === this.end ? start <= this.start && this.start < end : start < this.end && end > this.start;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* The `Hyperlink` character style on every run of a hyperlink, in place of any character style
|
|
359
|
+
* and with the direct colour removed, or off again when the link goes: what Word does (CR-002
|
|
360
|
+
* section 25). A run whose properties are then empty loses its `w:rPr`, as in Word's output.
|
|
361
|
+
*/
|
|
362
|
+
restyle(holder, linked) {
|
|
363
|
+
var _a, _b, _c;
|
|
364
|
+
const tracking = this.paragraph.fontTracking();
|
|
365
|
+
const runs = new Set([...runsOf(holder), ...runsOf(holder, { view: 'original' })].map((r) => r.value));
|
|
366
|
+
for (const run of runs) {
|
|
367
|
+
if (!linked && ((_b = (_a = run.rPr) === null || _a === void 0 ? void 0 : _a.rStyle) === null || _b === void 0 ? void 0 : _b.val) !== HYPERLINK_STYLE)
|
|
368
|
+
continue;
|
|
369
|
+
(_c = run.rPr) !== null && _c !== void 0 ? _c : (run.rPr = { TYPE_NAME: 'org_docx4j_wml.RPr' });
|
|
370
|
+
// the properties as they are now become w:rPrChange, unless this run is our own insertion
|
|
371
|
+
if (tracking && !tracking.ownInsertions.has(run))
|
|
372
|
+
tracking.tracker.recordRPrChange(run.rPr);
|
|
373
|
+
applyRunOptions(run.rPr, linked ? { style: HYPERLINK_STYLE, color: '' } : { style: '' });
|
|
374
|
+
if (Object.keys(run.rPr).every((key) => key === 'TYPE_NAME'))
|
|
375
|
+
delete run.rPr;
|
|
376
|
+
else
|
|
377
|
+
linkParents(run.rPr, run);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
332
380
|
/** The target of a relationship of this range's part, for the getter. */
|
|
333
381
|
relationshipTarget(id) {
|
|
334
382
|
var _a, _b, _c;
|
|
@@ -343,6 +391,16 @@ export class Range {
|
|
|
343
391
|
return part.getRelationshipsPart(true).addExternalRelationship(Namespaces.HYPERLINK, address).id;
|
|
344
392
|
}
|
|
345
393
|
}
|
|
394
|
+
/** The character style Word gives the runs of a hyperlink it makes. */
|
|
395
|
+
const HYPERLINK_STYLE = 'Hyperlink';
|
|
396
|
+
/** Whether `object` is `ancestor` or below it, following `PARENT` no further than `stop`. */
|
|
397
|
+
function within(object, ancestor, stop) {
|
|
398
|
+
for (let o = object; o !== undefined && o !== stop; o = o.PARENT) {
|
|
399
|
+
if (o === ancestor)
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
346
404
|
/** Every `w:hyperlink` of a paragraph, outermost first (they do not nest in practice). */
|
|
347
405
|
function hyperlinkHolders(paragraph) {
|
|
348
406
|
const out = [];
|
|
@@ -32,7 +32,7 @@ export interface BoundSnapshot {
|
|
|
32
32
|
* on Enter - so no cache is needed, which is what CR-006 section 2 was uncertain about.
|
|
33
33
|
*/
|
|
34
34
|
export declare function bindSnapshot<T>(element: Jsonix.TypedNamedValue<T>): Promise<BoundSnapshot>;
|
|
35
|
-
/** Turns selected nodes into `Bound`s against a snapshot. */
|
|
35
|
+
/** Turns selected nodes into `Bound`s against a snapshot, with one path cache for them all. */
|
|
36
36
|
export declare function boundsOf(nodes: readonly Node[], snapshot: BoundSnapshot): Bound[];
|
|
37
37
|
/**
|
|
38
38
|
* The node's path from the document element, in the prefixed form the part itself uses, with a
|
|
@@ -30,14 +30,15 @@ export async function bindSnapshot(element) {
|
|
|
30
30
|
});
|
|
31
31
|
return { root, objects };
|
|
32
32
|
}
|
|
33
|
-
/** Turns selected nodes into `Bound`s against a snapshot. */
|
|
33
|
+
/** Turns selected nodes into `Bound`s against a snapshot, with one path cache for them all. */
|
|
34
34
|
export function boundsOf(nodes, snapshot) {
|
|
35
|
+
const paths = new Paths();
|
|
35
36
|
return nodes.map((node) => {
|
|
36
37
|
const owner = owningElement(node);
|
|
37
38
|
const bound = {
|
|
38
39
|
node,
|
|
39
40
|
object: owner === undefined ? undefined : snapshot.objects.get(owner),
|
|
40
|
-
path:
|
|
41
|
+
path: paths.of(node),
|
|
41
42
|
};
|
|
42
43
|
if (node.nodeType === 2)
|
|
43
44
|
bound.attribute = node.name;
|
|
@@ -66,29 +67,61 @@ function owningElement(node) {
|
|
|
66
67
|
* than a short one, and docx4j's harness records the same form.
|
|
67
68
|
*/
|
|
68
69
|
export function pathOf(node) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
return new Paths().of(node);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Paths for the nodes of one snapshot. Each parent's children are counted once, on first need,
|
|
74
|
+
* and each element's path is kept for its descendants, so a hit list costs one pass over the
|
|
75
|
+
* parents it touches. Counting a node's preceding siblings afresh for every node and every
|
|
76
|
+
* ancestor was quadratic in a parent's children: the 6,972 hits of `//w:p[w:pPr/w:pStyle]` over a
|
|
77
|
+
* 7,088-paragraph body took 21 s in Chromium, where the marshal took 0.5 s (CR-006 section 10).
|
|
78
|
+
*/
|
|
79
|
+
class Paths {
|
|
80
|
+
constructor() {
|
|
81
|
+
/** For a parent, the position of each element child among its siblings of the same name. */
|
|
82
|
+
this.positions = new Map();
|
|
83
|
+
this.elementPaths = new Map();
|
|
72
84
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
of(node) {
|
|
86
|
+
if (node.nodeType === 2) {
|
|
87
|
+
const owner = node.ownerElement;
|
|
88
|
+
return `${owner === null ? '' : this.of(owner)}/@${node.name}`;
|
|
89
|
+
}
|
|
90
|
+
if (node.nodeType === 3 || node.nodeType === 4) {
|
|
91
|
+
const parent = node.parentNode;
|
|
92
|
+
return `${parent === null ? '' : this.of(parent)}/text()`;
|
|
93
|
+
}
|
|
94
|
+
if (node.nodeType !== 1)
|
|
95
|
+
return '';
|
|
96
|
+
const known = this.elementPaths.get(node);
|
|
97
|
+
if (known !== undefined)
|
|
98
|
+
return known;
|
|
99
|
+
const element = node;
|
|
100
|
+
const parent = element.parentNode;
|
|
101
|
+
const path = parent === null || parent.nodeType !== 1
|
|
102
|
+
? `/${element.nodeName}`
|
|
103
|
+
: `${this.of(parent)}/${element.nodeName}[${this.position(parent, element)}]`;
|
|
104
|
+
this.elementPaths.set(node, path);
|
|
105
|
+
return path;
|
|
76
106
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
position(parent, element) {
|
|
108
|
+
var _a, _b;
|
|
109
|
+
let positions = this.positions.get(parent);
|
|
110
|
+
if (positions === undefined) {
|
|
111
|
+
positions = new Map();
|
|
112
|
+
const counts = new Map();
|
|
113
|
+
for (let sibling = parent.firstChild; sibling; sibling = sibling.nextSibling) {
|
|
114
|
+
if (sibling.nodeType !== 1)
|
|
115
|
+
continue;
|
|
116
|
+
const name = sibling.nodeName;
|
|
117
|
+
const n = ((_a = counts.get(name)) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
118
|
+
counts.set(name, n);
|
|
119
|
+
positions.set(sibling, n);
|
|
120
|
+
}
|
|
121
|
+
this.positions.set(parent, positions);
|
|
89
122
|
}
|
|
123
|
+
return (_b = positions.get(element)) !== null && _b !== void 0 ? _b : 0;
|
|
90
124
|
}
|
|
91
|
-
return `${pathOf(parent)}/${element.nodeName}[${index}]`;
|
|
92
125
|
}
|
|
93
126
|
function isElement(node) {
|
|
94
127
|
return typeof node === 'object' && node !== null && node.nodeType === 1;
|
|
@@ -75,6 +75,12 @@ export declare class WordprocessingMLPackage extends OpcPackage implements Track
|
|
|
75
75
|
*/
|
|
76
76
|
private trackingScopes;
|
|
77
77
|
private beforeScopes;
|
|
78
|
+
/**
|
|
79
|
+
* Style ids a synchronous setter has given runs, to be defined in the styles part by the time the
|
|
80
|
+
* package is saved: `Range.hyperlink` writes `w:rStyle w:val="Hyperlink"` and cannot await
|
|
81
|
+
* `styles.ensure` (CR-002 section 25). `saveTo` ensures them; nothing is read before then.
|
|
82
|
+
*/
|
|
83
|
+
private requiredStyles;
|
|
78
84
|
/**
|
|
79
85
|
* The parts `seedAnnotationIds()` has read, with what it read: `true` for a part still holding its
|
|
80
86
|
* source, whose bytes cannot change, and otherwise the bytes that were set, which stay the same
|
|
@@ -228,7 +234,13 @@ export declare class WordprocessingMLPackage extends OpcPackage implements Track
|
|
|
228
234
|
seedAnnotationIds(): Promise<number>;
|
|
229
235
|
/** TrackingHost: the trees a new revision id must be above, which is every part already unmarshalled. */
|
|
230
236
|
markupRoots(): object[];
|
|
231
|
-
/**
|
|
237
|
+
/**
|
|
238
|
+
* Asks for style definitions a synchronous setter has referenced; they are ensured on save, as
|
|
239
|
+
* `pkg.styles.ensure(ids)` would, and a document that already has them keeps its styles part
|
|
240
|
+
* byte for byte (CR-002 section 25). Await `styles.ensure` instead to have them at once.
|
|
241
|
+
*/
|
|
242
|
+
requireStyles(ids: Iterable<string>): void;
|
|
243
|
+
/** A clone carries the identity, the tracking date, the theme setting, the XPath engine and the styles still to ensure. */
|
|
232
244
|
protected copyPackageSettingsTo(target: OpcPackage): void;
|
|
233
245
|
saveTo<R>(sink: PartSink<R>): Promise<R>;
|
|
234
246
|
/** The paragraph at an address anywhere in the document: 'body/3', 'header:rId5/0', a paraId, or a text match. */
|
|
@@ -75,6 +75,12 @@ export class WordprocessingMLPackage extends OpcPackage {
|
|
|
75
75
|
* applies again, and when none is left the package is as it was (CR-002 section 31).
|
|
76
76
|
*/
|
|
77
77
|
this.trackingScopes = [];
|
|
78
|
+
/**
|
|
79
|
+
* Style ids a synchronous setter has given runs, to be defined in the styles part by the time the
|
|
80
|
+
* package is saved: `Range.hyperlink` writes `w:rStyle w:val="Hyperlink"` and cannot await
|
|
81
|
+
* `styles.ensure` (CR-002 section 25). `saveTo` ensures them; nothing is read before then.
|
|
82
|
+
*/
|
|
83
|
+
this.requiredStyles = new Set();
|
|
78
84
|
/**
|
|
79
85
|
* The parts `seedAnnotationIds()` has read, with what it read: `true` for a part still holding its
|
|
80
86
|
* source, whose bytes cannot change, and otherwise the bytes that were set, which stay the same
|
|
@@ -454,10 +460,20 @@ export class WordprocessingMLPackage extends OpcPackage {
|
|
|
454
460
|
}
|
|
455
461
|
return out;
|
|
456
462
|
}
|
|
457
|
-
/**
|
|
463
|
+
/**
|
|
464
|
+
* Asks for style definitions a synchronous setter has referenced; they are ensured on save, as
|
|
465
|
+
* `pkg.styles.ensure(ids)` would, and a document that already has them keeps its styles part
|
|
466
|
+
* byte for byte (CR-002 section 25). Await `styles.ensure` instead to have them at once.
|
|
467
|
+
*/
|
|
468
|
+
requireStyles(ids) {
|
|
469
|
+
for (const id of ids)
|
|
470
|
+
this.requiredStyles.add(id);
|
|
471
|
+
}
|
|
472
|
+
/** A clone carries the identity, the tracking date, the theme setting, the XPath engine and the styles still to ensure. */
|
|
458
473
|
copyPackageSettingsTo(target) {
|
|
459
474
|
if (!(target instanceof WordprocessingMLPackage))
|
|
460
475
|
return;
|
|
476
|
+
target.requireStyles(this.requiredStyles);
|
|
461
477
|
target.author = { ...this.author };
|
|
462
478
|
target.trackedChangeDate = this.trackedChangeDate;
|
|
463
479
|
target.fonts.defaultTheme = this.fonts.defaultTheme;
|
|
@@ -467,6 +483,10 @@ export class WordprocessingMLPackage extends OpcPackage {
|
|
|
467
483
|
var _a;
|
|
468
484
|
if (this.trackingPending)
|
|
469
485
|
await this.setChangeTrackingMode((_a = this.trackingMode) !== null && _a !== void 0 ? _a : 'Off');
|
|
486
|
+
if (this.requiredStyles.size > 0) {
|
|
487
|
+
await this.styles.ensure([...this.requiredStyles]);
|
|
488
|
+
this.requiredStyles.clear();
|
|
489
|
+
}
|
|
470
490
|
return super.saveTo(sink);
|
|
471
491
|
}
|
|
472
492
|
/** The paragraph at an address anywhere in the document: 'body/3', 'header:rId5/0', a paraId, or a text match. */
|
package/dist/xml/dom.d.mts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
/** The declaration docx4j writes on every XML part. */
|
|
2
2
|
export declare const XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Parses XML text to a DOM document; throws on malformed input, in a browser as in Node.
|
|
5
|
+
*
|
|
6
|
+
* xmldom throws. A browser's `DOMParser` does not: Chromium returns the document up to the error
|
|
7
|
+
* with an XHTML `parsererror` element first in the root, and Firefox a `parsererror` root in its
|
|
8
|
+
* own namespace, and handing that on unmarshalled a truncated part without complaint - a settings
|
|
9
|
+
* part with a stray `<` kept its first child and lost the rest, which a save would have written
|
|
10
|
+
* (CR-002 section 33). Such a document is refused here, with the parser's own message, which
|
|
11
|
+
* carries the line and column in both browsers.
|
|
12
|
+
*/
|
|
4
13
|
export declare function parseXml(text: string): Document;
|
|
5
14
|
/** Serialises a node (document or element) without an XML declaration. */
|
|
6
15
|
export declare function serializeXml(node: Node): string;
|
package/dist/xml/dom.mjs
CHANGED
|
@@ -1,12 +1,56 @@
|
|
|
1
1
|
// DOM, text and base64 helpers over the runtime's DOM implementation. In Node the runtime brings
|
|
2
2
|
// @xmldom/xmldom; in browsers it uses the native DOM. Jsonix.DOM is typed since @docx4j/jsonix 3.2.1.
|
|
3
3
|
import { Jsonix } from '@docx4j/generated-objects-ts';
|
|
4
|
+
import { Docx4JException } from '../opc/exceptions.mjs';
|
|
4
5
|
const DOM = Jsonix.DOM;
|
|
5
6
|
/** The declaration docx4j writes on every XML part. */
|
|
6
7
|
export const XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
7
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Parses XML text to a DOM document; throws on malformed input, in a browser as in Node.
|
|
10
|
+
*
|
|
11
|
+
* xmldom throws. A browser's `DOMParser` does not: Chromium returns the document up to the error
|
|
12
|
+
* with an XHTML `parsererror` element first in the root, and Firefox a `parsererror` root in its
|
|
13
|
+
* own namespace, and handing that on unmarshalled a truncated part without complaint - a settings
|
|
14
|
+
* part with a stray `<` kept its first child and lost the rest, which a save would have written
|
|
15
|
+
* (CR-002 section 33). Such a document is refused here, with the parser's own message, which
|
|
16
|
+
* carries the line and column in both browsers.
|
|
17
|
+
*/
|
|
8
18
|
export function parseXml(text) {
|
|
9
|
-
|
|
19
|
+
const doc = DOM.parse(text);
|
|
20
|
+
const error = parserErrorOf(doc);
|
|
21
|
+
if (error !== undefined)
|
|
22
|
+
throw new Docx4JException(`Malformed XML: ${error}`);
|
|
23
|
+
return doc;
|
|
24
|
+
}
|
|
25
|
+
/** The namespaces a browser puts its `parsererror` element in: Chromium and WebKit's, Firefox's. */
|
|
26
|
+
const PARSER_ERROR_NAMESPACES = new Set([
|
|
27
|
+
'http://www.w3.org/1999/xhtml',
|
|
28
|
+
'http://www.mozilla.org/newlayout/xml/parsererror.xml',
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* The message of a browser's `parsererror` document - the root, or the root's first element child,
|
|
32
|
+
* named `parsererror` in one of the parser-error namespaces - or undefined for a document that
|
|
33
|
+
* parsed. Chromium's message is the `div` in its element; its `h3`s are boilerplate.
|
|
34
|
+
*/
|
|
35
|
+
function parserErrorOf(doc) {
|
|
36
|
+
var _a, _b;
|
|
37
|
+
const root = doc.documentElement;
|
|
38
|
+
if (!root)
|
|
39
|
+
return undefined;
|
|
40
|
+
let first = null;
|
|
41
|
+
for (let child = root.firstChild; child; child = child.nextSibling) {
|
|
42
|
+
if (child.nodeType === 1) {
|
|
43
|
+
first = child;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const element of [root, first]) {
|
|
48
|
+
if (element === null || element.localName !== 'parsererror' || !PARSER_ERROR_NAMESPACES.has((_a = element.namespaceURI) !== null && _a !== void 0 ? _a : ''))
|
|
49
|
+
continue;
|
|
50
|
+
const div = element.getElementsByTagNameNS('http://www.w3.org/1999/xhtml', 'div')[0];
|
|
51
|
+
return ((_b = (div !== null && div !== void 0 ? div : element).textContent) !== null && _b !== void 0 ? _b : '').replace(/\s+/g, ' ').trim() || 'the parser reported an error';
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
10
54
|
}
|
|
11
55
|
/** Serialises a node (document or element) without an XML declaration. */
|
|
12
56
|
export function serializeXml(node) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docx4j/core-ts",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "docx4j-core for TypeScript: Open Packaging (docx/pptx/xlsx zip and flat OPC), typed parts and relationships, style, numbering and font resolution over @docx4j/generated-objects-ts. Node, browsers, Word add-ins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|