toolcraft 0.0.789 → 0.0.791

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/composition.json CHANGED
@@ -138,7 +138,7 @@
138
138
  },
139
139
  {
140
140
  "name": "toolcraft",
141
- "version": "0.0.789",
141
+ "version": "0.0.791",
142
142
  "license": "MIT"
143
143
  },
144
144
  {
@@ -148,7 +148,7 @@
148
148
  },
149
149
  {
150
150
  "name": "toolcraft-schema",
151
- "version": "0.0.789",
151
+ "version": "0.0.791",
152
152
  "license": "MIT"
153
153
  },
154
154
  {
@@ -138,7 +138,7 @@
138
138
  },
139
139
  {
140
140
  "name": "toolcraft",
141
- "version": "0.0.789",
141
+ "version": "0.0.791",
142
142
  "license": "MIT"
143
143
  },
144
144
  {
@@ -148,7 +148,7 @@
148
148
  },
149
149
  {
150
150
  "name": "toolcraft-schema",
151
- "version": "0.0.789",
151
+ "version": "0.0.791",
152
152
  "license": "MIT"
153
153
  },
154
154
  {
@@ -9,7 +9,7 @@ export * from "./fs/webdav/index.js";
9
9
  export * from "./bridge/index.js";
10
10
  export * from "./python/index.js";
11
11
  export { parseXml, parseXmlSteps, parseXmlSourceSteps, normalizeXmlChunks, parseXmlStream, XmlLimitError } from "./xml.js";
12
- export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from "./xml.js";
12
+ export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits, XmlParserFrame, XmlFrameRequest } from "./xml.js";
13
13
  export * from "./contracts/object.js";
14
14
  export { ObjectAuthority } from "./fs/object-authority.js";
15
15
  export * from "./fs/s3/index.js";
@@ -54,3 +54,28 @@ limits)`: numbers are work checkpoints; read requests specify UTF-16 `offset` an
54
54
  iterator. The source must already be validated and normalized. This supports the
55
55
  same repairs and events without a complete source string; tokens and open ancestry
56
56
  still remain resident. Close the iterator and backing on errors or cancellation.
57
+
58
+ For tree-free external parsing, set `storeFrames: true` alongside `retainTree: false`.
59
+ The iterator also yields `frameOperation` requests: `push` saves the supplied frame,
60
+ `pop` removes the latest frame, and `peek` needs its `frame` filled with the saved
61
+ metadata. Keep the host stack in bounded backing storage. The parser caches one
62
+ current frame; individual frame metadata and namespace scopes still have their own
63
+ size cost. The default string and stream APIs retain their existing ancestry behavior.
64
+
65
+ Pass `undefined` as the source length to parse incrementally. Such read requests
66
+ have `streaming: true` and request at most 512 UTF-16 units. Return any nonempty
67
+ prefix available without pulling more input; set `complete: true` only when that
68
+ read reaches EOF (an empty EOF read is valid). Earlier offsets may be requested
69
+ again, so retain source spans in caller-backed storage. With `normalizeXmlChunks`,
70
+ pass `true` as the second argument to flush at input-chunk boundaries and preserve
71
+ early parser failures. The string parser and known-length protocol are unchanged.
72
+
73
+ With `retainTree: false`, `parseXmlSourceSteps` accepts `fragmentContent: true`
74
+ to emit text, CDATA, comment and processing-instruction bodies in at most 512 UTF-16 units without splitting surrogate
75
+ pairs. Content events with `continuation: true` belong to the preceding logical
76
+ node; store their bodies separately and retain one node identity. Yielded steps
77
+ let the host await each fragment before parsing more. Text validation and entity
78
+ decoding replay bounded source windows before publishing a logical node, retaining
79
+ its diagnostic and limit ordering. Numeric entity references are decoded without
80
+ materializing the token. Names, attribute values, namespace scopes and XML
81
+ declarations still require their own memory budget.
@@ -1,3 +1,3 @@
1
1
  export declare function validCharacter(point: number): boolean;
2
2
  /** Validate scalar values and normalize XML line endings in fixed windows. */
3
- export declare function normalizeXmlChunks(source: AsyncIterable<string> | Iterable<string>): AsyncGenerator<string>;
3
+ export declare function normalizeXmlChunks(source: AsyncIterable<string> | Iterable<string>, flushChunks?: boolean): AsyncGenerator<string>;
@@ -5,7 +5,7 @@ export function validCharacter(point) {
5
5
  || (point >= 0x10000 && point <= 0x10ffff);
6
6
  }
7
7
  /** Validate scalar values and normalize XML line endings in fixed windows. */
8
- export async function* normalizeXmlChunks(source) {
8
+ export async function* normalizeXmlChunks(source, flushChunks = false) {
9
9
  let first = true, afterCR = false, high = 0, output = '';
10
10
  for await (const chunk of source) {
11
11
  for (let index = 0; index < chunk.length; index++) {
@@ -41,6 +41,10 @@ export async function* normalizeXmlChunks(source) {
41
41
  output = '';
42
42
  }
43
43
  }
44
+ if (flushChunks && output) {
45
+ yield output;
46
+ output = '';
47
+ }
44
48
  }
45
49
  if (high)
46
50
  throw new SyntaxError('Invalid XML: invalid character');
@@ -0,0 +1,28 @@
1
+ import type { XmlContent, XmlElement } from './index.js';
2
+ export interface XmlParserFrame {
3
+ element: XmlElement;
4
+ content: XmlContent[] | undefined;
5
+ name: string;
6
+ namespaces: Map<string, string>;
7
+ }
8
+ export type XmlFrameRequest = {
9
+ readonly frameOperation: 'push';
10
+ readonly frame: XmlParserFrame;
11
+ } | {
12
+ readonly frameOperation: 'pop';
13
+ } | {
14
+ readonly frameOperation: 'peek';
15
+ frame?: XmlParserFrame;
16
+ };
17
+ /** External mode caches only the current frame. The host owns the linked stack. */
18
+ export declare class XmlFrames {
19
+ private readonly external;
20
+ private readonly frames;
21
+ private depth;
22
+ private current;
23
+ constructor(external: boolean);
24
+ get length(): number;
25
+ peek(): Generator<XmlFrameRequest, XmlParserFrame | undefined, void>;
26
+ push(frame: XmlParserFrame): Generator<XmlFrameRequest, void, void>;
27
+ pop(): Generator<XmlFrameRequest, XmlParserFrame | undefined, void>;
28
+ }
@@ -0,0 +1,45 @@
1
+ /** External mode caches only the current frame. The host owns the linked stack. */
2
+ export class XmlFrames {
3
+ external;
4
+ frames = [];
5
+ depth = 0;
6
+ current;
7
+ constructor(external) {
8
+ this.external = external;
9
+ }
10
+ get length() { return this.external ? this.depth : this.frames.length; }
11
+ *peek() {
12
+ if (!this.external)
13
+ return this.frames.at(-1);
14
+ if (!this.depth)
15
+ return undefined;
16
+ if (!this.current) {
17
+ const request = { frameOperation: 'peek' };
18
+ yield request;
19
+ if (!request.frame)
20
+ throw new TypeError('Incomplete XML frame read');
21
+ this.current = request.frame;
22
+ }
23
+ return this.current;
24
+ }
25
+ *push(frame) {
26
+ if (!this.external) {
27
+ this.frames.push(frame);
28
+ return;
29
+ }
30
+ yield { frameOperation: 'push', frame };
31
+ this.current = frame;
32
+ this.depth++;
33
+ }
34
+ *pop() {
35
+ if (!this.external)
36
+ return this.frames.pop();
37
+ if (!this.depth)
38
+ return undefined;
39
+ const frame = yield* this.peek();
40
+ yield { frameOperation: 'pop' };
41
+ this.depth--;
42
+ this.current = undefined;
43
+ return frame;
44
+ }
45
+ }
@@ -1,3 +1,5 @@
1
+ import { type XmlFrameRequest } from "./frames.js";
2
+ export type { XmlParserFrame, XmlFrameRequest } from "./frames.js";
1
3
  export { normalizeXmlChunks } from "./characters.js";
2
4
  import { type XmlSourceStep } from "./source.js";
3
5
  export type { XmlSourceRead, XmlSourceStep } from "./source.js";
@@ -51,8 +53,19 @@ export interface XmlStepLimits extends XmlLimits {
51
53
  * false. Drive the step iterator to await external consumers between windows. */
52
54
  readonly events?: (event: XmlStreamEvent) => void;
53
55
  }
56
+ export interface XmlSourceLimits extends XmlStepLimits {
57
+ /** Emit content bodies in bounded fragments, marking continuations of the same logical
58
+ * node. Requires retainTree: false; consumers must preserve fragment identity. */
59
+ readonly fragmentContent?: boolean;
60
+ /** Store active parser frames through host-serviced requests. Requires retainTree: false. */
61
+ readonly storeFrames?: boolean;
62
+ }
63
+ export type XmlParseStep = XmlSourceStep | XmlFrameRequest;
54
64
  /** Parse a validated, BOM-free, line-normalized UTF-16 source via bounded read requests. */
55
- export declare function parseXmlSourceSteps(length: number, limits?: XmlStepLimits): Generator<XmlSourceStep, XmlElement, void>;
65
+ export declare function parseXmlSourceSteps(length: number | undefined, limits?: XmlStepLimits & {
66
+ readonly storeFrames?: false;
67
+ }): Generator<XmlSourceStep, XmlElement, void>;
68
+ export declare function parseXmlSourceSteps(length: number | undefined, limits: XmlSourceLimits): Generator<XmlParseStep, XmlElement, void>;
56
69
  export declare function parseXmlSteps(input: string, limits?: XmlStepLimits): Generator<number, XmlElement, void>;
57
70
  export declare function parseXml(input: string, limits?: XmlLimits): XmlElement;
58
71
  export { parseXmlStream, type XmlStreamLimits, type XmlStreamEvent } from "./stream.js";
@@ -1,3 +1,4 @@
1
+ import { XmlFrames } from "./frames.js";
1
2
  import { validCharacter } from "./characters.js";
2
3
  export { normalizeXmlChunks } from "./characters.js";
3
4
  import { XmlSource } from "./source.js";
@@ -66,6 +67,95 @@ function qualifiedNameSync(name, cache) {
66
67
  }
67
68
  return parts;
68
69
  }
70
+ function* entity(source, start, end, recover) {
71
+ // Inspect only a bounded prefix, even for arbitrarily long numeric references.
72
+ const prefix = typeof source === "string" ? source.slice(start, Math.min(end, start + 4))
73
+ : yield* source.slice(start, Math.min(end, start + 4));
74
+ const predefined = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
75
+ if (end - start <= 4 && Object.hasOwn(predefined, prefix))
76
+ return predefined[prefix];
77
+ const hexadecimal = prefix.startsWith("#x"), digits = start + (hexadecimal ? 2 : 1);
78
+ if (!prefix.startsWith("#") || digits >= end) {
79
+ if (!recover)
80
+ invalid("undeclared entity");
81
+ recover("undeclared entity");
82
+ return "";
83
+ }
84
+ let point = 0;
85
+ for (let index = digits; index < end; index++) {
86
+ const code = typeof source === "string" ? source.charCodeAt(index) : yield* source.charCodeAt(index);
87
+ const digit = code >= 48 && code <= 57 ? code - 48 : hexadecimal && code >= 65 && code <= 70 ? code - 55
88
+ : hexadecimal && code >= 97 && code <= 102 ? code - 87 : -1;
89
+ if (digit < 0)
90
+ invalid("undeclared entity");
91
+ point = point * (hexadecimal ? 16 : 10) + digit;
92
+ if (point > 0x10ffff)
93
+ invalid("invalid character reference");
94
+ if ((index - digits + 1) % 512 === 0)
95
+ yield 512;
96
+ }
97
+ if ((end - digits) % 512)
98
+ yield (end - digits) % 512;
99
+ if (!validCharacter(point))
100
+ invalid("invalid character reference");
101
+ return String.fromCodePoint(point);
102
+ }
103
+ /** Decoded text fragments; source read/work requests remain host-serviced. */
104
+ function* textParts(source, start, end, decode, recover) {
105
+ let cursor = start;
106
+ while (cursor < end) {
107
+ let finish = Math.min(end, cursor + 512);
108
+ if (finish < end) {
109
+ const last = yield* source.charCodeAt(finish - 1);
110
+ if (last >= 0xd800 && last <= 0xdbff)
111
+ finish--;
112
+ }
113
+ const raw = yield* source.slice(cursor, finish);
114
+ const found = decode ? raw.indexOf("&") : -1;
115
+ if (found < 0) {
116
+ yield raw;
117
+ yield raw.length;
118
+ cursor = finish;
119
+ continue;
120
+ }
121
+ const amp = cursor + found;
122
+ if (found) {
123
+ yield raw.slice(0, found);
124
+ yield found;
125
+ }
126
+ const semicolon = yield* source.indexOf(";", amp + 1, end);
127
+ if (semicolon < 0) {
128
+ if (!recover)
129
+ invalid("unterminated entity");
130
+ recover("unterminated entity");
131
+ cursor = amp + 1;
132
+ decode = false;
133
+ continue;
134
+ }
135
+ const part = yield* entity(source, amp + 1, semicolon, recover);
136
+ if (part)
137
+ yield part;
138
+ yield 1;
139
+ cursor = semicolon + 1;
140
+ }
141
+ }
142
+ function* textFragments(source, start, end, decode, recover) {
143
+ let pending = "";
144
+ for (const part of textParts(source, start, end, decode, recover)) {
145
+ if (typeof part !== "string") {
146
+ yield part;
147
+ continue;
148
+ }
149
+ pending += part;
150
+ while (pending.length >= 512) {
151
+ const last = pending.charCodeAt(511), width = last >= 0xd800 && last <= 0xdbff ? 511 : 512;
152
+ yield pending.slice(0, width);
153
+ pending = pending.slice(width);
154
+ }
155
+ }
156
+ if (pending)
157
+ yield pending;
158
+ }
69
159
  function* entities(text, recover) {
70
160
  let result = "";
71
161
  let offset = 0;
@@ -81,39 +171,7 @@ function* entities(text, recover) {
81
171
  recover("unterminated entity");
82
172
  return result + text.slice(start + 1);
83
173
  }
84
- const entity = text.slice(start + 1, end);
85
- const predefined = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
86
- if (Object.hasOwn(predefined, entity))
87
- result += predefined[entity];
88
- else {
89
- const hexadecimal = entity.startsWith("#x");
90
- const digits = entity.slice(hexadecimal ? 2 : 1);
91
- if (!entity.startsWith("#") || !digits.length) {
92
- if (!recover)
93
- invalid("undeclared entity");
94
- recover("undeclared entity");
95
- offset = end + 1;
96
- continue;
97
- }
98
- let point = 0;
99
- for (let index = 0; index < digits.length; index++) {
100
- const code = digits.charCodeAt(index);
101
- const digit = code >= 48 && code <= 57 ? code - 48 : hexadecimal && code >= 65 && code <= 70 ? code - 55
102
- : hexadecimal && code >= 97 && code <= 102 ? code - 87 : -1;
103
- if (digit < 0)
104
- invalid("undeclared entity");
105
- point = point * (hexadecimal ? 16 : 10) + digit;
106
- if (point > 0x10ffff)
107
- invalid("invalid character reference");
108
- if ((index + 1) % 512 === 0)
109
- yield 512;
110
- }
111
- if (digits.length % 512)
112
- yield digits.length % 512;
113
- if (!validCharacter(point))
114
- invalid("invalid character reference");
115
- result += String.fromCodePoint(point);
116
- }
174
+ result += yield* entity(text, start + 1, end, recover);
117
175
  offset = end + 1;
118
176
  }
119
177
  return result;
@@ -189,9 +247,12 @@ function validateLimits(limits) {
189
247
  if (limits.events && limits.retainTree !== false)
190
248
  throw new TypeError("XML events require retainTree: false");
191
249
  }
192
- /** Parse a validated, BOM-free, line-normalized UTF-16 source via bounded read requests. */
193
250
  export function* parseXmlSourceSteps(length, limits = {}) {
194
251
  validateLimits(limits);
252
+ if (limits.storeFrames && limits.retainTree !== false)
253
+ throw new TypeError("Stored XML frames require retainTree: false");
254
+ if (limits.fragmentContent && limits.retainTree !== false)
255
+ throw new TypeError("XML content fragments require retainTree: false");
195
256
  const source = new XmlSource(length);
196
257
  const maxDepth = limits.maxDepth ?? Infinity;
197
258
  const maxNodes = limits.maxNodes ?? Infinity;
@@ -212,7 +273,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
212
273
  throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
213
274
  textLength += text.length;
214
275
  };
215
- const stack = [];
276
+ const stack = new XmlFrames(limits.storeFrames === true);
216
277
  let root;
217
278
  const prolog = [];
218
279
  const epilog = [];
@@ -230,7 +291,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
230
291
  let pendingWork = 0;
231
292
  const skipWhitespace = function* () {
232
293
  const start = offset;
233
- while (offset < source.length) {
294
+ while (yield* source.has(offset)) {
234
295
  const c = (yield* source.charCodeAt(offset));
235
296
  if (c !== 32 && c !== 9 && c !== 10 && c !== 13)
236
297
  break;
@@ -245,7 +306,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
245
306
  };
246
307
  const scanName = function* () {
247
308
  const start = offset;
248
- while (offset < source.length) {
309
+ while (yield* source.has(offset)) {
249
310
  const c = (yield* source.charCodeAt(offset));
250
311
  if (c === 32 || c === 9 || c === 13 || c === 10 || c === 47 || c === 61 || c === 62 || c === 63)
251
312
  break;
@@ -265,7 +326,34 @@ export function* parseXmlSourceSteps(length, limits = {}) {
265
326
  const [prefix, localName] = qualifiedNameSync(name, qualifiedNames);
266
327
  return [name, prefix, localName];
267
328
  };
268
- while (offset < source.length) {
329
+ const rawContent = function* (kind, start, end, parent, target = "") {
330
+ if (end - start > maxTextLength - textLength)
331
+ throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
332
+ textLength += end - start;
333
+ admitContent();
334
+ let cursor = start, continuation = false;
335
+ do {
336
+ let finish = limits.fragmentContent ? Math.min(end, cursor + 512) : end;
337
+ if (finish < end) {
338
+ const last = yield* source.charCodeAt(finish - 1);
339
+ if (last >= 0xd800 && last <= 0xdbff)
340
+ finish--;
341
+ }
342
+ const text = yield* source.slice(cursor, finish);
343
+ const content = kind === "processing-instruction" ? { kind, target, text } : { kind, text };
344
+ if (kind === "cdata" && retainTree)
345
+ parent.element.text += text;
346
+ if (retain)
347
+ (parent?.content ?? (root ? epilog : prolog)).push(content);
348
+ limits.events?.({ type: "content", content, parent: parent?.element,
349
+ ...(continuation ? { continuation: true } : {}) });
350
+ if (limits.fragmentContent)
351
+ yield Math.max(1, text.length);
352
+ continuation = true;
353
+ cursor = finish;
354
+ } while (cursor < end);
355
+ };
356
+ while (yield* source.has(offset)) {
269
357
  pendingWork += 1;
270
358
  if (pendingWork >= 512) {
271
359
  while (pendingWork >= 512) {
@@ -276,6 +364,49 @@ export function* parseXmlSourceSteps(length, limits = {}) {
276
364
  if ((yield* source.charCodeAt(offset)) !== 60) {
277
365
  const next = (yield* source.indexOf("<", offset));
278
366
  const endPos = next < 0 ? source.length : next;
367
+ if (limits.fragmentContent) {
368
+ if ((yield* source.indexOf("]]>", offset, endPos)) >= 0)
369
+ invalid("CDATA terminator in text");
370
+ const parent = yield* stack.peek();
371
+ let size = 0, outsideText = false;
372
+ // Validate before emitting any part of the logical node. This preserves
373
+ // entity/recovery diagnostics and limit precedence without retaining it.
374
+ for (const part of textFragments(source, offset, endPos, !!parent, limits.recover)) {
375
+ if (typeof part !== "string") {
376
+ yield part;
377
+ continue;
378
+ }
379
+ size += part.length;
380
+ if (!parent)
381
+ for (const character of part) {
382
+ if (!" \t\r\n".includes(character))
383
+ outsideText = true;
384
+ }
385
+ }
386
+ if (size > maxTextLength - textLength)
387
+ throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
388
+ textLength += size;
389
+ if (outsideText)
390
+ invalid("text outside the root");
391
+ if (size) {
392
+ admitContent();
393
+ if (limits.events) {
394
+ let continuation = false;
395
+ for (const part of textFragments(source, offset, endPos, !!parent, limits.recover ? () => { } : undefined)) {
396
+ if (typeof part !== "string") {
397
+ yield part;
398
+ continue;
399
+ }
400
+ limits.events({ type: "content", content: { kind: "text", text: part }, parent: parent?.element,
401
+ ...(continuation ? { continuation: true } : {}) });
402
+ continuation = true;
403
+ yield 1;
404
+ }
405
+ }
406
+ }
407
+ offset = endPos;
408
+ continue;
409
+ }
279
410
  const text = (yield* source.slice(offset, endPos));
280
411
  pendingWork += (endPos - offset) + text.length;
281
412
  while (pendingWork >= 512) {
@@ -290,7 +421,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
290
421
  pendingWork -= 512;
291
422
  }
292
423
  admitText(resolved);
293
- const parent = stack.at(-1);
424
+ const parent = (yield* stack.peek());
294
425
  if (parent) {
295
426
  if (retainTree)
296
427
  parent.element.text += resolved;
@@ -327,17 +458,10 @@ export function* parseXmlSourceSteps(length, limits = {}) {
327
458
  }
328
459
  else if ((yield* source.startsWith("<!--", offset))) {
329
460
  const end = yield* find(source, "-->", offset + 4);
330
- const text = end < 0 ? "" : yield* source.slice(offset + 4, end);
331
- if (end < 0 || (yield* find(text, "--", 0)) >= 0 || text.endsWith("-")) {
461
+ if (end < 0 || (yield* source.indexOf("--", offset + 4, end)) >= 0
462
+ || (end > offset + 4 && (yield* source.charCodeAt(end - 1)) === 45))
332
463
  invalid("malformed comment");
333
- }
334
- const parent = stack.at(-1);
335
- admitText(text);
336
- admitContent();
337
- if (retain) {
338
- (parent?.content ?? (root ? epilog : prolog)).push({ kind: "comment", text });
339
- }
340
- limits.events?.({ type: "content", content: { kind: "comment", text }, parent: parent?.element });
464
+ yield* rawContent("comment", offset + 4, end, yield* stack.peek());
341
465
  offset = end + 3;
342
466
  }
343
467
  else if ((yield* source.startsWith("<![CDATA[", offset))) {
@@ -346,15 +470,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
346
470
  const end = yield* find(source, "]]>", offset + 9);
347
471
  if (end < 0)
348
472
  invalid("unterminated CDATA");
349
- const cdataText = (yield* source.slice(offset + 9, end));
350
- admitText(cdataText);
351
- const parent = stack.at(-1);
352
- if (retainTree)
353
- parent.element.text += cdataText;
354
- admitContent();
355
- if (retain)
356
- parent.content.push({ kind: "cdata", text: cdataText });
357
- limits.events?.({ type: "content", content: { kind: "cdata", text: cdataText }, parent: parent.element });
473
+ yield* rawContent("cdata", offset + 9, end, yield* stack.peek());
358
474
  offset = end + 3;
359
475
  }
360
476
  else if ((yield* source.startsWith("<?", offset))) {
@@ -368,34 +484,31 @@ export function* parseXmlSourceSteps(length, limits = {}) {
368
484
  const end = yield* find(source, "?>", offset);
369
485
  if (end < 0)
370
486
  invalid("unterminated processing instruction");
371
- const content = (yield* source.slice(offset, end));
372
487
  if (target.length === 3 && target.toLowerCase() === "xml") {
488
+ const content = yield* source.slice(offset, end);
373
489
  if (start !== 0 || target !== "xml"
374
- || !(yield* validDeclaration(content, limits.expectedEncoding))) {
490
+ || !(yield* validDeclaration(content, limits.expectedEncoding)))
375
491
  invalid("unsupported XML declaration");
376
- }
377
492
  if (retainContent)
378
- declaration = (yield* source.slice(start, end + 2));
493
+ declaration = yield* source.slice(start, end + 2);
379
494
  }
380
- else if (content && !" \t\n\r".includes(content[0]))
381
- invalid("invalid processing instruction");
382
- if (!(target.length === 3 && target.toLowerCase() === "xml")) {
383
- const parent = stack.at(-1);
384
- let wsStart = 0;
385
- while (wsStart < content.length && " \t\n\r".includes(content[wsStart]))
386
- wsStart++;
387
- pendingWork += wsStart;
495
+ else {
496
+ if (offset < end && !" \t\n\r".includes(yield* source.slice(offset, offset + 1)))
497
+ invalid("invalid processing instruction");
498
+ let body = offset;
499
+ while (body < end && " \t\n\r".includes(yield* source.slice(body, body + 1))) {
500
+ body++;
501
+ pendingWork++;
502
+ if (pendingWork >= 512) {
503
+ yield 512;
504
+ pendingWork -= 512;
505
+ }
506
+ }
388
507
  while (pendingWork >= 512) {
389
508
  yield 512;
390
509
  pendingWork -= 512;
391
510
  }
392
- const text = content.slice(wsStart);
393
- admitText(text);
394
- admitContent();
395
- if (retain) {
396
- (parent?.content ?? (root ? epilog : prolog)).push({ kind: "processing-instruction", target, text });
397
- }
398
- limits.events?.({ type: "content", content: { kind: "processing-instruction", target, text }, parent: parent?.element });
511
+ yield* rawContent("processing-instruction", body, end, yield* stack.peek(), target);
399
512
  }
400
513
  offset = end + 2;
401
514
  }
@@ -410,14 +523,14 @@ export function* parseXmlSourceSteps(length, limits = {}) {
410
523
  yield 512;
411
524
  pendingWork -= 512;
412
525
  }
413
- const closed = (yield* source.slice(offset++, offset)) === ">" ? stack.pop() : undefined;
526
+ const closed = (yield* source.slice(offset++, offset)) === ">" ? (yield* stack.pop()) : undefined;
414
527
  if (closed?.name !== name) {
415
528
  if (!limits.recover)
416
529
  invalid("mismatched closing tag");
417
530
  limits.recover("mismatched closing tag");
418
531
  }
419
532
  if (closed)
420
- limits.events?.({ type: "close", element: closed.element, parent: stack.at(-1)?.element });
533
+ limits.events?.({ type: "close", element: closed.element, parent: (yield* stack.peek())?.element });
421
534
  }
422
535
  else {
423
536
  offset++;
@@ -436,7 +549,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
436
549
  }
437
550
  let attributes;
438
551
  const attrMeta = [];
439
- let namespaces = stack.at(-1)?.namespaces ?? new Map([["xml", xmlNamespace]]);
552
+ let namespaces = (yield* stack.peek())?.namespaces ?? new Map([["xml", xmlNamespace]]);
440
553
  let ownsNamespaces = stack.length === 0;
441
554
  while (!repeated) {
442
555
  const ws = (yield* skipWhitespace());
@@ -543,7 +656,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
543
656
  if (stack.length + 1 > maxDepth)
544
657
  throw new XmlLimitError("maxDepth", "XML resource limit exceeded");
545
658
  const namespace = namespaces.get(prefix) ?? "";
546
- limits.onElement?.({ name, namespace, localName }, stack.at(-1)?.element, stack.length + 1);
659
+ limits.onElement?.({ name, namespace, localName }, (yield* stack.peek())?.element, stack.length + 1);
547
660
  admitContent();
548
661
  const retainedAttributes = [];
549
662
  if (retainContent && attributes) {
@@ -562,7 +675,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
562
675
  }
563
676
  const content = retain ? [] : undefined;
564
677
  const element = { kind: "element", name, namespace, localName, children: [], text: "", content: content ?? emptyContent, attributes: retainContent ? retainedAttributes : emptyAttributes, namespaces: retainContent ? namespaces : emptyNamespaces, ...(root === undefined && retainContent ? { prolog, epilog, ...(declaration === undefined ? {} : { declaration }) } : {}) };
565
- const parent = stack.at(-1);
678
+ const parent = (yield* stack.peek());
566
679
  if (parent) {
567
680
  if (retainTree) {
568
681
  parent.element.children.push(element);
@@ -587,7 +700,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
587
700
  previousEmpty = { suffix: name + "/>", name, prefix, localName };
588
701
  limits.events?.({ type: "open", element, parent: parent?.element });
589
702
  if (!empty)
590
- stack.push({ element, content, name, namespaces });
703
+ yield* stack.push({ element, content, name, namespaces });
591
704
  else
592
705
  limits.events?.({ type: "close", element, parent: parent?.element });
593
706
  }
@@ -603,8 +716,8 @@ export function* parseXmlSourceSteps(length, limits = {}) {
603
716
  invalid("incomplete document");
604
717
  limits.recover("incomplete document");
605
718
  while (stack.length) {
606
- const closed = stack.pop();
607
- limits.events?.({ type: "close", element: closed.element, parent: stack.at(-1)?.element });
719
+ const closed = (yield* stack.pop());
720
+ limits.events?.({ type: "close", element: closed.element, parent: (yield* stack.peek())?.element });
608
721
  yield 1;
609
722
  }
610
723
  }
@@ -1,18 +1,24 @@
1
- /** Fill value with exactly the requested UTF-16 span before advancing the parser. */
1
+ /** Fill value before advancing. Streaming reads may be short; mark complete at EOF. */
2
2
  export interface XmlSourceRead {
3
3
  readonly offset: number;
4
4
  readonly length: number;
5
+ readonly streaming?: boolean;
5
6
  value?: string;
7
+ complete?: boolean;
6
8
  }
7
9
  export type XmlSourceStep = number | XmlSourceRead;
8
10
  /** One fixed window; individual returned tokens still have their own size cost. */
9
11
  export declare class XmlSource {
10
- readonly length: number;
11
12
  private window;
12
13
  private start;
13
- constructor(length: number);
14
+ private end;
15
+ private readonly streaming;
16
+ constructor(length: number | undefined);
17
+ get length(): number;
18
+ private load;
19
+ has(offset: number): Generator<XmlSourceStep, boolean, void>;
14
20
  slice(start: number, end?: number): Generator<XmlSourceStep, string, void>;
15
21
  charCodeAt(offset: number): Generator<XmlSourceStep, number, void>;
16
22
  startsWith(value: string, offset: number): Generator<XmlSourceStep, boolean, void>;
17
- indexOf(value: string, offset: number): Generator<XmlSourceStep, number, void>;
23
+ indexOf(value: string, offset: number, end?: number): Generator<XmlSourceStep, number, void>;
18
24
  }
@@ -1,27 +1,44 @@
1
1
  /** One fixed window; individual returned tokens still have their own size cost. */
2
2
  export class XmlSource {
3
- length;
4
3
  window = '';
5
4
  start = -1;
5
+ end;
6
+ streaming;
6
7
  constructor(length) {
7
- this.length = length;
8
- if (!Number.isSafeInteger(length) || length < 0)
9
- throw new RangeError("Invalid XML source length");
8
+ if (length !== undefined && (!Number.isSafeInteger(length) || length < 0))
9
+ throw new RangeError('Invalid XML source length');
10
+ this.streaming = length === undefined;
11
+ this.end = length ?? Infinity;
12
+ }
13
+ get length() { return this.end; }
14
+ *load(offset) {
15
+ if (offset >= this.end || offset >= this.start && offset < this.start + this.window.length)
16
+ return;
17
+ const start = this.streaming ? offset : Math.floor(offset / 4096) * 4096;
18
+ const request = { offset: start, length: Math.min(this.streaming ? 512 : 4096, this.end - start),
19
+ ...(this.streaming ? { streaming: true } : {}) };
20
+ yield request;
21
+ if (typeof request.value !== 'string' || request.value.length > request.length ||
22
+ (!this.streaming && request.value.length !== request.length) ||
23
+ (!request.value.length && !request.complete && this.streaming))
24
+ throw new TypeError('Incomplete XML source read');
25
+ if (this.streaming && request.complete)
26
+ this.end = start + request.value.length;
27
+ this.window = request.value;
28
+ this.start = start;
29
+ }
30
+ *has(offset) {
31
+ yield* this.load(offset);
32
+ return offset < this.end;
10
33
  }
11
34
  *slice(start, end = this.length) {
12
35
  start = start < 0 ? Math.max(0, this.length + start) : Math.min(start, this.length);
13
36
  end = end < 0 ? Math.max(0, this.length + end) : Math.min(end, this.length);
14
37
  let result = '';
15
- while (start < end) {
16
- if (start < this.start || start >= this.start + this.window.length) {
17
- const offset = Math.floor(start / 4096) * 4096;
18
- const read = { offset, length: Math.min(4096, this.length - offset) };
19
- yield read;
20
- if (typeof read.value !== 'string' || read.value.length !== read.length)
21
- throw new TypeError('Incomplete XML source read');
22
- this.window = read.value;
23
- this.start = offset;
24
- }
38
+ while (start < Math.min(end, this.length)) {
39
+ yield* this.load(start);
40
+ if (start >= this.end)
41
+ break;
25
42
  const stop = Math.min(end, this.start + this.window.length);
26
43
  result += this.window.slice(start - this.start, stop - this.start);
27
44
  start = stop;
@@ -32,15 +49,22 @@ export class XmlSource {
32
49
  return (yield* this.slice(offset, offset + 1)).charCodeAt(0);
33
50
  }
34
51
  *startsWith(value, offset) {
35
- return (yield* this.slice(offset, offset + value.length)) === value;
52
+ for (let index = 0; index < value.length; index++) {
53
+ if ((yield* this.charCodeAt(offset + index)) !== value.charCodeAt(index))
54
+ return false;
55
+ }
56
+ return true;
36
57
  }
37
- *indexOf(value, offset) {
38
- while (offset < this.length) {
39
- const end = Math.min(this.length, offset + 4096 + value.length - 1);
40
- const found = (yield* this.slice(offset, end)).indexOf(value);
58
+ *indexOf(value, offset, end = Infinity) {
59
+ let tail = '';
60
+ while (offset < end && (yield* this.has(offset))) {
61
+ const stop = Math.min(end, this.start + this.window.length);
62
+ const part = tail + this.window.slice(offset - this.start, stop - this.start);
63
+ const found = part.indexOf(value);
41
64
  if (found >= 0)
42
- return offset + found;
43
- offset += 4096;
65
+ return offset - tail.length + found;
66
+ tail = value.length > 1 ? part.slice(-(value.length - 1)) : '';
67
+ offset = stop;
44
68
  }
45
69
  return -1;
46
70
  }
@@ -5,6 +5,7 @@ export type XmlStreamEvent = {
5
5
  readonly parent: XmlElement | undefined;
6
6
  } | {
7
7
  readonly type: "content";
8
+ readonly continuation?: boolean;
8
9
  readonly content: Exclude<XmlContent, XmlElement>;
9
10
  readonly parent: XmlElement | undefined;
10
11
  };
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.789",
11
+ "version": "0.0.791",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.789",
3
+ "version": "0.0.791",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.789",
3
+ "version": "0.0.791",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -177,7 +177,7 @@
177
177
  "saxes"
178
178
  ],
179
179
  "optionalDependencies": {
180
- "toolcraft-schema": "0.0.789",
180
+ "toolcraft-schema": "0.0.791",
181
181
  "toolcraft-design": "*",
182
182
  "@poe-code/frontmatter": "*",
183
183
  "@poe-code/agent-mcp-config": "*",