From ac5b07397919ef34abf7da03227c877baea39b97 Mon Sep 17 00:00:00 2001 From: Asaf Yishai Date: Sun, 4 Oct 2026 11:28:41 +0300 Subject: [PATCH 1/2] fix(autoclose): heal shorthand component props in streaming tails --- .../comark/src/internal/parse/auto-close/index.ts | 9 +++++---- packages/comark/test/streaming.test.ts | 12 ++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/comark/src/internal/parse/auto-close/index.ts b/packages/comark/src/internal/parse/auto-close/index.ts index baaace55..be6c1e9e 100644 --- a/packages/comark/src/internal/parse/auto-close/index.ts +++ b/packages/comark/src/internal/parse/auto-close/index.ts @@ -144,7 +144,7 @@ export function autoCloseMarkdown(markdown: string, options: AutoCloseOptions = if (doc.fmOpen && doc.fmContent) result = closeDelimiterLine(result, '-', 3) - if (o.syntax) result = closeComponents(result, source, doc.comps) + if (o.syntax) result = closeComponents(result, source, doc.comps, doc.start >= 0) return result } @@ -828,11 +828,12 @@ function closeDelimiterLine(text: string, ch: string, width: number): string { } /** Closes an open props brace and every open component fence, innermost first. */ -function closeComponents(result: string, source: string, comps: Component[]): string { - if (!source.includes('::')) return result +function closeComponents(result: string, source: string, comps: Component[], healShorthand: boolean): string { + const lineStart = result.lastIndexOf('\n') + 1 + const shorthand = healShorthand && /^:[a-z$][\w$-]*\{/i.test(result.slice(lineStart).trimStart()) + if (!source.includes('::') && !shorthand) return result // `::alert{type="info` → close the quote and the brace. - const lineStart = result.lastIndexOf('\n') + 1 let brace = -1 for (let i = result.length - 1; i >= lineStart; i--) { const c = result.charCodeAt(i) diff --git a/packages/comark/test/streaming.test.ts b/packages/comark/test/streaming.test.ts index 2557683d..dff0b528 100644 --- a/packages/comark/test/streaming.test.ts +++ b/packages/comark/test/streaming.test.ts @@ -230,6 +230,18 @@ describe('streaming mode', () => { }) describe('streaming with MDC components', () => { + it('closes quoted attributes after reusing a completed component', async () => { + const parse = createMarkdownParser() + const source = '::note\nContent\n::\n\n:button{label="' + await parse(source, { streaming: true }) + + const partial = await parse(source + 'Review', { streaming: true }) + expect(partial.nodes[1]).toMatchObject(['button', { label: 'Review' }]) + + const completed = await parse(source + 'Review"}') + expect(completed.nodes[1]).toMatchObject(['button', { label: 'Review' }]) + }) + it('parses MDC block components in streaming mode', async () => { const parse = createMarkdownParser() From d20ff2c03b9191e03ca535516e6bd8d33fb7bf1f Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 8 Oct 2026 15:27:13 +0200 Subject: [PATCH 2/2] chore: update test --- packages/comark/test/streaming.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/comark/test/streaming.test.ts b/packages/comark/test/streaming.test.ts index dff0b528..06b9a211 100644 --- a/packages/comark/test/streaming.test.ts +++ b/packages/comark/test/streaming.test.ts @@ -233,7 +233,8 @@ describe('streaming mode', () => { it('closes quoted attributes after reusing a completed component', async () => { const parse = createMarkdownParser() const source = '::note\nContent\n::\n\n:button{label="' - await parse(source, { streaming: true }) + const partial0 = await parse(source, { streaming: true }) + expect(partial0.nodes[1]).toMatchObject(['button', { label: '' }]) const partial = await parse(source + 'Review', { streaming: true }) expect(partial.nodes[1]).toMatchObject(['button', { label: 'Review' }])