From 91cbff7bbef6fcf7888ea1b8a8e878a65e29c78f Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Fri, 11 Sep 2026 11:16:10 +0200 Subject: [PATCH 1/2] fix(streaming): preserve heading IDs and reference links Force a full reparse for document-scoped constructs (reference definitions, heading ID counters) so incremental reuse stays correct, omit reference tokens from the AST, and clear stale frontmatter when a stream switches documents. Co-authored-by: Max --- .../src/internal/parse/token-processor.ts | 2 + packages/comark/src/parse.ts | 18 ++++++--- packages/comark/test/streaming.test.ts | 37 +++++++++++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/packages/comark/src/internal/parse/token-processor.ts b/packages/comark/src/internal/parse/token-processor.ts index ad58fc8d..e8c3e001 100644 --- a/packages/comark/src/internal/parse/token-processor.ts +++ b/packages/comark/src/internal/parse/token-processor.ts @@ -296,6 +296,8 @@ function processBlockToken( ): { node: Node | null; nextIndex: number } { const token = tokens[startIndex] + if (token.type === 'reference') return { node: null, nextIndex: startIndex + 1 } + if (token.type === 'hr') { return { node: ['hr', {}] as Node, nextIndex: startIndex + 1 } } diff --git a/packages/comark/src/parse.ts b/packages/comark/src/parse.ts index fa55a422..7869b489 100644 --- a/packages/comark/src/parse.ts +++ b/packages/comark/src/parse.ts @@ -124,7 +124,11 @@ export function createMarkdownParser { + it.each([false, true])('omits reference definitions with streaming: %s', async (streaming) => { + const result = await createMarkdownParser()('# Heading\n\n[ref]: https://example.com\n\n[link][ref]', { streaming }) + expect(result.nodes).toMatchObject([ + ['h1', {}, 'Heading'], + ['p', {}, ['a', { href: 'https://example.com' }, 'link']], + ]) + }) + + it.each([ + ['duplicate headings', '# Same\n\nIntro\n\n# Same', '\n\nTail'], + ['nested headings', '## Parent\n\nIntro\n\n### Child', '\n\nTail'], + ['setext headings', 'Same\n====\n\nIntro\n\nSame\n====', '\n\nTail'], + ['CRLF setext headings', 'Same\r\n====\r\n\r\nIntro\r\n\r\nSame\r\n====', '\r\n\r\nTail'], + ['headings in lists', '# Same\n\nIntro\n\n- # Same', '\n\nTail'], + ['existing references', '[ref]: https://example.com\n\n# Heading\n\nIntro\n\n[link][ref]', ' grows'], + ['new references', '[link][ref]\n\nIntro\n\nTail', '\n\n[ref]: https://example.com'], + ])('matches a full parse after appending to %s', async (_, source, appended) => { + const parse = createMarkdownParser() + await parse(source, { streaming: true }) + + const result = await parse(source + appended, { streaming: true }) + + expect(result).toMatchObject(await createMarkdownParser()(source + appended)) + }) + describe('$.line metadata', () => { it('preserves position metadata on nodes in streaming mode', async () => { const parse = createMarkdownParser() @@ -190,6 +215,18 @@ describe('streaming mode', () => { expect(result1.frontmatter).toEqual({ title: 'Hello' }) expect(result2.frontmatter).toEqual({ title: 'Hello' }) }) + + it.each([ + ['without frontmatter', '# Replacement', {}], + ['with new frontmatter', '---\ntitle: New\n---\n\n# Replacement', { title: 'New' }], + ])('replaces stream frontmatter when the source changes %s', async (_, replacement, frontmatter) => { + const parse = createMarkdownParser() + await parse('---\ntitle: Old\n---\n\n# Original', { streaming: true }) + + const result = await parse(replacement, { streaming: true }) + + expect(result.frontmatter).toEqual(frontmatter) + }) }) describe('streaming with MDC components', () => { From 5adef9363a19ec22f6592a2a3b1325064d1f4e11 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:45:40 +0000 Subject: [PATCH 2/2] test: update bundle size snapshot --- test/bundle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bundle.test.ts b/test/bundle.test.ts index 6513ed58..53611c0f 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -67,7 +67,7 @@ describe('package bundle size', { timeout: 60_000 }, () => { "@comark/react": "36.8k (74 files)", "@comark/svelte": "43.9k (82 files)", "@comark/vue": "54.7k (78 files)", - "comark": "364k (158 files)", + "comark": "365k (158 files)", } `) })