toolcraft 0.0.781 → 0.0.783

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.781",
141
+ "version": "0.0.783",
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.781",
151
+ "version": "0.0.783",
152
152
  "license": "MIT"
153
153
  },
154
154
  {
@@ -138,7 +138,7 @@
138
138
  },
139
139
  {
140
140
  "name": "toolcraft",
141
- "version": "0.0.781",
141
+ "version": "0.0.783",
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.781",
151
+ "version": "0.0.783",
152
152
  "license": "MIT"
153
153
  },
154
154
  {
@@ -35,3 +35,10 @@ For validation or root-name detection, pass `retainTree: false` to `parseXmlStre
35
35
  Use `streamElements: { matches, consume }` to process selected complete subtrees asynchronously. Matching non-root elements are detached from their parent; nested matches remain inside the selected ancestor. `consume(element, parent, before?)` runs in source order and is awaited before the next 512-unit parse window. The producer closes on consumer failure. This mode requires normal tree/content retention and retains each selected subtree until consumption; individual large subtrees and XML tokens still need their own storage strategy.
36
36
 
37
37
  Set `streamElements.captureBefore: true` to detach preceding sibling content when each subtree is selected and receive it as `before`. This preserves text, CDATA, comments and sibling order even when multiple subtrees share one parse window. The remaining trailing content stays in the parent. Store both `before` and the subtree when you need to reconstruct the original mixed content.
38
+
39
+ `parseXmlStream` also accepts `events` with `retainTree: false` to consume ordered
40
+ open, content and close events without constructing a document tree. Each parser
41
+ window awaits the consumer before advancing. Event elements contain names,
42
+ attributes and namespace metadata; individual XML tokens and open ancestry still
43
+ remain resident. This mode lets a caller own document storage independently of
44
+ the codec. Consumer failures propagate unchanged and close the input iterator.
@@ -42,4 +42,4 @@ export interface XmlLimits {
42
42
  }
43
43
  export declare function parseXmlSteps(input: string, limits?: XmlLimits): Generator<number, XmlElement, void>;
44
44
  export declare function parseXml(input: string, limits?: XmlLimits): XmlElement;
45
- export { parseXmlStream, type XmlStreamLimits } from "./stream.js";
45
+ export { parseXmlStream, type XmlStreamLimits, type XmlStreamEvent } from "./stream.js";
@@ -1,7 +1,20 @@
1
1
  import type { XmlContent, XmlElement, XmlLimits, XmlName } from "./index.js";
2
+ export type XmlStreamEvent = {
3
+ readonly type: "open" | "close";
4
+ readonly element: XmlElement;
5
+ readonly parent: XmlElement | undefined;
6
+ } | {
7
+ readonly type: "content";
8
+ readonly content: Exclude<XmlContent, XmlElement>;
9
+ readonly parent: XmlElement | undefined;
10
+ };
2
11
  export interface XmlStreamLimits extends XmlLimits {
3
12
  /** Validate the full document but return only its root name, without a retained tree. */
4
13
  readonly retainTree?: boolean;
14
+ /** Ordered node events, awaited after each parser window. Requires retainTree:
15
+ * false; elements carry metadata without child/content arrays. Individual XML
16
+ * tokens remain resident until the underlying parser completes them. */
17
+ readonly events?: (event: XmlStreamEvent) => void | Promise<void>;
5
18
  /** Detach selected non-root subtrees and await their consumer before parsing
6
19
  * the next 512-unit window. By default nested matches belong to the selected ancestor. */
7
20
  readonly streamElements?: {
@@ -16,6 +16,11 @@ export async function parseXmlStream(source, limits = {}, checkpoint) {
16
16
  }
17
17
  function charge(key, value = 1) { counters[key] += value; bound(key, counters[key]); }
18
18
  const retainContent = limits.retainContent !== false, tree = limits.retainTree !== false, retain = tree && retainContent;
19
+ const events = limits.events;
20
+ if (events && tree)
21
+ throw new TypeError("XML events require retainTree: false");
22
+ const describe = retain || !!events;
23
+ const queued = [];
19
24
  const streaming = limits.streamElements;
20
25
  if (streaming && !retain)
21
26
  throw new TypeError("XML subtree streaming requires retained content and tree mode");
@@ -28,7 +33,7 @@ export async function parseXmlStream(source, limits = {}, checkpoint) {
28
33
  const parser = new SaxesParser({ xmlns: true, defaultXMLVersion: "1.0", forceXMLVersion: true });
29
34
  parser.on("error", error => { throw new SyntaxError(`Invalid XML: ${error.message}`); });
30
35
  parser.on("doctype", () => { throw new SyntaxError("Invalid XML: DTD and entity declarations are forbidden"); });
31
- let prefix = "", capturing = retain;
36
+ let prefix = "", capturing = describe;
32
37
  parser.on("xmldecl", value => {
33
38
  const encoding = value.encoding?.toUpperCase();
34
39
  if (value.version !== "1.0" || encoding !== undefined &&
@@ -36,7 +41,7 @@ export async function parseXmlStream(source, limits = {}, checkpoint) {
36
41
  limits.expectedEncoding !== undefined && encoding !== limits.expectedEncoding &&
37
42
  !(encoding === "UTF-16" && ["UTF-16LE", "UTF-16BE"].includes(limits.expectedEncoding))))
38
43
  throw new SyntaxError("Invalid XML: unsupported XML declaration");
39
- if (retain)
44
+ if (describe)
40
45
  declaration = prefix.slice(0, prefix.indexOf("?>") + 2);
41
46
  prefix = "";
42
47
  capturing = false;
@@ -72,11 +77,11 @@ export async function parseXmlStream(source, limits = {}, checkpoint) {
72
77
  parent.element.text = '';
73
78
  }
74
79
  }
75
- charge("maxContentNodes", 1 + (retainContent ? attrs.length : 0));
80
+ charge("maxContentNodes", 1 + (retainContent || events ? attrs.length : 0));
76
81
  const element = { kind: "element", ...name, children: [], content: [], text: "",
77
- attributes: retain ? attrs.map(attr => ({ name: attr.name, localName: attr.local, namespace: attr.uri, value: attr.value })) : [],
78
- namespaces: retain ? namespaces : emptyNamespaces,
79
- ...(!root && retain ? { prolog, epilog, ...(declaration === undefined ? {} : { declaration }) } : {}) };
82
+ attributes: describe ? attrs.map(attr => ({ name: attr.name, localName: attr.local, namespace: attr.uri, value: attr.value })) : [],
83
+ namespaces: describe ? namespaces : emptyNamespaces,
84
+ ...(!root && describe ? { prolog, epilog, ...(declaration === undefined ? {} : { declaration }) } : {}) };
80
85
  if (parent && tree && !selected) {
81
86
  parent.element.children.push(element);
82
87
  if (retain)
@@ -85,9 +90,13 @@ export async function parseXmlStream(source, limits = {}, checkpoint) {
85
90
  else if (!parent)
86
91
  root = element;
87
92
  stack.push({ element, namespaces, selected, before });
93
+ if (events)
94
+ queued.push({ type: "open", element, parent: parent?.element });
88
95
  });
89
96
  parser.on("closetag", () => {
90
97
  const frame = stack.pop();
98
+ if (events)
99
+ queued.push({ type: "close", element: frame.element, parent: stack.at(-1)?.element });
91
100
  if (frame.selected) {
92
101
  completed.push({ element: frame.element, parent: stack.at(-1).element, before: frame.before });
93
102
  selectedCount--;
@@ -99,11 +108,18 @@ export async function parseXmlStream(source, limits = {}, checkpoint) {
99
108
  await streaming.consume(entry.element, entry.parent, entry.before);
100
109
  }
101
110
  completed.length = 0;
111
+ for (const event of queued) {
112
+ await checkpoint?.(0);
113
+ await events(event);
114
+ }
115
+ queued.length = 0;
102
116
  }
103
117
  function append(content) {
104
118
  charge("maxTextLength", content.text.length);
105
119
  charge("maxContentNodes");
106
120
  const parent = stack.at(-1)?.element;
121
+ if (events)
122
+ queued.push({ type: "content", content, parent });
107
123
  if (tree && parent && (content.kind === "text" || content.kind === "cdata"))
108
124
  parent.text += content.text;
109
125
  if (retain)
@@ -148,14 +164,14 @@ export async function parseXmlStream(source, limits = {}, checkpoint) {
148
164
  for (let i = 0; i < text.length; i += 512) {
149
165
  await checkpoint?.(0);
150
166
  write(text.slice(i, i + 512));
151
- if (completed.length)
167
+ if (completed.length || queued.length)
152
168
  await drain();
153
169
  await checkpoint?.(Math.min(512, text.length - i));
154
170
  }
155
171
  }
156
172
  await checkpoint?.(0);
157
173
  parser.close();
158
- if (completed.length)
174
+ if (completed.length || queued.length)
159
175
  await drain();
160
176
  if (!root)
161
177
  throw new SyntaxError("Invalid XML: incomplete document");
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.781",
11
+ "version": "0.0.783",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.781",
3
+ "version": "0.0.783",
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.781",
3
+ "version": "0.0.783",
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.781",
180
+ "toolcraft-schema": "0.0.783",
181
181
  "toolcraft-design": "*",
182
182
  "@poe-code/frontmatter": "*",
183
183
  "@poe-code/agent-mcp-config": "*",