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