/**
* Streaming parser for ...
* tags. Simplified from packages/artifacts/src/parser.ts in the reference
* repo: handles one artifact at a time, ignores nesting.
*
* Feed deltas in, iterate events. Every event type here has a direct
* counterpart in the reference parser — the shape is intentionally preserved
* so you can upgrade later without rewriting consumers.
*/
export type ArtifactEvent =
| { type: 'text'; delta: string }
| { type: 'artifact:start'; identifier: string; artifactType: string; title: string }
| { type: 'artifact:chunk'; identifier: string; delta: string }
| { type: 'artifact:end'; identifier: string; fullContent: string };
const OPEN_PREFIX = ' {
const re = /(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
const out: Record = {};
let m: RegExpExecArray | null = re.exec(raw);
while (m !== null) {
out[m[1] as string] = (m[2] ?? m[3] ?? '') as string;
m = re.exec(raw);
}
return out;
}
type OpenTagMatch =
| { kind: 'complete'; start: number; end: number; attrs: string }
| { kind: 'partial'; start: number }
| { kind: 'none' };
function findOpenTag(buffer: string): OpenTagMatch {
let from = 0;
while (from <= buffer.length) {
const idx = buffer.indexOf(OPEN_PREFIX, from);
if (idx === -1) {
// Maybe a strict prefix at the tail (e.g. " open (e.g. "'.
let i = after;
let quote: '"' | "'" | null = null;
while (i < buffer.length) {
const c = buffer.charAt(i);
if (quote !== null) {
if (c === quote) quote = null;
} else if (c === '"' || c === "'") {
quote = c;
} else if (c === '>') {
return { kind: 'complete', start: idx, end: i + 1, attrs: buffer.slice(after, i) };
}
i++;
}
return { kind: 'partial', start: idx };
}
return { kind: 'none' };
}
export function createArtifactParser() {
const state: ParserState = {
inside: false,
buffer: '',
identifier: '',
artifactType: '',
title: '',
content: '',
};
function* feed(delta: string): Generator {
state.buffer += delta;
while (state.buffer.length > 0) {
if (!state.inside) {
const open = findOpenTag(state.buffer);
if (open.kind === 'none') {
yield { type: 'text', delta: state.buffer };
state.buffer = '';
return;
}
if (open.kind === 'partial') {
if (open.start > 0) {
yield { type: 'text', delta: state.buffer.slice(0, open.start) };
state.buffer = state.buffer.slice(open.start);
}
return;
}
if (open.start > 0) {
yield { type: 'text', delta: state.buffer.slice(0, open.start) };
}
const attrs = parseAttrs(open.attrs);
state.inside = true;
state.identifier = attrs['identifier'] ?? '';
state.artifactType = attrs['type'] ?? '';
state.title = attrs['title'] ?? '';
state.content = '';
state.buffer = state.buffer.slice(open.end);
yield {
type: 'artifact:start',
identifier: state.identifier,
artifactType: state.artifactType,
title: state.title,
};
continue;
}
const closeIdx = state.buffer.indexOf(CLOSE_TAG);
if (closeIdx === -1) {
// Hold back enough bytes to detect a partial close tag at the tail.
const flushUpTo = state.buffer.length - (CLOSE_TAG.length - 1);
if (flushUpTo > 0) {
const chunk = state.buffer.slice(0, flushUpTo);
state.content += chunk;
state.buffer = state.buffer.slice(flushUpTo);
yield { type: 'artifact:chunk', identifier: state.identifier, delta: chunk };
}
return;
}
const finalChunk = state.buffer.slice(0, closeIdx);
if (finalChunk.length > 0) {
state.content += finalChunk;
yield { type: 'artifact:chunk', identifier: state.identifier, delta: finalChunk };
}
yield { type: 'artifact:end', identifier: state.identifier, fullContent: state.content };
state.buffer = state.buffer.slice(closeIdx + CLOSE_TAG.length);
state.inside = false;
state.identifier = '';
state.artifactType = '';
state.title = '';
state.content = '';
}
}
function* flush(): Generator {
if (state.inside) {
if (state.buffer.length > 0) {
state.content += state.buffer;
yield { type: 'artifact:chunk', identifier: state.identifier, delta: state.buffer };
state.buffer = '';
}
yield { type: 'artifact:end', identifier: state.identifier, fullContent: state.content };
} else if (state.buffer.length > 0) {
yield { type: 'text', delta: state.buffer };
}
state.buffer = '';
state.inside = false;
}
return { feed, flush };
}