toolcraft 0.0.790 → 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.790",
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.790",
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.790",
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.790",
151
+ "version": "0.0.791",
152
152
  "license": "MIT"
153
153
  },
154
154
  {
@@ -69,3 +69,13 @@ read reaches EOF (an empty EOF read is valid). Earlier offsets may be requested
69
69
  again, so retain source spans in caller-backed storage. With `normalizeXmlChunks`,
70
70
  pass `true` as the second argument to flush at input-chunk boundaries and preserve
71
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.
@@ -54,6 +54,9 @@ export interface XmlStepLimits extends XmlLimits {
54
54
  readonly events?: (event: XmlStreamEvent) => void;
55
55
  }
56
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;
57
60
  /** Store active parser frames through host-serviced requests. Requires retainTree: false. */
58
61
  readonly storeFrames?: boolean;
59
62
  }
@@ -67,6 +67,95 @@ function qualifiedNameSync(name, cache) {
67
67
  }
68
68
  return parts;
69
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
+ }
70
159
  function* entities(text, recover) {
71
160
  let result = "";
72
161
  let offset = 0;
@@ -82,39 +171,7 @@ function* entities(text, recover) {
82
171
  recover("unterminated entity");
83
172
  return result + text.slice(start + 1);
84
173
  }
85
- const entity = text.slice(start + 1, end);
86
- const predefined = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
87
- if (Object.hasOwn(predefined, entity))
88
- result += predefined[entity];
89
- else {
90
- const hexadecimal = entity.startsWith("#x");
91
- const digits = entity.slice(hexadecimal ? 2 : 1);
92
- if (!entity.startsWith("#") || !digits.length) {
93
- if (!recover)
94
- invalid("undeclared entity");
95
- recover("undeclared entity");
96
- offset = end + 1;
97
- continue;
98
- }
99
- let point = 0;
100
- for (let index = 0; index < digits.length; index++) {
101
- const code = digits.charCodeAt(index);
102
- const digit = code >= 48 && code <= 57 ? code - 48 : hexadecimal && code >= 65 && code <= 70 ? code - 55
103
- : hexadecimal && code >= 97 && code <= 102 ? code - 87 : -1;
104
- if (digit < 0)
105
- invalid("undeclared entity");
106
- point = point * (hexadecimal ? 16 : 10) + digit;
107
- if (point > 0x10ffff)
108
- invalid("invalid character reference");
109
- if ((index + 1) % 512 === 0)
110
- yield 512;
111
- }
112
- if (digits.length % 512)
113
- yield digits.length % 512;
114
- if (!validCharacter(point))
115
- invalid("invalid character reference");
116
- result += String.fromCodePoint(point);
117
- }
174
+ result += yield* entity(text, start + 1, end, recover);
118
175
  offset = end + 1;
119
176
  }
120
177
  return result;
@@ -194,6 +251,8 @@ export function* parseXmlSourceSteps(length, limits = {}) {
194
251
  validateLimits(limits);
195
252
  if (limits.storeFrames && limits.retainTree !== false)
196
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");
197
256
  const source = new XmlSource(length);
198
257
  const maxDepth = limits.maxDepth ?? Infinity;
199
258
  const maxNodes = limits.maxNodes ?? Infinity;
@@ -267,6 +326,33 @@ export function* parseXmlSourceSteps(length, limits = {}) {
267
326
  const [prefix, localName] = qualifiedNameSync(name, qualifiedNames);
268
327
  return [name, prefix, localName];
269
328
  };
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
+ };
270
356
  while (yield* source.has(offset)) {
271
357
  pendingWork += 1;
272
358
  if (pendingWork >= 512) {
@@ -278,6 +364,49 @@ export function* parseXmlSourceSteps(length, limits = {}) {
278
364
  if ((yield* source.charCodeAt(offset)) !== 60) {
279
365
  const next = (yield* source.indexOf("<", offset));
280
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
+ }
281
410
  const text = (yield* source.slice(offset, endPos));
282
411
  pendingWork += (endPos - offset) + text.length;
283
412
  while (pendingWork >= 512) {
@@ -329,17 +458,10 @@ export function* parseXmlSourceSteps(length, limits = {}) {
329
458
  }
330
459
  else if ((yield* source.startsWith("<!--", offset))) {
331
460
  const end = yield* find(source, "-->", offset + 4);
332
- const text = end < 0 ? "" : yield* source.slice(offset + 4, end);
333
- 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))
334
463
  invalid("malformed comment");
335
- }
336
- const parent = (yield* stack.peek());
337
- admitText(text);
338
- admitContent();
339
- if (retain) {
340
- (parent?.content ?? (root ? epilog : prolog)).push({ kind: "comment", text });
341
- }
342
- limits.events?.({ type: "content", content: { kind: "comment", text }, parent: parent?.element });
464
+ yield* rawContent("comment", offset + 4, end, yield* stack.peek());
343
465
  offset = end + 3;
344
466
  }
345
467
  else if ((yield* source.startsWith("<![CDATA[", offset))) {
@@ -348,15 +470,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
348
470
  const end = yield* find(source, "]]>", offset + 9);
349
471
  if (end < 0)
350
472
  invalid("unterminated CDATA");
351
- const cdataText = (yield* source.slice(offset + 9, end));
352
- admitText(cdataText);
353
- const parent = (yield* stack.peek());
354
- if (retainTree)
355
- parent.element.text += cdataText;
356
- admitContent();
357
- if (retain)
358
- parent.content.push({ kind: "cdata", text: cdataText });
359
- limits.events?.({ type: "content", content: { kind: "cdata", text: cdataText }, parent: parent.element });
473
+ yield* rawContent("cdata", offset + 9, end, yield* stack.peek());
360
474
  offset = end + 3;
361
475
  }
362
476
  else if ((yield* source.startsWith("<?", offset))) {
@@ -370,34 +484,31 @@ export function* parseXmlSourceSteps(length, limits = {}) {
370
484
  const end = yield* find(source, "?>", offset);
371
485
  if (end < 0)
372
486
  invalid("unterminated processing instruction");
373
- const content = (yield* source.slice(offset, end));
374
487
  if (target.length === 3 && target.toLowerCase() === "xml") {
488
+ const content = yield* source.slice(offset, end);
375
489
  if (start !== 0 || target !== "xml"
376
- || !(yield* validDeclaration(content, limits.expectedEncoding))) {
490
+ || !(yield* validDeclaration(content, limits.expectedEncoding)))
377
491
  invalid("unsupported XML declaration");
378
- }
379
492
  if (retainContent)
380
- declaration = (yield* source.slice(start, end + 2));
493
+ declaration = yield* source.slice(start, end + 2);
381
494
  }
382
- else if (content && !" \t\n\r".includes(content[0]))
383
- invalid("invalid processing instruction");
384
- if (!(target.length === 3 && target.toLowerCase() === "xml")) {
385
- const parent = (yield* stack.peek());
386
- let wsStart = 0;
387
- while (wsStart < content.length && " \t\n\r".includes(content[wsStart]))
388
- wsStart++;
389
- 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
+ }
390
507
  while (pendingWork >= 512) {
391
508
  yield 512;
392
509
  pendingWork -= 512;
393
510
  }
394
- const text = content.slice(wsStart);
395
- admitText(text);
396
- admitContent();
397
- if (retain) {
398
- (parent?.content ?? (root ? epilog : prolog)).push({ kind: "processing-instruction", target, text });
399
- }
400
- limits.events?.({ type: "content", content: { kind: "processing-instruction", target, text }, parent: parent?.element });
511
+ yield* rawContent("processing-instruction", body, end, yield* stack.peek(), target);
401
512
  }
402
513
  offset = end + 2;
403
514
  }
@@ -20,5 +20,5 @@ export declare class XmlSource {
20
20
  slice(start: number, end?: number): Generator<XmlSourceStep, string, void>;
21
21
  charCodeAt(offset: number): Generator<XmlSourceStep, number, void>;
22
22
  startsWith(value: string, offset: number): Generator<XmlSourceStep, boolean, void>;
23
- indexOf(value: string, offset: number): Generator<XmlSourceStep, number, void>;
23
+ indexOf(value: string, offset: number, end?: number): Generator<XmlSourceStep, number, void>;
24
24
  }
@@ -55,15 +55,16 @@ export class XmlSource {
55
55
  }
56
56
  return true;
57
57
  }
58
- *indexOf(value, offset) {
58
+ *indexOf(value, offset, end = Infinity) {
59
59
  let tail = '';
60
- while (yield* this.has(offset)) {
61
- const part = tail + this.window.slice(offset - this.start);
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);
62
63
  const found = part.indexOf(value);
63
64
  if (found >= 0)
64
65
  return offset - tail.length + found;
65
66
  tail = value.length > 1 ? part.slice(-(value.length - 1)) : '';
66
- offset = this.start + this.window.length;
67
+ offset = stop;
67
68
  }
68
69
  return -1;
69
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.790",
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.790",
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.790",
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.790",
180
+ "toolcraft-schema": "0.0.791",
181
181
  "toolcraft-design": "*",
182
182
  "@poe-code/frontmatter": "*",
183
183
  "@poe-code/agent-mcp-config": "*",