toolcraft 0.0.788 → 0.0.790

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.788",
141
+ "version": "0.0.790",
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.788",
151
+ "version": "0.0.790",
152
152
  "license": "MIT"
153
153
  },
154
154
  {
@@ -138,7 +138,7 @@
138
138
  },
139
139
  {
140
140
  "name": "toolcraft",
141
- "version": "0.0.788",
141
+ "version": "0.0.790",
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.788",
151
+ "version": "0.0.790",
152
152
  "license": "MIT"
153
153
  },
154
154
  {
@@ -8,8 +8,8 @@ export * from "./fs/object-publication/index.js";
8
8
  export * from "./fs/webdav/index.js";
9
9
  export * from "./bridge/index.js";
10
10
  export * from "./python/index.js";
11
- export { parseXml, parseXmlSteps, parseXmlStream, XmlLimitError } from "./xml.js";
12
- export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from "./xml.js";
11
+ export { parseXml, parseXmlSteps, parseXmlSourceSteps, normalizeXmlChunks, parseXmlStream, XmlLimitError } 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";
@@ -8,7 +8,7 @@ export * from "./fs/object-publication/index.js";
8
8
  export * from "./fs/webdav/index.js";
9
9
  export * from "./bridge/index.js";
10
10
  export * from "./python/index.js";
11
- export { parseXml, parseXmlSteps, parseXmlStream, XmlLimitError } from "./xml.js";
11
+ export { parseXml, parseXmlSteps, parseXmlSourceSteps, normalizeXmlChunks, parseXmlStream, XmlLimitError } from "./xml.js";
12
12
  export * from "./contracts/object.js";
13
13
  export { ObjectAuthority } from "./fs/object-authority.js";
14
14
  export * from "./fs/s3/index.js";
@@ -31,7 +31,7 @@ Resource limits and the prohibition on DTDs remain in force. With `retainTree: f
31
31
  repaired closing events. Drain events between generator checkpoints to await
32
32
  external storage; the source string and open parser frames remain resident.
33
33
 
34
- `parseXmlStream` consumes each chunk before requesting the next and calls the optional asynchronous checkpoint with at most 512 UTF-16 units. It preserves the buffered parser’s tree shape, namespaces and retained content. The source is closed on parsing or checkpoint failure. Decode byte sources incrementally before passing their strings. This avoids a full source string; the resulting tree and individual XML tokens still reside in memory. Recovery remains available through the buffered parser.
34
+ `parseXmlStream` consumes each chunk before requesting the next and calls the optional asynchronous checkpoint with at most 512 UTF-16 units. It preserves the buffered parser’s tree shape, namespaces and retained content. The source is closed on parsing or checkpoint failure. Decode byte sources incrementally before passing their strings. This avoids a full source string; the resulting tree and individual XML tokens still reside in memory. Recovery is available through `parseXmlSteps` or the externally backed source API.
35
35
 
36
36
  For validation or root-name detection, pass `retainTree: false` to `parseXmlStream`. It validates the complete document and enforces the same limits, but returns a root element with empty data fields. Only open element frames, namespace scopes and current parser tokens remain resident; completed descendants, text, attributes and document content are not retained. `onElement` still observes each element.
37
37
 
@@ -45,3 +45,27 @@ window awaits the consumer before advancing. Event elements contain names,
45
45
  attributes and namespace metadata; individual XML tokens and open ancestry still
46
46
  remain resident. This mode lets a caller own document storage independently of
47
47
  the codec. Consumer failures propagate unchanged and close the input iterator.
48
+
49
+ `normalizeXmlChunks(chunks)` validates XML characters and normalizes BOM/line endings
50
+ in windows of at most 513 UTF-16 units, preserving surrogate pairs across chunks.
51
+ Store those windows in caller-owned backing and drive `parseXmlSourceSteps(length,
52
+ limits)`: numbers are work checkpoints; read requests specify UTF-16 `offset` and
53
+ `length` (at most 4096 units). Fill each request's `value` before advancing the
54
+ iterator. The source must already be validated and normalized. This supports the
55
+ same repairs and events without a complete source string; tokens and open ancestry
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.
@@ -0,0 +1,3 @@
1
+ export declare function validCharacter(point: number): boolean;
2
+ /** Validate scalar values and normalize XML line endings in fixed windows. */
3
+ export declare function normalizeXmlChunks(source: AsyncIterable<string> | Iterable<string>, flushChunks?: boolean): AsyncGenerator<string>;
@@ -0,0 +1,53 @@
1
+ export function validCharacter(point) {
2
+ return point === 9 || point === 10 || point === 13
3
+ || (point >= 0x20 && point <= 0xd7ff)
4
+ || (point >= 0xe000 && point <= 0xfffd)
5
+ || (point >= 0x10000 && point <= 0x10ffff);
6
+ }
7
+ /** Validate scalar values and normalize XML line endings in fixed windows. */
8
+ export async function* normalizeXmlChunks(source, flushChunks = false) {
9
+ let first = true, afterCR = false, high = 0, output = '';
10
+ for await (const chunk of source) {
11
+ for (let index = 0; index < chunk.length; index++) {
12
+ const code = chunk.charCodeAt(index);
13
+ if (first) {
14
+ first = false;
15
+ if (code === 0xfeff)
16
+ continue;
17
+ }
18
+ if (high) {
19
+ if (code < 0xdc00 || code > 0xdfff)
20
+ throw new SyntaxError('Invalid XML: invalid character');
21
+ output += String.fromCharCode(high, code);
22
+ high = 0;
23
+ }
24
+ else {
25
+ if (afterCR) {
26
+ afterCR = false;
27
+ if (code === 10)
28
+ continue;
29
+ }
30
+ if (code >= 0xd800 && code <= 0xdbff) {
31
+ high = code;
32
+ continue;
33
+ }
34
+ if (!validCharacter(code))
35
+ throw new SyntaxError('Invalid XML: invalid character');
36
+ afterCR = code === 13;
37
+ output += code === 13 ? '\n' : chunk[index];
38
+ }
39
+ if (output.length >= 512) {
40
+ yield output;
41
+ output = '';
42
+ }
43
+ }
44
+ if (flushChunks && output) {
45
+ yield output;
46
+ output = '';
47
+ }
48
+ }
49
+ if (high)
50
+ throw new SyntaxError('Invalid XML: invalid character');
51
+ if (output)
52
+ yield output;
53
+ }
@@ -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,8 @@
1
+ import { type XmlFrameRequest } from "./frames.js";
2
+ export type { XmlParserFrame, XmlFrameRequest } from "./frames.js";
3
+ export { normalizeXmlChunks } from "./characters.js";
4
+ import { type XmlSourceStep } from "./source.js";
5
+ export type { XmlSourceRead, XmlSourceStep } from "./source.js";
1
6
  import type { XmlStreamEvent } from "./stream.js";
2
7
  export { XmlLimitError } from "./errors.js";
3
8
  export interface XmlName {
@@ -48,6 +53,16 @@ export interface XmlStepLimits extends XmlLimits {
48
53
  * false. Drive the step iterator to await external consumers between windows. */
49
54
  readonly events?: (event: XmlStreamEvent) => void;
50
55
  }
56
+ export interface XmlSourceLimits extends XmlStepLimits {
57
+ /** Store active parser frames through host-serviced requests. Requires retainTree: false. */
58
+ readonly storeFrames?: boolean;
59
+ }
60
+ export type XmlParseStep = XmlSourceStep | XmlFrameRequest;
61
+ /** Parse a validated, BOM-free, line-normalized UTF-16 source via bounded read requests. */
62
+ export declare function parseXmlSourceSteps(length: number | undefined, limits?: XmlStepLimits & {
63
+ readonly storeFrames?: false;
64
+ }): Generator<XmlSourceStep, XmlElement, void>;
65
+ export declare function parseXmlSourceSteps(length: number | undefined, limits: XmlSourceLimits): Generator<XmlParseStep, XmlElement, void>;
51
66
  export declare function parseXmlSteps(input: string, limits?: XmlStepLimits): Generator<number, XmlElement, void>;
52
67
  export declare function parseXml(input: string, limits?: XmlLimits): XmlElement;
53
68
  export { parseXmlStream, type XmlStreamLimits, type XmlStreamEvent } from "./stream.js";
@@ -1,7 +1,11 @@
1
+ import { XmlFrames } from "./frames.js";
2
+ import { validCharacter } from "./characters.js";
3
+ export { normalizeXmlChunks } from "./characters.js";
4
+ import { XmlSource } from "./source.js";
1
5
  import { XmlLimitError } from "./errors.js";
2
6
  export { XmlLimitError } from "./errors.js";
3
7
  function* find(source, needle, start) {
4
- const found = source.indexOf(needle, start);
8
+ const found = typeof source === "string" ? source.indexOf(needle, start) : yield* source.indexOf(needle, start);
5
9
  const end = found < 0 ? source.length : found;
6
10
  let remaining = end - start;
7
11
  while (remaining >= 512) {
@@ -17,12 +21,6 @@ const xmlnsNamespace = "http://www.w3.org/2000/xmlns/";
17
21
  function invalid(message) {
18
22
  throw new SyntaxError(`Invalid XML: ${message}`);
19
23
  }
20
- function validCharacter(point) {
21
- return point === 9 || point === 10 || point === 13
22
- || (point >= 0x20 && point <= 0xd7ff)
23
- || (point >= 0xe000 && point <= 0xfffd)
24
- || (point >= 0x10000 && point <= 0x10ffff);
25
- }
26
24
  function nameStart(point) {
27
25
  return point === 95 || (point >= 65 && point <= 90) || (point >= 97 && point <= 122)
28
26
  || (point >= 0xc0 && point <= 0xd6) || (point >= 0xd8 && point <= 0xf6)
@@ -182,72 +180,41 @@ function* validDeclaration(content, expectedEncoding) {
182
180
  }
183
181
  return offset === content.length;
184
182
  }
185
- export function* parseXmlSteps(input, limits = {}) {
183
+ function validateLimits(limits) {
184
+ for (const limit of [limits.maxDepth, limits.maxNodes, limits.maxAttributes, limits.maxContentNodes, limits.maxAttributesPerElement, limits.maxNamespaces]) {
185
+ if (limit !== undefined && ((limit !== Infinity && !Number.isSafeInteger(limit)) || limit < 1))
186
+ throw new RangeError("XML limits must be positive integers");
187
+ }
188
+ if (limits.maxTextLength !== undefined && ((limits.maxTextLength !== Infinity && !Number.isSafeInteger(limits.maxTextLength)) || limits.maxTextLength < 1))
189
+ throw new RangeError("XML limits must be positive integers");
190
+ if (limits.events && limits.retainTree !== false)
191
+ throw new TypeError("XML events require retainTree: false");
192
+ }
193
+ export function* parseXmlSourceSteps(length, limits = {}) {
194
+ validateLimits(limits);
195
+ if (limits.storeFrames && limits.retainTree !== false)
196
+ throw new TypeError("Stored XML frames require retainTree: false");
197
+ const source = new XmlSource(length);
186
198
  const maxDepth = limits.maxDepth ?? Infinity;
187
199
  const maxNodes = limits.maxNodes ?? Infinity;
188
200
  const maxAttributes = limits.maxAttributes ?? Infinity;
189
201
  const retainContent = limits.retainContent !== false;
190
202
  const retainTree = limits.retainTree !== false;
191
203
  const retain = retainTree && retainContent;
192
- if (limits.events && retainTree)
193
- throw new TypeError("XML events require retainTree: false");
194
204
  const maxContentNodes = limits.maxContentNodes ?? Infinity;
195
205
  const emptyContent = Object.freeze([]);
196
206
  const emptyAttributes = Object.freeze([]);
197
207
  const emptyNamespaces = new Map();
198
208
  const maxAttributesPerElement = limits.maxAttributesPerElement ?? Infinity;
199
209
  const maxNamespaces = limits.maxNamespaces ?? Infinity;
200
- for (const limit of [limits.maxDepth, limits.maxNodes, limits.maxAttributes, limits.maxContentNodes, limits.maxAttributesPerElement, limits.maxNamespaces]) {
201
- if (limit !== undefined && ((limit !== Infinity && !Number.isSafeInteger(limit)) || limit < 1))
202
- throw new RangeError("XML limits must be positive integers");
203
- }
204
210
  const maxTextLength = limits.maxTextLength ?? Infinity;
205
- if (limits.maxTextLength !== undefined && ((maxTextLength !== Infinity && !Number.isSafeInteger(maxTextLength)) || maxTextLength < 1))
206
- throw new RangeError("XML limits must be positive integers");
207
211
  let textLength = 0;
208
212
  const admitText = (text) => {
209
213
  if (text.length > maxTextLength - textLength)
210
214
  throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
211
215
  textLength += text.length;
212
216
  };
213
- const chunks = [];
214
- const start = input.charCodeAt(0) === 0xfeff ? 1 : 0;
215
- let chunkStart = start;
216
- let chunkLength = 0;
217
- let normalizedChunk = "";
218
- // Keep validated spans intact; only changed line endings need new strings.
219
- for (let index = start; index < input.length; index++) {
220
- const point = input.codePointAt(index);
221
- if (!validCharacter(point))
222
- invalid("invalid character");
223
- if (point === 13) {
224
- normalizedChunk += input.slice(chunkStart, index) + "\n";
225
- if (input.charCodeAt(index + 1) === 10)
226
- index++;
227
- chunkStart = index + 1;
228
- chunkLength++;
229
- }
230
- else {
231
- chunkLength += point > 0xffff ? 2 : 1;
232
- if (point > 0xffff)
233
- index++;
234
- }
235
- if (chunkLength >= 512) {
236
- if (normalizedChunk) {
237
- chunks.push(normalizedChunk + input.slice(chunkStart, index + 1));
238
- normalizedChunk = "";
239
- chunkStart = index + 1;
240
- }
241
- chunkLength = 0;
242
- yield 512;
243
- }
244
- }
245
- if (chunkLength)
246
- yield chunkLength;
247
- if (chunks.length || normalizedChunk)
248
- chunks.push(normalizedChunk + input.slice(chunkStart));
249
- const source = chunks.length ? chunks.join("") : input.slice(start);
250
- const stack = [];
217
+ const stack = new XmlFrames(limits.storeFrames === true);
251
218
  let root;
252
219
  const prolog = [];
253
220
  const epilog = [];
@@ -265,8 +232,8 @@ export function* parseXmlSteps(input, limits = {}) {
265
232
  let pendingWork = 0;
266
233
  const skipWhitespace = function* () {
267
234
  const start = offset;
268
- while (offset < source.length) {
269
- const c = source.charCodeAt(offset);
235
+ while (yield* source.has(offset)) {
236
+ const c = (yield* source.charCodeAt(offset));
270
237
  if (c !== 32 && c !== 9 && c !== 10 && c !== 13)
271
238
  break;
272
239
  offset++;
@@ -280,8 +247,8 @@ export function* parseXmlSteps(input, limits = {}) {
280
247
  };
281
248
  const scanName = function* () {
282
249
  const start = offset;
283
- while (offset < source.length) {
284
- const c = source.charCodeAt(offset);
250
+ while (yield* source.has(offset)) {
251
+ const c = (yield* source.charCodeAt(offset));
285
252
  if (c === 32 || c === 9 || c === 13 || c === 10 || c === 47 || c === 61 || c === 62 || c === 63)
286
253
  break;
287
254
  offset++;
@@ -291,7 +258,7 @@ export function* parseXmlSteps(input, limits = {}) {
291
258
  pendingWork -= 512;
292
259
  }
293
260
  }
294
- const name = source.slice(start, offset);
261
+ const name = (yield* source.slice(start, offset));
295
262
  pendingWork += offset - start;
296
263
  while (pendingWork >= 512) {
297
264
  yield 512;
@@ -300,7 +267,7 @@ export function* parseXmlSteps(input, limits = {}) {
300
267
  const [prefix, localName] = qualifiedNameSync(name, qualifiedNames);
301
268
  return [name, prefix, localName];
302
269
  };
303
- while (offset < source.length) {
270
+ while (yield* source.has(offset)) {
304
271
  pendingWork += 1;
305
272
  if (pendingWork >= 512) {
306
273
  while (pendingWork >= 512) {
@@ -308,10 +275,10 @@ export function* parseXmlSteps(input, limits = {}) {
308
275
  pendingWork -= 512;
309
276
  }
310
277
  }
311
- if (source.charCodeAt(offset) !== 60) {
312
- const next = source.indexOf("<", offset);
278
+ if ((yield* source.charCodeAt(offset)) !== 60) {
279
+ const next = (yield* source.indexOf("<", offset));
313
280
  const endPos = next < 0 ? source.length : next;
314
- const text = source.slice(offset, endPos);
281
+ const text = (yield* source.slice(offset, endPos));
315
282
  pendingWork += (endPos - offset) + text.length;
316
283
  while (pendingWork >= 512) {
317
284
  yield 512;
@@ -325,7 +292,7 @@ export function* parseXmlSteps(input, limits = {}) {
325
292
  pendingWork -= 512;
326
293
  }
327
294
  admitText(resolved);
328
- const parent = stack.at(-1);
295
+ const parent = (yield* stack.peek());
329
296
  if (parent) {
330
297
  if (retainTree)
331
298
  parent.element.text += resolved;
@@ -360,13 +327,13 @@ export function* parseXmlSteps(input, limits = {}) {
360
327
  }
361
328
  offset = endPos;
362
329
  }
363
- else if (source.startsWith("<!--", offset)) {
330
+ else if ((yield* source.startsWith("<!--", offset))) {
364
331
  const end = yield* find(source, "-->", offset + 4);
365
- if (end < 0 || (yield* find(source.slice(offset + 4, end), "--", 0)) >= 0 || source.slice(offset + 4, end).endsWith("-")) {
332
+ const text = end < 0 ? "" : yield* source.slice(offset + 4, end);
333
+ if (end < 0 || (yield* find(text, "--", 0)) >= 0 || text.endsWith("-")) {
366
334
  invalid("malformed comment");
367
335
  }
368
- const parent = stack.at(-1);
369
- const text = source.slice(offset + 4, end);
336
+ const parent = (yield* stack.peek());
370
337
  admitText(text);
371
338
  admitContent();
372
339
  if (retain) {
@@ -375,15 +342,15 @@ export function* parseXmlSteps(input, limits = {}) {
375
342
  limits.events?.({ type: "content", content: { kind: "comment", text }, parent: parent?.element });
376
343
  offset = end + 3;
377
344
  }
378
- else if (source.startsWith("<![CDATA[", offset)) {
345
+ else if ((yield* source.startsWith("<![CDATA[", offset))) {
379
346
  if (!stack.length)
380
347
  invalid("CDATA outside root");
381
348
  const end = yield* find(source, "]]>", offset + 9);
382
349
  if (end < 0)
383
350
  invalid("unterminated CDATA");
384
- const cdataText = source.slice(offset + 9, end);
351
+ const cdataText = (yield* source.slice(offset + 9, end));
385
352
  admitText(cdataText);
386
- const parent = stack.at(-1);
353
+ const parent = (yield* stack.peek());
387
354
  if (retainTree)
388
355
  parent.element.text += cdataText;
389
356
  admitContent();
@@ -392,7 +359,7 @@ export function* parseXmlSteps(input, limits = {}) {
392
359
  limits.events?.({ type: "content", content: { kind: "cdata", text: cdataText }, parent: parent.element });
393
360
  offset = end + 3;
394
361
  }
395
- else if (source.startsWith("<?", offset)) {
362
+ else if ((yield* source.startsWith("<?", offset))) {
396
363
  const start = offset;
397
364
  offset += 2;
398
365
  const [target] = yield* scanName();
@@ -403,19 +370,19 @@ export function* parseXmlSteps(input, limits = {}) {
403
370
  const end = yield* find(source, "?>", offset);
404
371
  if (end < 0)
405
372
  invalid("unterminated processing instruction");
406
- const content = source.slice(offset, end);
373
+ const content = (yield* source.slice(offset, end));
407
374
  if (target.length === 3 && target.toLowerCase() === "xml") {
408
375
  if (start !== 0 || target !== "xml"
409
376
  || !(yield* validDeclaration(content, limits.expectedEncoding))) {
410
377
  invalid("unsupported XML declaration");
411
378
  }
412
379
  if (retainContent)
413
- declaration = source.slice(start, end + 2);
380
+ declaration = (yield* source.slice(start, end + 2));
414
381
  }
415
382
  else if (content && !" \t\n\r".includes(content[0]))
416
383
  invalid("invalid processing instruction");
417
384
  if (!(target.length === 3 && target.toLowerCase() === "xml")) {
418
- const parent = stack.at(-1);
385
+ const parent = (yield* stack.peek());
419
386
  let wsStart = 0;
420
387
  while (wsStart < content.length && " \t\n\r".includes(content[wsStart]))
421
388
  wsStart++;
@@ -434,10 +401,10 @@ export function* parseXmlSteps(input, limits = {}) {
434
401
  }
435
402
  offset = end + 2;
436
403
  }
437
- else if (source.startsWith("<!", offset)) {
404
+ else if ((yield* source.startsWith("<!", offset))) {
438
405
  invalid("DTD and entity declarations are forbidden");
439
406
  }
440
- else if (source.charCodeAt(offset + 1) === 47) {
407
+ else if ((yield* source.charCodeAt(offset + 1)) === 47) {
441
408
  offset += 2;
442
409
  const [name] = yield* scanName();
443
410
  yield* skipWhitespace();
@@ -445,18 +412,18 @@ export function* parseXmlSteps(input, limits = {}) {
445
412
  yield 512;
446
413
  pendingWork -= 512;
447
414
  }
448
- const closed = source[offset++] === ">" ? stack.pop() : undefined;
415
+ const closed = (yield* source.slice(offset++, offset)) === ">" ? (yield* stack.pop()) : undefined;
449
416
  if (closed?.name !== name) {
450
417
  if (!limits.recover)
451
418
  invalid("mismatched closing tag");
452
419
  limits.recover("mismatched closing tag");
453
420
  }
454
421
  if (closed)
455
- limits.events?.({ type: "close", element: closed.element, parent: stack.at(-1)?.element });
422
+ limits.events?.({ type: "close", element: closed.element, parent: (yield* stack.peek())?.element });
456
423
  }
457
424
  else {
458
425
  offset++;
459
- const repeated = previousEmpty && source.startsWith(previousEmpty.suffix, offset) ? previousEmpty : undefined;
426
+ const repeated = previousEmpty && (yield* source.startsWith(previousEmpty.suffix, offset)) ? previousEmpty : undefined;
460
427
  const [name, prefix, localName] = repeated
461
428
  ? [repeated.name, repeated.prefix, repeated.localName]
462
429
  : (yield* scanName());
@@ -471,7 +438,7 @@ export function* parseXmlSteps(input, limits = {}) {
471
438
  }
472
439
  let attributes;
473
440
  const attrMeta = [];
474
- let namespaces = stack.at(-1)?.namespaces ?? new Map([["xml", xmlNamespace]]);
441
+ let namespaces = (yield* stack.peek())?.namespaces ?? new Map([["xml", xmlNamespace]]);
475
442
  let ownsNamespaces = stack.length === 0;
476
443
  while (!repeated) {
477
444
  const ws = (yield* skipWhitespace());
@@ -479,7 +446,7 @@ export function* parseXmlSteps(input, limits = {}) {
479
446
  yield 512;
480
447
  pendingWork -= 512;
481
448
  }
482
- const ch = source.charCodeAt(offset);
449
+ const ch = (yield* source.charCodeAt(offset));
483
450
  if (ch === 47 || ch === 62 || limits.recover && offset === source.length)
484
451
  break;
485
452
  if (ws === 0)
@@ -496,13 +463,13 @@ export function* parseXmlSteps(input, limits = {}) {
496
463
  if ((attributes?.size ?? 0) >= maxAttributesPerElement)
497
464
  throw new XmlLimitError("maxAttributesPerElement", "XML attribute limit exceeded");
498
465
  yield* skipWhitespace();
499
- if (source[offset++] !== "=")
466
+ if ((yield* source.slice(offset++, offset)) !== "=")
500
467
  invalid("missing attribute equals");
501
468
  yield* skipWhitespace();
502
- const quote = source[offset++];
469
+ const quote = (yield* source.slice(offset++, offset));
503
470
  if (quote !== '"' && quote !== "'")
504
471
  invalid("unquoted attribute");
505
- const end = source.indexOf(quote, offset);
472
+ const end = (yield* source.indexOf(quote, offset));
506
473
  const scanLen = (end < 0 ? source.length : end) - offset;
507
474
  pendingWork += scanLen;
508
475
  while (pendingWork >= 512) {
@@ -511,7 +478,7 @@ export function* parseXmlSteps(input, limits = {}) {
511
478
  }
512
479
  if (end < 0)
513
480
  invalid("unterminated attribute");
514
- const raw = source.slice(offset, end);
481
+ const raw = (yield* source.slice(offset, end));
515
482
  pendingWork += raw.length * 2;
516
483
  while (pendingWork >= 512) {
517
484
  yield 512;
@@ -578,7 +545,7 @@ export function* parseXmlSteps(input, limits = {}) {
578
545
  if (stack.length + 1 > maxDepth)
579
546
  throw new XmlLimitError("maxDepth", "XML resource limit exceeded");
580
547
  const namespace = namespaces.get(prefix) ?? "";
581
- limits.onElement?.({ name, namespace, localName }, stack.at(-1)?.element, stack.length + 1);
548
+ limits.onElement?.({ name, namespace, localName }, (yield* stack.peek())?.element, stack.length + 1);
582
549
  admitContent();
583
550
  const retainedAttributes = [];
584
551
  if (retainContent && attributes) {
@@ -597,7 +564,7 @@ export function* parseXmlSteps(input, limits = {}) {
597
564
  }
598
565
  const content = retain ? [] : undefined;
599
566
  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 }) } : {}) };
600
- const parent = stack.at(-1);
567
+ const parent = (yield* stack.peek());
601
568
  if (parent) {
602
569
  if (retainTree) {
603
570
  parent.element.children.push(element);
@@ -608,21 +575,21 @@ export function* parseXmlSteps(input, limits = {}) {
608
575
  invalid("multiple root elements");
609
576
  else
610
577
  root = element;
611
- const empty = source[offset] === "/";
578
+ const empty = (yield* source.slice(offset, offset + 1)) === "/";
612
579
  if (empty)
613
580
  offset++;
614
- if (source[offset++] !== ">") {
581
+ if ((yield* source.slice(offset++, offset)) !== ">") {
615
582
  if (!limits.recover)
616
583
  invalid("unterminated start tag");
617
584
  limits.recover("unterminated start tag");
618
585
  }
619
586
  // Only cache compact, attribute-free spellings. Namespace resolution and
620
587
  // every admission counter still run for each distinct physical element.
621
- if (empty && !attributes && name.length <= 512 && source.slice(offset - name.length - 3, offset) === `<${name}/>`)
588
+ if (empty && !attributes && name.length <= 512 && (yield* source.slice(offset - name.length - 3, offset)) === `<${name}/>`)
622
589
  previousEmpty = { suffix: name + "/>", name, prefix, localName };
623
590
  limits.events?.({ type: "open", element, parent: parent?.element });
624
591
  if (!empty)
625
- stack.push({ element, content, name, namespaces });
592
+ yield* stack.push({ element, content, name, namespaces });
626
593
  else
627
594
  limits.events?.({ type: "close", element, parent: parent?.element });
628
595
  }
@@ -638,13 +605,72 @@ export function* parseXmlSteps(input, limits = {}) {
638
605
  invalid("incomplete document");
639
606
  limits.recover("incomplete document");
640
607
  while (stack.length) {
641
- const closed = stack.pop();
642
- limits.events?.({ type: "close", element: closed.element, parent: stack.at(-1)?.element });
608
+ const closed = (yield* stack.pop());
609
+ limits.events?.({ type: "close", element: closed.element, parent: (yield* stack.peek())?.element });
643
610
  yield 1;
644
611
  }
645
612
  }
646
613
  return root;
647
614
  }
615
+ function* normalizeXmlSteps(input) {
616
+ const chunks = [];
617
+ const start = input.charCodeAt(0) === 0xfeff ? 1 : 0;
618
+ let chunkStart = start;
619
+ let chunkLength = 0;
620
+ let normalizedChunk = "";
621
+ // Keep validated spans intact; only changed line endings need new strings.
622
+ for (let index = start; index < input.length; index++) {
623
+ const point = input.codePointAt(index);
624
+ if (!validCharacter(point))
625
+ invalid("invalid character");
626
+ if (point === 13) {
627
+ normalizedChunk += input.slice(chunkStart, index) + "\n";
628
+ if (input.charCodeAt(index + 1) === 10)
629
+ index++;
630
+ chunkStart = index + 1;
631
+ chunkLength++;
632
+ }
633
+ else {
634
+ chunkLength += point > 0xffff ? 2 : 1;
635
+ if (point > 0xffff)
636
+ index++;
637
+ }
638
+ if (chunkLength >= 512) {
639
+ if (normalizedChunk) {
640
+ chunks.push(normalizedChunk + input.slice(chunkStart, index + 1));
641
+ normalizedChunk = "";
642
+ chunkStart = index + 1;
643
+ }
644
+ chunkLength = 0;
645
+ yield 512;
646
+ }
647
+ }
648
+ if (chunkLength)
649
+ yield chunkLength;
650
+ if (chunks.length || normalizedChunk)
651
+ chunks.push(normalizedChunk + input.slice(chunkStart));
652
+ return chunks.length ? chunks.join("") : input.slice(start);
653
+ }
654
+ export function* parseXmlSteps(input, limits = {}) {
655
+ validateLimits(limits);
656
+ const source = yield* normalizeXmlSteps(input);
657
+ const parser = parseXmlSourceSteps(source.length, limits);
658
+ let step = parser.next();
659
+ try {
660
+ while (!step.done) {
661
+ if (typeof step.value === "number")
662
+ yield step.value;
663
+ else
664
+ step.value.value = source.slice(step.value.offset, step.value.offset + step.value.length);
665
+ step = parser.next();
666
+ }
667
+ return step.value;
668
+ }
669
+ finally {
670
+ if (!step.done)
671
+ parser.return(undefined);
672
+ }
673
+ }
648
674
  export function parseXml(input, limits = {}) {
649
675
  const parser = parseXmlSteps(input, limits);
650
676
  let result = parser.next();
@@ -0,0 +1,24 @@
1
+ /** Fill value before advancing. Streaming reads may be short; mark complete at EOF. */
2
+ export interface XmlSourceRead {
3
+ readonly offset: number;
4
+ readonly length: number;
5
+ readonly streaming?: boolean;
6
+ value?: string;
7
+ complete?: boolean;
8
+ }
9
+ export type XmlSourceStep = number | XmlSourceRead;
10
+ /** One fixed window; individual returned tokens still have their own size cost. */
11
+ export declare class XmlSource {
12
+ private window;
13
+ private start;
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>;
20
+ slice(start: number, end?: number): Generator<XmlSourceStep, string, void>;
21
+ charCodeAt(offset: number): Generator<XmlSourceStep, number, void>;
22
+ startsWith(value: string, offset: number): Generator<XmlSourceStep, boolean, void>;
23
+ indexOf(value: string, offset: number): Generator<XmlSourceStep, number, void>;
24
+ }
@@ -0,0 +1,70 @@
1
+ /** One fixed window; individual returned tokens still have their own size cost. */
2
+ export class XmlSource {
3
+ window = '';
4
+ start = -1;
5
+ end;
6
+ streaming;
7
+ constructor(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;
33
+ }
34
+ *slice(start, end = this.length) {
35
+ start = start < 0 ? Math.max(0, this.length + start) : Math.min(start, this.length);
36
+ end = end < 0 ? Math.max(0, this.length + end) : Math.min(end, this.length);
37
+ let result = '';
38
+ while (start < Math.min(end, this.length)) {
39
+ yield* this.load(start);
40
+ if (start >= this.end)
41
+ break;
42
+ const stop = Math.min(end, this.start + this.window.length);
43
+ result += this.window.slice(start - this.start, stop - this.start);
44
+ start = stop;
45
+ }
46
+ return result;
47
+ }
48
+ *charCodeAt(offset) {
49
+ return (yield* this.slice(offset, offset + 1)).charCodeAt(0);
50
+ }
51
+ *startsWith(value, offset) {
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;
57
+ }
58
+ *indexOf(value, offset) {
59
+ let tail = '';
60
+ while (yield* this.has(offset)) {
61
+ const part = tail + this.window.slice(offset - this.start);
62
+ const found = part.indexOf(value);
63
+ if (found >= 0)
64
+ return offset - tail.length + found;
65
+ tail = value.length > 1 ? part.slice(-(value.length - 1)) : '';
66
+ offset = this.start + this.window.length;
67
+ }
68
+ return -1;
69
+ }
70
+ }
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.788",
11
+ "version": "0.0.790",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.788",
3
+ "version": "0.0.790",
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.788",
3
+ "version": "0.0.790",
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.788",
180
+ "toolcraft-schema": "0.0.790",
181
181
  "toolcraft-design": "*",
182
182
  "@poe-code/frontmatter": "*",
183
183
  "@poe-code/agent-mcp-config": "*",