toolcraft 0.0.797 → 0.0.799

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.797",
141
+ "version": "0.0.799",
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.797",
151
+ "version": "0.0.799",
152
152
  "license": "MIT"
153
153
  },
154
154
  {
@@ -138,7 +138,7 @@
138
138
  },
139
139
  {
140
140
  "name": "toolcraft",
141
- "version": "0.0.797",
141
+ "version": "0.0.799",
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.797",
151
+ "version": "0.0.799",
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, XmlParserFrame, XmlFrameRequest, XmlNamespaceScope, XmlNamespaceRequest } from "./xml.js";
12
+ export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits, XmlParserFrame, XmlFrameRequest, XmlNamespaceScope, XmlNamespaceRequest, XmlAttributeRequest, XmlAttributeRecord } 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";
@@ -72,6 +72,24 @@ never changes ancestor scopes. In this mode event elements have an empty
72
72
  The codec remains filesystem-independent, and individual names, attribute values
73
73
  and namespace URIs still need a token storage strategy.
74
74
 
75
+ Set `storeAttributes: true` to move current-tag attribute values, source order,
76
+ and duplicate-name state behind `attributeOperation` requests (`has`, `append`,
77
+ `read`, and `expanded`). Service the request's `found` or `result` field as described
78
+ by `XmlAttributeRequest`, keeping records in caller-owned backing. With this flag,
79
+ element `attributes` arrays are empty. Supply `onAttribute(attribute, element)` to
80
+ receive resolved attributes in source order, after the open event and before any
81
+ content or close event. The parser yields between attributes so a host can await
82
+ storage or a consumer. Namespace declarations may follow prefixed attributes;
83
+ resolution and duplicate-expanded-name validation still happen after the complete
84
+ start tag. With `fragmentAttributes: true`, ordinary values are validated and
85
+ stored as source spans on attribute records, then replayed in at most 512 UTF-16
86
+ units. Hosts must preserve the optional `source` field in append/read requests.
87
+ The third `onAttribute` argument is `true` for continuations of the same logical
88
+ attribute; await each fragment before requesting the next step. Empty attributes
89
+ still emit once. Entity diagnostics are emitted during validation, not replay.
90
+ This mode requires `storeAttributes: true`. Namespace declaration values and
91
+ individual names still retain their token cost.
92
+
75
93
  Pass `undefined` as the source length to parse incrementally. Such read requests
76
94
  have `streaming: true` and request at most 512 UTF-16 units. Return any nonempty
77
95
  prefix available without pulling more input; set `complete: true` only when that
@@ -89,5 +107,4 @@ decoding replay bounded source windows before publishing a logical node, retaini
89
107
  its diagnostic and limit ordering. Numeric entity references are decoded without
90
108
  materializing the token. Set `compactDeclaration: true` to retain normalized declaration fields without
91
109
  copying long whitespace spans; validation always reads bounded windows. The
92
- buffered API keeps the original declaration spelling. Names, attribute values
93
- and namespace scopes still require their own memory budget.
110
+ buffered API keeps the original declaration spelling. Names and namespace declaration values still require their own memory budget.
@@ -0,0 +1,54 @@
1
+ export interface XmlAttributeRecord {
2
+ name: string;
3
+ prefix: string;
4
+ localName: string;
5
+ value: string;
6
+ next: number;
7
+ source?: {
8
+ start: number;
9
+ end: number;
10
+ };
11
+ }
12
+ export interface XmlAttributeState {
13
+ reference: number;
14
+ first: number;
15
+ length: number;
16
+ size: number;
17
+ }
18
+ export type XmlAttributeRequest = {
19
+ attributeOperation: 'has' | 'expanded';
20
+ state: XmlAttributeState;
21
+ name: string;
22
+ found?: boolean;
23
+ } | {
24
+ attributeOperation: 'append';
25
+ state: XmlAttributeState;
26
+ attribute: Omit<XmlAttributeRecord, 'next'>;
27
+ result?: XmlAttributeState;
28
+ } | {
29
+ attributeOperation: 'read';
30
+ state: XmlAttributeState;
31
+ reference: number;
32
+ values: boolean;
33
+ result?: XmlAttributeRecord;
34
+ };
35
+ /** Buffered APIs use local collections; external parsing retains only a store
36
+ * handle and one current attribute. Hosts preserve order and last-value recovery. */
37
+ export declare class XmlAttributes {
38
+ private readonly external;
39
+ private state;
40
+ private readonly names;
41
+ private readonly records;
42
+ private readonly expandedNames;
43
+ constructor(external: boolean);
44
+ get length(): number;
45
+ get size(): number;
46
+ get first(): number;
47
+ has(name: string): Generator<XmlAttributeRequest, boolean, void>;
48
+ append(name: string, prefix: string, localName: string, value: string, source?: {
49
+ start: number;
50
+ end: number;
51
+ }): Generator<XmlAttributeRequest, void, void>;
52
+ read(reference: number, values: boolean): Generator<XmlAttributeRequest, XmlAttributeRecord, void>;
53
+ expanded(name: string): Generator<XmlAttributeRequest, boolean, void>;
54
+ }
@@ -0,0 +1,62 @@
1
+ /** Buffered APIs use local collections; external parsing retains only a store
2
+ * handle and one current attribute. Hosts preserve order and last-value recovery. */
3
+ export class XmlAttributes {
4
+ external;
5
+ state = { reference: 0, first: 0, length: 0, size: 0 };
6
+ names = new Map();
7
+ records = [];
8
+ expandedNames = new Set();
9
+ constructor(external) {
10
+ this.external = external;
11
+ }
12
+ get length() { return this.state.length; }
13
+ get size() { return this.state.size; }
14
+ get first() { return this.state.first; }
15
+ *has(name) {
16
+ if (!this.length)
17
+ return false;
18
+ if (!this.external)
19
+ return this.names.has(name);
20
+ const request = { attributeOperation: 'has', state: this.state, name };
21
+ yield request;
22
+ if (request.found === undefined)
23
+ throw new TypeError('Incomplete XML attribute lookup');
24
+ return request.found;
25
+ }
26
+ *append(name, prefix, localName, value, source) {
27
+ if (!this.external) {
28
+ this.names.set(name, value);
29
+ this.records.push({ name, prefix, localName });
30
+ this.state = { reference: 0, first: 1, length: this.records.length, size: this.names.size };
31
+ return;
32
+ }
33
+ const request = { attributeOperation: 'append', state: this.state, attribute: { name, prefix, localName, value, ...(source ? { source } : {}) } };
34
+ yield request;
35
+ if (!request.result)
36
+ throw new TypeError('Incomplete XML attribute append');
37
+ this.state = request.result;
38
+ }
39
+ *read(reference, values) {
40
+ if (!this.external) {
41
+ const attribute = this.records[reference - 1];
42
+ return { ...attribute, value: values ? this.names.get(attribute.name) : '', next: reference < this.length ? reference + 1 : 0 };
43
+ }
44
+ const request = { attributeOperation: 'read', state: this.state, reference, values };
45
+ yield request;
46
+ if (!request.result)
47
+ throw new TypeError('Incomplete XML attribute read');
48
+ return request.result;
49
+ }
50
+ *expanded(name) {
51
+ if (!this.external) {
52
+ const found = this.expandedNames.has(name);
53
+ this.expandedNames.add(name);
54
+ return found;
55
+ }
56
+ const request = { attributeOperation: 'expanded', state: this.state, name };
57
+ yield request;
58
+ if (request.found === undefined)
59
+ throw new TypeError('Incomplete XML expanded attribute lookup');
60
+ return request.found;
61
+ }
62
+ }
@@ -1,3 +1,5 @@
1
+ import { type XmlAttributeRequest } from "./attributes.js";
2
+ export type { XmlAttributeRequest, XmlAttributeRecord, XmlAttributeState } from "./attributes.js";
1
3
  import { type XmlNamespaceRequest, type XmlFrameRequest } from "./frames.js";
2
4
  export type { XmlParserFrame, XmlFrameRequest, XmlNamespaceScope, XmlNamespaceRequest } from "./frames.js";
3
5
  export { normalizeXmlChunks } from "./characters.js";
@@ -63,12 +65,17 @@ export interface XmlSourceLimits extends XmlStepLimits {
63
65
  readonly storeFrames?: boolean;
64
66
  /** Resolve namespace scopes through host-serviced requests. Requires retainTree: false. */
65
67
  readonly storeNamespaces?: boolean;
68
+ /** Store attribute collections through host requests and emit resolved attributes individually. */
69
+ readonly storeAttributes?: boolean;
70
+ readonly fragmentAttributes?: boolean;
71
+ readonly onAttribute?: (attribute: XmlAttribute, element: XmlElement, continuation?: boolean) => void;
66
72
  }
67
- export type XmlParseStep = XmlSourceStep | XmlFrameRequest | XmlNamespaceRequest;
73
+ export type XmlParseStep = XmlSourceStep | XmlFrameRequest | XmlNamespaceRequest | XmlAttributeRequest;
68
74
  /** Parse a validated, BOM-free, line-normalized UTF-16 source via bounded read requests. */
69
75
  export declare function parseXmlSourceSteps(length: number | undefined, limits?: XmlStepLimits & {
70
76
  readonly storeFrames?: false;
71
77
  readonly storeNamespaces?: false;
78
+ readonly storeAttributes?: false;
72
79
  }): Generator<XmlSourceStep, XmlElement, void>;
73
80
  export declare function parseXmlSourceSteps(length: number | undefined, limits: XmlSourceLimits): Generator<XmlParseStep, XmlElement, void>;
74
81
  export declare function parseXmlSteps(input: string, limits?: XmlStepLimits): Generator<number, XmlElement, void>;
@@ -1,3 +1,4 @@
1
+ import { XmlAttributes } from "./attributes.js";
1
2
  import { XmlFrames, bindNamespace, namespaceValue } from "./frames.js";
2
3
  import { validCharacter } from "./characters.js";
3
4
  export { normalizeXmlChunks } from "./characters.js";
@@ -101,7 +102,7 @@ function* entity(source, start, end, recover) {
101
102
  return String.fromCodePoint(point);
102
103
  }
103
104
  /** Decoded text fragments; source read/work requests remain host-serviced. */
104
- function* textParts(source, start, end, decode, recover) {
105
+ function* textParts(source, start, end, decode, recover, attribute = false) {
105
106
  let cursor = start;
106
107
  while (cursor < end) {
107
108
  let finish = Math.min(end, cursor + 512);
@@ -110,7 +111,13 @@ function* textParts(source, start, end, decode, recover) {
110
111
  if (last >= 0xd800 && last <= 0xdbff)
111
112
  finish--;
112
113
  }
113
- const raw = yield* source.slice(cursor, finish);
114
+ let raw = yield* source.slice(cursor, finish);
115
+ if (attribute) {
116
+ let normalized = "";
117
+ for (const character of raw)
118
+ normalized += "\t\n\r".includes(character) ? " " : character;
119
+ raw = normalized;
120
+ }
114
121
  const found = decode ? raw.indexOf("&") : -1;
115
122
  if (found < 0) {
116
123
  yield raw;
@@ -139,9 +146,9 @@ function* textParts(source, start, end, decode, recover) {
139
146
  cursor = semicolon + 1;
140
147
  }
141
148
  }
142
- function* textFragments(source, start, end, decode, recover) {
149
+ function* textFragments(source, start, end, decode, recover, attribute = false) {
143
150
  let pending = "";
144
- for (const part of textParts(source, start, end, decode, recover)) {
151
+ for (const part of textParts(source, start, end, decode, recover, attribute)) {
145
152
  if (typeof part !== "string") {
146
153
  yield part;
147
154
  continue;
@@ -253,8 +260,12 @@ function validateLimits(limits) {
253
260
  }
254
261
  export function* parseXmlSourceSteps(length, limits = {}) {
255
262
  validateLimits(limits);
263
+ if (limits.fragmentAttributes && !limits.storeAttributes)
264
+ throw new TypeError("XML attribute fragments require storeAttributes: true");
256
265
  if (limits.storeFrames && limits.retainTree !== false)
257
266
  throw new TypeError("Stored XML frames require retainTree: false");
267
+ if (limits.storeAttributes && limits.retainTree !== false)
268
+ throw new TypeError("Stored XML attributes require retainTree: false");
258
269
  if (limits.storeNamespaces && limits.retainTree !== false)
259
270
  throw new TypeError("Stored XML namespaces require retainTree: false");
260
271
  if (limits.fragmentContent && limits.retainTree !== false)
@@ -555,8 +566,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
555
566
  yield 512;
556
567
  pendingWork -= 512;
557
568
  }
558
- let attributes;
559
- const attrMeta = [];
569
+ const attributes = new XmlAttributes(limits.storeAttributes === true);
560
570
  const namespaceParent = yield* stack.peek();
561
571
  let namespaces = namespaceParent?.namespaceScope ?? namespaceParent?.namespaces ?? (initialNamespaces instanceof Map ? new Map(initialNamespaces) : initialNamespaces);
562
572
  let ownsNamespaces = stack.length === 0;
@@ -576,11 +586,11 @@ export function* parseXmlSourceSteps(length, limits = {}) {
576
586
  yield 512;
577
587
  pendingWork -= 512;
578
588
  }
579
- if (attributes?.has(attribute))
589
+ if ((yield* attributes.has(attribute)))
580
590
  invalid("duplicate attribute");
581
591
  if (++attributeCount > maxAttributes)
582
592
  throw new XmlLimitError("maxAttributes", "XML attribute limit exceeded");
583
- if ((attributes?.size ?? 0) >= maxAttributesPerElement)
593
+ if (attributes.size >= maxAttributesPerElement)
584
594
  throw new XmlLimitError("maxAttributesPerElement", "XML attribute limit exceeded");
585
595
  yield* skipWhitespace();
586
596
  if ((yield* source.slice(offset++, offset)) !== "=")
@@ -598,23 +608,45 @@ export function* parseXmlSourceSteps(length, limits = {}) {
598
608
  }
599
609
  if (end < 0)
600
610
  invalid("unterminated attribute");
601
- const raw = (yield* source.slice(offset, end));
602
- pendingWork += raw.length * 2;
603
- while (pendingWork >= 512) {
604
- yield 512;
605
- pendingWork -= 512;
611
+ const fragmented = limits.fragmentAttributes && attribute !== "xmlns" && attrPrefix !== "xmlns";
612
+ let value = "";
613
+ if (fragmented) {
614
+ if ((yield* source.indexOf("<", offset, end)) >= 0)
615
+ invalid("less-than in attribute");
616
+ let size = 0;
617
+ for (const part of textFragments(source, offset, end, true, limits.recover, true)) {
618
+ if (typeof part !== "string")
619
+ yield part;
620
+ else
621
+ size += part.length;
622
+ }
623
+ if (size > maxTextLength - textLength)
624
+ throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
625
+ textLength += size;
606
626
  }
607
- if (raw.indexOf("<") >= 0)
608
- invalid("less-than in attribute");
609
- const normalized = /[\t\n\r]/.test(raw) ? raw.replace(/[\t\n\r]/g, " ") : raw;
610
- const value = normalized.indexOf("&") < 0 ? (pendingWork += normalized.length, normalized) : yield* entities(normalized, limits.recover);
611
- while (pendingWork >= 512) {
612
- yield 512;
613
- pendingWork -= 512;
627
+ else {
628
+ const raw = (yield* source.slice(offset, end));
629
+ pendingWork += raw.length * 2;
630
+ while (pendingWork >= 512) {
631
+ yield 512;
632
+ pendingWork -= 512;
633
+ }
634
+ if (raw.indexOf("<") >= 0)
635
+ invalid("less-than in attribute");
636
+ let normalized = raw;
637
+ if (raw.includes("\t") || raw.includes("\n") || raw.includes("\r")) {
638
+ normalized = "";
639
+ for (const character of raw)
640
+ normalized += "\t\n\r".includes(character) ? " " : character;
641
+ }
642
+ value = normalized.indexOf("&") < 0 ? (pendingWork += normalized.length, normalized) : yield* entities(normalized, limits.recover);
643
+ while (pendingWork >= 512) {
644
+ yield 512;
645
+ pendingWork -= 512;
646
+ }
647
+ admitText(value);
614
648
  }
615
- admitText(value);
616
- (attributes ??= new Map()).set(attribute, value);
617
- attrMeta.push([attribute, attrPrefix, attrLocal]);
649
+ yield* attributes.append(attribute, attrPrefix, attrLocal, value, fragmented ? { start: offset, end } : undefined);
618
650
  offset = end + 1;
619
651
  if (attribute === "xmlns" || attribute.startsWith("xmlns:")) {
620
652
  const nsPrefix = attribute === "xmlns" ? "" : attribute.slice(6);
@@ -636,20 +668,20 @@ export function* parseXmlSourceSteps(length, limits = {}) {
636
668
  throw new XmlLimitError("maxNamespaces", "XML namespace scope limit exceeded");
637
669
  }
638
670
  }
639
- if (attrMeta.length > 0) {
640
- const expanded = attrMeta.length > 1 ? new Set() : undefined;
641
- for (let i = 0; i < attrMeta.length; i++) {
642
- const [attribute, attrPrefix, attrLocal] = attrMeta[i];
671
+ if (attributes.length > 0) {
672
+ for (let cursor = attributes.first; cursor;) {
673
+ const record = yield* attributes.read(cursor, false);
674
+ cursor = record.next;
675
+ const { name: attribute, prefix: attrPrefix, localName: attrLocal } = record;
643
676
  if (attribute === "xmlns" || attribute.startsWith("xmlns:"))
644
677
  continue;
645
678
  pendingWork += attribute.length;
646
679
  if (attrPrefix && (yield* namespaceValue(namespaces, attrPrefix)) === undefined)
647
680
  invalid("unbound attribute prefix");
648
- if (expanded) {
681
+ if (attributes.length > 1) {
649
682
  const key = `${attrPrefix ? (yield* namespaceValue(namespaces, attrPrefix)) : ""}\0${attrLocal}`;
650
- if (expanded.has(key))
683
+ if (yield* attributes.expanded(key))
651
684
  invalid("duplicate expanded attribute");
652
- expanded.add(key);
653
685
  }
654
686
  }
655
687
  }
@@ -668,13 +700,15 @@ export function* parseXmlSourceSteps(length, limits = {}) {
668
700
  limits.onElement?.({ name, namespace, localName }, (yield* stack.peek())?.element, stack.length + 1);
669
701
  admitContent();
670
702
  const retainedAttributes = [];
671
- if (retainContent && attributes) {
672
- for (let i = 0; i < attrMeta.length; i++) {
673
- const [attribute, attrPrefix, attrLocal] = attrMeta[i];
674
- const value = attributes.get(attribute);
703
+ if (retainContent) {
704
+ for (let cursor = attributes.first; cursor;) {
705
+ const record = yield* attributes.read(cursor, !limits.storeAttributes);
706
+ cursor = record.next;
707
+ const { name: attribute, prefix: attrPrefix, localName: attrLocal, value } = record;
675
708
  admitContent();
676
- retainedAttributes.push({ name: attribute, localName: attrLocal,
677
- namespace: attribute === "xmlns" || attrPrefix === "xmlns" ? xmlnsNamespace : attrPrefix ? (yield* namespaceValue(namespaces, attrPrefix)) : "", value });
709
+ if (!limits.storeAttributes)
710
+ retainedAttributes.push({ name: attribute, localName: attrLocal,
711
+ namespace: attribute === "xmlns" || attrPrefix === "xmlns" ? xmlnsNamespace : attrPrefix ? (yield* namespaceValue(namespaces, attrPrefix)) : "", value });
678
712
  pendingWork += attribute.length + 1;
679
713
  }
680
714
  }
@@ -705,9 +739,34 @@ export function* parseXmlSourceSteps(length, limits = {}) {
705
739
  }
706
740
  // Only cache compact, attribute-free spellings. Namespace resolution and
707
741
  // every admission counter still run for each distinct physical element.
708
- if (empty && !attributes && name.length <= 512 && (yield* source.slice(offset - name.length - 3, offset)) === `<${name}/>`)
742
+ if (empty && !attributes.length && name.length <= 512 && (yield* source.slice(offset - name.length - 3, offset)) === `<${name}/>`)
709
743
  previousEmpty = { suffix: name + "/>", name, prefix, localName };
710
744
  limits.events?.({ type: "open", element, parent: parent?.element });
745
+ if (limits.storeAttributes && limits.onAttribute && retainContent) {
746
+ yield 1;
747
+ for (let cursor = attributes.first; cursor;) {
748
+ const attribute = yield* attributes.read(cursor, true);
749
+ cursor = attribute.next;
750
+ const namespace = attribute.name === "xmlns" || attribute.prefix === "xmlns" ? xmlnsNamespace
751
+ : attribute.prefix ? (yield* namespaceValue(namespaces, attribute.prefix)) : "";
752
+ let continuation = false;
753
+ if (attribute.source) {
754
+ for (const part of textFragments(source, attribute.source.start, attribute.source.end, true, limits.recover ? () => { } : undefined, true)) {
755
+ if (typeof part !== "string") {
756
+ yield part;
757
+ continue;
758
+ }
759
+ limits.onAttribute({ name: attribute.name, localName: attribute.localName, namespace, value: part }, element, continuation);
760
+ continuation = true;
761
+ yield 1;
762
+ }
763
+ }
764
+ if (!continuation) {
765
+ limits.onAttribute({ name: attribute.name, localName: attribute.localName, namespace, value: attribute.value }, element);
766
+ yield 1;
767
+ }
768
+ }
769
+ }
711
770
  if (!empty)
712
771
  yield* stack.push({ element, content, name, namespaces: namespaces instanceof Map ? namespaces : new Map(), ...(namespaces instanceof Map ? {} : { namespaceScope: namespaces }) });
713
772
  else
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.797",
11
+ "version": "0.0.799",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.797",
3
+ "version": "0.0.799",
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.797",
3
+ "version": "0.0.799",
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.797",
180
+ "toolcraft-schema": "0.0.799",
181
181
  "toolcraft-design": "*",
182
182
  "@poe-code/frontmatter": "*",
183
183
  "@poe-code/agent-mcp-config": "*",