svelte-streamdown 2.6.0 → 2.6.1

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/README.md CHANGED
@@ -490,6 +490,7 @@ This heading will use a custom component!`;
490
490
  | `animation.tokenize` | `'word' \| 'char'` | `'word'` | Tokenization method for text animations |
491
491
  | `animation.animateOnMount` | `boolean` | `false` | Run the token animation on mount or not, useful if you render the Streamdown component in the same time as the first token is receive from the LLM |
492
492
  | `extensions` | `Array<Extension>` | `[]` | Custom marked tokenizers to render special markdown blocks or inline tokens |
493
+ | `mdxComponents` | `Record<string, Component>` | `{}` | Map of MDX component names to Svelte components (e.g., `{ Card, Button }`) |
493
494
  | `children` | `Snippet<[{token:GenericToken, streamdown: StreamdownContext, children: Snippet` | `undefined` | Snippet used to render elements not supported by Streamdown, custom extensions, and MDX components |
494
495
 
495
496
  #### All Available Customizable Elements:
@@ -506,9 +507,9 @@ This heading will use a custom component!`;
506
507
 
507
508
  **Special Content**: `blockquote`, `hr`, `alert`, `mermaid`, `math`, `footnoteRef`, `inlineCitation`
508
509
 
509
- **MDX Components**: Any PascalCase component (e.g., `Card`, `Button`, `MyComponent`) - pass as snippets with the component name
510
+ **MDX Components**: Handled via a single `mdx` snippet that receives `token`, `props`, and `children`. Use `token.tagName` to differentiate between components.
510
511
 
511
- **Note**: The above elements are **supported by Streamdown** and should be customized using individual props or the theme system.
512
+ **Note**: The above elements are **supported by Streamdown** and should be customized using individual props or the theme system. MDX components require the `mdx` snippet.
512
513
 
513
514
  ## 🎨 Theming System
514
515
 
@@ -637,24 +638,80 @@ This is **markdown content** inside a component!
637
638
  </script>
638
639
 
639
640
  <Streamdown {content}>
640
- {#snippet Card({ title, count, children })}
641
- <div class="rounded-lg border border-gray-200 p-4 shadow-sm">
642
- <h3 class="text-xl font-bold">{title}</h3>
643
- <p class="text-gray-600">Count: {count}</p>
644
- <div class="mt-2">
645
- {@render children()}
641
+ {#snippet mdx({ token, props, children })}
642
+ {#if token.tagName === 'Card'}
643
+ <div class="rounded-lg border border-gray-200 p-4 shadow-sm">
644
+ <h3 class="text-xl font-bold">{props.title}</h3>
645
+ <p class="text-gray-600">Count: {props.count}</p>
646
+ <div class="mt-2">
647
+ {@render children()}
648
+ </div>
646
649
  </div>
647
- </div>
648
- {/snippet}
649
-
650
- {#snippet Button({ label, active })}
651
- <button class="rounded px-4 py-2 {active ? 'bg-blue-500 text-white' : 'bg-gray-200'}">
652
- {label}
653
- </button>
650
+ {:else if token.tagName === 'Button'}
651
+ <button class="rounded px-4 py-2 {props.active ? 'bg-blue-500 text-white' : 'bg-gray-200'}">
652
+ {props.label}
653
+ </button>
654
+ {:else}
655
+ {@render children()}
656
+ {/if}
654
657
  {/snippet}
655
658
  </Streamdown>
656
659
  ```
657
660
 
661
+ ### Alternative: Using Svelte Components Directly
662
+
663
+ Instead of using the `mdx` snippet with conditional logic, you can pass Svelte components directly using the `mdxComponents` prop:
664
+
665
+ ```svelte
666
+ <script>
667
+ import { Streamdown } from 'svelte-streamdown';
668
+ import Card from './Card.svelte';
669
+ import Button from './Button.svelte';
670
+
671
+ let content = `
672
+ # Using MDX Components
673
+
674
+ <Card title="Hello" count={42}>
675
+ This is **markdown content** inside a component!
676
+ </Card>
677
+
678
+ <Button label="Click me" active={true} />
679
+ `;
680
+ </script>
681
+
682
+ <Streamdown {content} mdxComponents={{ Card, Button }} />
683
+ ```
684
+
685
+ **Your Svelte components** (`Card.svelte`, `Button.svelte`) should accept props and a `children` snippet:
686
+
687
+ ```svelte
688
+ <!-- Card.svelte -->
689
+ <script>
690
+ let { title, count, children } = $props();
691
+ </script>
692
+
693
+ <div class="rounded-lg border border-gray-200 p-4 shadow-sm">
694
+ <h3 class="text-xl font-bold">{title}</h3>
695
+ <p class="text-gray-600">Count: {count}</p>
696
+ <div class="mt-2">
697
+ {@render children()}
698
+ </div>
699
+ </div>
700
+ ```
701
+
702
+ ```svelte
703
+ <!-- Button.svelte -->
704
+ <script>
705
+ let { label, active } = $props();
706
+ </script>
707
+
708
+ <button class="rounded px-4 py-2 {active ? 'bg-blue-500 text-white' : 'bg-gray-200'}">
709
+ {label}
710
+ </button>
711
+ ```
712
+
713
+ This approach is cleaner when you have standalone component files, while the `mdx` snippet approach is better for inline component definitions or when you need shared logic across components.
714
+
658
715
  ### Supported Syntax
659
716
 
660
717
  **Self-closing components:**
@@ -689,7 +746,7 @@ MDX components support three attribute value types:
689
746
 
690
747
  MDX components are streaming-safe. Incomplete components are automatically handled during AI streaming:
691
748
 
692
- - Incomplete tags like `<Component attr` are escaped with backticks
749
+ - Incomplete tags like `<Component attr` not rendered to prevent runtime errors
693
750
  - Unclosed components like `<Card>content` are auto-closed with `</Card>`
694
751
  - Malformed attributes are escaped to prevent rendering errors
695
752
 
@@ -697,21 +754,31 @@ This ensures your UI remains stable even when receiving partial markdown from st
697
754
 
698
755
  ### Component Props
699
756
 
700
- MDX component snippets receive:
701
- - `token`: The full MdxToken with `tagName`, `attributes`, etc.
757
+ The `mdx` snippet receives three parameters:
758
+ - `token`: The full MdxToken with `tagName`, `attributes`, `selfClosing`, etc.
759
+ - `props`: Object containing all parsed attributes (e.g., `props.title`, `props.count`)
702
760
  - `children`: Snippet containing parsed markdown content
703
- - **All attributes spread directly**: Access attributes by name (e.g., `title`, `count`, `active`)
704
761
 
705
- Example:
762
+ Use `token.tagName` to determine which component is being rendered:
763
+
706
764
  ```svelte
707
765
  <!-- Markdown: <Card title="Hello" count={5}>Content</Card> -->
708
766
  <Streamdown {content}>
709
- {#snippet Card({ title, count, children })}
710
- <div>
711
- <h3>{title}</h3>
712
- <span>Count: {count}</span>
767
+ {#snippet mdx({ token, props, children })}
768
+ {#if token.tagName === 'Card'}
769
+ <div>
770
+ <h3>{props.title}</h3>
771
+ <span>Count: {props.count}</span>
772
+ {@render children()}
773
+ </div>
774
+ {:else if token.tagName === 'Alert'}
775
+ <div class="alert alert-{props.type}">
776
+ {@render children()}
777
+ </div>
778
+ {:else}
779
+ <!-- Fallback for unknown components -->
713
780
  {@render children()}
714
- </div>
781
+ {/if}
715
782
  {/snippet}
716
783
  </Streamdown>
717
784
  ```
@@ -299,16 +299,13 @@
299
299
  {@html content}
300
300
  {/if}
301
301
  {:else if token.type === 'mdx'}
302
- {#if token.tagName in streamdown.snippets}
303
- <Slot
304
- props={{ token, children, ...token.attributes }}
305
- render={streamdown.snippets[token.tagName as keyof typeof streamdown.snippets]}
306
- >
302
+ {@const Component = streamdown.mdxComponents?.[token.tagName]}
303
+ {#if Component}
304
+ <Component {token} {children} props={token.attributes} />
305
+ {:else}
306
+ <Slot props={{ token, children, props: token.attributes }} render={streamdown.snippets.mdx}>
307
307
  {@render children()}
308
308
  </Slot>
309
- {:else}
310
- <!-- Fallback if no snippet provided for this component -->
311
- {@render children()}
312
309
  {/if}
313
310
  {:else}
314
311
  <!-- For tokens we don't handle specifically, it may certainely be a custom extension to to the children props to handle -->
@@ -29,6 +29,7 @@
29
29
  extensions,
30
30
  sources,
31
31
  inlineCitationsMode = 'carousel',
32
+ mdxComponents,
32
33
  ...snippets
33
34
  }: StreamdownProps<Source> = $props();
34
35
 
@@ -118,6 +119,9 @@
118
119
  },
119
120
  get icons() {
120
121
  return icons;
122
+ },
123
+ get mdxComponents() {
124
+ return mdxComponents;
121
125
  }
122
126
  });
123
127
 
@@ -1,4 +1,4 @@
1
- import type { Snippet } from 'svelte';
1
+ import type { Component, Snippet } from 'svelte';
2
2
  import type { DeepPartialTheme, Theme } from './theme.js';
3
3
  import type { MermaidConfig } from 'mermaid';
4
4
  import type { KatexOptions } from 'katex';
@@ -30,7 +30,7 @@ export declare class StreamdownContext<Source extends Record<string, any> = Reco
30
30
  });
31
31
  }
32
32
  export declare const useStreamdown: () => StreamdownContext<Record<string, any>>;
33
- import type { AlertToken, MathToken, SubSupToken, TableToken, THead, TBody, TFoot, THeadRow, TRow, TD, TH, Extension, GenericToken, CitationToken } from './marked/index.js';
33
+ import type { AlertToken, MathToken, SubSupToken, TableToken, THead, TBody, TFoot, THeadRow, TRow, TD, TH, Extension, GenericToken, CitationToken, MdxToken } from './marked/index.js';
34
34
  import type { Tokens } from 'marked';
35
35
  import type { ListItemToken, ListToken } from './marked/marked-list.js';
36
36
  import type { Footnote, FootnoteRef, FootnoteToken } from './marked/marked-footnotes.js';
@@ -73,6 +73,7 @@ type TokenSnippet = {
73
73
  inlineCitationPopover: CitationToken;
74
74
  inlineCitationContent: CitationToken;
75
75
  inlineCitationPreview: CitationToken;
76
+ mdx: MdxToken;
76
77
  };
77
78
  type PredefinedElements = keyof TokenSnippet;
78
79
  export type Snippets<Source extends Record<string, any> = Record<string, any>> = {
@@ -83,6 +84,8 @@ export type Snippets<Source extends Record<string, any> = Record<string, any>> =
83
84
  } & (K extends 'inlineCitationContent' ? {
84
85
  source: Source;
85
86
  key: string;
87
+ } : K extends 'mdx' ? {
88
+ props: Record<string, number | string | boolean | null | undefined>;
86
89
  } : {})
87
90
  ]>;
88
91
  };
@@ -151,5 +154,10 @@ export type StreamdownProps<Source extends Record<string, any> = Record<string,
151
154
  token: GenericToken;
152
155
  children: Snippet;
153
156
  }]>;
157
+ mdxComponents?: Record<string, Component<{
158
+ token: MdxToken;
159
+ children: Snippet;
160
+ props: any;
161
+ }, any, any>>;
154
162
  } & Partial<Snippets<Source>>;
155
163
  export {};
@@ -762,7 +762,7 @@ class IncompleteMarkdownParser {
762
762
  };
763
763
  },
764
764
  handler: ({ line, state }) => {
765
- // Escape incomplete MDX syntax by wrapping in backticks
765
+ // Remove incomplete MDX syntax (don't render it)
766
766
  const lineStates = state.mdxLineStates || [];
767
767
  const currentState = lineStates[state.currentLine];
768
768
  if (currentState?.incompletePositions && currentState.incompletePositions.length > 0) {
@@ -771,8 +771,8 @@ class IncompleteMarkdownParser {
771
771
  for (let i = currentState.incompletePositions.length - 1; i >= 0; i--) {
772
772
  const pos = currentState.incompletePositions[i];
773
773
  const before = result.substring(0, pos);
774
- const incomplete = result.substring(pos);
775
- result = before + '`' + incomplete + '`';
774
+ // Simply remove the incomplete MDX tag
775
+ result = before;
776
776
  }
777
777
  return result;
778
778
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-streamdown",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",