@ai-gui/core 0.1.0 → 0.2.0
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/dist/index.cjs +495 -179
- package/dist/index.d.cts +58 -32
- package/dist/index.d.ts +58 -32
- package/dist/index.js +494 -180
- package/package.json +14 -4
package/dist/index.js
CHANGED
|
@@ -102,7 +102,10 @@ function repair(str) {
|
|
|
102
102
|
break;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
-
if (inString && !stringIsKey)
|
|
105
|
+
if (inString && !stringIsKey) {
|
|
106
|
+
const body = escaped ? str.slice(0, -1) : str;
|
|
107
|
+
return [closeContainers(body + "\"", stack)];
|
|
108
|
+
}
|
|
106
109
|
const fallback = lastGoodEnd === -1 ? [] : [closeContainers(str.slice(0, lastGoodEnd), stack)];
|
|
107
110
|
if (inLiteral) return [closeContainers(str, stack), ...fallback];
|
|
108
111
|
return fallback;
|
|
@@ -126,20 +129,68 @@ function closeContainers(body, stack) {
|
|
|
126
129
|
*/
|
|
127
130
|
function repairMarkdown(buffer) {
|
|
128
131
|
let out = buffer;
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
132
|
+
const openFence = findOpenFence(out);
|
|
133
|
+
if (openFence) {
|
|
131
134
|
if (!out.endsWith("\n")) out += "\n";
|
|
132
|
-
out +=
|
|
135
|
+
out += openFence;
|
|
133
136
|
return out;
|
|
134
137
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
const bold = (
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
const inline = findOpenInlineCode(out);
|
|
139
|
+
const visible = inline ? out.slice(0, inline.index) : out;
|
|
140
|
+
const bold = countUnescaped(visible, "**");
|
|
141
|
+
const strike = countUnescaped(visible, "~~");
|
|
142
|
+
if (inline) out += inline.marker;
|
|
140
143
|
if (strike % 2 === 1) out += "~~";
|
|
144
|
+
if (bold % 2 === 1) out += "**";
|
|
141
145
|
return out;
|
|
142
146
|
}
|
|
147
|
+
function findOpenFence(buffer) {
|
|
148
|
+
let open;
|
|
149
|
+
for (const line of buffer.split(/\r\n|\r|\n/)) {
|
|
150
|
+
const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
151
|
+
if (!match) continue;
|
|
152
|
+
const marker = match[1];
|
|
153
|
+
if (!open) open = {
|
|
154
|
+
char: marker[0],
|
|
155
|
+
length: marker.length,
|
|
156
|
+
marker
|
|
157
|
+
};
|
|
158
|
+
else if (marker[0] === open.char && marker.length >= open.length && match[2].trim() === "") open = void 0;
|
|
159
|
+
}
|
|
160
|
+
return open?.marker;
|
|
161
|
+
}
|
|
162
|
+
function findOpenInlineCode(buffer) {
|
|
163
|
+
let open;
|
|
164
|
+
for (let i = 0; i < buffer.length;) {
|
|
165
|
+
if (buffer[i] !== "`" || isEscaped(buffer, i)) {
|
|
166
|
+
i++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
let end = i + 1;
|
|
170
|
+
while (buffer[end] === "`") end++;
|
|
171
|
+
const marker = buffer.slice(i, end);
|
|
172
|
+
if (!open) open = {
|
|
173
|
+
index: i,
|
|
174
|
+
marker
|
|
175
|
+
};
|
|
176
|
+
else if (open.marker === marker) open = void 0;
|
|
177
|
+
i = end;
|
|
178
|
+
}
|
|
179
|
+
return open;
|
|
180
|
+
}
|
|
181
|
+
function countUnescaped(buffer, marker) {
|
|
182
|
+
let count = 0;
|
|
183
|
+
for (let i = 0; i <= buffer.length - marker.length; i++) if (buffer.startsWith(marker, i) && !isEscaped(buffer, i)) {
|
|
184
|
+
count++;
|
|
185
|
+
i += marker.length - 1;
|
|
186
|
+
}
|
|
187
|
+
return count;
|
|
188
|
+
}
|
|
189
|
+
function isEscaped(buffer, index) {
|
|
190
|
+
let slashes = 0;
|
|
191
|
+
for (let i = index - 1; i >= 0 && buffer[i] === "\\"; i--) slashes++;
|
|
192
|
+
return slashes % 2 === 1;
|
|
193
|
+
}
|
|
143
194
|
|
|
144
195
|
//#endregion
|
|
145
196
|
//#region src/sanitizer.ts
|
|
@@ -155,8 +206,12 @@ function escapeHtml(html) {
|
|
|
155
206
|
* fall back to escaping the HTML-significant characters. This never emits raw
|
|
156
207
|
* markup and never throws.
|
|
157
208
|
*/
|
|
158
|
-
function sanitizeHtml(html) {
|
|
159
|
-
if (
|
|
209
|
+
function sanitizeHtml(html, options = {}) {
|
|
210
|
+
if (options.sanitizer) return options.sanitizer(html);
|
|
211
|
+
if (typeof window === "undefined") {
|
|
212
|
+
if (options.ssr === "throw") throw new Error("sanitizeHtml requires a DOM or an injected sanitizer");
|
|
213
|
+
return escapeHtml(html);
|
|
214
|
+
}
|
|
160
215
|
return DOMPurify.sanitize(html);
|
|
161
216
|
}
|
|
162
217
|
|
|
@@ -189,11 +244,16 @@ var CardRegistry = class {
|
|
|
189
244
|
};
|
|
190
245
|
}
|
|
191
246
|
validate(def, data) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
247
|
+
try {
|
|
248
|
+
if (def.validate) return def.validate(data);
|
|
249
|
+
if (def.schema) return validateSchema(def.schema, data);
|
|
250
|
+
return true;
|
|
251
|
+
} catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
195
254
|
}
|
|
196
255
|
toPromptSpec() {
|
|
256
|
+
if (this.cards.size === 0) return "";
|
|
197
257
|
const lines = ["You can output cards. Format: a ```card:<type> fenced block with JSON inside. Available cards:"];
|
|
198
258
|
for (const def of this.cards.values()) {
|
|
199
259
|
lines.push(`- \`card:${def.type}\`: ${def.description}`);
|
|
@@ -205,6 +265,9 @@ var CardRegistry = class {
|
|
|
205
265
|
}
|
|
206
266
|
return lines.join("\n");
|
|
207
267
|
}
|
|
268
|
+
get size() {
|
|
269
|
+
return this.cards.size;
|
|
270
|
+
}
|
|
208
271
|
toJSONSchema() {
|
|
209
272
|
const properties = {};
|
|
210
273
|
for (const def of this.cards.values()) if (def.schema) properties[def.type] = def.schema;
|
|
@@ -238,29 +301,63 @@ function validateSchema(schema, data) {
|
|
|
238
301
|
/** Merge every plugin's `nodeRenderers` into a single map (later plugins win). */
|
|
239
302
|
function collectNodeRenderers(plugins = []) {
|
|
240
303
|
const map = {};
|
|
241
|
-
for (const p of plugins) for (const [k,
|
|
304
|
+
for (const p of plugins) for (const [k, render] of Object.entries(p.nodeRenderers ?? {})) map[k] = (node) => node.complete === false ? {
|
|
305
|
+
kind: "html",
|
|
306
|
+
html: `<div data-aigui-block-loading="" data-block-type="${escapeAttr(node.type)}"></div>`
|
|
307
|
+
} : render(node);
|
|
242
308
|
return map;
|
|
243
309
|
}
|
|
244
310
|
/** The set of node types claimed by the given plugins. */
|
|
245
311
|
function pluginNodeTypes(plugins = []) {
|
|
246
312
|
return new Set(Object.keys(collectNodeRenderers(plugins)));
|
|
247
313
|
}
|
|
314
|
+
function escapeAttr(value) {
|
|
315
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
316
|
+
}
|
|
248
317
|
|
|
249
318
|
//#endregion
|
|
250
319
|
//#region src/parser.ts
|
|
251
320
|
/** Build a parser that turns markdown source into a flat list of ASTNodes. */
|
|
252
321
|
function createParser(options = {}) {
|
|
322
|
+
const parse = createParserWithMetadata(options);
|
|
323
|
+
return (src, rawSrc = src) => parse(src, rawSrc).nodes;
|
|
324
|
+
}
|
|
325
|
+
/** Build a parser that also reports the source range for each top-level block. */
|
|
326
|
+
function createParserWithMetadata(options = {}) {
|
|
253
327
|
const md = new MarkdownIt({
|
|
254
328
|
html: true,
|
|
255
329
|
linkify: true
|
|
256
330
|
});
|
|
257
331
|
options.configureMd?.(md);
|
|
258
332
|
for (const plugin of options.plugins ?? []) plugin.extendParser?.(md);
|
|
333
|
+
const hasParserExtensions = Boolean(options.configureMd || options.plugins?.some((plugin) => plugin.extendParser));
|
|
259
334
|
const pluginTypes = pluginNodeTypes(options.plugins);
|
|
260
|
-
|
|
261
|
-
|
|
335
|
+
const completeness = new Map();
|
|
336
|
+
for (const plugin of options.plugins ?? []) for (const type of Object.keys(plugin.nodeRenderers ?? {})) if (plugin.isBlockComplete) completeness.set(type, plugin.isBlockComplete);
|
|
337
|
+
return (src, rawSrc = src, sourceOffset = 0) => {
|
|
338
|
+
const env = {};
|
|
339
|
+
const tokens = md.parse(normalizeLineEndings(src), env);
|
|
262
340
|
const nodes = [];
|
|
263
|
-
|
|
341
|
+
const blocks = [];
|
|
342
|
+
const lineOffsets = getLineOffsets(rawSrc);
|
|
343
|
+
const startSlots = new Map();
|
|
344
|
+
const addNode = (node, map) => {
|
|
345
|
+
const range = sourceRange(map, rawSrc.length, lineOffsets, sourceOffset);
|
|
346
|
+
const previous = blocks.at(-1);
|
|
347
|
+
const block = previous && previous.start === range.start && previous.end === range.end ? previous : void 0;
|
|
348
|
+
const slot = startSlots.get(range.start) ?? 0;
|
|
349
|
+
startSlots.set(range.start, slot + 1);
|
|
350
|
+
nodes.push({
|
|
351
|
+
key: `${range.start}:${slot}`,
|
|
352
|
+
...node
|
|
353
|
+
});
|
|
354
|
+
if (block) block.nodeEnd = nodes.length;
|
|
355
|
+
else blocks.push({
|
|
356
|
+
...range,
|
|
357
|
+
nodeStart: nodes.length - 1,
|
|
358
|
+
nodeEnd: nodes.length
|
|
359
|
+
});
|
|
360
|
+
};
|
|
264
361
|
for (let i = 0; i < tokens.length; i++) {
|
|
265
362
|
const t = tokens[i];
|
|
266
363
|
if (t.type === "fence") {
|
|
@@ -268,79 +365,82 @@ function createParser(options = {}) {
|
|
|
268
365
|
if (info.startsWith("card:") && options.registry) {
|
|
269
366
|
const cardType = info.slice(5);
|
|
270
367
|
const res = options.registry.parse(cardType, t.content);
|
|
271
|
-
|
|
272
|
-
|
|
368
|
+
const complete = res.complete && isClosedFence(rawSrc, t.map, t.markup);
|
|
369
|
+
addNode({
|
|
273
370
|
type: "card",
|
|
274
371
|
card: {
|
|
275
372
|
type: cardType,
|
|
276
373
|
data: res.data,
|
|
277
|
-
complete
|
|
278
|
-
valid: res.valid
|
|
374
|
+
complete,
|
|
375
|
+
valid: complete && res.valid
|
|
279
376
|
}
|
|
280
|
-
});
|
|
281
|
-
} else if (pluginTypes.has(info))
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
377
|
+
}, t.map);
|
|
378
|
+
} else if (pluginTypes.has(info)) {
|
|
379
|
+
const predicate = completeness.get(info);
|
|
380
|
+
const fenceComplete = isClosedFence(rawSrc, t.map, t.markup);
|
|
381
|
+
let complete = fenceComplete;
|
|
382
|
+
if (complete && predicate) try {
|
|
383
|
+
complete = predicate(info, t.content);
|
|
384
|
+
} catch {
|
|
385
|
+
complete = false;
|
|
386
|
+
}
|
|
387
|
+
addNode({
|
|
388
|
+
type: info,
|
|
389
|
+
content: t.content,
|
|
390
|
+
attrs: { info },
|
|
391
|
+
complete
|
|
392
|
+
}, t.map);
|
|
393
|
+
} else addNode({
|
|
289
394
|
type: "code",
|
|
290
395
|
tag: "code",
|
|
291
396
|
attrs: info ? { lang: info } : void 0,
|
|
292
397
|
content: t.content
|
|
293
|
-
});
|
|
398
|
+
}, t.map);
|
|
294
399
|
continue;
|
|
295
400
|
}
|
|
296
401
|
if (t.type === "hr") {
|
|
297
|
-
|
|
298
|
-
key: `${index++}:hr`,
|
|
402
|
+
addNode({
|
|
299
403
|
type: "hr",
|
|
300
404
|
tag: "hr"
|
|
301
|
-
});
|
|
405
|
+
}, t.map);
|
|
302
406
|
continue;
|
|
303
407
|
}
|
|
304
408
|
if (t.type === "code_block") {
|
|
305
|
-
|
|
306
|
-
key: `${index++}:code`,
|
|
409
|
+
addNode({
|
|
307
410
|
type: "code",
|
|
308
411
|
tag: "code",
|
|
309
412
|
content: t.content
|
|
310
|
-
});
|
|
413
|
+
}, t.map);
|
|
311
414
|
continue;
|
|
312
415
|
}
|
|
313
416
|
if (t.type === "html_block") {
|
|
314
|
-
|
|
315
|
-
key: `${index++}:html`,
|
|
417
|
+
addNode({
|
|
316
418
|
type: "html",
|
|
317
419
|
content: t.content
|
|
318
|
-
});
|
|
420
|
+
}, t.map);
|
|
319
421
|
continue;
|
|
320
422
|
}
|
|
321
423
|
if (t.type === "heading_open") {
|
|
322
424
|
const inline = tokens[i + 1];
|
|
323
425
|
const raw = inline?.content ?? "";
|
|
324
|
-
|
|
325
|
-
key: `${index++}:heading`,
|
|
426
|
+
addNode({
|
|
326
427
|
type: "heading",
|
|
327
428
|
tag: t.tag,
|
|
328
429
|
content: raw,
|
|
329
|
-
html: md.renderInline(
|
|
330
|
-
});
|
|
430
|
+
html: md.renderer.renderInline(inline?.children ?? [], md.options, env)
|
|
431
|
+
}, t.map);
|
|
331
432
|
i += 2;
|
|
332
433
|
continue;
|
|
333
434
|
}
|
|
334
435
|
if (t.type === "paragraph_open") {
|
|
335
436
|
const inline = tokens[i + 1];
|
|
336
437
|
const raw = inline?.content ?? "";
|
|
337
|
-
|
|
338
|
-
key: `${index++}:paragraph`,
|
|
438
|
+
addNode({
|
|
339
439
|
type: "paragraph",
|
|
340
440
|
tag: "p",
|
|
341
441
|
content: raw,
|
|
342
|
-
html: md.renderInline(
|
|
343
|
-
});
|
|
442
|
+
html: md.renderer.renderInline(inline?.children ?? [], md.options, env)
|
|
443
|
+
}, t.map);
|
|
344
444
|
i += 2;
|
|
345
445
|
continue;
|
|
346
446
|
}
|
|
@@ -356,54 +456,127 @@ function createParser(options = {}) {
|
|
|
356
456
|
}
|
|
357
457
|
}
|
|
358
458
|
const slice = tokens.slice(i, j + 1);
|
|
359
|
-
|
|
360
|
-
key: `${index++}:${t.type}`,
|
|
459
|
+
addNode({
|
|
361
460
|
type: "html",
|
|
362
|
-
content: md.renderer.render(slice, md.options,
|
|
363
|
-
});
|
|
461
|
+
content: md.renderer.render(slice, md.options, env)
|
|
462
|
+
}, t.map);
|
|
364
463
|
i = j;
|
|
365
464
|
continue;
|
|
366
465
|
}
|
|
367
466
|
if (t.block && t.level === 0) {
|
|
368
|
-
|
|
369
|
-
key: `${index++}:${t.type}`,
|
|
467
|
+
addNode({
|
|
370
468
|
type: "html",
|
|
371
|
-
content: md.renderer.render([t], md.options,
|
|
372
|
-
});
|
|
469
|
+
content: md.renderer.render([t], md.options, env)
|
|
470
|
+
}, t.map);
|
|
373
471
|
continue;
|
|
374
472
|
}
|
|
375
473
|
}
|
|
376
|
-
return
|
|
474
|
+
return {
|
|
475
|
+
nodes,
|
|
476
|
+
blocks,
|
|
477
|
+
incrementalSafe: !hasParserExtensions && !hasReferenceSyntax(rawSrc)
|
|
478
|
+
};
|
|
377
479
|
};
|
|
378
480
|
}
|
|
481
|
+
function getLineOffsets(src) {
|
|
482
|
+
const offsets = [0];
|
|
483
|
+
for (let i = 0; i < src.length; i++) if (src[i] === "\r") {
|
|
484
|
+
if (src[i + 1] === "\n") i++;
|
|
485
|
+
offsets.push(i + 1);
|
|
486
|
+
} else if (src[i] === "\n") offsets.push(i + 1);
|
|
487
|
+
offsets.push(src.length);
|
|
488
|
+
return offsets;
|
|
489
|
+
}
|
|
490
|
+
function sourceRange(map, sourceLength, lineOffsets, sourceOffset) {
|
|
491
|
+
if (!map) return {
|
|
492
|
+
start: sourceOffset,
|
|
493
|
+
end: sourceOffset + sourceLength
|
|
494
|
+
};
|
|
495
|
+
return {
|
|
496
|
+
start: sourceOffset + Math.min(lineOffsets[map[0]] ?? sourceLength, sourceLength),
|
|
497
|
+
end: sourceOffset + Math.min(lineOffsets[map[1]] ?? sourceLength, sourceLength)
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function hasReferenceSyntax(src) {
|
|
501
|
+
return /^ {0,3}\[[^\]\n]+\]:/m.test(src) || /\[[^\]\n]+\]\s*\[[^\]\n]*\]/.test(src) || /(^|[^!])\[[^\]\n]+\](?![([])/m.test(src);
|
|
502
|
+
}
|
|
503
|
+
function isClosedFence(src, map, markup) {
|
|
504
|
+
if (!map) return false;
|
|
505
|
+
const line = src.split(/\r\n|\r|\n/)[map[1] - 1]?.trim() ?? "";
|
|
506
|
+
if (line.length < markup.length || line[0] !== markup[0]) return false;
|
|
507
|
+
return line.split("").every((char) => char === markup[0]);
|
|
508
|
+
}
|
|
509
|
+
function normalizeLineEndings(src) {
|
|
510
|
+
return src.replace(/\r\n|\r/g, "\n");
|
|
511
|
+
}
|
|
379
512
|
|
|
380
513
|
//#endregion
|
|
381
514
|
//#region src/diff.ts
|
|
382
515
|
/** Produce a minimal set of patches turning `prev` into `next`, keyed by node key. */
|
|
383
516
|
function diffAst(prev, next) {
|
|
384
517
|
const patches = [];
|
|
385
|
-
const prevByKey = new Map(prev.map((n) => [n.key, n]));
|
|
386
518
|
const nextKeys = new Set(next.map((n) => n.key));
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
op: "
|
|
391
|
-
index
|
|
392
|
-
node
|
|
393
|
-
});
|
|
394
|
-
else if (!nodeEqual(old, node)) patches.push({
|
|
395
|
-
op: "update",
|
|
396
|
-
key: node.key,
|
|
397
|
-
node
|
|
519
|
+
const working = [...prev];
|
|
520
|
+
for (let index = working.length - 1; index >= 0; index--) if (!nextKeys.has(working[index].key)) {
|
|
521
|
+
patches.push({
|
|
522
|
+
op: "remove",
|
|
523
|
+
key: working[index].key
|
|
398
524
|
});
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
525
|
+
working.splice(index, 1);
|
|
526
|
+
}
|
|
527
|
+
for (let index = 0; index < next.length; index++) {
|
|
528
|
+
const node = next[index];
|
|
529
|
+
const currentIndex = working.findIndex((candidate) => candidate.key === node.key);
|
|
530
|
+
if (currentIndex === -1) {
|
|
531
|
+
patches.push({
|
|
532
|
+
op: "insert",
|
|
533
|
+
index,
|
|
534
|
+
node
|
|
535
|
+
});
|
|
536
|
+
working.splice(index, 0, node);
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (currentIndex !== index) {
|
|
540
|
+
patches.push({
|
|
541
|
+
op: "move",
|
|
542
|
+
key: node.key,
|
|
543
|
+
index
|
|
544
|
+
});
|
|
545
|
+
const [moved] = working.splice(currentIndex, 1);
|
|
546
|
+
working.splice(index, 0, moved);
|
|
547
|
+
}
|
|
548
|
+
if (!nodeEqual(working[index], node)) {
|
|
549
|
+
patches.push({
|
|
550
|
+
op: "update",
|
|
551
|
+
key: node.key,
|
|
552
|
+
node
|
|
553
|
+
});
|
|
554
|
+
working[index] = node;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
404
557
|
return patches;
|
|
405
558
|
}
|
|
559
|
+
/** Apply patches in order, primarily for framework adapters and verification. */
|
|
560
|
+
function applyPatches(nodes, patches) {
|
|
561
|
+
const next = [...nodes];
|
|
562
|
+
for (const patch of patches) if (patch.op === "insert") next.splice(patch.index, 0, patch.node);
|
|
563
|
+
else if (patch.op === "remove") {
|
|
564
|
+
const index = next.findIndex((node) => node.key === patch.key);
|
|
565
|
+
if (index !== -1) next.splice(index, 1);
|
|
566
|
+
} else if (patch.op === "move") {
|
|
567
|
+
const index = next.findIndex((node) => node.key === patch.key);
|
|
568
|
+
if (index !== -1) {
|
|
569
|
+
const [node] = next.splice(index, 1);
|
|
570
|
+
next.splice(patch.index, 0, node);
|
|
571
|
+
}
|
|
572
|
+
} else {
|
|
573
|
+
const index = next.findIndex((node) => node.key === patch.key);
|
|
574
|
+
if (index !== -1) next[index] = patch.node;
|
|
575
|
+
}
|
|
576
|
+
return next;
|
|
577
|
+
}
|
|
406
578
|
function nodeEqual(a, b) {
|
|
579
|
+
if (a === b) return true;
|
|
407
580
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
408
581
|
}
|
|
409
582
|
|
|
@@ -418,42 +591,170 @@ var Renderer = class {
|
|
|
418
591
|
buffer = "";
|
|
419
592
|
prevAst = [];
|
|
420
593
|
parse;
|
|
594
|
+
parsed;
|
|
421
595
|
options;
|
|
422
596
|
sanitize;
|
|
597
|
+
generation = 0;
|
|
598
|
+
activeFeed;
|
|
599
|
+
renderScheduled = false;
|
|
600
|
+
scheduleGeneration = 0;
|
|
423
601
|
constructor(options = {}) {
|
|
424
602
|
this.options = options;
|
|
425
|
-
this.sanitize = options.sanitize
|
|
603
|
+
this.sanitize = options.sanitize === false ? false : typeof options.sanitize === "object" ? options.sanitize : {};
|
|
426
604
|
if (options.registry) for (const plugin of options.plugins ?? []) for (const card of plugin.cards ?? []) options.registry.register(card);
|
|
427
|
-
this.parse =
|
|
605
|
+
this.parse = createParserWithMetadata({
|
|
428
606
|
registry: options.registry,
|
|
429
607
|
plugins: options.plugins
|
|
430
608
|
});
|
|
431
609
|
}
|
|
432
610
|
push(chunk) {
|
|
433
611
|
this.buffer += chunk;
|
|
434
|
-
this.
|
|
612
|
+
this.scheduleRender();
|
|
435
613
|
}
|
|
436
|
-
async feed(source) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
614
|
+
async feed(source, options = {}) {
|
|
615
|
+
const generation = ++this.generation;
|
|
616
|
+
this.cancelActiveFeed(createAbortError("Superseded by a newer feed"));
|
|
617
|
+
const decoder = new TextDecoder();
|
|
618
|
+
const signal = options.signal;
|
|
619
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
620
|
+
let aborted = false;
|
|
621
|
+
let abortError;
|
|
622
|
+
const abort = () => {
|
|
623
|
+
aborted = true;
|
|
624
|
+
abortError = abortReason(signal);
|
|
625
|
+
if (this.activeFeed?.generation === generation) this.cancelActiveFeed(abortError);
|
|
626
|
+
};
|
|
627
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
628
|
+
const consume = (chunk) => {
|
|
629
|
+
if (generation !== this.generation || aborted) return;
|
|
630
|
+
const text = typeof chunk === "string" ? decoder.decode() + chunk : decoder.decode(chunk, { stream: true });
|
|
631
|
+
if (text) this.push(text);
|
|
632
|
+
};
|
|
633
|
+
try {
|
|
634
|
+
if (isReadableStream$1(source)) {
|
|
635
|
+
const reader = source.getReader();
|
|
636
|
+
let cancelled = false;
|
|
637
|
+
this.activeFeed = {
|
|
638
|
+
generation,
|
|
639
|
+
cancel: async (reason) => {
|
|
640
|
+
if (cancelled) return;
|
|
641
|
+
cancelled = true;
|
|
642
|
+
await reader.cancel(reason);
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
try {
|
|
646
|
+
for (;;) {
|
|
647
|
+
const { done, value } = await reader.read();
|
|
648
|
+
if (done || generation !== this.generation || aborted) break;
|
|
649
|
+
if (value != null) consume(value);
|
|
650
|
+
}
|
|
651
|
+
} finally {
|
|
652
|
+
reader.releaseLock();
|
|
653
|
+
}
|
|
654
|
+
} else {
|
|
655
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
656
|
+
let returned = false;
|
|
657
|
+
this.activeFeed = {
|
|
658
|
+
generation,
|
|
659
|
+
cancel: async () => {
|
|
660
|
+
if (returned) return;
|
|
661
|
+
returned = true;
|
|
662
|
+
await iterator.return?.();
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
try {
|
|
666
|
+
for (;;) {
|
|
667
|
+
const { done, value } = await iterator.next();
|
|
668
|
+
if (done || generation !== this.generation || aborted) break;
|
|
669
|
+
consume(value);
|
|
670
|
+
}
|
|
671
|
+
} finally {
|
|
672
|
+
if ((generation !== this.generation || aborted) && !returned) {
|
|
673
|
+
returned = true;
|
|
674
|
+
await iterator.return?.();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (generation === this.generation && !aborted) {
|
|
679
|
+
const tail = decoder.decode();
|
|
680
|
+
if (tail) this.push(tail);
|
|
681
|
+
this.flush();
|
|
682
|
+
}
|
|
683
|
+
if (aborted) throw abortError;
|
|
684
|
+
} finally {
|
|
685
|
+
signal?.removeEventListener("abort", abort);
|
|
686
|
+
if (this.activeFeed?.generation === generation) this.activeFeed = void 0;
|
|
446
687
|
}
|
|
447
688
|
}
|
|
448
689
|
reset() {
|
|
690
|
+
this.generation++;
|
|
691
|
+
this.cancelActiveFeed(createAbortError("Renderer reset"));
|
|
692
|
+
this.activeFeed = void 0;
|
|
449
693
|
this.buffer = "";
|
|
694
|
+
this.renderScheduled = false;
|
|
695
|
+
this.scheduleGeneration++;
|
|
696
|
+
const patches = diffAst(this.prevAst, []);
|
|
450
697
|
this.prevAst = [];
|
|
698
|
+
this.parsed = void 0;
|
|
699
|
+
this.options.onPatch?.(patches, []);
|
|
700
|
+
}
|
|
701
|
+
/** Immediately render pending buffered input, bypassing the scheduler. */
|
|
702
|
+
flush() {
|
|
703
|
+
if (!this.renderScheduled) return;
|
|
704
|
+
this.renderScheduled = false;
|
|
705
|
+
this.scheduleGeneration++;
|
|
706
|
+
this.render();
|
|
707
|
+
}
|
|
708
|
+
scheduleRender() {
|
|
709
|
+
if (!this.options.scheduler) {
|
|
710
|
+
this.render();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (this.renderScheduled) return;
|
|
714
|
+
this.renderScheduled = true;
|
|
715
|
+
const scheduledGeneration = ++this.scheduleGeneration;
|
|
716
|
+
this.options.scheduler(() => {
|
|
717
|
+
if (!this.renderScheduled || scheduledGeneration !== this.scheduleGeneration) return;
|
|
718
|
+
this.renderScheduled = false;
|
|
719
|
+
this.render();
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
cancelActiveFeed(reason) {
|
|
723
|
+
const active = this.activeFeed;
|
|
724
|
+
if (!active) return;
|
|
725
|
+
Promise.resolve(active.cancel(reason)).catch(() => {});
|
|
451
726
|
}
|
|
452
727
|
render() {
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
if (
|
|
728
|
+
const previous = this.parsed;
|
|
729
|
+
let next;
|
|
730
|
+
if (previous?.incrementalSafe && previous.blocks.length > 1) {
|
|
731
|
+
const mutable = previous.blocks.at(-1);
|
|
732
|
+
const stableBlocks = previous.blocks.slice(0, -1);
|
|
733
|
+
const stableNodeEnd = mutable.nodeStart;
|
|
734
|
+
const rawTail = this.buffer.slice(mutable.start);
|
|
735
|
+
const tail = this.parse(repairMarkdown(rawTail), rawTail, mutable.start);
|
|
736
|
+
if (tail.incrementalSafe) {
|
|
737
|
+
if (this.sanitize !== false) sanitizeNodes(tail.nodes, this.sanitize);
|
|
738
|
+
next = {
|
|
739
|
+
nodes: [...previous.nodes.slice(0, stableNodeEnd), ...tail.nodes],
|
|
740
|
+
blocks: [...stableBlocks, ...tail.blocks.map((block) => ({
|
|
741
|
+
...block,
|
|
742
|
+
nodeStart: block.nodeStart + stableNodeEnd,
|
|
743
|
+
nodeEnd: block.nodeEnd + stableNodeEnd
|
|
744
|
+
}))],
|
|
745
|
+
incrementalSafe: true
|
|
746
|
+
};
|
|
747
|
+
} else {
|
|
748
|
+
next = this.parse(repairMarkdown(this.buffer), this.buffer);
|
|
749
|
+
if (this.sanitize !== false) sanitizeNodes(next.nodes, this.sanitize);
|
|
750
|
+
}
|
|
751
|
+
} else {
|
|
752
|
+
next = this.parse(repairMarkdown(this.buffer), this.buffer);
|
|
753
|
+
if (this.sanitize !== false) sanitizeNodes(next.nodes, this.sanitize);
|
|
754
|
+
}
|
|
755
|
+
const nextAst = next.nodes;
|
|
456
756
|
const patches = diffAst(this.prevAst, nextAst);
|
|
757
|
+
this.parsed = next;
|
|
457
758
|
this.prevAst = nextAst;
|
|
458
759
|
if (patches.length > 0) this.options.onPatch?.(patches, nextAst);
|
|
459
760
|
}
|
|
@@ -462,119 +763,132 @@ var Renderer = class {
|
|
|
462
763
|
* Recursively sanitize node markup in place: the content of `html` nodes and
|
|
463
764
|
* the rendered inline `html` field carried by any node.
|
|
464
765
|
*/
|
|
465
|
-
function sanitizeNodes(nodes) {
|
|
766
|
+
function sanitizeNodes(nodes, options) {
|
|
466
767
|
for (const node of nodes) {
|
|
467
|
-
if (node.type === "html" && typeof node.content === "string") node.content = sanitizeHtml(node.content);
|
|
468
|
-
if (node.html) node.html = sanitizeHtml(node.html);
|
|
469
|
-
if (node.children) sanitizeNodes(node.children);
|
|
768
|
+
if (node.type === "html" && typeof node.content === "string") node.content = sanitizeHtml(node.content, options);
|
|
769
|
+
if (node.html) node.html = sanitizeHtml(node.html, options);
|
|
770
|
+
if (node.children) sanitizeNodes(node.children, options);
|
|
470
771
|
}
|
|
471
772
|
}
|
|
773
|
+
function isReadableStream$1(source) {
|
|
774
|
+
return "getReader" in source && typeof source.getReader === "function";
|
|
775
|
+
}
|
|
776
|
+
function abortReason(signal) {
|
|
777
|
+
return signal?.reason ?? createAbortError("The operation was aborted");
|
|
778
|
+
}
|
|
779
|
+
function createAbortError(message) {
|
|
780
|
+
const error = new Error(message);
|
|
781
|
+
error.name = "AbortError";
|
|
782
|
+
return error;
|
|
783
|
+
}
|
|
472
784
|
|
|
473
785
|
//#endregion
|
|
474
786
|
//#region src/stream-router.ts
|
|
475
787
|
const DEFAULT_CHANNEL = "content";
|
|
476
|
-
/**
|
|
477
|
-
* Demultiplexes one incoming stream into multiple named channels so different
|
|
478
|
-
* UI regions can update independently (e.g. `progress` + `content` from a single
|
|
479
|
-
* SSE). Framing is auto-detected per line and supports:
|
|
480
|
-
*
|
|
481
|
-
* 1. JSON envelope: `{"ch":"<channel>","delta":"text"}` (a text delta) or
|
|
482
|
-
* `{"ch":"<channel>","data":<any>}` (a structured value). May appear bare or
|
|
483
|
-
* after a `data: ` prefix.
|
|
484
|
-
* 2. SSE `event: <name>` sets the channel for the following `data:` line(s).
|
|
485
|
-
* 3. A plain `data: <text>` line with no `ch` and no preceding `event:` is a
|
|
486
|
-
* text delta on the default `content` channel.
|
|
487
|
-
*/
|
|
788
|
+
/** Demultiplex JSON-lines and standards-compliant SSE into named channels. */
|
|
488
789
|
var StreamRouter = class {
|
|
489
790
|
sinks = new Map();
|
|
490
791
|
handlers = new Map();
|
|
491
|
-
/**
|
|
792
|
+
/** Most recently received SSE `id` field. */
|
|
793
|
+
lastEventId = "";
|
|
794
|
+
/** Most recently received valid non-negative SSE reconnection delay. */
|
|
795
|
+
retry;
|
|
492
796
|
channel(name, sink) {
|
|
493
797
|
this.sinks.set(name, sink);
|
|
494
798
|
return this;
|
|
495
799
|
}
|
|
496
|
-
/** Bind a handler to a channel: data values (and deltas as strings) call it. */
|
|
497
800
|
on(name, handler) {
|
|
498
801
|
this.handlers.set(name, handler);
|
|
499
802
|
return this;
|
|
500
803
|
}
|
|
501
|
-
/**
|
|
502
|
-
* Consume a stream to completion, dispatching each parsed line to its channel.
|
|
503
|
-
* Accepts an async iterable of strings, or a `ReadableStream` of either strings
|
|
504
|
-
* or `Uint8Array` bytes (decoded as UTF-8).
|
|
505
|
-
*/
|
|
506
804
|
async feed(source) {
|
|
507
805
|
let buffer = "";
|
|
508
|
-
let
|
|
806
|
+
let eventName = "";
|
|
807
|
+
let dataLines = [];
|
|
808
|
+
const decoder = new TextDecoder();
|
|
809
|
+
const dispatchEvent = () => {
|
|
810
|
+
if (dataLines.length === 0) {
|
|
811
|
+
eventName = "";
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const payload = dataLines.join("\n");
|
|
815
|
+
const channel = eventName || DEFAULT_CHANNEL;
|
|
816
|
+
dataLines = [];
|
|
817
|
+
eventName = "";
|
|
818
|
+
this.dispatchPayload(channel, payload, channel === DEFAULT_CHANNEL);
|
|
819
|
+
};
|
|
820
|
+
const processLine = (rawLine) => {
|
|
821
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
822
|
+
if (line === "") {
|
|
823
|
+
dispatchEvent();
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (line.startsWith(":")) return;
|
|
827
|
+
const bareJson = tryParseJson(line);
|
|
828
|
+
if (isEnvelope(bareJson)) {
|
|
829
|
+
this.dispatchEnvelope(bareJson);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const colon = line.indexOf(":");
|
|
833
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
834
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
835
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
836
|
+
if (field === "event") eventName = value;
|
|
837
|
+
else if (field === "data") {
|
|
838
|
+
const envelope = tryParseJson(value);
|
|
839
|
+
if (!eventName && isEnvelope(envelope)) this.dispatchEnvelope(envelope);
|
|
840
|
+
else dataLines.push(value);
|
|
841
|
+
} else if (field === "id") {
|
|
842
|
+
if (!value.includes("\0")) this.lastEventId = value;
|
|
843
|
+
} else if (field === "retry") {
|
|
844
|
+
if (/^\d+$/.test(value)) this.retry = Number(value);
|
|
845
|
+
} else if (colon === -1) dataLines.push(line);
|
|
846
|
+
};
|
|
509
847
|
const consume = (chunk) => {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
buffer
|
|
515
|
-
|
|
848
|
+
const text = typeof chunk === "string" ? decoder.decode() + chunk : decoder.decode(chunk, { stream: true });
|
|
849
|
+
buffer += text;
|
|
850
|
+
let newline;
|
|
851
|
+
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
852
|
+
processLine(buffer.slice(0, newline));
|
|
853
|
+
buffer = buffer.slice(newline + 1);
|
|
516
854
|
}
|
|
517
855
|
};
|
|
518
|
-
if (
|
|
856
|
+
if (isReadableStream(source)) {
|
|
519
857
|
const reader = source.getReader();
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
858
|
+
try {
|
|
859
|
+
for (;;) {
|
|
860
|
+
const { done, value } = await reader.read();
|
|
861
|
+
if (done) break;
|
|
862
|
+
if (value != null) consume(value);
|
|
863
|
+
}
|
|
864
|
+
} finally {
|
|
865
|
+
reader.releaseLock();
|
|
526
866
|
}
|
|
527
|
-
const tail = decoder.decode();
|
|
528
|
-
if (tail) consume(tail);
|
|
529
867
|
} else for await (const chunk of source) consume(chunk);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
if ("delta" in parsed) this.routeDelta(parsed.ch, String(parsed.delta));
|
|
546
|
-
else this.routeData(parsed.ch, parsed.data);
|
|
547
|
-
return currentEvent;
|
|
548
|
-
}
|
|
549
|
-
if (parsed !== void 0) {
|
|
550
|
-
this.routeData(currentEvent ?? DEFAULT_CHANNEL, parsed);
|
|
551
|
-
return currentEvent;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
if (currentEvent !== void 0) {
|
|
555
|
-
const parsed = tryParseJson(payload);
|
|
556
|
-
this.routeData(currentEvent, parsed !== void 0 ? parsed : payload);
|
|
557
|
-
return currentEvent;
|
|
558
|
-
}
|
|
559
|
-
this.routeDelta(DEFAULT_CHANNEL, payload);
|
|
560
|
-
return currentEvent;
|
|
868
|
+
const tail = decoder.decode();
|
|
869
|
+
if (tail) consume(tail);
|
|
870
|
+
if (buffer.length > 0) processLine(buffer);
|
|
871
|
+
dispatchEvent();
|
|
872
|
+
}
|
|
873
|
+
dispatchPayload(channel, payload, defaultIsDelta) {
|
|
874
|
+
const parsed = tryParseJson(payload);
|
|
875
|
+
if (isEnvelope(parsed)) this.dispatchEnvelope(parsed);
|
|
876
|
+
else if (defaultIsDelta) this.routeDelta(channel, payload);
|
|
877
|
+
else this.routeData(channel, parsed !== void 0 ? parsed : payload);
|
|
878
|
+
}
|
|
879
|
+
dispatchEnvelope(envelope) {
|
|
880
|
+
const channel = envelope.ch;
|
|
881
|
+
if ("delta" in envelope) this.routeDelta(channel, String(envelope.delta));
|
|
882
|
+
else this.routeData(channel, envelope.data);
|
|
561
883
|
}
|
|
562
|
-
/** Route a text delta: to a bound sink and/or an on() handler (either/both). */
|
|
563
884
|
routeDelta(channel, text) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (sink) sink.push(text);
|
|
567
|
-
if (handler) handler(text);
|
|
885
|
+
this.sinks.get(channel)?.push(text);
|
|
886
|
+
this.handlers.get(channel)?.(text);
|
|
568
887
|
}
|
|
569
|
-
/** Route a structured value: to the handler, or a string value to a lone sink. */
|
|
570
888
|
routeData(channel, value) {
|
|
571
889
|
const handler = this.handlers.get(channel);
|
|
572
|
-
if (handler)
|
|
573
|
-
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
const sink = this.sinks.get(channel);
|
|
577
|
-
if (sink && typeof value === "string") sink.push(value);
|
|
890
|
+
if (handler) handler(value);
|
|
891
|
+
else if (typeof value === "string") this.sinks.get(channel)?.push(value);
|
|
578
892
|
}
|
|
579
893
|
};
|
|
580
894
|
function tryParseJson(text) {
|
|
@@ -584,8 +898,11 @@ function tryParseJson(text) {
|
|
|
584
898
|
return void 0;
|
|
585
899
|
}
|
|
586
900
|
}
|
|
587
|
-
function
|
|
588
|
-
return typeof value === "object" && value !== null;
|
|
901
|
+
function isEnvelope(value) {
|
|
902
|
+
return typeof value === "object" && value !== null && typeof value.ch === "string";
|
|
903
|
+
}
|
|
904
|
+
function isReadableStream(source) {
|
|
905
|
+
return "getReader" in source && typeof source.getReader === "function";
|
|
589
906
|
}
|
|
590
907
|
|
|
591
908
|
//#endregion
|
|
@@ -599,13 +916,10 @@ function buildSystemPrompt(options = {}) {
|
|
|
599
916
|
const parts = [];
|
|
600
917
|
if (options.base) parts.push(options.base);
|
|
601
918
|
const cardSpec = options.registry?.toPromptSpec();
|
|
602
|
-
if (
|
|
919
|
+
if (cardSpec) parts.push(cardSpec);
|
|
603
920
|
for (const p of options.plugins ?? []) if (p.promptSpec) parts.push(p.promptSpec);
|
|
604
921
|
return parts.join("\n\n");
|
|
605
922
|
}
|
|
606
|
-
function hasCards(registry) {
|
|
607
|
-
return registry.toPromptSpec().includes("card:");
|
|
608
|
-
}
|
|
609
923
|
|
|
610
924
|
//#endregion
|
|
611
|
-
export { CardRegistry, Renderer, StreamRouter, buildSystemPrompt, collectNodeRenderers, createParser, diffAst, parsePartialJSON, pluginNodeTypes, repairMarkdown, sanitizeHtml };
|
|
925
|
+
export { CardRegistry, Renderer, StreamRouter, applyPatches, buildSystemPrompt, collectNodeRenderers, createParser, createParserWithMetadata, diffAst, parsePartialJSON, pluginNodeTypes, repairMarkdown, sanitizeHtml };
|