@shbernal/ts-xlsx 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -24
- package/dist/bytes.d.ts +10 -2
- package/dist/bytes.js +22 -0
- package/dist/core/address.d.ts +96 -2
- package/dist/core/address.js +87 -33
- package/dist/core/array-formula-ranges.d.ts +33 -0
- package/dist/core/array-formula-ranges.js +62 -0
- package/dist/core/autofilter.d.ts +4 -3
- package/dist/core/autofilter.js +31 -30
- package/dist/core/axis-handle.d.ts +2 -0
- package/dist/core/axis-handle.js +5 -2
- package/dist/core/cell.d.ts +4 -3
- package/dist/core/cell.js +4 -6
- package/dist/core/clone.d.ts +29 -0
- package/dist/core/clone.js +24 -0
- package/dist/core/color-resolution.js +4 -6
- package/dist/core/column.d.ts +1 -0
- package/dist/core/column.js +6 -2
- package/dist/core/comment-thread.d.ts +7 -3
- package/dist/core/comment-thread.js +2 -1
- package/dist/core/conditional-formatting-overlay.d.ts +11 -1
- package/dist/core/conditional-formatting-overlay.js +30 -3
- package/dist/core/conditional-formatting.d.ts +107 -11
- package/dist/core/conditional-formatting.js +76 -19
- package/dist/core/containers.d.ts +12 -0
- package/dist/core/containers.js +5 -0
- package/dist/core/data-validation-overlay.d.ts +7 -1
- package/dist/core/data-validation-overlay.js +19 -4
- package/dist/core/data-validation.d.ts +22 -0
- package/dist/core/data-validation.js +30 -4
- package/dist/core/date-format.d.ts +20 -0
- package/dist/core/date-format.js +170 -0
- package/dist/core/date.d.ts +43 -8
- package/dist/core/date.js +19 -3
- package/dist/core/formula-references.d.ts +26 -0
- package/dist/core/formula-references.js +173 -0
- package/dist/core/formula-scan.d.ts +2 -0
- package/dist/core/formula-scan.js +58 -0
- package/dist/core/formula.d.ts +76 -17
- package/dist/core/formula.js +87 -80
- package/dist/core/function-values.d.ts +4 -0
- package/dist/core/function-values.js +58 -0
- package/dist/core/future-functions.d.ts +3 -0
- package/dist/core/{modern-functions.js → future-functions.js} +119 -88
- package/dist/core/grid-edits.d.ts +30 -3
- package/dist/core/grid-edits.js +248 -81
- package/dist/core/grid-shift.d.ts +58 -9
- package/dist/core/grid-shift.js +26 -4
- package/dist/core/hyperlink.d.ts +39 -0
- package/dist/core/hyperlink.js +88 -0
- package/dist/core/image.d.ts +49 -14
- package/dist/core/image.js +16 -5
- package/dist/core/internal.d.ts +10 -2
- package/dist/core/limits.d.ts +25 -0
- package/dist/core/limits.js +4 -0
- package/dist/core/merge.d.ts +18 -4
- package/dist/core/merge.js +33 -20
- package/dist/core/page-setup.d.ts +26 -11
- package/dist/core/pivot-table.d.ts +49 -10
- package/dist/core/pivot-table.js +112 -60
- package/dist/core/protection.d.ts +66 -4
- package/dist/core/protection.js +6 -11
- package/dist/core/range.js +34 -30
- package/dist/core/row-input.js +2 -2
- package/dist/core/row.d.ts +1 -0
- package/dist/core/row.js +6 -2
- package/dist/core/structured-reference.d.ts +7 -0
- package/dist/core/structured-reference.js +7 -0
- package/dist/core/style.d.ts +85 -70
- package/dist/core/style.js +92 -28
- package/dist/core/table-style.d.ts +1 -1
- package/dist/core/table-style.js +4 -4
- package/dist/core/table.d.ts +64 -16
- package/dist/core/table.js +87 -40
- package/dist/core/theme.d.ts +2 -0
- package/dist/core/theme.js +9 -5
- package/dist/core/value.d.ts +60 -28
- package/dist/core/value.js +48 -43
- package/dist/core/workbook-media.d.ts +34 -0
- package/dist/core/workbook-media.js +44 -0
- package/dist/core/workbook-styles.d.ts +83 -1
- package/dist/core/workbook-styles.js +4 -0
- package/dist/core/workbook-theme.js +5 -3
- package/dist/core/workbook-vba.js +3 -2
- package/dist/core/workbook.d.ts +41 -3
- package/dist/core/workbook.js +46 -41
- package/dist/core/worksheet-comments.d.ts +2 -1
- package/dist/core/worksheet-comments.js +20 -13
- package/dist/core/worksheet-merges.d.ts +49 -0
- package/dist/core/worksheet-merges.js +65 -0
- package/dist/core/worksheet-model.d.ts +10 -2
- package/dist/core/worksheet-model.js +28 -9
- package/dist/core/worksheet-pictures.d.ts +3 -3
- package/dist/core/worksheet-pictures.js +7 -6
- package/dist/core/worksheet.d.ts +165 -27
- package/dist/core/worksheet.js +206 -89
- package/dist/customui/ribbon.js +2 -1
- package/dist/entries/core.d.ts +21 -14
- package/dist/entries/core.js +14 -7
- package/dist/entries/errors.d.ts +1 -0
- package/dist/entries/errors.js +1 -0
- package/dist/entries/xlsx.d.ts +1 -1
- package/dist/errors.d.ts +64 -0
- package/dist/errors.js +22 -0
- package/dist/hex.d.ts +8 -0
- package/dist/hex.js +3 -0
- package/dist/io/cell-metadata/metadata.d.ts +42 -0
- package/dist/io/cell-metadata/metadata.js +35 -0
- package/dist/io/cell-metadata/rich-values.d.ts +11 -0
- package/dist/io/cell-metadata/rich-values.js +80 -0
- package/dist/io/csv/delimiter.d.ts +21 -0
- package/dist/io/csv/delimiter.js +18 -0
- package/dist/io/csv/errors.d.ts +14 -0
- package/dist/io/csv/errors.js +5 -0
- package/dist/io/csv/read.d.ts +6 -1
- package/dist/io/csv/read.js +51 -22
- package/dist/io/csv/write.d.ts +19 -5
- package/dist/io/csv/write.js +24 -44
- package/dist/io/opc/inflate.js +14 -0
- package/dist/io/opc/namespaces.d.ts +16 -0
- package/dist/io/opc/namespaces.js +4 -0
- package/dist/io/opc/part-paths.d.ts +9 -0
- package/dist/io/opc/part-paths.js +5 -5
- package/dist/io/opc/read-opc.d.ts +58 -14
- package/dist/io/opc/read-opc.js +49 -21
- package/dist/io/opc/read-options.d.ts +5 -1
- package/dist/io/opc/sniff-format.d.ts +3 -3
- package/dist/io/opc/sniff-format.js +2 -2
- package/dist/io/opc/strict-relationships.d.ts +2 -0
- package/dist/io/opc/strict-relationships.js +12 -0
- package/dist/io/read-policy/column-budget.d.ts +26 -0
- package/dist/io/read-policy/column-budget.js +18 -0
- package/dist/io/read-policy/read-repair.d.ts +74 -0
- package/dist/io/read-policy/read-repair.js +119 -0
- package/dist/io/style/cell-style-resolution.d.ts +25 -0
- package/dist/io/style/cell-style-resolution.js +30 -0
- package/dist/io/style/xf-style.d.ts +23 -1
- package/dist/io/style/xf-style.js +8 -0
- package/dist/io/xlsb/formula.d.ts +2 -2
- package/dist/io/xlsb/formula.js +52 -35
- package/dist/io/xlsb/primitives.js +8 -14
- package/dist/io/xlsb/ptg-functions.d.ts +19 -10
- package/dist/io/xlsb/ptg-functions.js +495 -596
- package/dist/io/xlsb/read-metadata.d.ts +7 -0
- package/dist/io/xlsb/read-metadata.js +69 -0
- package/dist/io/xlsb/read-styles.js +45 -81
- package/dist/io/xlsb/read-worksheet.d.ts +26 -1
- package/dist/io/xlsb/read-worksheet.js +148 -106
- package/dist/io/xlsb/read.d.ts +6 -3
- package/dist/io/xlsb/read.js +70 -31
- package/dist/io/xlsb/record-stream.d.ts +30 -0
- package/dist/io/xlsb/record-stream.js +20 -0
- package/dist/io/xlsb/record-types.d.ts +14 -5
- package/dist/io/xlsb/record-types.js +14 -5
- package/dist/io/xlsb/sheet-protection.d.ts +20 -0
- package/dist/io/xlsb/sheet-protection.js +60 -0
- package/dist/io/xlsx/cell-accumulator.d.ts +24 -9
- package/dist/io/xlsx/cell-accumulator.js +74 -21
- package/dist/io/xlsx/cell-metadata.d.ts +39 -0
- package/dist/io/xlsx/cell-metadata.js +162 -0
- package/dist/io/xlsx/cell-value.d.ts +19 -5
- package/dist/io/xlsx/cell-value.js +45 -30
- package/dist/io/xlsx/color-xml.js +3 -2
- package/dist/io/xlsx/column-span.d.ts +11 -0
- package/dist/io/xlsx/column-span.js +9 -0
- package/dist/io/xlsx/comments.d.ts +9 -30
- package/dist/io/xlsx/comments.js +25 -83
- package/dist/io/xlsx/conditional-formatting.d.ts +43 -23
- package/dist/io/xlsx/conditional-formatting.js +437 -138
- package/dist/io/xlsx/data-validation.d.ts +12 -7
- package/dist/io/xlsx/data-validation.js +67 -53
- package/dist/io/xlsx/edit-vba.d.ts +7 -2
- package/dist/io/xlsx/edit-vba.js +52 -52
- package/dist/io/xlsx/font-xml.d.ts +7 -0
- package/dist/io/xlsx/font-xml.js +104 -0
- package/dist/io/xlsx/hyperlinks.d.ts +19 -21
- package/dist/io/xlsx/hyperlinks.js +12 -32
- package/dist/io/xlsx/images.d.ts +49 -16
- package/dist/io/xlsx/images.js +149 -50
- package/dist/io/xlsx/namespaces.d.ts +24 -0
- package/dist/io/xlsx/namespaces.js +11 -0
- package/dist/io/xlsx/package-plan.d.ts +60 -15
- package/dist/io/xlsx/package-plan.js +83 -18
- package/dist/io/xlsx/part-names.d.ts +6 -8
- package/dist/io/xlsx/part-names.js +3 -7
- package/dist/io/xlsx/pivot.d.ts +5 -5
- package/dist/io/xlsx/pivot.js +26 -24
- package/dist/io/xlsx/preserved-splices.d.ts +6 -0
- package/dist/io/xlsx/preserved-splices.js +74 -0
- package/dist/io/xlsx/read-comments.d.ts +29 -0
- package/dist/io/xlsx/read-comments.js +75 -0
- package/dist/io/xlsx/read-parts.d.ts +59 -0
- package/dist/io/xlsx/read-parts.js +259 -0
- package/dist/io/xlsx/read-pivot.js +11 -4
- package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +8 -5
- package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +24 -4
- package/dist/io/xlsx/read-rows.d.ts +35 -11
- package/dist/io/xlsx/read-rows.js +73 -49
- package/dist/io/xlsx/read-shared-strings.js +3 -2
- package/dist/io/xlsx/read-styles.d.ts +0 -30
- package/dist/io/xlsx/read-styles.js +45 -105
- package/dist/io/xlsx/read-workbook-xml.d.ts +42 -0
- package/dist/io/xlsx/read-workbook-xml.js +175 -0
- package/dist/io/xlsx/read-worksheet.d.ts +9 -3
- package/dist/io/xlsx/read-worksheet.js +70 -177
- package/dist/io/xlsx/read.d.ts +3 -14
- package/dist/io/xlsx/read.js +74 -411
- package/dist/io/xlsx/relationships.d.ts +3 -0
- package/dist/io/xlsx/relationships.js +4 -0
- package/dist/io/xlsx/rich-text.js +1 -1
- package/dist/io/xlsx/row-position.d.ts +20 -0
- package/dist/io/xlsx/row-position.js +11 -0
- package/dist/io/xlsx/row-xml.d.ts +139 -0
- package/dist/io/xlsx/row-xml.js +222 -0
- package/dist/io/xlsx/shared-formulas.js +2 -2
- package/dist/io/xlsx/sheet-properties.d.ts +10 -1
- package/dist/io/xlsx/sheet-properties.js +182 -20
- package/dist/io/xlsx/strict-parts.d.ts +7 -0
- package/dist/io/xlsx/strict-parts.js +171 -0
- package/dist/io/xlsx/style-elements.d.ts +41 -0
- package/dist/io/xlsx/style-elements.js +136 -0
- package/dist/io/xlsx/styles.d.ts +17 -3
- package/dist/io/xlsx/styles.js +13 -209
- package/dist/io/xlsx/tables.d.ts +10 -2
- package/dist/io/xlsx/tables.js +55 -52
- package/dist/io/xlsx/theme-xml.d.ts +32 -4
- package/dist/io/xlsx/theme-xml.js +106 -71
- package/dist/io/xlsx/threaded-comments.d.ts +14 -11
- package/dist/io/xlsx/threaded-comments.js +16 -8
- package/dist/io/xlsx/workbook-xml.d.ts +45 -6
- package/dist/io/xlsx/workbook-xml.js +62 -54
- package/dist/io/xlsx/worksheet-xml.d.ts +26 -70
- package/dist/io/xlsx/worksheet-xml.js +69 -282
- package/dist/io/xlsx/write-stream.d.ts +90 -19
- package/dist/io/xlsx/write-stream.js +80 -25
- package/dist/io/xlsx/write.d.ts +12 -6
- package/dist/io/xlsx/write.js +226 -123
- package/dist/rel-type.d.ts +25 -0
- package/dist/rel-type.js +9 -0
- package/dist/token-set.d.ts +13 -0
- package/dist/token-set.js +4 -1
- package/dist/vba/bytes.d.ts +16 -5
- package/dist/vba/bytes.js +27 -6
- package/dist/vba/cfb-format.d.ts +55 -0
- package/dist/vba/cfb-format.js +35 -0
- package/dist/vba/cfb-writer.d.ts +8 -0
- package/dist/vba/cfb-writer.js +40 -40
- package/dist/vba/cfb.d.ts +21 -4
- package/dist/vba/cfb.js +124 -90
- package/dist/vba/codepage.js +2 -1
- package/dist/vba/dir-records.d.ts +6 -0
- package/dist/vba/dir-records.js +10 -2
- package/dist/vba/errors.d.ts +11 -4
- package/dist/vba/index.d.ts +0 -2
- package/dist/vba/index.js +0 -2
- package/dist/vba/ms-ovba.d.ts +6 -2
- package/dist/vba/ms-ovba.js +66 -22
- package/dist/vba/project-editor.js +129 -119
- package/dist/vba/project.d.ts +15 -1
- package/dist/vba/project.js +38 -13
- package/dist/vba/vba-encoding.d.ts +4 -10
- package/dist/vba/vba-encoding.js +3 -17
- package/dist/xml/xml-attrs.d.ts +37 -0
- package/dist/xml/xml-attrs.js +30 -0
- package/dist/xml/xml-chars.d.ts +40 -0
- package/dist/xml/xml-chars.js +12 -0
- package/dist/xml/xml-namespaces.d.ts +31 -0
- package/dist/xml/xml-namespaces.js +50 -0
- package/dist/xml/xml-read.d.ts +101 -11
- package/dist/xml/xml-read.js +150 -18
- package/dist/xml/xml-scan.d.ts +25 -34
- package/dist/xml/xml-scan.js +69 -46
- package/dist/xml/xml.d.ts +59 -26
- package/dist/xml/xml.js +45 -23
- package/package.json +15 -9
- package/skills/ts-xlsx-upstream/SKILL.md +21 -13
- package/dist/core/modern-functions.d.ts +0 -1
package/dist/vba/project.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { quoted } from '../errors.js';
|
|
2
|
+
import { relTypeSegment } from '../rel-type.js';
|
|
1
3
|
import { readU16, readU32 } from './bytes.js';
|
|
2
4
|
import { CompoundFile } from './cfb.js';
|
|
3
5
|
import { decoderForCodePage } from './codepage.js';
|
|
@@ -13,14 +15,24 @@ const SIGNATURE_KIND_BY_REL_SEGMENT = new Map([
|
|
|
13
15
|
['vbaProjectSignatureV3', 'v3'],
|
|
14
16
|
]);
|
|
15
17
|
export function vbaProjectSignatureKind(relType) {
|
|
16
|
-
return SIGNATURE_KIND_BY_REL_SEGMENT.get(
|
|
18
|
+
return SIGNATURE_KIND_BY_REL_SEGMENT.get(relTypeSegment(relType));
|
|
17
19
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
function isComplete(module) {
|
|
21
|
+
return module.streamName !== undefined && module.offset !== undefined;
|
|
22
|
+
}
|
|
23
|
+
export const VBA_STORAGE = 'VBA';
|
|
24
|
+
export const DIR_PATH = [VBA_STORAGE, 'dir'];
|
|
25
|
+
export const PROJECT_PATH = ['PROJECT'];
|
|
26
|
+
export const PROJECTWM_PATH = ['PROJECTwm'];
|
|
27
|
+
export function parseVbaProject(bin, maxOutput = DEFAULT_MAX_PROJECT_OUTPUT) {
|
|
28
|
+
return parseVbaProjectIn(new CompoundFile(bin), maxOutput);
|
|
29
|
+
}
|
|
30
|
+
export function parseVbaProjectIn(cfb, maxOutput = DEFAULT_MAX_PROJECT_OUTPUT) {
|
|
31
|
+
const budget = new DecompressionBudget(maxOutput);
|
|
32
|
+
const dirCompressed = cfb.readStream(DIR_PATH);
|
|
21
33
|
if (!dirCompressed)
|
|
22
34
|
throw new VbaParseError("VBA project has no 'dir' stream");
|
|
23
|
-
const dir =
|
|
35
|
+
const dir = budget.spend(dirCompressed, 0);
|
|
24
36
|
let codePage = 1252;
|
|
25
37
|
const rawModules = [];
|
|
26
38
|
let pending = {};
|
|
@@ -47,7 +59,7 @@ export function parseVbaProject(bin) {
|
|
|
47
59
|
pending.offset = readU32(dir, dataStart);
|
|
48
60
|
break;
|
|
49
61
|
case REC_MODULE_TERMINATOR:
|
|
50
|
-
if (pending
|
|
62
|
+
if (isComplete(pending))
|
|
51
63
|
rawModules.push(pending);
|
|
52
64
|
pending = {};
|
|
53
65
|
break;
|
|
@@ -65,16 +77,29 @@ export function parseVbaProject(bin) {
|
|
|
65
77
|
name,
|
|
66
78
|
streamName,
|
|
67
79
|
kind,
|
|
68
|
-
source: readModuleSource(cfb, streamName, m.offset, decoder),
|
|
80
|
+
source: readModuleSource(cfb, streamName, m.offset, decoder, budget),
|
|
69
81
|
};
|
|
70
82
|
});
|
|
71
83
|
return { codePage, modules };
|
|
72
84
|
}
|
|
73
|
-
function readModuleSource(cfb, streamName, textOffset, decoder) {
|
|
74
|
-
const stream = cfb.readStream(streamName);
|
|
75
|
-
if (!stream)
|
|
76
|
-
throw new VbaParseError(`module stream
|
|
77
|
-
|
|
85
|
+
function readModuleSource(cfb, streamName, textOffset, decoder, budget) {
|
|
86
|
+
const stream = cfb.readStream([VBA_STORAGE, streamName]);
|
|
87
|
+
if (!stream) {
|
|
88
|
+
throw new VbaParseError(`module stream ${quoted(streamName)} not found in the ${quoted(VBA_STORAGE)} storage`);
|
|
89
|
+
}
|
|
90
|
+
return decoder.decode(budget.spend(stream, textOffset));
|
|
91
|
+
}
|
|
92
|
+
const DEFAULT_MAX_PROJECT_OUTPUT = 64 * 1024 * 1024;
|
|
93
|
+
class DecompressionBudget {
|
|
94
|
+
#remaining;
|
|
95
|
+
constructor(total) {
|
|
96
|
+
this.#remaining = total;
|
|
97
|
+
}
|
|
98
|
+
spend(stream, start) {
|
|
99
|
+
const out = decompressContainer(stream, start, this.#remaining);
|
|
100
|
+
this.#remaining -= out.length;
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
78
103
|
}
|
|
79
104
|
const MODULE_KIND_BY_PROJECT_KEYWORD = new Map([
|
|
80
105
|
['Document', 'document'],
|
|
@@ -84,7 +109,7 @@ const MODULE_KIND_BY_PROJECT_KEYWORD = new Map([
|
|
|
84
109
|
]);
|
|
85
110
|
function readProjectStreamKinds(cfb, decoder) {
|
|
86
111
|
const kinds = new Map();
|
|
87
|
-
const stream = cfb.readStream(
|
|
112
|
+
const stream = cfb.readStream(PROJECT_PATH);
|
|
88
113
|
if (!stream)
|
|
89
114
|
return kinds;
|
|
90
115
|
const text = decoder.decode(stream);
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Validate a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Validate a library reference's name against the VBA identifier contract: a valid identifier, at
|
|
3
|
+
* most 31 characters (the CFB stream-name limit, which VBA also applies to its names). The reference
|
|
4
|
+
* is the one new name {@link project-editor.ts | project-editor}'s structural edits write.
|
|
5
5
|
*
|
|
6
6
|
* @throws {VbaAuthorError} if `name` is not a valid VBA identifier or exceeds 31 characters.
|
|
7
7
|
*/
|
|
8
|
-
export declare function
|
|
9
|
-
/** Append one `dir`-stream TLV record (Id, Size, data) to `out`. */
|
|
10
|
-
export declare function push(out: number[], id: number, data: number[]): void;
|
|
11
|
-
export declare function u16(n: number): number[];
|
|
12
|
-
export declare function u32(n: number): number[];
|
|
13
|
-
/** A name as NUL-free UTF-16LE code units: the encoding [MS-OVBA] uses for every "Unicode" name field. */
|
|
14
|
-
export declare function utf16le(s: string): number[];
|
|
8
|
+
export declare function validateReferenceName(name: string): void;
|
package/dist/vba/vba-encoding.js
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
|
+
import { quoted } from '../errors.js';
|
|
1
2
|
import { MAX_NAME_CHARS } from './cfb-format.js';
|
|
2
3
|
import { VbaAuthorError } from './errors.js';
|
|
3
4
|
const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
4
|
-
export function
|
|
5
|
+
export function validateReferenceName(name) {
|
|
5
6
|
if (!IDENTIFIER.test(name) || name.length > MAX_NAME_CHARS) {
|
|
6
|
-
throw new VbaAuthorError(`invalid
|
|
7
|
+
throw new VbaAuthorError(`invalid reference name ${quoted(name)} (must be a VBA identifier ≤ 31 chars)`);
|
|
7
8
|
}
|
|
8
9
|
}
|
|
9
|
-
export function push(out, id, data) {
|
|
10
|
-
out.push(...u16(id), ...u32(data.length), ...data);
|
|
11
|
-
}
|
|
12
|
-
export function u16(n) {
|
|
13
|
-
return [n & 0xff, (n >> 8) & 0xff];
|
|
14
|
-
}
|
|
15
|
-
export function u32(n) {
|
|
16
|
-
return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff];
|
|
17
|
-
}
|
|
18
|
-
export function utf16le(s) {
|
|
19
|
-
const out = [];
|
|
20
|
-
for (let i = 0; i < s.length; i++)
|
|
21
|
-
out.push(...u16(s.charCodeAt(i)));
|
|
22
|
-
return out;
|
|
23
|
-
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SpreadsheetML `_xHHHH_` escape, in the only place it may appear: a complete cell-text value.
|
|
3
|
+
*
|
|
4
|
+
* The mirror of `escapeSpreadsheetText` in `./xml.ts`, and it sits here rather than beside it for
|
|
5
|
+
* the same reason `decodeEntities` sits apart from `escapeText`: the write helpers carry an
|
|
6
|
+
* `AuthoringError` and a whole serialisation vocabulary the reader has no business importing.
|
|
7
|
+
*
|
|
8
|
+
* **One left-to-right pass, and that is load-bearing.** `005F` maps to `_` like any other code
|
|
9
|
+
* point, with no special case, because a single pass already gives the underscore escape its
|
|
10
|
+
* meaning: in `_x005F_x0041_` the match at 0 yields `_` and scanning resumes at `x0041_`, which has
|
|
11
|
+
* no leading underscore left to start an escape. So the value reads back as the literal seven
|
|
12
|
+
* characters `_x0041_` the author wrote. Decoding `_x005F_` in a pass of its own, before or after
|
|
13
|
+
* the rest, collapses that to `A` and loses the distinction the encoder went to trouble to keep.
|
|
14
|
+
* Excel agrees: it reads that cell as `_x0041_`.
|
|
15
|
+
*
|
|
16
|
+
* The decode is unconditional, not a repair of characters XML cannot carry. Excel reads
|
|
17
|
+
* `a_x0009_b` as a tab even though a literal tab would have been perfectly legal there, so a
|
|
18
|
+
* decoder that only handled the illegal range would disagree with Excel on files Excel wrote.
|
|
19
|
+
*/
|
|
20
|
+
export declare function decodeSpreadsheetText(value: string): string;
|
|
21
|
+
/** An OOXML integer attribute at or above `min` (default: unbounded below); `undefined` when the
|
|
22
|
+
* attribute is absent, blank, fractional, not a number, or below the floor. Integers past
|
|
23
|
+
* `Number.MAX_SAFE_INTEGER` read as `undefined` too: no index or count is usable out there, and
|
|
24
|
+
* arithmetic on one silently lies. */
|
|
25
|
+
export declare function numInteger(val: string | undefined, min?: number): number | undefined;
|
|
26
|
+
/** An OOXML decimal attribute at or above `min` (default: unbounded below); `undefined` when the
|
|
27
|
+
* attribute is absent, blank, not a number, or below the floor. Infinities are not finite numbers
|
|
28
|
+
* and read as `undefined`. */
|
|
29
|
+
export declare function numFinite(val: string | undefined, min?: number): number | undefined;
|
|
30
|
+
/** Read an operand's text as a number only when it is a canonical decimal literal (optional sign,
|
|
31
|
+
* digits, optional fraction). A cell reference, defined name, expression, or exotically-spelled
|
|
32
|
+
* number (`1E5`, hex) keeps its verbatim text, so it is neither coerced to `NaN` and lost nor
|
|
33
|
+
* re-spelled into a number that would not re-write byte-clean. Callers layer their own type rules
|
|
34
|
+
* (a data-validation `list`/`custom` operand stays a string regardless of what it looks like). */
|
|
35
|
+
export declare function coerceNumericLiteral(text: string): string | number;
|
|
36
|
+
/** Narrow an enumerated attribute through its guard; `undefined` when absent or not a member. */
|
|
37
|
+
export declare function enumToken<T extends string>(val: string | undefined, isMember: (candidate: string) => candidate is T): T | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function decodeSpreadsheetText(value) {
|
|
2
|
+
if (!value.includes('_'))
|
|
3
|
+
return value;
|
|
4
|
+
return value.replace(/_x([0-9A-Fa-f]{4})_/g, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
|
|
5
|
+
}
|
|
6
|
+
export function numInteger(val, min = -Number.MAX_SAFE_INTEGER) {
|
|
7
|
+
const n = parseAttrNumber(val);
|
|
8
|
+
if (n === undefined || !Number.isSafeInteger(n) || n < min)
|
|
9
|
+
return undefined;
|
|
10
|
+
return n;
|
|
11
|
+
}
|
|
12
|
+
export function numFinite(val, min = -Infinity) {
|
|
13
|
+
const n = parseAttrNumber(val);
|
|
14
|
+
if (n === undefined || n < min)
|
|
15
|
+
return undefined;
|
|
16
|
+
return n;
|
|
17
|
+
}
|
|
18
|
+
function parseAttrNumber(val) {
|
|
19
|
+
if (val === undefined || val.trim() === '')
|
|
20
|
+
return undefined;
|
|
21
|
+
const n = Number(val);
|
|
22
|
+
return Number.isFinite(n) ? n : undefined;
|
|
23
|
+
}
|
|
24
|
+
export function coerceNumericLiteral(text) {
|
|
25
|
+
const trimmed = text.trim();
|
|
26
|
+
return /^-?\d+(?:\.\d+)?$/.test(trimmed) ? Number(trimmed) : text;
|
|
27
|
+
}
|
|
28
|
+
export function enumToken(val, isMember) {
|
|
29
|
+
return val !== undefined && isMember(val) ? val : undefined;
|
|
30
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The characters an XML 1.0 document cannot carry, whatever escape you reach for.
|
|
3
|
+
*
|
|
4
|
+
* Three classes: the C0 controls outside the tab/LF/CR the `Char` production allows, the two
|
|
5
|
+
* noncharacters at the top of the BMP, and unpaired surrogates. The last are not an XML
|
|
6
|
+
* problem but a UTF-8 one: the encoder substitutes U+FFFD for a lone surrogate, so the
|
|
7
|
+
* package validates and the value is quietly gone, which is the same loss by a different
|
|
8
|
+
* route. U+007F and the C1 controls are deliberately absent: XML 1.1 forbids them, OOXML is
|
|
9
|
+
* 1.0.
|
|
10
|
+
*
|
|
11
|
+
* The `u` flag is load-bearing. It makes the pattern match code points, so an astral
|
|
12
|
+
* character is one unit that no surrogate range can match, and `[\u{D800}-\u{DFFF}]` means
|
|
13
|
+
* exactly "a surrogate that is not part of a pair" with no lookaround.
|
|
14
|
+
*/
|
|
15
|
+
export declare const XML_UNREPRESENTABLE: RegExp;
|
|
16
|
+
/** The same rule with the `g` flag, for the escapes that rewrite every occurrence. */
|
|
17
|
+
export declare const XML_UNREPRESENTABLE_GLOBAL: RegExp;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a code point may appear in an XML 1.0 document at all.
|
|
20
|
+
*
|
|
21
|
+
* The reader's question, and the reason this module exists. A numeric character reference names a
|
|
22
|
+
* code point directly, so a file is free to name one the format has no representation for; decoding
|
|
23
|
+
* it anyway puts a value in the model that the writer is *guaranteed* to refuse, turning a hostile
|
|
24
|
+
* file into an `AuthoringError` blaming the caller on the next save.
|
|
25
|
+
*
|
|
26
|
+
* A code point outside Unicode entirely is not this function's business -- the caller has already
|
|
27
|
+
* bounded the number before it can name a character -- so an out-of-range argument is refused here
|
|
28
|
+
* as well rather than throwing out of `String.fromCodePoint`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isRepresentableCodePoint(codePoint: number): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Drop every code point XML 1.0 cannot carry.
|
|
33
|
+
*
|
|
34
|
+
* For text the scanner read out of a document, where there is no verbatim form to fall back to: a
|
|
35
|
+
* *reference* to an unrepresentable character can be left as the `` the file wrote, but a raw
|
|
36
|
+
* one has no spelling of its own to keep. Such a character makes the document ill-formed by the
|
|
37
|
+
* `Char` production, so it was never legally in the file; admitting it would put a value in the
|
|
38
|
+
* model that the writer must then refuse, which reports a corrupt input as the caller's mistake.
|
|
39
|
+
*/
|
|
40
|
+
export declare function stripUnrepresentable(text: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const XML_UNREPRESENTABLE = /[\u{0}-\u{8}\u{B}\u{C}\u{E}-\u{1F}\u{FFFE}\u{FFFF}\u{D800}-\u{DFFF}]/u;
|
|
2
|
+
export const XML_UNREPRESENTABLE_GLOBAL = new RegExp(XML_UNREPRESENTABLE.source, 'gu');
|
|
3
|
+
export function isRepresentableCodePoint(codePoint) {
|
|
4
|
+
if (!Number.isInteger(codePoint) || codePoint < 0 || codePoint > 0x10ffff)
|
|
5
|
+
return false;
|
|
6
|
+
return !XML_UNREPRESENTABLE.test(String.fromCodePoint(codePoint));
|
|
7
|
+
}
|
|
8
|
+
export function stripUnrepresentable(text) {
|
|
9
|
+
if (!XML_UNREPRESENTABLE.test(text))
|
|
10
|
+
return text;
|
|
11
|
+
return text.replace(XML_UNREPRESENTABLE_GLOBAL, '');
|
|
12
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type XmlAttributes } from './xml-scan.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The prefix-to-namespace bindings in force at the current point of a scan.
|
|
4
|
+
*
|
|
5
|
+
* Properly scoped: a declaration binds only within the element that carries it, so the bindings are
|
|
6
|
+
* pushed and popped with the element stack. In practice OOXML puts them all on the part's root, but
|
|
7
|
+
* a reader that assumed so would be making the same kind of assumption this class exists to remove.
|
|
8
|
+
*/
|
|
9
|
+
export declare class NamespaceScope {
|
|
10
|
+
#private;
|
|
11
|
+
/** Enter an element, taking any `xmlns` declarations it carries. */
|
|
12
|
+
open(attrs: XmlAttributes): void;
|
|
13
|
+
/** Leave an element, restoring whatever its declarations shadowed. */
|
|
14
|
+
close(): void;
|
|
15
|
+
/**
|
|
16
|
+
* The namespace URI a qualified *element* name resolves to, or `undefined` when its prefix is
|
|
17
|
+
* unbound. An unprefixed element takes the default namespace, which is what makes
|
|
18
|
+
* `<worksheet xmlns="…main">` and `<x:worksheet xmlns:x="…main">` the same document.
|
|
19
|
+
*/
|
|
20
|
+
elementNamespace(qualified: string): string | undefined;
|
|
21
|
+
/** Whether a qualified element name is in `uri`. */
|
|
22
|
+
isElementIn(qualified: string, uri: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* An attribute's value looked up by namespace and local name rather than by qualified name.
|
|
25
|
+
*
|
|
26
|
+
* An *unprefixed* attribute is in no namespace at all, never the default one, which is why this
|
|
27
|
+
* only ever matches a prefixed spelling. Every namespaced attribute OOXML uses (`r:id`, `r:embed`,
|
|
28
|
+
* `xml:space`) is written with a prefix for exactly that reason.
|
|
29
|
+
*/
|
|
30
|
+
attr(attrs: XmlAttributes, uri: string, local: string): string | undefined;
|
|
31
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {} from './xml-scan.js';
|
|
2
|
+
const XMLNS = 'xmlns';
|
|
3
|
+
const XMLNS_PREFIX = 'xmlns:';
|
|
4
|
+
export class NamespaceScope {
|
|
5
|
+
#bindings = new Map();
|
|
6
|
+
#undo = [];
|
|
7
|
+
open(attrs) {
|
|
8
|
+
let undo;
|
|
9
|
+
for (const name in attrs) {
|
|
10
|
+
const prefix = name.startsWith(XMLNS_PREFIX)
|
|
11
|
+
? name.slice(XMLNS_PREFIX.length)
|
|
12
|
+
: name === XMLNS
|
|
13
|
+
? ''
|
|
14
|
+
: null;
|
|
15
|
+
if (prefix === null)
|
|
16
|
+
continue;
|
|
17
|
+
(undo ??= []).push([prefix, this.#bindings.get(prefix)]);
|
|
18
|
+
this.#bindings.set(prefix, attrs[name] ?? '');
|
|
19
|
+
}
|
|
20
|
+
this.#undo.push(undo);
|
|
21
|
+
}
|
|
22
|
+
close() {
|
|
23
|
+
const undo = this.#undo.pop();
|
|
24
|
+
if (undo === undefined)
|
|
25
|
+
return;
|
|
26
|
+
for (const [prefix, previous] of undo) {
|
|
27
|
+
if (previous === undefined)
|
|
28
|
+
this.#bindings.delete(prefix);
|
|
29
|
+
else
|
|
30
|
+
this.#bindings.set(prefix, previous);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
elementNamespace(qualified) {
|
|
34
|
+
const colon = qualified.indexOf(':');
|
|
35
|
+
return this.#bindings.get(colon === -1 ? '' : qualified.slice(0, colon));
|
|
36
|
+
}
|
|
37
|
+
isElementIn(qualified, uri) {
|
|
38
|
+
return this.elementNamespace(qualified) === uri;
|
|
39
|
+
}
|
|
40
|
+
attr(attrs, uri, local) {
|
|
41
|
+
for (const [prefix, bound] of this.#bindings) {
|
|
42
|
+
if (prefix === '' || bound !== uri)
|
|
43
|
+
continue;
|
|
44
|
+
const value = attrs[`${prefix}:${local}`];
|
|
45
|
+
if (value !== undefined)
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/xml/xml-read.d.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
+
import { NamespaceScope } from './xml-namespaces.ts';
|
|
1
2
|
import { type XmlAttributes, type XmlEvent } from './xml-scan.ts';
|
|
2
3
|
export interface SaxHandlers {
|
|
3
|
-
/**
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* An element start. `selfClosing` is true for `<x/>`; no matching {@link onClose} fires for it.
|
|
6
|
+
*
|
|
7
|
+
* `scope` carries the namespace bindings in force here, for the handful of readings whose identity
|
|
8
|
+
* is a namespace rather than a prefix: an `r:id` under whatever prefix the file bound the
|
|
9
|
+
* relationships namespace to, or telling an extension element from a main-namespace one in a file
|
|
10
|
+
* that prefixes the main namespace. Most handlers match on {@link localName} and ignore it.
|
|
11
|
+
*/
|
|
12
|
+
onOpen(name: string, attrs: XmlAttributes, selfClosing: boolean, scope: NamespaceScope): void;
|
|
5
13
|
/** A run of character data (already entity-decoded; CDATA delivered verbatim). Omit to ignore text. */
|
|
6
14
|
onText?(text: string): void;
|
|
7
15
|
/** An element end (`</x>`); the synthetic end of a self-closing element is *not* reported here.
|
|
8
|
-
* Omit to ignore closes.
|
|
9
|
-
|
|
16
|
+
* Omit to ignore closes. `scope` is the element's own bindings, still in force: it is popped after
|
|
17
|
+
* this returns, so a close handler resolves the same namespaces its open handler did. */
|
|
18
|
+
onClose?(name: string, scope: NamespaceScope): void;
|
|
10
19
|
}
|
|
11
|
-
/**
|
|
12
|
-
* One parse event from {@link xmlEvents}. The payloads match {@link SaxHandlers} exactly: `text`
|
|
13
|
-
* is already entity-decoded (or verbatim CDATA), and a `<x/>` yields one `open` with
|
|
14
|
-
* `selfClosing: true` and no matching `close`. The discriminated `kind` lets a *pull* consumer
|
|
15
|
-
* drive the parse: the shape the streaming reader needs, where a push callback cannot `yield`.
|
|
16
|
-
|
|
17
20
|
/**
|
|
18
21
|
* What {@link elementSubtrees} is to capture: for each container element's local name, the local name
|
|
19
22
|
* of the children to take verbatim inside it (`'dxfs' -> 'dxf'`). Scoping the child to a container is
|
|
@@ -49,12 +52,89 @@ export interface SubtreeCapture {
|
|
|
49
52
|
* verbatim is broken markup handed on as though it were content.
|
|
50
53
|
*/
|
|
51
54
|
export declare function elementSubtrees(source: string, selection: SubtreeSelection): SubtreeCapture;
|
|
55
|
+
/**
|
|
56
|
+
* Where an element sits in the source: the offsets an editor splices at, and the parsed attributes of
|
|
57
|
+
* its opening tag.
|
|
58
|
+
*
|
|
59
|
+
* `contentStart`/`contentEnd` bound the element's children; for an empty element they are equal and
|
|
60
|
+
* both sit just past the `/>`, so replacing that range turns `<latin/>` into `<latin>…</latin>` and
|
|
61
|
+
* a caller that means to replace the *element* uses `start`/`end` instead.
|
|
62
|
+
*/
|
|
63
|
+
export interface ElementRange {
|
|
64
|
+
/** Offset of the element's `<`. */
|
|
65
|
+
readonly start: number;
|
|
66
|
+
/** One past the element's final `>`. */
|
|
67
|
+
readonly end: number;
|
|
68
|
+
/** One past the opening tag's `>`. */
|
|
69
|
+
readonly contentStart: number;
|
|
70
|
+
/** Offset of the closing tag's `<`, or `end` for an empty element. */
|
|
71
|
+
readonly contentEnd: number;
|
|
72
|
+
/** The name as written, namespace prefix included. */
|
|
73
|
+
readonly name: string;
|
|
74
|
+
readonly attrs: XmlAttributes;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Locate the first element reachable by a path of local names, as offsets into the source.
|
|
78
|
+
*
|
|
79
|
+
* The primitive for *editing* a part rather than reading one: everything outside the returned range
|
|
80
|
+
* is spliced through byte for byte, so an edit changes what it names and nothing else -- the
|
|
81
|
+
* whitespace, the comments, the attribute order, the prefix the source chose. That is a guarantee no
|
|
82
|
+
* re-serialisation can make, and it is why this returns offsets instead of text.
|
|
83
|
+
*
|
|
84
|
+
* The alternative it replaces is a `<container>[\s\S]*?</container>` regular expression, which is a
|
|
85
|
+
* regular expression parsing XML over untrusted input: it terminates on a `</container>` inside a
|
|
86
|
+
* comment or a CDATA section, it cannot see an element written `<x></x>` where it expected `<x/>`,
|
|
87
|
+
* and its container name is interpolated into a pattern where an NCName's legal `.` and `-` are
|
|
88
|
+
* metacharacters. This scan classifies markup with {@link markupAt}, so a comment is skipped rather
|
|
89
|
+
* than matched, and compares parsed names rather than raw text.
|
|
90
|
+
*
|
|
91
|
+
* Each step of `path` matches at any depth below the previous one, and the *first* match wins, which
|
|
92
|
+
* is the single block these documents declare. An element that never closes throws
|
|
93
|
+
* {@link XmlParseError}, on the same grounds as the other truncation cases here: an unterminated
|
|
94
|
+
* range is not a range.
|
|
95
|
+
*/
|
|
96
|
+
export declare function elementRange(source: string, path: readonly string[]): ElementRange | undefined;
|
|
97
|
+
/** A tag as {@link tagRanges} yields it. */
|
|
98
|
+
export interface TagRange {
|
|
99
|
+
/** Offset of the tag's `<`. */
|
|
100
|
+
readonly start: number;
|
|
101
|
+
/** One past the tag's `>`. */
|
|
102
|
+
readonly end: number;
|
|
103
|
+
/** The name as written, namespace prefix included. */
|
|
104
|
+
readonly name: string;
|
|
105
|
+
/** The parsed attributes of an opening tag; empty for a closing one. */
|
|
106
|
+
readonly attrs: XmlAttributes;
|
|
107
|
+
readonly close: boolean;
|
|
108
|
+
readonly selfClosing: boolean;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Every tag in `source`, opening and closing, as offsets into the source: for an edit that rewrites a
|
|
112
|
+
* tag wherever it sits rather than one element a path names, such as translating the namespaces a
|
|
113
|
+
* part declares. A caller that resolves names keeps its own `NamespaceScope` in step, opening it on
|
|
114
|
+
* each opening tag and closing it on each closing one and after each self-closing one.
|
|
115
|
+
*/
|
|
116
|
+
export declare function tagRanges(source: string): Generator<TagRange>;
|
|
117
|
+
/**
|
|
118
|
+
* Locate every element with the local name `local`, at any depth, as offsets into the source, in
|
|
119
|
+
* document order: {@link elementRange} for a part that repeats the element an edit is after, such as
|
|
120
|
+
* the `<c:f>` behind each of a chart's series. A same-named element nested in a match is part of that
|
|
121
|
+
* match's content rather than a range of its own. An element that never closes throws
|
|
122
|
+
* {@link XmlParseError}.
|
|
123
|
+
*/
|
|
124
|
+
export declare function elementRanges(source: string, local: string): Generator<ElementRange>;
|
|
52
125
|
/** An element start surfaced by {@link openElements}: its qualified `name`, the namespace-stripped
|
|
53
126
|
* `local` name the filter matched on, and its already-decoded `attrs`. */
|
|
54
127
|
export interface OpenElement {
|
|
55
128
|
readonly name: string;
|
|
56
129
|
readonly local: string;
|
|
57
130
|
readonly attrs: XmlAttributes;
|
|
131
|
+
/**
|
|
132
|
+
* The namespace bindings in force at this element, for the attributes and elements whose identity
|
|
133
|
+
* is a namespace rather than a prefix (`r:id`, the x14 extension elements). One shared, mutating
|
|
134
|
+
* instance rather than a snapshot: it is valid while this element is the current one, which is the
|
|
135
|
+
* whole of a `for..of` body, and copying it per element would cost every scan for the few that ask.
|
|
136
|
+
*/
|
|
137
|
+
readonly scope: NamespaceScope;
|
|
58
138
|
}
|
|
59
139
|
/**
|
|
60
140
|
* Yield each element start in `source` as an {@link OpenElement}, optionally restricted to the given
|
|
@@ -136,7 +216,7 @@ export declare function parseXml(source: string, handlers: SaxHandlers, options?
|
|
|
136
216
|
* The other thing the open-coded versions disagreed on is what an unrelated element opening
|
|
137
217
|
* mid-capture should do. Ending the capture is never what a caller wants: the text belongs to the
|
|
138
218
|
* element that opened it, and a nested or sibling element is not that element. So an open that is
|
|
139
|
-
* not for a captured name leaves
|
|
219
|
+
* not for a captured name leaves a capture in progress alone, and {@link close} answers only for
|
|
140
220
|
* the element that started it.
|
|
141
221
|
*
|
|
142
222
|
* Decoding stays outside. A `<t>` needs `decodeSpreadsheetText` over the whole element and never
|
|
@@ -154,6 +234,16 @@ export declare class TextCapture {
|
|
|
154
234
|
text(chunk: string): void;
|
|
155
235
|
/** The gathered text when `local` closes the captured element, else `undefined`. Unlatches. */
|
|
156
236
|
close(local: string): string | undefined;
|
|
237
|
+
/**
|
|
238
|
+
* Abandon any capture in progress, for a caller whose own container has just ended or begun.
|
|
239
|
+
*
|
|
240
|
+
* {@link close} unlatches only for the element that opened the capture, which is the right answer
|
|
241
|
+
* within one element but not across a boundary the caller owns: markup that opens a `<t>` and is
|
|
242
|
+
* then truncated leaves the capture armed, and the next container's text lands in the abandoned
|
|
243
|
+
* buffer instead of where that container wanted it. A caller that resets its own per-container
|
|
244
|
+
* state resets this with it.
|
|
245
|
+
*/
|
|
246
|
+
reset(): void;
|
|
157
247
|
}
|
|
158
248
|
/**
|
|
159
249
|
* Yield each named element's text as that element closes, as `{local, text}`.
|