@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.
Files changed (100) hide show
  1. package/dist/core/autofilter.d.ts +1 -1
  2. package/dist/core/autofilter.js +3 -5
  3. package/dist/core/axis-handle.d.ts +8 -0
  4. package/dist/core/axis-handle.js +14 -0
  5. package/dist/core/cell.d.ts +24 -0
  6. package/dist/core/cell.js +9 -1
  7. package/dist/core/column.d.ts +4 -1
  8. package/dist/core/column.js +31 -35
  9. package/dist/core/conditional-formatting.d.ts +5 -5
  10. package/dist/core/conditional-formatting.js +11 -25
  11. package/dist/core/data-validation.d.ts +3 -3
  12. package/dist/core/data-validation.js +7 -15
  13. package/dist/core/image.d.ts +1 -1
  14. package/dist/core/image.js +2 -4
  15. package/dist/core/merge-index.d.ts +31 -0
  16. package/dist/core/merge-index.js +60 -0
  17. package/dist/core/merge.d.ts +0 -10
  18. package/dist/core/merge.js +0 -8
  19. package/dist/core/page-setup.d.ts +72 -2
  20. package/dist/core/page-setup.js +34 -8
  21. package/dist/core/pivot-table.d.ts +1 -1
  22. package/dist/core/pivot-table.js +4 -8
  23. package/dist/core/row.d.ts +4 -1
  24. package/dist/core/row.js +19 -23
  25. package/dist/core/style.d.ts +51 -7
  26. package/dist/core/style.js +29 -34
  27. package/dist/core/table.d.ts +1 -1
  28. package/dist/core/table.js +3 -5
  29. package/dist/core/used-extent.d.ts +44 -0
  30. package/dist/core/used-extent.js +102 -0
  31. package/dist/core/worksheet.d.ts +17 -6
  32. package/dist/core/worksheet.js +48 -42
  33. package/dist/customui/ribbon.js +4 -6
  34. package/dist/io/opc/inflate.js +1 -1
  35. package/dist/io/opc/part-paths.d.ts +1 -0
  36. package/dist/io/opc/part-paths.js +15 -0
  37. package/dist/io/opc/read-opc.d.ts +22 -3
  38. package/dist/io/opc/read-opc.js +11 -39
  39. package/dist/io/opc/rels.d.ts +1 -6
  40. package/dist/io/opc/rels.js +1 -4
  41. package/dist/io/style/xf-style.d.ts +26 -2
  42. package/dist/io/style/xf-style.js +6 -0
  43. package/dist/io/xlsb/read-styles.js +6 -3
  44. package/dist/io/xlsb/read.d.ts +1 -1
  45. package/dist/io/xlsb/read.js +5 -8
  46. package/dist/io/xlsx/cell-accumulator.d.ts +1 -1
  47. package/dist/io/xlsx/cell-accumulator.js +1 -1
  48. package/dist/io/xlsx/cell-value.js +1 -1
  49. package/dist/io/xlsx/color-xml.js +1 -1
  50. package/dist/io/xlsx/comments.js +2 -1
  51. package/dist/io/xlsx/conditional-formatting.d.ts +3 -2
  52. package/dist/io/xlsx/conditional-formatting.js +22 -21
  53. package/dist/io/xlsx/data-validation.d.ts +9 -7
  54. package/dist/io/xlsx/data-validation.js +10 -9
  55. package/dist/io/xlsx/edit-vba.js +4 -11
  56. package/dist/io/xlsx/hyperlinks.d.ts +4 -2
  57. package/dist/io/xlsx/hyperlinks.js +20 -17
  58. package/dist/io/xlsx/images.js +15 -14
  59. package/dist/io/xlsx/package-plan.d.ts +9 -1
  60. package/dist/io/xlsx/package-plan.js +29 -16
  61. package/dist/io/xlsx/part-names.d.ts +41 -0
  62. package/dist/io/xlsx/part-names.js +46 -0
  63. package/dist/io/xlsx/read-pivot.js +2 -1
  64. package/dist/io/xlsx/read-rows.js +12 -13
  65. package/dist/io/xlsx/read-shared-strings.js +2 -1
  66. package/dist/io/xlsx/read-styles.d.ts +1 -1
  67. package/dist/io/xlsx/read-styles.js +63 -42
  68. package/dist/io/xlsx/read-worksheet.d.ts +10 -1
  69. package/dist/io/xlsx/read-worksheet.js +43 -56
  70. package/dist/io/xlsx/read.d.ts +2 -3
  71. package/dist/io/xlsx/read.js +81 -73
  72. package/dist/io/xlsx/rich-runs.d.ts +1 -1
  73. package/dist/io/xlsx/rich-runs.js +2 -1
  74. package/dist/io/xlsx/sheet-properties.js +21 -28
  75. package/dist/io/xlsx/styles.js +23 -20
  76. package/dist/io/xlsx/tables.js +2 -1
  77. package/dist/io/xlsx/theme-xml.js +1 -1
  78. package/dist/io/xlsx/threaded-comments.js +2 -1
  79. package/dist/io/xlsx/workbook-xml.js +29 -28
  80. package/dist/io/xlsx/worksheet-xml.d.ts +1 -1
  81. package/dist/io/xlsx/worksheet-xml.js +14 -16
  82. package/dist/io/xlsx/write-stream.d.ts +6 -0
  83. package/dist/io/xlsx/write-stream.js +30 -22
  84. package/dist/io/xlsx/write.d.ts +5 -0
  85. package/dist/io/xlsx/write.js +155 -131
  86. package/dist/token-set.d.ts +15 -0
  87. package/dist/token-set.js +4 -0
  88. package/dist/vba/cfb-format.d.ts +24 -0
  89. package/dist/vba/cfb-format.js +12 -0
  90. package/dist/vba/cfb-writer.js +17 -17
  91. package/dist/vba/cfb.js +1 -7
  92. package/dist/vba/ms-ovba.js +32 -11
  93. package/dist/vba/project.js +13 -13
  94. package/dist/vba/vba-encoding.js +1 -1
  95. package/dist/xml/xml-read.d.ts +59 -74
  96. package/dist/xml/xml-read.js +83 -176
  97. package/dist/xml/xml-scan.d.ts +100 -0
  98. package/dist/xml/xml-scan.js +208 -0
  99. package/dist/xml/xml.d.ts +1 -1
  100. package/package.json +2 -3
@@ -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[src + i]);
59
- guardOutput(out.length, maxOutput);
56
+ out.push(out.at(src + i));
60
57
  }
61
58
  }
62
59
  pos = chunkEnd;
63
60
  }
64
- return Uint8Array.from(out);
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;
@@ -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: 'legacy',
12
- vbaProjectSignatureAgile: 'agile',
13
- vbaProjectSignatureV3: 'v3',
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[relType.slice(relType.lastIndexOf('/') + 1)];
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 = keyword[line.slice(0, eq)];
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();
@@ -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)`);
@@ -1,6 +1,4 @@
1
- export interface XmlAttributes {
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
- * Decode XML character references and the five predefined entities. An unrecognised
33
- * `&name;` is left verbatim rather than expanded: there is no DTD, so there is nothing
34
- * to expand it to, and refusing to invent one is what makes entity-expansion attacks
35
- * impossible.
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 declare function decodeEntities(value: string): string;
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
- * The SpreadsheetML `_xHHHH_` escape, in the only place it may appear: a complete cell-text value.
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
- * **One left-to-right pass, and that is load-bearing.** `005F` maps to `_` like any other code
46
- * point, with no special case, because a single pass already gives the underscore escape its
47
- * meaning: in `_x005F_x0041_` the match at 0 yields `_` and scanning resumes at `x0041_`, which has
48
- * no leading underscore left to start an escape. So the value reads back as the literal seven
49
- * characters `_x0041_` the author wrote. Decoding `_x005F_` in a pass of its own, before or after
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
- * 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
- * Throws {@link XmlParseError} on malformed markup.
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 xmlEvents(source: string): Generator<XmlEvent>;
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
  *
@@ -1,137 +1,77 @@
1
1
  import { XmlParseError } from './errors.js';
2
- const PREDEFINED_ENTITIES = {
3
- amp: '&',
4
- lt: '<',
5
- gt: '>',
6
- quot: '"',
7
- apos: "'",
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
- const chunk = source.slice(i);
85
- if (chunk.length > 0)
86
- yield { kind: 'text', text: decodeEntities(normalizeLineEndings(chunk)) };
87
- return;
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
- if (source.startsWith('<!', lt)) {
117
- i = skipDeclaration(source, lt);
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 gt = findTagEnd(source, lt);
121
- const raw = source.slice(lt + 1, gt);
122
- if (raw.charCodeAt(0) === 0x2f) {
123
- yield { kind: 'close', name: raw.slice(1).trim() };
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 selfClosing = raw.charCodeAt(raw.length - 1) === 0x2f;
127
- const body = selfClosing ? raw.slice(0, -1) : raw;
128
- const nameEnd = firstWhitespace(body);
129
- const name = nameEnd === -1 ? body : body.slice(0, nameEnd);
130
- const attrs = nameEnd === -1 ? {} : parseAttributes(body.slice(nameEnd));
131
- yield { kind: 'open', name, attrs, selfClosing };
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 = gt + 1;
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;