diff --git a/AGENTS.md b/AGENTS.md index 35d0310b..6dd6cfdd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -239,6 +239,7 @@ packages/comark-svelte/ │ │ ├── Comark.svelte # High-level markdown → render ($state + $effect) │ │ ├── ComarkRenderer.svelte # Low-level AST → render component │ │ ├── ComarkNode.svelte # Recursive AST node renderer +│ │ ├── ComarkComponent.svelte # Custom component renderer with named snippets │ │ └── Resolve.svelte # Stable promise resolver for lazy components │ ├── async/ │ │ ├── index.ts # Async export (@comark/svelte/async) diff --git a/packages/comark-svelte/src/components/ComarkComponent.svelte b/packages/comark-svelte/src/components/ComarkComponent.svelte new file mode 100644 index 00000000..d593b0e6 --- /dev/null +++ b/packages/comark-svelte/src/components/ComarkComponent.svelte @@ -0,0 +1,78 @@ + + +{#if slotIndex < namedSlots.length} + {@const slot = namedSlots[slotIndex]} + {#snippet namedSlot()} + {#each slot.children as child, i (i)} + + {/each} + {/snippet} + + + {@render children?.()} + +{:else if Component} + + {@render children?.()} + +{:else if componentPromise} + + {@render children?.()} + +{/if} diff --git a/packages/comark-svelte/src/components/ComarkNode.svelte b/packages/comark-svelte/src/components/ComarkNode.svelte index e9265083..13b1fa1d 100644 --- a/packages/comark-svelte/src/components/ComarkNode.svelte +++ b/packages/comark-svelte/src/components/ComarkNode.svelte @@ -68,9 +68,9 @@ naturally appears inline after the deepest trailing text node. {#snippet renderChildren()} @@ -270,12 +250,36 @@ naturally appears inline after the deepest trailing text node. class={caretClass || undefined} style={CARET_STYLE}>{CARET_TEXT}{/if} +{:else if Component && namedSlots.length > 0} + + {@render renderChildren()} + {:else if Component} - + {@render renderChildren()} +{:else if componentPromise && namedSlots.length > 0} + + {@render renderChildren()} + {:else if componentPromise} - + {@render renderChildren()} {:else if isVoid} diff --git a/packages/comark-svelte/test/ComarkNode.svelte.test.ts b/packages/comark-svelte/test/ComarkNode.svelte.test.ts index aee6a3ff..3671c8bb 100644 --- a/packages/comark-svelte/test/ComarkNode.svelte.test.ts +++ b/packages/comark-svelte/test/ComarkNode.svelte.test.ts @@ -4,6 +4,7 @@ import { parse } from 'comark' import ComarkRenderer from '../src/components/ComarkRenderer.svelte' import ComarkNode from '../src/components/ComarkNode.svelte' import Alert from './test-components/Alert.svelte' +import CardWithHeaderFooter from './test-components/CardWithHeaderFooter.svelte' import CardWithFooter from './test-components/CardWithFooter.svelte' import ProseH1 from './test-components/ProseH1.svelte' @@ -192,6 +193,34 @@ Footer slot content. expect(screen.container.querySelector('template[name="footer"]')).toBeNull() }) + it('passes multiple named slots as Svelte snippet props', async () => { + const tree = await parse(`::card{title="My Card"} +Default slot content. + +#header +Header slot content. + +#footer +Footer slot content. +::`) + const screen = render(ComarkRenderer, { + tree, + components: { card: CardWithHeaderFooter }, + }) + const header = screen.container.querySelector('header')! + const main = screen.container.querySelector('main')! + const footer = screen.container.querySelector('footer')! + + expect(header).not.toBeNull() + expect(main).not.toBeNull() + expect(footer).not.toBeNull() + await expect.element(header).toHaveTextContent('Header slot content.') + await expect.element(main).toHaveTextContent('Default slot content.') + await expect.element(footer).toHaveTextContent('Footer slot content.') + expect(screen.container.querySelector('template[name="header"]')).toBeNull() + expect(screen.container.querySelector('template[name="footer"]')).toBeNull() + }) + it('resolves custom components from componentsManifest', async () => { const tree = await parse('::alert{type="warning"}\nLazy content\n::') const screen = render(ComarkRenderer, { diff --git a/packages/comark-svelte/test/ComarkNode.test.ts b/packages/comark-svelte/test/ComarkNode.test.ts index ffea8f3e..8915de56 100644 --- a/packages/comark-svelte/test/ComarkNode.test.ts +++ b/packages/comark-svelte/test/ComarkNode.test.ts @@ -6,6 +6,7 @@ import ComarkNode from '../src/components/ComarkNode.svelte' import ComarkAsync from '../src/async/ComarkAsync.svelte' import Alert from './test-components/Alert.svelte' import Card from './test-components/Card.svelte' +import CardWithHeaderFooter from './test-components/CardWithHeaderFooter.svelte' import CardWithFooter from './test-components/CardWithFooter.svelte' import ProseH1 from './test-components/ProseH1.svelte' @@ -265,6 +266,26 @@ Footer slot content. expect(output).not.toContain(' { + const tree = await parse(`::card{title="My Card"} +Default slot content. + +#header +Header slot content. + +#footer +Footer slot content. +::`) + const { body } = render(ComarkRenderer, { + props: { tree, components: { card: CardWithHeaderFooter } }, + }) + const output = html(body) + expect(output).toContain('
Header slot content.
') + expect(output).toContain('

Default slot content.

') + expect(output).toContain('
Footer slot content.
') + expect(output).not.toContain(' { const tree = await parse('::alert{type="warning"}\nLazy content\n::') const { body } = render(ComarkRenderer, { diff --git a/packages/comark-svelte/test/test-components/CardWithHeaderFooter.svelte b/packages/comark-svelte/test/test-components/CardWithHeaderFooter.svelte new file mode 100644 index 00000000..d92565b6 --- /dev/null +++ b/packages/comark-svelte/test/test-components/CardWithHeaderFooter.svelte @@ -0,0 +1,22 @@ + + +
+

{title}

+
{@render header?.()}
+
{@render children?.()}
+ +