@shbernal/ts-xlsx 2.1.0 → 3.0.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/dist/core/autofilter.d.ts +1 -1
- package/dist/core/autofilter.js +3 -5
- package/dist/core/axis-handle.d.ts +8 -0
- package/dist/core/axis-handle.js +14 -0
- package/dist/core/cell.d.ts +24 -0
- package/dist/core/cell.js +9 -1
- package/dist/core/column.d.ts +4 -1
- package/dist/core/column.js +31 -35
- package/dist/core/conditional-formatting.d.ts +5 -5
- package/dist/core/conditional-formatting.js +11 -25
- package/dist/core/data-validation.d.ts +3 -3
- package/dist/core/data-validation.js +7 -15
- package/dist/core/image.d.ts +1 -1
- package/dist/core/image.js +2 -4
- package/dist/core/merge-index.d.ts +31 -0
- package/dist/core/merge-index.js +60 -0
- package/dist/core/merge.d.ts +0 -10
- package/dist/core/merge.js +0 -8
- package/dist/core/page-setup.d.ts +72 -2
- package/dist/core/page-setup.js +34 -8
- package/dist/core/pivot-table.d.ts +1 -1
- package/dist/core/pivot-table.js +4 -8
- package/dist/core/row.d.ts +4 -1
- package/dist/core/row.js +19 -23
- package/dist/core/style.d.ts +51 -7
- package/dist/core/style.js +29 -34
- package/dist/core/table.d.ts +1 -1
- package/dist/core/table.js +3 -5
- package/dist/core/used-extent.d.ts +44 -0
- package/dist/core/used-extent.js +102 -0
- package/dist/core/worksheet.d.ts +17 -6
- package/dist/core/worksheet.js +48 -42
- package/dist/customui/ribbon.js +4 -6
- package/dist/io/opc/inflate.js +1 -1
- package/dist/io/opc/part-paths.d.ts +1 -0
- package/dist/io/opc/part-paths.js +15 -0
- package/dist/io/opc/read-opc.d.ts +22 -3
- package/dist/io/opc/read-opc.js +11 -39
- package/dist/io/opc/rels.d.ts +1 -6
- package/dist/io/opc/rels.js +1 -4
- package/dist/io/style/xf-style.d.ts +26 -2
- package/dist/io/style/xf-style.js +6 -0
- package/dist/io/xlsb/read-styles.js +6 -3
- package/dist/io/xlsb/read.d.ts +1 -1
- package/dist/io/xlsb/read.js +5 -8
- package/dist/io/xlsx/cell-accumulator.d.ts +1 -1
- package/dist/io/xlsx/cell-accumulator.js +1 -1
- package/dist/io/xlsx/cell-value.js +1 -1
- package/dist/io/xlsx/color-xml.js +1 -1
- package/dist/io/xlsx/comments.js +2 -1
- package/dist/io/xlsx/conditional-formatting.d.ts +3 -2
- package/dist/io/xlsx/conditional-formatting.js +22 -21
- package/dist/io/xlsx/data-validation.d.ts +9 -7
- package/dist/io/xlsx/data-validation.js +10 -9
- package/dist/io/xlsx/edit-vba.js +4 -11
- package/dist/io/xlsx/hyperlinks.d.ts +4 -2
- package/dist/io/xlsx/hyperlinks.js +20 -17
- package/dist/io/xlsx/images.js +15 -14
- package/dist/io/xlsx/package-plan.d.ts +9 -1
- package/dist/io/xlsx/package-plan.js +29 -16
- package/dist/io/xlsx/part-names.d.ts +41 -0
- package/dist/io/xlsx/part-names.js +46 -0
- package/dist/io/xlsx/read-pivot.js +2 -1
- package/dist/io/xlsx/read-rows.js +12 -13
- package/dist/io/xlsx/read-shared-strings.js +2 -1
- package/dist/io/xlsx/read-styles.d.ts +1 -1
- package/dist/io/xlsx/read-styles.js +63 -42
- package/dist/io/xlsx/read-worksheet.d.ts +10 -1
- package/dist/io/xlsx/read-worksheet.js +43 -56
- package/dist/io/xlsx/read.d.ts +2 -3
- package/dist/io/xlsx/read.js +81 -73
- package/dist/io/xlsx/rich-runs.d.ts +1 -1
- package/dist/io/xlsx/rich-runs.js +2 -1
- package/dist/io/xlsx/sheet-properties.js +21 -28
- package/dist/io/xlsx/styles.js +23 -20
- package/dist/io/xlsx/tables.js +2 -1
- package/dist/io/xlsx/theme-xml.js +1 -1
- package/dist/io/xlsx/threaded-comments.js +2 -1
- package/dist/io/xlsx/workbook-xml.js +29 -28
- package/dist/io/xlsx/worksheet-xml.d.ts +1 -1
- package/dist/io/xlsx/worksheet-xml.js +14 -16
- package/dist/io/xlsx/write-stream.d.ts +6 -0
- package/dist/io/xlsx/write-stream.js +30 -22
- package/dist/io/xlsx/write.d.ts +5 -0
- package/dist/io/xlsx/write.js +155 -131
- package/dist/token-set.d.ts +15 -0
- package/dist/token-set.js +4 -0
- package/dist/vba/cfb-format.d.ts +24 -0
- package/dist/vba/cfb-format.js +12 -0
- package/dist/vba/cfb-writer.js +17 -17
- package/dist/vba/cfb.js +1 -7
- package/dist/vba/ms-ovba.js +32 -11
- package/dist/vba/project.js +13 -13
- package/dist/vba/vba-encoding.js +1 -1
- package/dist/xml/xml-read.d.ts +59 -74
- package/dist/xml/xml-read.js +83 -176
- package/dist/xml/xml-scan.d.ts +100 -0
- package/dist/xml/xml-scan.js +208 -0
- package/dist/xml/xml.d.ts +1 -1
- package/package.json +2 -3
package/dist/vba/ms-ovba.js
CHANGED
|
@@ -11,7 +11,7 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
|
|
|
11
11
|
if (buf[start] !== 0x01) {
|
|
12
12
|
throw new VbaParseError(`compressed container must begin with a 0x01 signature byte, found 0x${(buf[start] ?? 0).toString(16)}`);
|
|
13
13
|
}
|
|
14
|
-
const out =
|
|
14
|
+
const out = new DecompressedBytes(maxOutput);
|
|
15
15
|
let pos = start + 1;
|
|
16
16
|
while (pos + 2 <= buf.length) {
|
|
17
17
|
const header = readU16(buf, pos);
|
|
@@ -28,7 +28,6 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
|
|
|
28
28
|
if (!compressed) {
|
|
29
29
|
for (let i = pos; i < chunkEnd; i++)
|
|
30
30
|
out.push(buf[i]);
|
|
31
|
-
guardOutput(out.length, maxOutput);
|
|
32
31
|
pos = chunkEnd;
|
|
33
32
|
continue;
|
|
34
33
|
}
|
|
@@ -39,7 +38,6 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
|
|
|
39
38
|
const isCopy = (flagByte >> bit) & 1;
|
|
40
39
|
if (!isCopy) {
|
|
41
40
|
out.push(buf[pos++]);
|
|
42
|
-
guardOutput(out.length, maxOutput);
|
|
43
41
|
continue;
|
|
44
42
|
}
|
|
45
43
|
if (pos + 2 > chunkEnd) {
|
|
@@ -55,13 +53,41 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
|
|
|
55
53
|
throw new VbaParseError('copy token references before the start of its chunk');
|
|
56
54
|
}
|
|
57
55
|
for (let i = 0; i < length; i++)
|
|
58
|
-
out.push(out
|
|
59
|
-
guardOutput(out.length, maxOutput);
|
|
56
|
+
out.push(out.at(src + i));
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
59
|
pos = chunkEnd;
|
|
63
60
|
}
|
|
64
|
-
return
|
|
61
|
+
return out.bytes();
|
|
62
|
+
}
|
|
63
|
+
class DecompressedBytes {
|
|
64
|
+
#buf;
|
|
65
|
+
#length = 0;
|
|
66
|
+
#limit;
|
|
67
|
+
constructor(limit) {
|
|
68
|
+
this.#limit = limit;
|
|
69
|
+
this.#buf = new Uint8Array(Math.min(MAX_CHUNK_DECOMPRESSED, limit));
|
|
70
|
+
}
|
|
71
|
+
get length() {
|
|
72
|
+
return this.#length;
|
|
73
|
+
}
|
|
74
|
+
at(index) {
|
|
75
|
+
return this.#buf[index];
|
|
76
|
+
}
|
|
77
|
+
push(byte) {
|
|
78
|
+
if (this.#length >= this.#limit) {
|
|
79
|
+
throw new VbaParseError(`decompressed output exceeds the ${this.#limit}-byte ceiling (possible bomb)`);
|
|
80
|
+
}
|
|
81
|
+
if (this.#length === this.#buf.length) {
|
|
82
|
+
const grown = new Uint8Array(Math.min(Math.max(this.#buf.length * 2, MAX_CHUNK_DECOMPRESSED), this.#limit));
|
|
83
|
+
grown.set(this.#buf);
|
|
84
|
+
this.#buf = grown;
|
|
85
|
+
}
|
|
86
|
+
this.#buf[this.#length++] = byte;
|
|
87
|
+
}
|
|
88
|
+
bytes() {
|
|
89
|
+
return this.#buf.slice(0, this.#length);
|
|
90
|
+
}
|
|
65
91
|
}
|
|
66
92
|
export function compressContainer(data) {
|
|
67
93
|
const out = [0x01];
|
|
@@ -119,11 +145,6 @@ function compressChunk(chunk) {
|
|
|
119
145
|
}
|
|
120
146
|
return tokens;
|
|
121
147
|
}
|
|
122
|
-
function guardOutput(size, maxOutput) {
|
|
123
|
-
if (size > maxOutput) {
|
|
124
|
-
throw new VbaParseError(`decompressed output exceeds the ${maxOutput}-byte ceiling (possible bomb)`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
148
|
function copyTokenHelp(decompressedSoFar) {
|
|
128
149
|
const bitCount = Math.max(Math.ceil(Math.log2(Math.max(decompressedSoFar, 1))), 4);
|
|
129
150
|
const lengthMask = 0xffff >> bitCount;
|
package/dist/vba/project.js
CHANGED
|
@@ -7,13 +7,13 @@ import { decompressContainer } from './ms-ovba.js';
|
|
|
7
7
|
export const VBA_PROJECT_REL_TYPE = 'http://schemas.microsoft.com/office/2006/relationships/vbaProject';
|
|
8
8
|
export const VBA_PROJECT_CONTENT_TYPE = 'application/vnd.ms-office.vbaProject';
|
|
9
9
|
export const VBA_PROJECT_PART_PATH = 'xl/vbaProject.bin';
|
|
10
|
-
const SIGNATURE_KIND_BY_REL_SEGMENT =
|
|
11
|
-
vbaProjectSignature
|
|
12
|
-
vbaProjectSignatureAgile
|
|
13
|
-
vbaProjectSignatureV3
|
|
14
|
-
|
|
10
|
+
const SIGNATURE_KIND_BY_REL_SEGMENT = new Map([
|
|
11
|
+
['vbaProjectSignature', 'legacy'],
|
|
12
|
+
['vbaProjectSignatureAgile', 'agile'],
|
|
13
|
+
['vbaProjectSignatureV3', 'v3'],
|
|
14
|
+
]);
|
|
15
15
|
export function vbaProjectSignatureKind(relType) {
|
|
16
|
-
return SIGNATURE_KIND_BY_REL_SEGMENT
|
|
16
|
+
return SIGNATURE_KIND_BY_REL_SEGMENT.get(relType.slice(relType.lastIndexOf('/') + 1));
|
|
17
17
|
}
|
|
18
18
|
export function parseVbaProject(bin) {
|
|
19
19
|
const cfb = new CompoundFile(bin);
|
|
@@ -76,23 +76,23 @@ function readModuleSource(cfb, streamName, textOffset, decoder) {
|
|
|
76
76
|
throw new VbaParseError(`module stream '${streamName}' not found in container`);
|
|
77
77
|
return decoder.decode(decompressContainer(stream, textOffset));
|
|
78
78
|
}
|
|
79
|
+
const MODULE_KIND_BY_PROJECT_KEYWORD = new Map([
|
|
80
|
+
['Document', 'document'],
|
|
81
|
+
['Module', 'procedural'],
|
|
82
|
+
['Class', 'class'],
|
|
83
|
+
['BaseClass', 'designer'],
|
|
84
|
+
]);
|
|
79
85
|
function readProjectStreamKinds(cfb, decoder) {
|
|
80
86
|
const kinds = new Map();
|
|
81
87
|
const stream = cfb.readStream('PROJECT');
|
|
82
88
|
if (!stream)
|
|
83
89
|
return kinds;
|
|
84
90
|
const text = decoder.decode(stream);
|
|
85
|
-
const keyword = {
|
|
86
|
-
Document: 'document',
|
|
87
|
-
Module: 'procedural',
|
|
88
|
-
Class: 'class',
|
|
89
|
-
BaseClass: 'designer',
|
|
90
|
-
};
|
|
91
91
|
for (const line of text.split(/\r\n|\r|\n/)) {
|
|
92
92
|
const eq = line.indexOf('=');
|
|
93
93
|
if (eq < 0)
|
|
94
94
|
continue;
|
|
95
|
-
const kind =
|
|
95
|
+
const kind = MODULE_KIND_BY_PROJECT_KEYWORD.get(line.slice(0, eq));
|
|
96
96
|
if (kind === undefined)
|
|
97
97
|
continue;
|
|
98
98
|
const name = (line.slice(eq + 1).split('/')[0] ?? '').trim();
|
package/dist/vba/vba-encoding.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { MAX_NAME_CHARS } from './cfb-format.js';
|
|
1
2
|
import { VbaAuthorError } from './errors.js';
|
|
2
3
|
const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
3
|
-
const MAX_NAME_CHARS = 31;
|
|
4
4
|
export function validateVbaName(name, what) {
|
|
5
5
|
if (!IDENTIFIER.test(name) || name.length > MAX_NAME_CHARS) {
|
|
6
6
|
throw new VbaAuthorError(`invalid ${what} name '${name}' (must be a VBA identifier ≤ 31 chars)`);
|
package/dist/xml/xml-read.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
readonly [name: string]: string;
|
|
3
|
-
}
|
|
1
|
+
import { type XmlAttributes, type XmlEvent } from './xml-scan.ts';
|
|
4
2
|
export interface SaxHandlers {
|
|
5
3
|
/** An element start. `selfClosing` is true for `<x/>`; no matching {@link onClose} fires for it. */
|
|
6
4
|
onOpen(name: string, attrs: XmlAttributes, selfClosing: boolean): void;
|
|
@@ -15,55 +13,42 @@ export interface SaxHandlers {
|
|
|
15
13
|
* is already entity-decoded (or verbatim CDATA), and a `<x/>` yields one `open` with
|
|
16
14
|
* `selfClosing: true` and no matching `close`. The discriminated `kind` lets a *pull* consumer
|
|
17
15
|
* drive the parse: the shape the streaming reader needs, where a push callback cannot `yield`.
|
|
18
|
-
|
|
19
|
-
export type XmlEvent = {
|
|
20
|
-
readonly kind: 'open';
|
|
21
|
-
readonly name: string;
|
|
22
|
-
readonly attrs: XmlAttributes;
|
|
23
|
-
readonly selfClosing: boolean;
|
|
24
|
-
} | {
|
|
25
|
-
readonly kind: 'text';
|
|
26
|
-
readonly text: string;
|
|
27
|
-
} | {
|
|
28
|
-
readonly kind: 'close';
|
|
29
|
-
readonly name: string;
|
|
30
|
-
};
|
|
16
|
+
|
|
31
17
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
18
|
+
* What {@link elementSubtrees} is to capture: for each container element's local name, the local name
|
|
19
|
+
* of the children to take verbatim inside it (`'dxfs' -> 'dxf'`). Scoping the child to a container is
|
|
20
|
+
* what keeps a `<color>` in `<mruColors>` from being confused with the many other `<color>` elements
|
|
21
|
+
* a stylesheet carries.
|
|
36
22
|
*/
|
|
37
|
-
export
|
|
23
|
+
export type SubtreeSelection = ReadonlyMap<string, string>;
|
|
24
|
+
/** What {@link elementSubtrees} captured: the verbatim source of each selected child, keyed by its
|
|
25
|
+
* container's local name, and each container's own attributes as written. */
|
|
26
|
+
export interface SubtreeCapture {
|
|
27
|
+
readonly fragments: ReadonlyMap<string, readonly string[]>;
|
|
28
|
+
readonly attributes: ReadonlyMap<string, XmlAttributes>;
|
|
29
|
+
}
|
|
38
30
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* The mirror of `escapeSpreadsheetText` in `./xml.ts`, and it sits here rather than beside it for
|
|
42
|
-
* the same reason `decodeEntities` sits apart from `escapeText`: the write helpers carry an
|
|
43
|
-
* `AuthoringError` and a whole serialisation vocabulary the reader has no business importing.
|
|
31
|
+
* Capture the verbatim source text of selected elements, in one scan.
|
|
44
32
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* the rest, collapses that to `A` and loses the distinction the encoder went to trouble to keep.
|
|
51
|
-
* Excel agrees: it reads that cell as `_x0041_`.
|
|
33
|
+
* Some content is re-emitted byte for byte rather than modelled: a differential style, a custom
|
|
34
|
+
* indexed palette, an author's recent-colour swatches, a table-style definition. Preserving the raw
|
|
35
|
+
* text is what keeps a foreign `<dxf>`'s number format a real format code across a re-write instead
|
|
36
|
+
* of a coerced `"[object Object]"`, so the reader needs a subtree's *source*, which an event stream
|
|
37
|
+
* by definition cannot hand back.
|
|
52
38
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
* Scan an XML document as a *pull* stream of {@link XmlEvent}s in a single O(n) pass with no
|
|
60
|
-
* recursion. This is the parser's core; {@link parseXml} is a thin push adapter over it. A
|
|
61
|
-
* consumer that must produce output incrementally (the streaming row reader) pulls events and
|
|
62
|
-
* yields as it goes, holding only its own running state; a push callback cannot.
|
|
39
|
+
* That gap is why four callers each grew a `<container>([\s\S]*?)</container>` scanner of their own,
|
|
40
|
+
* which is a regular expression parsing XML, over untrusted input, in the same directory as the
|
|
41
|
+
* reader written specifically to avoid that (ADR-0004). This is the same capability done properly:
|
|
42
|
+
* one linear scan with comments, CDATA, processing instructions and declarations skipped as markup
|
|
43
|
+
* rather than matched as text, and the nesting depth counted so a same-named descendant does not end
|
|
44
|
+
* a capture early.
|
|
63
45
|
*
|
|
64
|
-
*
|
|
46
|
+
* Only a container's *first* occurrence is read, matching the single block these documents declare;
|
|
47
|
+
* a second is ignored rather than merged. A captured element that never closes throws
|
|
48
|
+
* {@link XmlParseError}, like the reader's other truncation cases: a partial subtree re-emitted
|
|
49
|
+
* verbatim is broken markup handed on as though it were content.
|
|
65
50
|
*/
|
|
66
|
-
export declare function
|
|
51
|
+
export declare function elementSubtrees(source: string, selection: SubtreeSelection): SubtreeCapture;
|
|
67
52
|
/** An element start surfaced by {@link openElements}: its qualified `name`, the namespace-stripped
|
|
68
53
|
* `local` name the filter matched on, and its already-decoded `attrs`. */
|
|
69
54
|
export interface OpenElement {
|
|
@@ -101,41 +86,41 @@ export interface ParseXmlOptions {
|
|
|
101
86
|
*/
|
|
102
87
|
readonly closeEmptyElements?: ReadonlySet<string>;
|
|
103
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* One reader's share of a parse: the handlers it wants the events delivered to, and the self-closing
|
|
91
|
+
* elements it needs expanded. Named separately from {@link ParseXmlOptions} because several readers
|
|
92
|
+
* of the same part run over a single parse of it, and each has to bring its own requirements rather
|
|
93
|
+
* than have the caller remember them; see {@link parseXmlPasses}.
|
|
94
|
+
*/
|
|
95
|
+
export interface SaxPass {
|
|
96
|
+
readonly handlers: SaxHandlers;
|
|
97
|
+
/** As {@link ParseXmlOptions.closeEmptyElements}, for this reader's elements. */
|
|
98
|
+
readonly closeEmptyElements?: ReadonlySet<string>;
|
|
99
|
+
}
|
|
100
|
+
/** A {@link SaxPass} that gathers something during the parse rather than committing as it goes. */
|
|
101
|
+
export interface CollectingPass<T> extends SaxPass {
|
|
102
|
+
/** What the pass collected. Meaningful only once the parse driving it has finished. */
|
|
103
|
+
result(): T;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Parse `source` once, delivering every event to each pass in turn. The alternative, a parse per
|
|
107
|
+
* reader, costs a full scan of the document per reader and finds nothing in most of them: the
|
|
108
|
+
* worksheet part is the largest in a package, and reading it five times over spent 45% of a large
|
|
109
|
+
* file's read on four scans that matched no element.
|
|
110
|
+
*
|
|
111
|
+
* The expansions the passes ask for are unioned, so *every* pass sees `<x/>` as an open plus a close
|
|
112
|
+
* for any name *any* of them named. That is the one way a pass can observe that it is sharing a
|
|
113
|
+
* parse, and it is why the option is a set of element names rather than a flag: a pass sees an extra
|
|
114
|
+
* close only for elements another pass had to name, and reaching a close for an element a reader
|
|
115
|
+
* does not handle is already the ordinary case.
|
|
116
|
+
*/
|
|
117
|
+
export declare function parseXmlPasses(source: string, passes: readonly SaxPass[]): void;
|
|
104
118
|
/**
|
|
105
119
|
* Parse an XML document, dispatching SAX events to `handlers`. A thin push adapter over
|
|
106
120
|
* {@link xmlEvents}: one scanning core serves both the callback and the pull consumers.
|
|
107
121
|
* Throws {@link XmlParseError} on malformed markup.
|
|
108
122
|
*/
|
|
109
123
|
export declare function parseXml(source: string, handlers: SaxHandlers, options?: ParseXmlOptions): void;
|
|
110
|
-
/** Strip a namespace prefix from a qualified name (`r:id` → `id`, `sheet` → `sheet`). */
|
|
111
|
-
export declare function localName(qualified: string): string;
|
|
112
|
-
/** An OOXML boolean that is on when present with no value (`<b/>` is bold) and off only on an
|
|
113
|
-
* explicit `"0"`/`"false"`; absence reads as on. */
|
|
114
|
-
export declare function boolPresent(val: string | undefined): boolean;
|
|
115
|
-
/** An OOXML boolean that is on only when explicitly `"1"`/`"true"`; anything else, including
|
|
116
|
-
* absence and a truthy-looking `"0"`, is off. */
|
|
117
|
-
export declare function boolStrict(val: string | undefined): boolean;
|
|
118
|
-
/** An optional OOXML boolean: `undefined` when the attribute is absent or carries an unrecognised
|
|
119
|
-
* token, otherwise its `"1"`/`"true"` vs `"0"`/`"false"` value. Lets a caller store only the
|
|
120
|
-
* attributes the source actually carried, so a re-write stays byte-clean. */
|
|
121
|
-
export declare function boolTristate(val: string | undefined): boolean | undefined;
|
|
122
|
-
/** An OOXML integer attribute at or above `min` (default: unbounded below); `undefined` when the
|
|
123
|
-
* attribute is absent, blank, fractional, not a number, or below the floor. Integers past
|
|
124
|
-
* `Number.MAX_SAFE_INTEGER` read as `undefined` too: no index or count is usable out there, and
|
|
125
|
-
* arithmetic on one silently lies. */
|
|
126
|
-
export declare function numInteger(val: string | undefined, min?: number): number | undefined;
|
|
127
|
-
/** An OOXML decimal attribute at or above `min` (default: unbounded below); `undefined` when the
|
|
128
|
-
* attribute is absent, blank, not a number, or below the floor. Infinities are not finite numbers
|
|
129
|
-
* and read as `undefined`. */
|
|
130
|
-
export declare function numFinite(val: string | undefined, min?: number): number | undefined;
|
|
131
|
-
/** Read an operand's text as a number only when it is a canonical decimal literal (optional sign,
|
|
132
|
-
* digits, optional fraction). A cell reference, defined name, expression, or exotically-spelled
|
|
133
|
-
* number (`1E5`, hex) keeps its verbatim text, so it is neither coerced to `NaN` and lost nor
|
|
134
|
-
* re-spelled into a number that would not re-write byte-clean. Callers layer their own type rules
|
|
135
|
-
* (a data-validation `list`/`custom` operand stays a string regardless of what it looks like). */
|
|
136
|
-
export declare function coerceNumericLiteral(text: string): string | number;
|
|
137
|
-
/** Narrow an enumerated attribute through its guard; `undefined` when absent or not a member. */
|
|
138
|
-
export declare function enumToken<T extends string>(val: string | undefined, isMember: (candidate: string) => candidate is T): T | undefined;
|
|
139
124
|
/**
|
|
140
125
|
* Gathers one element's character data across the open/text/close events a SAX parse delivers it in.
|
|
141
126
|
*
|
package/dist/xml/xml-read.js
CHANGED
|
@@ -1,137 +1,77 @@
|
|
|
1
1
|
import { XmlParseError } from './errors.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const ENTITY = /&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z][a-zA-Z0-9]*);/g;
|
|
10
|
-
export function decodeEntities(value) {
|
|
11
|
-
if (!value.includes('&'))
|
|
12
|
-
return value;
|
|
13
|
-
return value.replace(ENTITY, (match, body) => {
|
|
14
|
-
if (body.charCodeAt(0) === 0x23) {
|
|
15
|
-
const codePoint = body.charCodeAt(1) === 0x78
|
|
16
|
-
? parseInt(body.slice(2), 16)
|
|
17
|
-
: parseInt(body.slice(1), 10);
|
|
18
|
-
if (!Number.isInteger(codePoint) || codePoint < 0 || codePoint > 0x10ffff)
|
|
19
|
-
return match;
|
|
20
|
-
try {
|
|
21
|
-
return String.fromCodePoint(codePoint);
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return match;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
const named = PREDEFINED_ENTITIES[body];
|
|
28
|
-
return named ?? match;
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
export function decodeSpreadsheetText(value) {
|
|
32
|
-
if (!value.includes('_'))
|
|
33
|
-
return value;
|
|
34
|
-
return value.replace(/_x([0-9A-Fa-f]{4})_/g, (_match, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
35
|
-
}
|
|
36
|
-
const ATTRIBUTE = /([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
37
|
-
function parseAttributes(source) {
|
|
38
|
-
const attrs = {};
|
|
39
|
-
ATTRIBUTE.lastIndex = 0;
|
|
40
|
-
let match = ATTRIBUTE.exec(source);
|
|
41
|
-
while (match !== null) {
|
|
42
|
-
const value = match[2] ?? match[3] ?? '';
|
|
43
|
-
attrs[match[1]] = decodeEntities(value);
|
|
44
|
-
match = ATTRIBUTE.exec(source);
|
|
45
|
-
}
|
|
46
|
-
return attrs;
|
|
47
|
-
}
|
|
48
|
-
function findTagEnd(source, start) {
|
|
49
|
-
let quote = '';
|
|
50
|
-
for (let i = start + 1; i < source.length; i++) {
|
|
51
|
-
const ch = source[i];
|
|
52
|
-
if (quote !== '') {
|
|
53
|
-
if (ch === quote)
|
|
54
|
-
quote = '';
|
|
55
|
-
}
|
|
56
|
-
else if (ch === '"' || ch === "'") {
|
|
57
|
-
quote = ch;
|
|
58
|
-
}
|
|
59
|
-
else if (ch === '>') {
|
|
60
|
-
return i;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
throw new XmlParseError('unterminated tag: missing ">"');
|
|
64
|
-
}
|
|
65
|
-
function skipDeclaration(source, start) {
|
|
66
|
-
let depth = 0;
|
|
67
|
-
for (let i = start; i < source.length; i++) {
|
|
68
|
-
const ch = source[i];
|
|
69
|
-
if (ch === '[')
|
|
70
|
-
depth++;
|
|
71
|
-
else if (ch === ']')
|
|
72
|
-
depth--;
|
|
73
|
-
else if (ch === '>' && depth <= 0)
|
|
74
|
-
return i + 1;
|
|
75
|
-
}
|
|
76
|
-
throw new XmlParseError('unterminated markup declaration: missing ">"');
|
|
77
|
-
}
|
|
78
|
-
export function* xmlEvents(source) {
|
|
2
|
+
import { localName, markupAt, parseAttributes, tagAt, xmlEvents, } from './xml-scan.js';
|
|
3
|
+
export function elementSubtrees(source, selection) {
|
|
4
|
+
const fragments = new Map();
|
|
5
|
+
const attributes = new Map();
|
|
6
|
+
const finished = new Set();
|
|
7
|
+
let container;
|
|
8
|
+
let capture;
|
|
79
9
|
const length = source.length;
|
|
80
10
|
let i = 0;
|
|
81
11
|
while (i < length) {
|
|
82
12
|
const lt = source.indexOf('<', i);
|
|
83
|
-
if (lt === -1)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
if (lt > i) {
|
|
90
|
-
const chunk = source.slice(i, lt);
|
|
91
|
-
if (chunk.length > 0)
|
|
92
|
-
yield { kind: 'text', text: decodeEntities(normalizeLineEndings(chunk)) };
|
|
93
|
-
}
|
|
94
|
-
if (source.startsWith('<!--', lt)) {
|
|
95
|
-
const end = source.indexOf('-->', lt + 4);
|
|
96
|
-
if (end === -1)
|
|
97
|
-
throw new XmlParseError('unterminated comment');
|
|
98
|
-
i = end + 3;
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (source.startsWith('<![CDATA[', lt)) {
|
|
102
|
-
const end = source.indexOf(']]>', lt + 9);
|
|
103
|
-
if (end === -1)
|
|
104
|
-
throw new XmlParseError('unterminated CDATA section');
|
|
105
|
-
yield { kind: 'text', text: source.slice(lt + 9, end) };
|
|
106
|
-
i = end + 3;
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (source.startsWith('<?', lt)) {
|
|
110
|
-
const end = source.indexOf('?>', lt + 2);
|
|
111
|
-
if (end === -1)
|
|
112
|
-
throw new XmlParseError('unterminated processing instruction');
|
|
113
|
-
i = end + 2;
|
|
13
|
+
if (lt === -1)
|
|
14
|
+
break;
|
|
15
|
+
const markup = markupAt(source, lt);
|
|
16
|
+
if (markup !== undefined) {
|
|
17
|
+
i = markup.next;
|
|
114
18
|
continue;
|
|
115
19
|
}
|
|
116
|
-
|
|
117
|
-
|
|
20
|
+
const tag = tagAt(source, lt);
|
|
21
|
+
const local = localName(tag.name);
|
|
22
|
+
if (tag.close) {
|
|
23
|
+
if (capture !== undefined && local === capture.local) {
|
|
24
|
+
if (capture.depth > 0)
|
|
25
|
+
capture.depth -= 1;
|
|
26
|
+
else {
|
|
27
|
+
fragments.get(container?.local ?? '')?.push(source.slice(capture.start, tag.next));
|
|
28
|
+
capture = undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (capture === undefined && container !== undefined && local === container.local) {
|
|
32
|
+
if (container.depth > 0)
|
|
33
|
+
container.depth -= 1;
|
|
34
|
+
else {
|
|
35
|
+
finished.add(container.local);
|
|
36
|
+
container = undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
i = tag.next;
|
|
118
40
|
continue;
|
|
119
41
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
42
|
+
const { selfClosing } = tag;
|
|
43
|
+
if (capture !== undefined) {
|
|
44
|
+
if (!selfClosing && local === capture.local)
|
|
45
|
+
capture.depth += 1;
|
|
46
|
+
}
|
|
47
|
+
else if (container !== undefined) {
|
|
48
|
+
if (!selfClosing && local === container.local)
|
|
49
|
+
container.depth += 1;
|
|
50
|
+
else if (local === container.child) {
|
|
51
|
+
if (selfClosing)
|
|
52
|
+
fragments.get(container.local)?.push(source.slice(lt, tag.next));
|
|
53
|
+
else
|
|
54
|
+
capture = { local, start: lt, depth: 0 };
|
|
55
|
+
}
|
|
124
56
|
}
|
|
125
57
|
else {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
58
|
+
const child = selection.get(local);
|
|
59
|
+
if (child !== undefined && !finished.has(local)) {
|
|
60
|
+
attributes.set(local, parseAttributes(tag.attrSource));
|
|
61
|
+
if (selfClosing)
|
|
62
|
+
finished.add(local);
|
|
63
|
+
else {
|
|
64
|
+
fragments.set(local, fragments.get(local) ?? []);
|
|
65
|
+
container = { local, child, depth: 0 };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
132
68
|
}
|
|
133
|
-
i =
|
|
69
|
+
i = tag.next;
|
|
134
70
|
}
|
|
71
|
+
if (capture !== undefined) {
|
|
72
|
+
throw new XmlParseError(`unterminated <${capture.local}> element`);
|
|
73
|
+
}
|
|
74
|
+
return { fragments, attributes };
|
|
135
75
|
}
|
|
136
76
|
export function* openElements(source, ...localNames) {
|
|
137
77
|
const filter = localNames.length > 0 ? new Set(localNames) : undefined;
|
|
@@ -155,6 +95,28 @@ export function* closeEmptyElements(events, names) {
|
|
|
155
95
|
}
|
|
156
96
|
}
|
|
157
97
|
}
|
|
98
|
+
export function parseXmlPasses(source, passes) {
|
|
99
|
+
const expanded = new Set();
|
|
100
|
+
for (const pass of passes) {
|
|
101
|
+
for (const name of pass.closeEmptyElements ?? [])
|
|
102
|
+
expanded.add(name);
|
|
103
|
+
}
|
|
104
|
+
const handlers = passes.map((pass) => pass.handlers);
|
|
105
|
+
parseXml(source, {
|
|
106
|
+
onOpen(name, attrs, selfClosing) {
|
|
107
|
+
for (const handler of handlers)
|
|
108
|
+
handler.onOpen(name, attrs, selfClosing);
|
|
109
|
+
},
|
|
110
|
+
onText(text) {
|
|
111
|
+
for (const handler of handlers)
|
|
112
|
+
handler.onText?.(text);
|
|
113
|
+
},
|
|
114
|
+
onClose(name) {
|
|
115
|
+
for (const handler of handlers)
|
|
116
|
+
handler.onClose?.(name);
|
|
117
|
+
},
|
|
118
|
+
}, expanded.size > 0 ? { closeEmptyElements: expanded } : undefined);
|
|
119
|
+
}
|
|
158
120
|
export function parseXml(source, handlers, options) {
|
|
159
121
|
const events = options?.closeEmptyElements
|
|
160
122
|
? closeEmptyElements(xmlEvents(source), options.closeEmptyElements)
|
|
@@ -173,61 +135,6 @@ export function parseXml(source, handlers, options) {
|
|
|
173
135
|
}
|
|
174
136
|
}
|
|
175
137
|
}
|
|
176
|
-
function normalizeLineEndings(chunk) {
|
|
177
|
-
if (!chunk.includes('\r'))
|
|
178
|
-
return chunk;
|
|
179
|
-
return chunk.replace(/\r\n?/g, '\n');
|
|
180
|
-
}
|
|
181
|
-
function firstWhitespace(source) {
|
|
182
|
-
for (let i = 0; i < source.length; i++) {
|
|
183
|
-
const code = source.charCodeAt(i);
|
|
184
|
-
if (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d)
|
|
185
|
-
return i;
|
|
186
|
-
}
|
|
187
|
-
return -1;
|
|
188
|
-
}
|
|
189
|
-
export function localName(qualified) {
|
|
190
|
-
const colon = qualified.indexOf(':');
|
|
191
|
-
return colon === -1 ? qualified : qualified.slice(colon + 1);
|
|
192
|
-
}
|
|
193
|
-
export function boolPresent(val) {
|
|
194
|
-
return val === undefined || (val !== '0' && val !== 'false');
|
|
195
|
-
}
|
|
196
|
-
export function boolStrict(val) {
|
|
197
|
-
return val === '1' || val === 'true';
|
|
198
|
-
}
|
|
199
|
-
export function boolTristate(val) {
|
|
200
|
-
if (val === '1' || val === 'true')
|
|
201
|
-
return true;
|
|
202
|
-
if (val === '0' || val === 'false')
|
|
203
|
-
return false;
|
|
204
|
-
return undefined;
|
|
205
|
-
}
|
|
206
|
-
export function numInteger(val, min = -Number.MAX_SAFE_INTEGER) {
|
|
207
|
-
const n = parseAttrNumber(val);
|
|
208
|
-
if (n === undefined || !Number.isSafeInteger(n) || n < min)
|
|
209
|
-
return undefined;
|
|
210
|
-
return n;
|
|
211
|
-
}
|
|
212
|
-
export function numFinite(val, min = -Infinity) {
|
|
213
|
-
const n = parseAttrNumber(val);
|
|
214
|
-
if (n === undefined || n < min)
|
|
215
|
-
return undefined;
|
|
216
|
-
return n;
|
|
217
|
-
}
|
|
218
|
-
function parseAttrNumber(val) {
|
|
219
|
-
if (val === undefined || val.trim() === '')
|
|
220
|
-
return undefined;
|
|
221
|
-
const n = Number(val);
|
|
222
|
-
return Number.isFinite(n) ? n : undefined;
|
|
223
|
-
}
|
|
224
|
-
export function coerceNumericLiteral(text) {
|
|
225
|
-
const trimmed = text.trim();
|
|
226
|
-
return /^-?\d+(?:\.\d+)?$/.test(trimmed) ? Number(trimmed) : text;
|
|
227
|
-
}
|
|
228
|
-
export function enumToken(val, isMember) {
|
|
229
|
-
return val !== undefined && isMember(val) ? val : undefined;
|
|
230
|
-
}
|
|
231
138
|
export class TextCapture {
|
|
232
139
|
#names;
|
|
233
140
|
#capturing;
|