toolcraft 0.0.787 → 0.0.789
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 +2 -2
- package/dist/composition.json +2 -2
- package/node_modules/@poe-code/safe-fs/README.md +5 -0
- package/node_modules/@poe-code/safe-fs/dist/core.d.ts +2 -2
- package/node_modules/@poe-code/safe-fs/dist/core.js +2 -2
- package/node_modules/@poe-code/safe-fs/dist/storage.d.ts +21 -1
- package/node_modules/@poe-code/safe-fs/dist/storage.js +78 -23
- package/node_modules/@poe-code/xml-ast/README.md +14 -2
- package/node_modules/@poe-code/xml-ast/dist/characters.d.ts +3 -0
- package/node_modules/@poe-code/xml-ast/dist/characters.js +49 -0
- package/node_modules/@poe-code/xml-ast/dist/index.d.ts +14 -1
- package/node_modules/@poe-code/xml-ast/dist/index.js +135 -87
- package/node_modules/@poe-code/xml-ast/dist/source.d.ts +18 -0
- package/node_modules/@poe-code/xml-ast/dist/source.js +47 -0
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +2 -2
package/composition.json
CHANGED
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"name": "toolcraft",
|
|
141
|
-
"version": "0.0.
|
|
141
|
+
"version": "0.0.789",
|
|
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.
|
|
151
|
+
"version": "0.0.789",
|
|
152
152
|
"license": "MIT"
|
|
153
153
|
},
|
|
154
154
|
{
|
package/dist/composition.json
CHANGED
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"name": "toolcraft",
|
|
141
|
-
"version": "0.0.
|
|
141
|
+
"version": "0.0.789",
|
|
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.
|
|
151
|
+
"version": "0.0.789",
|
|
152
152
|
"license": "MIT"
|
|
153
153
|
},
|
|
154
154
|
{
|
|
@@ -10,6 +10,11 @@ working data. Supply `{fs, cwd, env, signal}` and an optional page-cache count
|
|
|
10
10
|
`env.TMPDIR` or `cwd`, with conditional unlink and cleanup through `close()`.
|
|
11
11
|
Providers must support those capabilities; there is no host fallback. Use an
|
|
12
12
|
external backend for large data because memory filesystems retain spilled bytes.
|
|
13
|
+
To bound aggregate pages across multiple live stores, pass the same
|
|
14
|
+
`new PagedStorageCache(16)` as the third argument to each `PagedStorage` constructor.
|
|
15
|
+
This shares a 256 KiB resident-page budget; `cache.residentBytes` reports current
|
|
16
|
+
page retention. Eviction uses each store's filesystem, and each store still owns
|
|
17
|
+
its `close()` cleanup. The budget excludes caller buffers and backend storage.
|
|
13
18
|
`IntegerTable.entries()` replays records in unsigned key order with a fixed
|
|
14
19
|
16-level node stack and batches of at most 128 records. It accepts zero-filled
|
|
15
20
|
caller storage with `allocate`, `read` and `write`; mutation during traversal
|
|
@@ -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 type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from "./xml.js";
|
|
13
13
|
export * from "./contracts/object.js";
|
|
14
14
|
export { ObjectAuthority } from "./fs/object-authority.js";
|
|
@@ -18,4 +18,4 @@ export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./
|
|
|
18
18
|
export type { FileSystemConfig, FileSystemAdapterDescriptor, FileSystemAdapterRegistry } from "./config.js";
|
|
19
19
|
export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
|
|
20
20
|
export { readFileStream } from "./fs/read-file-stream.js";
|
|
21
|
-
export { PagedStorage, IntegerTable, type PagedStorageContext } from "./storage.js";
|
|
21
|
+
export { PagedStorage, PagedStorageCache, IntegerTable, type PagedStorageContext } from "./storage.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";
|
|
@@ -16,4 +16,4 @@ export * from "./fs/s3/http/index.js";
|
|
|
16
16
|
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
17
17
|
export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
|
|
18
18
|
export { readFileStream } from "./fs/read-file-stream.js";
|
|
19
|
-
export { PagedStorage, IntegerTable } from "./storage.js";
|
|
19
|
+
export { PagedStorage, PagedStorageCache, IntegerTable } from "./storage.js";
|
|
@@ -5,12 +5,31 @@ export interface PagedStorageContext {
|
|
|
5
5
|
readonly env: Readonly<Record<string, string | undefined>>;
|
|
6
6
|
readonly signal: AbortSignal;
|
|
7
7
|
}
|
|
8
|
+
type Page = {
|
|
9
|
+
bytes: Uint8Array;
|
|
10
|
+
dirty: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** Shares one resident-page limit across stores, retaining each store's caller
|
|
13
|
+
* filesystem and cleanup ownership. Serializes cache users through eviction IO. */
|
|
14
|
+
export declare class PagedStorageCache {
|
|
15
|
+
private readonly maxPages;
|
|
16
|
+
private readonly pages;
|
|
17
|
+
private active;
|
|
18
|
+
constructor(maxPages: number);
|
|
19
|
+
get residentBytes(): number;
|
|
20
|
+
run<T>(action: () => Promise<T>): Promise<T>;
|
|
21
|
+
acquire(): Promise<Page>;
|
|
22
|
+
retain(page: Page, release: () => Promise<void>): void;
|
|
23
|
+
touch(page: Page): void;
|
|
24
|
+
forget(page: Page): void;
|
|
25
|
+
}
|
|
8
26
|
/** A bounded page cache backed only by the caller filesystem. The default is
|
|
9
27
|
* one MiB; callers own close() on every outcome. Memory filesystems still retain
|
|
10
28
|
* spilled data in RAM, so large workloads require an external backing provider. */
|
|
11
29
|
export declare class PagedStorage {
|
|
12
30
|
private readonly context;
|
|
13
31
|
private readonly maxPages;
|
|
32
|
+
private readonly cache?;
|
|
14
33
|
private readonly pages;
|
|
15
34
|
private readonly controller;
|
|
16
35
|
private readonly signal;
|
|
@@ -23,7 +42,7 @@ export declare class PagedStorage {
|
|
|
23
42
|
private end;
|
|
24
43
|
private active;
|
|
25
44
|
private closing;
|
|
26
|
-
constructor(context: PagedStorageContext, maxPages?: number);
|
|
45
|
+
constructor(context: PagedStorageContext, maxPages?: number, cache?: PagedStorageCache | undefined);
|
|
27
46
|
allocate(length: number): number;
|
|
28
47
|
private operation;
|
|
29
48
|
private spill;
|
|
@@ -55,3 +74,4 @@ export declare class IntegerTable {
|
|
|
55
74
|
private lookup;
|
|
56
75
|
private persist;
|
|
57
76
|
}
|
|
77
|
+
export {};
|
|
@@ -1,12 +1,50 @@
|
|
|
1
1
|
import { FsError, isFsError } from "./contracts/errors.js";
|
|
2
2
|
const pageBytes = 16 * 1024;
|
|
3
3
|
let serial = 0;
|
|
4
|
+
/** Shares one resident-page limit across stores, retaining each store's caller
|
|
5
|
+
* filesystem and cleanup ownership. Serializes cache users through eviction IO. */
|
|
6
|
+
export class PagedStorageCache {
|
|
7
|
+
maxPages;
|
|
8
|
+
pages = new Map();
|
|
9
|
+
active = Promise.resolve();
|
|
10
|
+
constructor(maxPages) {
|
|
11
|
+
this.maxPages = maxPages;
|
|
12
|
+
if (!Number.isSafeInteger(maxPages) || maxPages < 1)
|
|
13
|
+
throw new RangeError("Invalid storage page cache size");
|
|
14
|
+
}
|
|
15
|
+
get residentBytes() { return this.pages.size * pageBytes; }
|
|
16
|
+
run(action) {
|
|
17
|
+
const work = this.active.then(action);
|
|
18
|
+
this.active = work.then(() => undefined, () => undefined);
|
|
19
|
+
return work;
|
|
20
|
+
}
|
|
21
|
+
async acquire() {
|
|
22
|
+
if (this.pages.size < this.maxPages)
|
|
23
|
+
return { bytes: new Uint8Array(pageBytes), dirty: false };
|
|
24
|
+
const [page, release] = this.pages.entries().next().value;
|
|
25
|
+
await release();
|
|
26
|
+
this.pages.delete(page);
|
|
27
|
+
page.bytes.fill(0);
|
|
28
|
+
return page;
|
|
29
|
+
}
|
|
30
|
+
retain(page, release) {
|
|
31
|
+
this.pages.delete(page);
|
|
32
|
+
this.pages.set(page, release);
|
|
33
|
+
}
|
|
34
|
+
touch(page) {
|
|
35
|
+
const release = this.pages.get(page);
|
|
36
|
+
this.pages.delete(page);
|
|
37
|
+
this.pages.set(page, release);
|
|
38
|
+
}
|
|
39
|
+
forget(page) { this.pages.delete(page); }
|
|
40
|
+
}
|
|
4
41
|
/** A bounded page cache backed only by the caller filesystem. The default is
|
|
5
42
|
* one MiB; callers own close() on every outcome. Memory filesystems still retain
|
|
6
43
|
* spilled data in RAM, so large workloads require an external backing provider. */
|
|
7
44
|
export class PagedStorage {
|
|
8
45
|
context;
|
|
9
46
|
maxPages;
|
|
47
|
+
cache;
|
|
10
48
|
pages = new Map();
|
|
11
49
|
controller = new AbortController();
|
|
12
50
|
signal;
|
|
@@ -19,9 +57,10 @@ export class PagedStorage {
|
|
|
19
57
|
end = 8; // Zero is the absent-pointer sentinel.
|
|
20
58
|
active = Promise.resolve();
|
|
21
59
|
closing;
|
|
22
|
-
constructor(context, maxPages = 64) {
|
|
60
|
+
constructor(context, maxPages = 64, cache) {
|
|
23
61
|
this.context = context;
|
|
24
62
|
this.maxPages = maxPages;
|
|
63
|
+
this.cache = cache;
|
|
25
64
|
if (!Number.isSafeInteger(maxPages) || maxPages < 1)
|
|
26
65
|
throw new RangeError("Invalid storage page cache size");
|
|
27
66
|
this.signal = AbortSignal.any([context.signal, this.controller.signal]);
|
|
@@ -36,10 +75,11 @@ export class PagedStorage {
|
|
|
36
75
|
}
|
|
37
76
|
operation(action) {
|
|
38
77
|
this.signal.throwIfAborted();
|
|
39
|
-
const
|
|
78
|
+
const execute = () => {
|
|
40
79
|
this.signal.throwIfAborted();
|
|
41
80
|
return action();
|
|
42
|
-
}
|
|
81
|
+
};
|
|
82
|
+
const work = this.active.then(() => this.cache ? this.cache.run(execute) : execute());
|
|
43
83
|
this.active = work.then(() => undefined, () => undefined);
|
|
44
84
|
return work;
|
|
45
85
|
}
|
|
@@ -101,6 +141,7 @@ export class PagedStorage {
|
|
|
101
141
|
if (cached) {
|
|
102
142
|
this.pages.delete(number);
|
|
103
143
|
this.pages.set(number, cached);
|
|
144
|
+
this.cache?.touch(cached);
|
|
104
145
|
return cached;
|
|
105
146
|
}
|
|
106
147
|
let reusable;
|
|
@@ -110,11 +151,14 @@ export class PagedStorage {
|
|
|
110
151
|
const [oldNumber, oldPage] = this.pages.entries().next().value;
|
|
111
152
|
await this.flush(oldNumber, oldPage);
|
|
112
153
|
this.pages.delete(oldNumber);
|
|
154
|
+
this.cache?.forget(oldPage);
|
|
113
155
|
reusable = oldPage;
|
|
114
156
|
}
|
|
115
157
|
// Flush owns the bytes until its awaited write completes. Reuse only then;
|
|
116
158
|
// zero new/unwritten ranges so the previous page cannot leak into them.
|
|
117
|
-
const page = reusable ?? { bytes: new Uint8Array(pageBytes), dirty: false };
|
|
159
|
+
const page = reusable ?? (this.cache ? await this.cache.acquire() : { bytes: new Uint8Array(pageBytes), dirty: false });
|
|
160
|
+
// Shared eviction may await IO owned by a different, still-live signal.
|
|
161
|
+
this.signal.throwIfAborted();
|
|
118
162
|
if (reusable)
|
|
119
163
|
page.bytes.fill(0);
|
|
120
164
|
if (number * pageBytes < this.diskLength) {
|
|
@@ -128,6 +172,12 @@ export class PagedStorage {
|
|
|
128
172
|
}
|
|
129
173
|
}
|
|
130
174
|
this.pages.set(number, page);
|
|
175
|
+
this.cache?.retain(page, async () => {
|
|
176
|
+
if (!this.descriptor)
|
|
177
|
+
await this.spill();
|
|
178
|
+
await this.flush(number, page);
|
|
179
|
+
this.pages.delete(number);
|
|
180
|
+
});
|
|
131
181
|
return page;
|
|
132
182
|
}
|
|
133
183
|
write(position, bytes) {
|
|
@@ -168,28 +218,33 @@ export class PagedStorage {
|
|
|
168
218
|
this.controller.abort(new FsError("ECANCELED"));
|
|
169
219
|
return this.closing ??= (async () => {
|
|
170
220
|
await this.active.catch(() => { });
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
221
|
+
const retire = async () => {
|
|
222
|
+
for (const page of this.pages.values())
|
|
223
|
+
this.cache?.forget(page);
|
|
224
|
+
this.pages.clear();
|
|
225
|
+
const descriptor = this.descriptor;
|
|
226
|
+
if (!descriptor)
|
|
227
|
+
return;
|
|
228
|
+
const cancellation = new FsError("ECANCELED");
|
|
229
|
+
try {
|
|
230
|
+
if (!this.unlinked) {
|
|
231
|
+
const expected = this.initial ?? await descriptor.stat();
|
|
232
|
+
await this.context.fs.removeFileConditional(this.path, { parent: this.parent, expected }).catch(error => {
|
|
233
|
+
if (!isFsError(error) || error.code !== "ENOENT")
|
|
234
|
+
throw error;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
// Conditional descriptors must discard their private staged writes, not
|
|
240
|
+
// publish the scratch data when the command retires its handle.
|
|
241
|
+
await descriptor.close({ signal: AbortSignal.abort(cancellation) }).catch(error => {
|
|
242
|
+
if (error !== cancellation)
|
|
181
243
|
throw error;
|
|
182
244
|
});
|
|
183
245
|
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Conditional descriptors must discard their private staged writes, not
|
|
187
|
-
// publish the scratch data when the command retires its handle.
|
|
188
|
-
await descriptor.close({ signal: AbortSignal.abort(cancellation) }).catch(error => {
|
|
189
|
-
if (error !== cancellation)
|
|
190
|
-
throw error;
|
|
191
|
-
});
|
|
192
|
-
}
|
|
246
|
+
};
|
|
247
|
+
await (this.cache ? this.cache.run(retire) : retire());
|
|
193
248
|
})();
|
|
194
249
|
}
|
|
195
250
|
}
|
|
@@ -26,9 +26,12 @@ Pass positive integer limits for depth, element and content counts, attributes,
|
|
|
26
26
|
`parseXmlSteps` yields work counts so a host can account for parsing and yield between checkpoints. Syntax errors and resource limits remain errors by default; parsing never fetches external entities.
|
|
27
27
|
Pass `recover: message => report(message)` to repair truncated elements,
|
|
28
28
|
mismatched closing tags, and undeclared entities. Each repair calls the callback.
|
|
29
|
-
Resource limits and the prohibition on DTDs remain in force.
|
|
29
|
+
Resource limits and the prohibition on DTDs remain in force. With `retainTree: false`,
|
|
30
|
+
`parseXmlSteps` discards completed nodes and can emit synchronous `events`, including
|
|
31
|
+
repaired closing events. Drain events between generator checkpoints to await
|
|
32
|
+
external storage; the source string and open parser frames remain resident.
|
|
30
33
|
|
|
31
|
-
`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
|
|
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.
|
|
32
35
|
|
|
33
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.
|
|
34
37
|
|
|
@@ -42,3 +45,12 @@ window awaits the consumer before advancing. Event elements contain names,
|
|
|
42
45
|
attributes and namespace metadata; individual XML tokens and open ancestry still
|
|
43
46
|
remain resident. This mode lets a caller own document storage independently of
|
|
44
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.
|
|
@@ -0,0 +1,49 @@
|
|
|
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) {
|
|
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
|
+
}
|
|
45
|
+
if (high)
|
|
46
|
+
throw new SyntaxError('Invalid XML: invalid character');
|
|
47
|
+
if (output)
|
|
48
|
+
yield output;
|
|
49
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export { normalizeXmlChunks } from "./characters.js";
|
|
2
|
+
import { type XmlSourceStep } from "./source.js";
|
|
3
|
+
export type { XmlSourceRead, XmlSourceStep } from "./source.js";
|
|
4
|
+
import type { XmlStreamEvent } from "./stream.js";
|
|
1
5
|
export { XmlLimitError } from "./errors.js";
|
|
2
6
|
export interface XmlName {
|
|
3
7
|
readonly name: string;
|
|
@@ -40,6 +44,15 @@ export interface XmlLimits {
|
|
|
40
44
|
readonly maxTextLength?: number;
|
|
41
45
|
readonly onElement?: (element: XmlName, parent: XmlName | undefined, depth: number) => void;
|
|
42
46
|
}
|
|
43
|
-
export
|
|
47
|
+
export interface XmlStepLimits extends XmlLimits {
|
|
48
|
+
/** Return root metadata without retaining descendants or text. */
|
|
49
|
+
readonly retainTree?: boolean;
|
|
50
|
+
/** Synchronous ordered events, including repaired closes. Requires retainTree:
|
|
51
|
+
* false. Drive the step iterator to await external consumers between windows. */
|
|
52
|
+
readonly events?: (event: XmlStreamEvent) => void;
|
|
53
|
+
}
|
|
54
|
+
/** 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>;
|
|
56
|
+
export declare function parseXmlSteps(input: string, limits?: XmlStepLimits): Generator<number, XmlElement, void>;
|
|
44
57
|
export declare function parseXml(input: string, limits?: XmlLimits): XmlElement;
|
|
45
58
|
export { parseXmlStream, type XmlStreamLimits, type XmlStreamEvent } from "./stream.js";
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { validCharacter } from "./characters.js";
|
|
2
|
+
export { normalizeXmlChunks } from "./characters.js";
|
|
3
|
+
import { XmlSource } from "./source.js";
|
|
1
4
|
import { XmlLimitError } from "./errors.js";
|
|
2
5
|
export { XmlLimitError } from "./errors.js";
|
|
3
6
|
function* find(source, needle, start) {
|
|
4
|
-
const found = source.indexOf(needle, start);
|
|
7
|
+
const found = typeof source === "string" ? source.indexOf(needle, start) : yield* source.indexOf(needle, start);
|
|
5
8
|
const end = found < 0 ? source.length : found;
|
|
6
9
|
let remaining = end - start;
|
|
7
10
|
while (remaining >= 512) {
|
|
@@ -17,12 +20,6 @@ const xmlnsNamespace = "http://www.w3.org/2000/xmlns/";
|
|
|
17
20
|
function invalid(message) {
|
|
18
21
|
throw new SyntaxError(`Invalid XML: ${message}`);
|
|
19
22
|
}
|
|
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
23
|
function nameStart(point) {
|
|
27
24
|
return point === 95 || (point >= 65 && point <= 90) || (point >= 97 && point <= 122)
|
|
28
25
|
|| (point >= 0xc0 && point <= 0xd6) || (point >= 0xd8 && point <= 0xf6)
|
|
@@ -182,67 +179,39 @@ function* validDeclaration(content, expectedEncoding) {
|
|
|
182
179
|
}
|
|
183
180
|
return offset === content.length;
|
|
184
181
|
}
|
|
185
|
-
|
|
182
|
+
function validateLimits(limits) {
|
|
183
|
+
for (const limit of [limits.maxDepth, limits.maxNodes, limits.maxAttributes, limits.maxContentNodes, limits.maxAttributesPerElement, limits.maxNamespaces]) {
|
|
184
|
+
if (limit !== undefined && ((limit !== Infinity && !Number.isSafeInteger(limit)) || limit < 1))
|
|
185
|
+
throw new RangeError("XML limits must be positive integers");
|
|
186
|
+
}
|
|
187
|
+
if (limits.maxTextLength !== undefined && ((limits.maxTextLength !== Infinity && !Number.isSafeInteger(limits.maxTextLength)) || limits.maxTextLength < 1))
|
|
188
|
+
throw new RangeError("XML limits must be positive integers");
|
|
189
|
+
if (limits.events && limits.retainTree !== false)
|
|
190
|
+
throw new TypeError("XML events require retainTree: false");
|
|
191
|
+
}
|
|
192
|
+
/** Parse a validated, BOM-free, line-normalized UTF-16 source via bounded read requests. */
|
|
193
|
+
export function* parseXmlSourceSteps(length, limits = {}) {
|
|
194
|
+
validateLimits(limits);
|
|
195
|
+
const source = new XmlSource(length);
|
|
186
196
|
const maxDepth = limits.maxDepth ?? Infinity;
|
|
187
197
|
const maxNodes = limits.maxNodes ?? Infinity;
|
|
188
198
|
const maxAttributes = limits.maxAttributes ?? Infinity;
|
|
189
199
|
const retainContent = limits.retainContent !== false;
|
|
200
|
+
const retainTree = limits.retainTree !== false;
|
|
201
|
+
const retain = retainTree && retainContent;
|
|
190
202
|
const maxContentNodes = limits.maxContentNodes ?? Infinity;
|
|
191
203
|
const emptyContent = Object.freeze([]);
|
|
192
204
|
const emptyAttributes = Object.freeze([]);
|
|
193
205
|
const emptyNamespaces = new Map();
|
|
194
206
|
const maxAttributesPerElement = limits.maxAttributesPerElement ?? Infinity;
|
|
195
207
|
const maxNamespaces = limits.maxNamespaces ?? Infinity;
|
|
196
|
-
for (const limit of [limits.maxDepth, limits.maxNodes, limits.maxAttributes, limits.maxContentNodes, limits.maxAttributesPerElement, limits.maxNamespaces]) {
|
|
197
|
-
if (limit !== undefined && ((limit !== Infinity && !Number.isSafeInteger(limit)) || limit < 1))
|
|
198
|
-
throw new RangeError("XML limits must be positive integers");
|
|
199
|
-
}
|
|
200
208
|
const maxTextLength = limits.maxTextLength ?? Infinity;
|
|
201
|
-
if (limits.maxTextLength !== undefined && ((maxTextLength !== Infinity && !Number.isSafeInteger(maxTextLength)) || maxTextLength < 1))
|
|
202
|
-
throw new RangeError("XML limits must be positive integers");
|
|
203
209
|
let textLength = 0;
|
|
204
210
|
const admitText = (text) => {
|
|
205
211
|
if (text.length > maxTextLength - textLength)
|
|
206
212
|
throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
|
|
207
213
|
textLength += text.length;
|
|
208
214
|
};
|
|
209
|
-
const chunks = [];
|
|
210
|
-
const start = input.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
211
|
-
let chunkStart = start;
|
|
212
|
-
let chunkLength = 0;
|
|
213
|
-
let normalizedChunk = "";
|
|
214
|
-
// Keep validated spans intact; only changed line endings need new strings.
|
|
215
|
-
for (let index = start; index < input.length; index++) {
|
|
216
|
-
const point = input.codePointAt(index);
|
|
217
|
-
if (!validCharacter(point))
|
|
218
|
-
invalid("invalid character");
|
|
219
|
-
if (point === 13) {
|
|
220
|
-
normalizedChunk += input.slice(chunkStart, index) + "\n";
|
|
221
|
-
if (input.charCodeAt(index + 1) === 10)
|
|
222
|
-
index++;
|
|
223
|
-
chunkStart = index + 1;
|
|
224
|
-
chunkLength++;
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
chunkLength += point > 0xffff ? 2 : 1;
|
|
228
|
-
if (point > 0xffff)
|
|
229
|
-
index++;
|
|
230
|
-
}
|
|
231
|
-
if (chunkLength >= 512) {
|
|
232
|
-
if (normalizedChunk) {
|
|
233
|
-
chunks.push(normalizedChunk + input.slice(chunkStart, index + 1));
|
|
234
|
-
normalizedChunk = "";
|
|
235
|
-
chunkStart = index + 1;
|
|
236
|
-
}
|
|
237
|
-
chunkLength = 0;
|
|
238
|
-
yield 512;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
if (chunkLength)
|
|
242
|
-
yield chunkLength;
|
|
243
|
-
if (chunks.length || normalizedChunk)
|
|
244
|
-
chunks.push(normalizedChunk + input.slice(chunkStart));
|
|
245
|
-
const source = chunks.length ? chunks.join("") : input.slice(start);
|
|
246
215
|
const stack = [];
|
|
247
216
|
let root;
|
|
248
217
|
const prolog = [];
|
|
@@ -262,7 +231,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
262
231
|
const skipWhitespace = function* () {
|
|
263
232
|
const start = offset;
|
|
264
233
|
while (offset < source.length) {
|
|
265
|
-
const c = source.charCodeAt(offset);
|
|
234
|
+
const c = (yield* source.charCodeAt(offset));
|
|
266
235
|
if (c !== 32 && c !== 9 && c !== 10 && c !== 13)
|
|
267
236
|
break;
|
|
268
237
|
offset++;
|
|
@@ -277,7 +246,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
277
246
|
const scanName = function* () {
|
|
278
247
|
const start = offset;
|
|
279
248
|
while (offset < source.length) {
|
|
280
|
-
const c = source.charCodeAt(offset);
|
|
249
|
+
const c = (yield* source.charCodeAt(offset));
|
|
281
250
|
if (c === 32 || c === 9 || c === 13 || c === 10 || c === 47 || c === 61 || c === 62 || c === 63)
|
|
282
251
|
break;
|
|
283
252
|
offset++;
|
|
@@ -287,7 +256,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
287
256
|
pendingWork -= 512;
|
|
288
257
|
}
|
|
289
258
|
}
|
|
290
|
-
const name = source.slice(start, offset);
|
|
259
|
+
const name = (yield* source.slice(start, offset));
|
|
291
260
|
pendingWork += offset - start;
|
|
292
261
|
while (pendingWork >= 512) {
|
|
293
262
|
yield 512;
|
|
@@ -304,10 +273,10 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
304
273
|
pendingWork -= 512;
|
|
305
274
|
}
|
|
306
275
|
}
|
|
307
|
-
if (source.charCodeAt(offset) !== 60) {
|
|
308
|
-
const next = source.indexOf("<", offset);
|
|
276
|
+
if ((yield* source.charCodeAt(offset)) !== 60) {
|
|
277
|
+
const next = (yield* source.indexOf("<", offset));
|
|
309
278
|
const endPos = next < 0 ? source.length : next;
|
|
310
|
-
const text = source.slice(offset, endPos);
|
|
279
|
+
const text = (yield* source.slice(offset, endPos));
|
|
311
280
|
pendingWork += (endPos - offset) + text.length;
|
|
312
281
|
while (pendingWork >= 512) {
|
|
313
282
|
yield 512;
|
|
@@ -323,11 +292,13 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
323
292
|
admitText(resolved);
|
|
324
293
|
const parent = stack.at(-1);
|
|
325
294
|
if (parent) {
|
|
326
|
-
|
|
295
|
+
if (retainTree)
|
|
296
|
+
parent.element.text += resolved;
|
|
327
297
|
if (resolved.length) {
|
|
328
298
|
admitContent();
|
|
329
|
-
if (
|
|
299
|
+
if (retain)
|
|
330
300
|
parent.content.push({ kind: "text", text: resolved });
|
|
301
|
+
limits.events?.({ type: "content", content: { kind: "text", text: resolved }, parent: parent.element });
|
|
331
302
|
}
|
|
332
303
|
}
|
|
333
304
|
else {
|
|
@@ -347,42 +318,46 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
347
318
|
}
|
|
348
319
|
if (resolved.length) {
|
|
349
320
|
admitContent();
|
|
350
|
-
if (
|
|
321
|
+
if (retain)
|
|
351
322
|
(root ? epilog : prolog).push({ kind: "text", text: resolved });
|
|
323
|
+
limits.events?.({ type: "content", content: { kind: "text", text: resolved }, parent: undefined });
|
|
352
324
|
}
|
|
353
325
|
}
|
|
354
326
|
offset = endPos;
|
|
355
327
|
}
|
|
356
|
-
else if (source.startsWith("<!--", offset)) {
|
|
328
|
+
else if ((yield* source.startsWith("<!--", offset))) {
|
|
357
329
|
const end = yield* find(source, "-->", offset + 4);
|
|
358
|
-
|
|
330
|
+
const text = end < 0 ? "" : yield* source.slice(offset + 4, end);
|
|
331
|
+
if (end < 0 || (yield* find(text, "--", 0)) >= 0 || text.endsWith("-")) {
|
|
359
332
|
invalid("malformed comment");
|
|
360
333
|
}
|
|
361
334
|
const parent = stack.at(-1);
|
|
362
|
-
const text = source.slice(offset + 4, end);
|
|
363
335
|
admitText(text);
|
|
364
336
|
admitContent();
|
|
365
|
-
if (
|
|
337
|
+
if (retain) {
|
|
366
338
|
(parent?.content ?? (root ? epilog : prolog)).push({ kind: "comment", text });
|
|
367
339
|
}
|
|
340
|
+
limits.events?.({ type: "content", content: { kind: "comment", text }, parent: parent?.element });
|
|
368
341
|
offset = end + 3;
|
|
369
342
|
}
|
|
370
|
-
else if (source.startsWith("<![CDATA[", offset)) {
|
|
343
|
+
else if ((yield* source.startsWith("<![CDATA[", offset))) {
|
|
371
344
|
if (!stack.length)
|
|
372
345
|
invalid("CDATA outside root");
|
|
373
346
|
const end = yield* find(source, "]]>", offset + 9);
|
|
374
347
|
if (end < 0)
|
|
375
348
|
invalid("unterminated CDATA");
|
|
376
|
-
const cdataText = source.slice(offset + 9, end);
|
|
349
|
+
const cdataText = (yield* source.slice(offset + 9, end));
|
|
377
350
|
admitText(cdataText);
|
|
378
351
|
const parent = stack.at(-1);
|
|
379
|
-
|
|
352
|
+
if (retainTree)
|
|
353
|
+
parent.element.text += cdataText;
|
|
380
354
|
admitContent();
|
|
381
|
-
if (
|
|
355
|
+
if (retain)
|
|
382
356
|
parent.content.push({ kind: "cdata", text: cdataText });
|
|
357
|
+
limits.events?.({ type: "content", content: { kind: "cdata", text: cdataText }, parent: parent.element });
|
|
383
358
|
offset = end + 3;
|
|
384
359
|
}
|
|
385
|
-
else if (source.startsWith("<?", offset)) {
|
|
360
|
+
else if ((yield* source.startsWith("<?", offset))) {
|
|
386
361
|
const start = offset;
|
|
387
362
|
offset += 2;
|
|
388
363
|
const [target] = yield* scanName();
|
|
@@ -393,14 +368,14 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
393
368
|
const end = yield* find(source, "?>", offset);
|
|
394
369
|
if (end < 0)
|
|
395
370
|
invalid("unterminated processing instruction");
|
|
396
|
-
const content = source.slice(offset, end);
|
|
371
|
+
const content = (yield* source.slice(offset, end));
|
|
397
372
|
if (target.length === 3 && target.toLowerCase() === "xml") {
|
|
398
373
|
if (start !== 0 || target !== "xml"
|
|
399
374
|
|| !(yield* validDeclaration(content, limits.expectedEncoding))) {
|
|
400
375
|
invalid("unsupported XML declaration");
|
|
401
376
|
}
|
|
402
377
|
if (retainContent)
|
|
403
|
-
declaration = source.slice(start, end + 2);
|
|
378
|
+
declaration = (yield* source.slice(start, end + 2));
|
|
404
379
|
}
|
|
405
380
|
else if (content && !" \t\n\r".includes(content[0]))
|
|
406
381
|
invalid("invalid processing instruction");
|
|
@@ -417,16 +392,17 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
417
392
|
const text = content.slice(wsStart);
|
|
418
393
|
admitText(text);
|
|
419
394
|
admitContent();
|
|
420
|
-
if (
|
|
395
|
+
if (retain) {
|
|
421
396
|
(parent?.content ?? (root ? epilog : prolog)).push({ kind: "processing-instruction", target, text });
|
|
422
397
|
}
|
|
398
|
+
limits.events?.({ type: "content", content: { kind: "processing-instruction", target, text }, parent: parent?.element });
|
|
423
399
|
}
|
|
424
400
|
offset = end + 2;
|
|
425
401
|
}
|
|
426
|
-
else if (source.startsWith("<!", offset)) {
|
|
402
|
+
else if ((yield* source.startsWith("<!", offset))) {
|
|
427
403
|
invalid("DTD and entity declarations are forbidden");
|
|
428
404
|
}
|
|
429
|
-
else if (source.charCodeAt(offset + 1) === 47) {
|
|
405
|
+
else if ((yield* source.charCodeAt(offset + 1)) === 47) {
|
|
430
406
|
offset += 2;
|
|
431
407
|
const [name] = yield* scanName();
|
|
432
408
|
yield* skipWhitespace();
|
|
@@ -434,15 +410,18 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
434
410
|
yield 512;
|
|
435
411
|
pendingWork -= 512;
|
|
436
412
|
}
|
|
437
|
-
|
|
413
|
+
const closed = (yield* source.slice(offset++, offset)) === ">" ? stack.pop() : undefined;
|
|
414
|
+
if (closed?.name !== name) {
|
|
438
415
|
if (!limits.recover)
|
|
439
416
|
invalid("mismatched closing tag");
|
|
440
417
|
limits.recover("mismatched closing tag");
|
|
441
418
|
}
|
|
419
|
+
if (closed)
|
|
420
|
+
limits.events?.({ type: "close", element: closed.element, parent: stack.at(-1)?.element });
|
|
442
421
|
}
|
|
443
422
|
else {
|
|
444
423
|
offset++;
|
|
445
|
-
const repeated = previousEmpty && source.startsWith(previousEmpty.suffix, offset) ? previousEmpty : undefined;
|
|
424
|
+
const repeated = previousEmpty && (yield* source.startsWith(previousEmpty.suffix, offset)) ? previousEmpty : undefined;
|
|
446
425
|
const [name, prefix, localName] = repeated
|
|
447
426
|
? [repeated.name, repeated.prefix, repeated.localName]
|
|
448
427
|
: (yield* scanName());
|
|
@@ -465,7 +444,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
465
444
|
yield 512;
|
|
466
445
|
pendingWork -= 512;
|
|
467
446
|
}
|
|
468
|
-
const ch = source.charCodeAt(offset);
|
|
447
|
+
const ch = (yield* source.charCodeAt(offset));
|
|
469
448
|
if (ch === 47 || ch === 62 || limits.recover && offset === source.length)
|
|
470
449
|
break;
|
|
471
450
|
if (ws === 0)
|
|
@@ -482,13 +461,13 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
482
461
|
if ((attributes?.size ?? 0) >= maxAttributesPerElement)
|
|
483
462
|
throw new XmlLimitError("maxAttributesPerElement", "XML attribute limit exceeded");
|
|
484
463
|
yield* skipWhitespace();
|
|
485
|
-
if (source
|
|
464
|
+
if ((yield* source.slice(offset++, offset)) !== "=")
|
|
486
465
|
invalid("missing attribute equals");
|
|
487
466
|
yield* skipWhitespace();
|
|
488
|
-
const quote = source
|
|
467
|
+
const quote = (yield* source.slice(offset++, offset));
|
|
489
468
|
if (quote !== '"' && quote !== "'")
|
|
490
469
|
invalid("unquoted attribute");
|
|
491
|
-
const end = source.indexOf(quote, offset);
|
|
470
|
+
const end = (yield* source.indexOf(quote, offset));
|
|
492
471
|
const scanLen = (end < 0 ? source.length : end) - offset;
|
|
493
472
|
pendingWork += scanLen;
|
|
494
473
|
while (pendingWork >= 512) {
|
|
@@ -497,7 +476,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
497
476
|
}
|
|
498
477
|
if (end < 0)
|
|
499
478
|
invalid("unterminated attribute");
|
|
500
|
-
const raw = source.slice(offset, end);
|
|
479
|
+
const raw = (yield* source.slice(offset, end));
|
|
501
480
|
pendingWork += raw.length * 2;
|
|
502
481
|
while (pendingWork >= 512) {
|
|
503
482
|
yield 512;
|
|
@@ -581,31 +560,36 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
581
560
|
yield 512;
|
|
582
561
|
pendingWork -= 512;
|
|
583
562
|
}
|
|
584
|
-
const content =
|
|
563
|
+
const content = retain ? [] : undefined;
|
|
585
564
|
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 }) } : {}) };
|
|
586
565
|
const parent = stack.at(-1);
|
|
587
566
|
if (parent) {
|
|
588
|
-
|
|
589
|
-
|
|
567
|
+
if (retainTree) {
|
|
568
|
+
parent.element.children.push(element);
|
|
569
|
+
parent.content?.push(element);
|
|
570
|
+
}
|
|
590
571
|
}
|
|
591
572
|
else if (root)
|
|
592
573
|
invalid("multiple root elements");
|
|
593
574
|
else
|
|
594
575
|
root = element;
|
|
595
|
-
const empty = source
|
|
576
|
+
const empty = (yield* source.slice(offset, offset + 1)) === "/";
|
|
596
577
|
if (empty)
|
|
597
578
|
offset++;
|
|
598
|
-
if (source
|
|
579
|
+
if ((yield* source.slice(offset++, offset)) !== ">") {
|
|
599
580
|
if (!limits.recover)
|
|
600
581
|
invalid("unterminated start tag");
|
|
601
582
|
limits.recover("unterminated start tag");
|
|
602
583
|
}
|
|
603
584
|
// Only cache compact, attribute-free spellings. Namespace resolution and
|
|
604
585
|
// every admission counter still run for each distinct physical element.
|
|
605
|
-
if (empty && !attributes && name.length <= 512 && source.slice(offset - name.length - 3, offset) === `<${name}/>`)
|
|
586
|
+
if (empty && !attributes && name.length <= 512 && (yield* source.slice(offset - name.length - 3, offset)) === `<${name}/>`)
|
|
606
587
|
previousEmpty = { suffix: name + "/>", name, prefix, localName };
|
|
588
|
+
limits.events?.({ type: "open", element, parent: parent?.element });
|
|
607
589
|
if (!empty)
|
|
608
590
|
stack.push({ element, content, name, namespaces });
|
|
591
|
+
else
|
|
592
|
+
limits.events?.({ type: "close", element, parent: parent?.element });
|
|
609
593
|
}
|
|
610
594
|
}
|
|
611
595
|
if (pendingWork > 0) {
|
|
@@ -618,9 +602,73 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
618
602
|
if (!limits.recover)
|
|
619
603
|
invalid("incomplete document");
|
|
620
604
|
limits.recover("incomplete document");
|
|
605
|
+
while (stack.length) {
|
|
606
|
+
const closed = stack.pop();
|
|
607
|
+
limits.events?.({ type: "close", element: closed.element, parent: stack.at(-1)?.element });
|
|
608
|
+
yield 1;
|
|
609
|
+
}
|
|
621
610
|
}
|
|
622
611
|
return root;
|
|
623
612
|
}
|
|
613
|
+
function* normalizeXmlSteps(input) {
|
|
614
|
+
const chunks = [];
|
|
615
|
+
const start = input.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
616
|
+
let chunkStart = start;
|
|
617
|
+
let chunkLength = 0;
|
|
618
|
+
let normalizedChunk = "";
|
|
619
|
+
// Keep validated spans intact; only changed line endings need new strings.
|
|
620
|
+
for (let index = start; index < input.length; index++) {
|
|
621
|
+
const point = input.codePointAt(index);
|
|
622
|
+
if (!validCharacter(point))
|
|
623
|
+
invalid("invalid character");
|
|
624
|
+
if (point === 13) {
|
|
625
|
+
normalizedChunk += input.slice(chunkStart, index) + "\n";
|
|
626
|
+
if (input.charCodeAt(index + 1) === 10)
|
|
627
|
+
index++;
|
|
628
|
+
chunkStart = index + 1;
|
|
629
|
+
chunkLength++;
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
chunkLength += point > 0xffff ? 2 : 1;
|
|
633
|
+
if (point > 0xffff)
|
|
634
|
+
index++;
|
|
635
|
+
}
|
|
636
|
+
if (chunkLength >= 512) {
|
|
637
|
+
if (normalizedChunk) {
|
|
638
|
+
chunks.push(normalizedChunk + input.slice(chunkStart, index + 1));
|
|
639
|
+
normalizedChunk = "";
|
|
640
|
+
chunkStart = index + 1;
|
|
641
|
+
}
|
|
642
|
+
chunkLength = 0;
|
|
643
|
+
yield 512;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (chunkLength)
|
|
647
|
+
yield chunkLength;
|
|
648
|
+
if (chunks.length || normalizedChunk)
|
|
649
|
+
chunks.push(normalizedChunk + input.slice(chunkStart));
|
|
650
|
+
return chunks.length ? chunks.join("") : input.slice(start);
|
|
651
|
+
}
|
|
652
|
+
export function* parseXmlSteps(input, limits = {}) {
|
|
653
|
+
validateLimits(limits);
|
|
654
|
+
const source = yield* normalizeXmlSteps(input);
|
|
655
|
+
const parser = parseXmlSourceSteps(source.length, limits);
|
|
656
|
+
let step = parser.next();
|
|
657
|
+
try {
|
|
658
|
+
while (!step.done) {
|
|
659
|
+
if (typeof step.value === "number")
|
|
660
|
+
yield step.value;
|
|
661
|
+
else
|
|
662
|
+
step.value.value = source.slice(step.value.offset, step.value.offset + step.value.length);
|
|
663
|
+
step = parser.next();
|
|
664
|
+
}
|
|
665
|
+
return step.value;
|
|
666
|
+
}
|
|
667
|
+
finally {
|
|
668
|
+
if (!step.done)
|
|
669
|
+
parser.return(undefined);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
624
672
|
export function parseXml(input, limits = {}) {
|
|
625
673
|
const parser = parseXmlSteps(input, limits);
|
|
626
674
|
let result = parser.next();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Fill value with exactly the requested UTF-16 span before advancing the parser. */
|
|
2
|
+
export interface XmlSourceRead {
|
|
3
|
+
readonly offset: number;
|
|
4
|
+
readonly length: number;
|
|
5
|
+
value?: string;
|
|
6
|
+
}
|
|
7
|
+
export type XmlSourceStep = number | XmlSourceRead;
|
|
8
|
+
/** One fixed window; individual returned tokens still have their own size cost. */
|
|
9
|
+
export declare class XmlSource {
|
|
10
|
+
readonly length: number;
|
|
11
|
+
private window;
|
|
12
|
+
private start;
|
|
13
|
+
constructor(length: number);
|
|
14
|
+
slice(start: number, end?: number): Generator<XmlSourceStep, string, void>;
|
|
15
|
+
charCodeAt(offset: number): Generator<XmlSourceStep, number, void>;
|
|
16
|
+
startsWith(value: string, offset: number): Generator<XmlSourceStep, boolean, void>;
|
|
17
|
+
indexOf(value: string, offset: number): Generator<XmlSourceStep, number, void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** One fixed window; individual returned tokens still have their own size cost. */
|
|
2
|
+
export class XmlSource {
|
|
3
|
+
length;
|
|
4
|
+
window = '';
|
|
5
|
+
start = -1;
|
|
6
|
+
constructor(length) {
|
|
7
|
+
this.length = length;
|
|
8
|
+
if (!Number.isSafeInteger(length) || length < 0)
|
|
9
|
+
throw new RangeError("Invalid XML source length");
|
|
10
|
+
}
|
|
11
|
+
*slice(start, end = this.length) {
|
|
12
|
+
start = start < 0 ? Math.max(0, this.length + start) : Math.min(start, this.length);
|
|
13
|
+
end = end < 0 ? Math.max(0, this.length + end) : Math.min(end, this.length);
|
|
14
|
+
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
|
+
}
|
|
25
|
+
const stop = Math.min(end, this.start + this.window.length);
|
|
26
|
+
result += this.window.slice(start - this.start, stop - this.start);
|
|
27
|
+
start = stop;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
*charCodeAt(offset) {
|
|
32
|
+
return (yield* this.slice(offset, offset + 1)).charCodeAt(0);
|
|
33
|
+
}
|
|
34
|
+
*startsWith(value, offset) {
|
|
35
|
+
return (yield* this.slice(offset, offset + value.length)) === value;
|
|
36
|
+
}
|
|
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);
|
|
41
|
+
if (found >= 0)
|
|
42
|
+
return offset + found;
|
|
43
|
+
offset += 4096;
|
|
44
|
+
}
|
|
45
|
+
return -1;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.789",
|
|
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.
|
|
180
|
+
"toolcraft-schema": "0.0.789",
|
|
181
181
|
"toolcraft-design": "*",
|
|
182
182
|
"@poe-code/frontmatter": "*",
|
|
183
183
|
"@poe-code/agent-mcp-config": "*",
|