@shbernal/ts-xlsx 2.0.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/README.md +20 -10
- package/dist/bytes.d.ts +24 -0
- package/dist/bytes.js +34 -0
- package/dist/core/address.d.ts +26 -4
- package/dist/core/address.js +21 -9
- package/dist/core/autofilter.d.ts +12 -1
- package/dist/core/autofilter.js +35 -12
- 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/color-resolution.d.ts +1 -1
- package/dist/core/color-resolution.js +7 -8
- package/dist/core/column.d.ts +4 -1
- package/dist/core/column.js +31 -35
- package/dist/core/conditional-formatting-overlay.d.ts +5 -0
- package/dist/core/conditional-formatting-overlay.js +11 -0
- package/dist/core/conditional-formatting.d.ts +45 -5
- package/dist/core/conditional-formatting.js +74 -0
- package/dist/core/data-validation-overlay.d.ts +11 -0
- package/dist/core/data-validation-overlay.js +21 -2
- package/dist/core/data-validation.d.ts +9 -2
- package/dist/core/data-validation.js +26 -0
- package/dist/core/grid-edits.d.ts +16 -0
- package/dist/core/grid-edits.js +26 -8
- package/dist/core/grid-shift.d.ts +19 -0
- package/dist/core/grid-shift.js +8 -0
- package/dist/core/image.d.ts +2 -0
- package/dist/core/image.js +2 -0
- package/dist/core/merge-index.d.ts +31 -0
- package/dist/core/merge-index.js +60 -0
- package/dist/core/merge.d.ts +12 -11
- package/dist/core/merge.js +36 -10
- package/dist/core/page-setup.d.ts +86 -2
- package/dist/core/page-setup.js +34 -1
- package/dist/core/pivot-table.d.ts +2 -0
- package/dist/core/pivot-table.js +23 -18
- package/dist/core/protection.d.ts +3 -0
- package/dist/core/protection.js +20 -14
- package/dist/core/row-input.d.ts +7 -0
- package/dist/core/row-input.js +10 -8
- package/dist/core/row.d.ts +4 -1
- package/dist/core/row.js +19 -23
- package/dist/core/style.d.ts +78 -7
- package/dist/core/style.js +90 -80
- package/dist/core/table.d.ts +26 -12
- package/dist/core/table.js +67 -33
- package/dist/core/theme.d.ts +3 -1
- package/dist/core/theme.js +4 -0
- package/dist/core/used-extent.d.ts +44 -0
- package/dist/core/used-extent.js +102 -0
- package/dist/core/workbook.d.ts +2 -2
- package/dist/core/worksheet-comments.d.ts +8 -0
- package/dist/core/worksheet-comments.js +18 -0
- package/dist/core/worksheet.d.ts +31 -7
- package/dist/core/worksheet.js +72 -86
- package/dist/customui/ribbon.js +23 -24
- package/dist/entries/core.d.ts +3 -3
- package/dist/entries/csv.d.ts +1 -1
- package/dist/entries/node-unavailable.d.ts +15 -0
- package/dist/entries/node-unavailable.js +20 -0
- package/dist/entries/node.d.ts +1 -0
- package/dist/entries/node.js +1 -0
- package/dist/entries/xlsx.d.ts +0 -1
- package/dist/entries/xlsx.js +0 -1
- package/dist/io/csv/read.js +1 -1
- package/dist/io/csv/write.d.ts +12 -2
- package/dist/io/csv/write.js +17 -2
- package/dist/io/opc/inflate.js +3 -13
- 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 +12 -6
- package/dist/io/opc/rels.js +3 -5
- package/dist/io/style/xf-style.d.ts +33 -4
- package/dist/io/style/xf-style.js +12 -1
- 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 +2 -4
- package/dist/io/xlsx/cell-accumulator.js +20 -58
- package/dist/io/xlsx/cell-value.d.ts +1 -1
- package/dist/io/xlsx/cell-value.js +7 -4
- package/dist/io/xlsx/color-xml.d.ts +1 -1
- package/dist/io/xlsx/color-xml.js +8 -7
- package/dist/io/xlsx/comments.js +6 -3
- package/dist/io/xlsx/conditional-formatting.d.ts +4 -3
- package/dist/io/xlsx/conditional-formatting.js +81 -65
- package/dist/io/xlsx/data-validation.d.ts +10 -8
- package/dist/io/xlsx/data-validation.js +44 -41
- package/dist/io/xlsx/edit-vba.js +4 -11
- package/dist/io/xlsx/hyperlinks.d.ts +4 -2
- package/dist/io/xlsx/hyperlinks.js +26 -31
- package/dist/io/xlsx/images.js +33 -35
- 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 +4 -9
- package/dist/io/xlsx/read-rows.js +12 -13
- package/dist/io/xlsx/read-shared-strings.js +9 -47
- package/dist/io/xlsx/read-styles.d.ts +1 -1
- package/dist/io/xlsx/read-styles.js +65 -43
- package/dist/io/xlsx/read-worksheet.d.ts +10 -1
- package/dist/io/xlsx/read-worksheet.js +48 -63
- package/dist/io/xlsx/read.d.ts +3 -4
- package/dist/io/xlsx/read.js +124 -146
- package/dist/io/xlsx/rich-runs.d.ts +41 -7
- package/dist/io/xlsx/rich-runs.js +95 -30
- package/dist/io/xlsx/sheet-properties.d.ts +1 -1
- package/dist/io/xlsx/sheet-properties.js +23 -26
- package/dist/io/xlsx/styles.d.ts +3 -15
- package/dist/io/xlsx/styles.js +83 -105
- package/dist/io/xlsx/tables.js +14 -13
- package/dist/io/xlsx/theme-xml.js +8 -5
- package/dist/io/xlsx/threaded-comments.js +20 -19
- package/dist/io/xlsx/workbook-xml.js +43 -36
- package/dist/io/xlsx/worksheet-xml.d.ts +1 -1
- package/dist/io/xlsx/worksheet-xml.js +47 -52
- package/dist/io/xlsx/write-stream.d.ts +14 -1
- package/dist/io/xlsx/write-stream.js +40 -33
- package/dist/io/xlsx/write.d.ts +5 -0
- package/dist/io/xlsx/write.js +155 -131
- package/dist/sha512.d.ts +2 -0
- package/dist/sha512.js +146 -0
- package/dist/token-set.d.ts +15 -0
- package/dist/token-set.js +4 -0
- package/dist/vba/bytes.d.ts +1 -2
- package/dist/vba/bytes.js +1 -12
- 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/dir-records.d.ts +53 -0
- package/dist/vba/dir-records.js +28 -0
- package/dist/vba/ms-ovba.js +32 -11
- package/dist/vba/project-editor.js +5 -34
- package/dist/vba/project.js +15 -31
- package/dist/vba/vba-encoding.js +1 -1
- package/dist/xml/xml-read.d.ts +117 -72
- package/dist/xml/xml-read.js +123 -169
- package/dist/xml/xml-scan.d.ts +100 -0
- package/dist/xml/xml-scan.js +208 -0
- package/dist/xml/xml.d.ts +44 -2
- package/dist/xml/xml.js +17 -5
- package/package.json +22 -6
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
|
-
*
|
|
31
|
+
* Capture the verbatim source text of selected elements, in one scan.
|
|
40
32
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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.
|
|
44
38
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* Excel agrees: it reads that cell as `_x0041_`.
|
|
52
|
-
*
|
|
53
|
-
* The decode is unconditional, not a repair of characters XML cannot carry. Excel reads
|
|
54
|
-
* `a_x0009_b` as a tab even though a literal tab would have been perfectly legal there, so a
|
|
55
|
-
* decoder that only handled the illegal range would disagree with Excel on files Excel wrote.
|
|
56
|
-
*/
|
|
57
|
-
export declare function decodeSpreadsheetText(value: string): string;
|
|
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,36 +86,96 @@ 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
|
-
/**
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
*
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
*
|
|
129
|
-
* and
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Gathers one element's character data across the open/text/close events a SAX parse delivers it in.
|
|
126
|
+
*
|
|
127
|
+
* Nine parsers used to open-code this: latch a flag and clear a buffer on the open, append every
|
|
128
|
+
* chunk while latched, consume the buffer and unlatch on the close. Six spellings of one idea, and
|
|
129
|
+
* none of them honoured the one thing {@link SaxHandlers.onOpen} warns about. A self-closing `<x/>`
|
|
130
|
+
* fires no matching close, so `<t/>`, `<text/>`, `<xm:f/>` and `<totalsRowFormula/>`, all legal and
|
|
131
|
+
* all written by real files, latched a capture that nothing would ever close. What kept that from
|
|
132
|
+
* corrupting anything was the order the next open happened to reset things in, which is an accident
|
|
133
|
+
* rather than a property anyone chose, on a path that reads untrusted input. Taking `selfClosing`
|
|
134
|
+
* here makes it structural, once.
|
|
135
|
+
*
|
|
136
|
+
* The other thing the open-coded versions disagreed on is what an unrelated element opening
|
|
137
|
+
* mid-capture should do. Ending the capture is never what a caller wants: the text belongs to the
|
|
138
|
+
* 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 an capture in progress alone, and {@link close} answers only for
|
|
140
|
+
* the element that started it.
|
|
141
|
+
*
|
|
142
|
+
* Decoding stays outside. A `<t>` needs `decodeSpreadsheetText` over the whole element and never
|
|
143
|
+
* over a chunk, an `<xm:f>` needs nothing, and a coordinate needs a number: the caller knows which.
|
|
144
|
+
*/
|
|
145
|
+
export declare class TextCapture {
|
|
146
|
+
#private;
|
|
147
|
+
/** @param names the element local name, or the set of names this instance may capture. */
|
|
148
|
+
constructor(names: string | Iterable<string>);
|
|
149
|
+
/** Whether a capture is currently open. */
|
|
150
|
+
get capturing(): boolean;
|
|
151
|
+
/** Begin capturing `local` if it is one of this instance's names and is not self-closing. */
|
|
152
|
+
open(local: string, selfClosing: boolean): void;
|
|
153
|
+
/** Feed a chunk of character data; ignored when no capture is open. */
|
|
154
|
+
text(chunk: string): void;
|
|
155
|
+
/** The gathered text when `local` closes the captured element, else `undefined`. Unlatches. */
|
|
156
|
+
close(local: string): string | undefined;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Yield each named element's text as that element closes, as `{local, text}`.
|
|
160
|
+
*
|
|
161
|
+
* The third member of the pull-shaped family beside {@link openElements} ("scan opens, read
|
|
162
|
+
* attributes") and {@link closeEmptyElements}: this one is "capture these elements' text, tell me
|
|
163
|
+
* each as it closes". A parser whose whole job is reading a handful of text elements out of a part
|
|
164
|
+
* writes a `for..of` over it instead of a {@link parseXml} handler triple whose open and text arms
|
|
165
|
+
* are the same three lines every time.
|
|
166
|
+
*
|
|
167
|
+
* It is deliberately not for every {@link TextCapture} caller. A parser that interleaves capture
|
|
168
|
+
* with per-element state of its own (a `<dataValidation>` gathering formulae, a `<tableColumn>`
|
|
169
|
+
* attaching a totals formula to the column it is inside) needs the open and attribute events too,
|
|
170
|
+
* and stays bespoke; forcing it through here would trade a handler triple for a second pass.
|
|
171
|
+
*
|
|
172
|
+
* A self-closing `<x/>` carries no text and fires no close, so it yields nothing, which is the
|
|
173
|
+
* behaviour {@link TextCapture} exists to make structural rather than a branch each caller
|
|
174
|
+
* remembers.
|
|
175
|
+
*
|
|
176
|
+
* Throws {@link XmlParseError} on malformed markup.
|
|
177
|
+
*/
|
|
178
|
+
export declare function capturedText(source: string, names: string | Iterable<string>): Generator<{
|
|
179
|
+
local: string;
|
|
180
|
+
text: string;
|
|
181
|
+
}>;
|
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;
|
|
13
|
+
if (lt === -1)
|
|
14
|
+
break;
|
|
15
|
+
const markup = markupAt(source, lt);
|
|
16
|
+
if (markup !== undefined) {
|
|
17
|
+
i = markup.next;
|
|
99
18
|
continue;
|
|
100
19
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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;
|
|
114
40
|
continue;
|
|
115
41
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
42
|
+
const { selfClosing } = tag;
|
|
43
|
+
if (capture !== undefined) {
|
|
44
|
+
if (!selfClosing && local === capture.local)
|
|
45
|
+
capture.depth += 1;
|
|
119
46
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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;
|
|
70
|
+
}
|
|
71
|
+
if (capture !== undefined) {
|
|
72
|
+
throw new XmlParseError(`unterminated <${capture.local}> element`);
|
|
134
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,55 +135,47 @@ export function parseXml(source, handlers, options) {
|
|
|
173
135
|
}
|
|
174
136
|
}
|
|
175
137
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
138
|
+
export class TextCapture {
|
|
139
|
+
#names;
|
|
140
|
+
#capturing;
|
|
141
|
+
#text = '';
|
|
142
|
+
constructor(names) {
|
|
143
|
+
this.#names = new Set(typeof names === 'string' ? [names] : names);
|
|
144
|
+
}
|
|
145
|
+
get capturing() {
|
|
146
|
+
return this.#capturing !== undefined;
|
|
147
|
+
}
|
|
148
|
+
open(local, selfClosing) {
|
|
149
|
+
if (selfClosing || !this.#names.has(local))
|
|
150
|
+
return;
|
|
151
|
+
this.#capturing = local;
|
|
152
|
+
this.#text = '';
|
|
153
|
+
}
|
|
154
|
+
text(chunk) {
|
|
155
|
+
if (this.#capturing !== undefined)
|
|
156
|
+
this.#text += chunk;
|
|
157
|
+
}
|
|
158
|
+
close(local) {
|
|
159
|
+
if (this.#capturing !== local)
|
|
160
|
+
return undefined;
|
|
161
|
+
this.#capturing = undefined;
|
|
162
|
+
return this.#text;
|
|
186
163
|
}
|
|
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
164
|
}
|
|
224
|
-
export function
|
|
225
|
-
const
|
|
226
|
-
|
|
165
|
+
export function* capturedText(source, names) {
|
|
166
|
+
const capture = new TextCapture(names);
|
|
167
|
+
for (const event of xmlEvents(source)) {
|
|
168
|
+
if (event.kind === 'open') {
|
|
169
|
+
capture.open(localName(event.name), event.selfClosing);
|
|
170
|
+
}
|
|
171
|
+
else if (event.kind === 'text') {
|
|
172
|
+
capture.text(event.text);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const local = localName(event.name);
|
|
176
|
+
const text = capture.close(local);
|
|
177
|
+
if (text !== undefined)
|
|
178
|
+
yield { local, text };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
227
181
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export interface XmlAttributes {
|
|
2
|
+
readonly [name: string]: string;
|
|
3
|
+
}
|
|
4
|
+
export type XmlEvent = {
|
|
5
|
+
readonly kind: 'open';
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly attrs: XmlAttributes;
|
|
8
|
+
readonly selfClosing: boolean;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'text';
|
|
11
|
+
readonly text: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: 'close';
|
|
14
|
+
readonly name: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Decode XML character references and the five predefined entities. An unrecognised
|
|
18
|
+
* `&name;` is left verbatim rather than expanded: there is no DTD, so there is nothing
|
|
19
|
+
* to expand it to, and refusing to invent one is what makes entity-expansion attacks
|
|
20
|
+
* impossible.
|
|
21
|
+
*/
|
|
22
|
+
export declare function decodeEntities(value: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* The SpreadsheetML `_xHHHH_` escape, in the only place it may appear: a complete cell-text value.
|
|
25
|
+
*
|
|
26
|
+
* The mirror of `escapeSpreadsheetText` in `./xml.ts`, and it sits here rather than beside it for
|
|
27
|
+
* the same reason `decodeEntities` sits apart from `escapeText`: the write helpers carry an
|
|
28
|
+
* `AuthoringError` and a whole serialisation vocabulary the reader has no business importing.
|
|
29
|
+
*
|
|
30
|
+
* **One left-to-right pass, and that is load-bearing.** `005F` maps to `_` like any other code
|
|
31
|
+
* point, with no special case, because a single pass already gives the underscore escape its
|
|
32
|
+
* meaning: in `_x005F_x0041_` the match at 0 yields `_` and scanning resumes at `x0041_`, which has
|
|
33
|
+
* no leading underscore left to start an escape. So the value reads back as the literal seven
|
|
34
|
+
* characters `_x0041_` the author wrote. Decoding `_x005F_` in a pass of its own, before or after
|
|
35
|
+
* the rest, collapses that to `A` and loses the distinction the encoder went to trouble to keep.
|
|
36
|
+
* Excel agrees: it reads that cell as `_x0041_`.
|
|
37
|
+
*
|
|
38
|
+
* The decode is unconditional, not a repair of characters XML cannot carry. Excel reads
|
|
39
|
+
* `a_x0009_b` as a tab even though a literal tab would have been perfectly legal there, so a
|
|
40
|
+
* decoder that only handled the illegal range would disagree with Excel on files Excel wrote.
|
|
41
|
+
*/
|
|
42
|
+
export declare function decodeSpreadsheetText(value: string): string;
|
|
43
|
+
export declare function parseAttributes(source: string): XmlAttributes;
|
|
44
|
+
export type Markup = {
|
|
45
|
+
readonly kind: 'comment' | 'pi' | 'declaration';
|
|
46
|
+
readonly next: number;
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: 'cdata';
|
|
49
|
+
readonly contentStart: number;
|
|
50
|
+
readonly contentEnd: number;
|
|
51
|
+
readonly next: number;
|
|
52
|
+
};
|
|
53
|
+
export declare function markupAt(source: string, lt: number): Markup | undefined;
|
|
54
|
+
export interface Tag {
|
|
55
|
+
readonly close: boolean;
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly attrSource: string;
|
|
58
|
+
readonly selfClosing: boolean;
|
|
59
|
+
/** One past the tag's `>`, so a caller capturing verbatim source can slice up to it. */
|
|
60
|
+
readonly next: number;
|
|
61
|
+
}
|
|
62
|
+
export declare function tagAt(source: string, lt: number): Tag;
|
|
63
|
+
/**
|
|
64
|
+
* Scan an XML document as a *pull* stream of {@link XmlEvent}s in a single O(n) pass with no
|
|
65
|
+
* recursion. This is the parser's core; {@link parseXml} is a thin push adapter over it. A
|
|
66
|
+
* consumer that must produce output incrementally (the streaming row reader) pulls events and
|
|
67
|
+
* yields as it goes, holding only its own running state; a push callback cannot.
|
|
68
|
+
*
|
|
69
|
+
* Throws {@link XmlParseError} on malformed markup.
|
|
70
|
+
*/
|
|
71
|
+
export declare function xmlEvents(source: string): Generator<XmlEvent>;
|
|
72
|
+
/** Strip a namespace prefix from a qualified name (`r:id` → `id`, `sheet` → `sheet`). */
|
|
73
|
+
export declare function localName(qualified: string): string;
|
|
74
|
+
/** An OOXML boolean that is on when present with no value (`<b/>` is bold) and off only on an
|
|
75
|
+
* explicit `"0"`/`"false"`; absence reads as on. */
|
|
76
|
+
export declare function boolPresent(val: string | undefined): boolean;
|
|
77
|
+
/** An OOXML boolean that is on only when explicitly `"1"`/`"true"`; anything else, including
|
|
78
|
+
* absence and a truthy-looking `"0"`, is off. */
|
|
79
|
+
export declare function boolStrict(val: string | undefined): boolean;
|
|
80
|
+
/** An optional OOXML boolean: `undefined` when the attribute is absent or carries an unrecognised
|
|
81
|
+
* token, otherwise its `"1"`/`"true"` vs `"0"`/`"false"` value. Lets a caller store only the
|
|
82
|
+
* attributes the source actually carried, so a re-write stays byte-clean. */
|
|
83
|
+
export declare function boolTristate(val: string | undefined): boolean | undefined;
|
|
84
|
+
/** An OOXML integer attribute at or above `min` (default: unbounded below); `undefined` when the
|
|
85
|
+
* attribute is absent, blank, fractional, not a number, or below the floor. Integers past
|
|
86
|
+
* `Number.MAX_SAFE_INTEGER` read as `undefined` too: no index or count is usable out there, and
|
|
87
|
+
* arithmetic on one silently lies. */
|
|
88
|
+
export declare function numInteger(val: string | undefined, min?: number): number | undefined;
|
|
89
|
+
/** An OOXML decimal attribute at or above `min` (default: unbounded below); `undefined` when the
|
|
90
|
+
* attribute is absent, blank, not a number, or below the floor. Infinities are not finite numbers
|
|
91
|
+
* and read as `undefined`. */
|
|
92
|
+
export declare function numFinite(val: string | undefined, min?: number): number | undefined;
|
|
93
|
+
/** Read an operand's text as a number only when it is a canonical decimal literal (optional sign,
|
|
94
|
+
* digits, optional fraction). A cell reference, defined name, expression, or exotically-spelled
|
|
95
|
+
* number (`1E5`, hex) keeps its verbatim text, so it is neither coerced to `NaN` and lost nor
|
|
96
|
+
* re-spelled into a number that would not re-write byte-clean. Callers layer their own type rules
|
|
97
|
+
* (a data-validation `list`/`custom` operand stays a string regardless of what it looks like). */
|
|
98
|
+
export declare function coerceNumericLiteral(text: string): string | number;
|
|
99
|
+
/** Narrow an enumerated attribute through its guard; `undefined` when absent or not a member. */
|
|
100
|
+
export declare function enumToken<T extends string>(val: string | undefined, isMember: (candidate: string) => candidate is T): T | undefined;
|