svelte-streamdown 2.3.1 → 2.3.3

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
@@ -231,6 +231,13 @@ III. Third item
231
231
  > [!IMPORTANT]
232
232
  > Native support for Github style Alert
233
233
 
234
+ ### Description List
235
+
236
+ : Topic 1 : Description 1
237
+ : **Topic 2** : *Description 2*
238
+ : Topic 3 : Description 3
239
+ : Topic 3 : Description 3
240
+
234
241
  ## 🔄 Differences from Original React Version
235
242
 
236
243
  This Svelte port maintains feature parity with the original [Streamdown](https://streamdown.ai/) while adapting to Svelte's patterns:
@@ -261,14 +268,14 @@ This Svelte port maintains feature parity with the original [Streamdown](https:/
261
268
 
262
269
  ## 🎭 Animation System
263
270
 
264
- Streamdown includes a sophisticated animation system designed specifically for streaming AI content, providing smooth and engaging visual feedback as text appears on screen.
271
+ Streamdown includes an animation system designed specifically for streaming AI content, providing smooth and engaging visual feedback as text appears on screen.
265
272
 
266
273
  ### How It Works
267
274
 
268
275
  The animation system works by:
269
276
 
270
277
  1. **Tokenization**: Text is broken down into tokens (words or characters) based on your configuration
271
- 2. **Sequential Animation**: Each token animates in sequence with configurable timing
278
+ 2. **Sequential Animation**: Each token animates as it is received
272
279
  3. **Block-level Animation**: Entire blocks (paragraphs, headings, code blocks) animate as units
273
280
 
274
281
  ### Animation Types
@@ -293,6 +300,8 @@ Text slides down from above while fading in, creating a dynamic downward motion.
293
300
 
294
301
  > [!TIP]
295
302
  > For production applications where the LLM is not streaming (static content), disable animations entirely by setting `animation.enabled = false` to minimize DOM elements and improve performance.
303
+ >
304
+ > If using AI SDK mind to smooth stream the content to using word-level tokenization to avoid partial words not being animated.
296
305
 
297
306
  > [!WARNING]
298
307
  > Character-level tokenization (`tokenize: 'char'`) creates significantly more DOM elements than word-level tokenization. Use character tokenization sparingly and only when the typewriter effect is essential for your user experience.
@@ -327,17 +336,15 @@ console.log('Hello from Streamdown!');
327
336
  let content = `# Custom Components Example
328
337
 
329
338
  This heading will use a custom component!`;
330
-
331
- // Custom heading component
332
339
  </script>
333
340
 
334
- {#snippet customHeading({ children, token })}
335
- <h1 class="mb-4 text-4xl font-bold text-blue-600" {...token.props}>
336
- {@render children()}
337
- </h1>
338
- {/snippet}
339
-
340
- <Streamdown {content} heading={customHeading} />
341
+ <Streamdown {content}>
342
+ {#snippet heading({ children })}
343
+ <h1 class="mb-4 text-4xl font-bold text-blue-600">
344
+ {@render children()}
345
+ </h1>
346
+ {/snippet}
347
+ </Streamdown>
341
348
  ```
342
349
 
343
350
  ### Security Configuration
@@ -359,73 +366,32 @@ This heading will use a custom component!`;
359
366
 
360
367
  ## 📋 Props API
361
368
 
362
- | Prop | Type | Default | Description |
363
- | -------------------------- | ---------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
364
- | `content` | `string` | - | **Required.** The markdown content to render |
365
- | `class` | `string` | - | CSS class names for the wrapper element |
366
- | `parseIncompleteMarkdown` | `boolean` | `true` | Parse and fix incomplete markdown syntax |
367
- | `defaultOrigin` | `string` | - | Default origin for relative URLs |
368
- | `allowedLinkPrefixes` | `string[]` | `['*']` | Allowed URL prefixes for links |
369
- | `allowedImagePrefixes` | `string[]` | `['*']` | Allowed URL prefixes for images |
370
- | `skipHtml` | `boolean` | - | Skip HTML parsing entirely |
371
- | `unwrapDisallowed` | `boolean` | - | Unwrap instead of removing disallowed elements |
372
- | `urlTransform` | `UrlTransform \| null` | - | Custom URL transformation function |
373
- | `theme` | `DeepPartial<Theme>` | - | Custom theme overrides |
374
- | `baseTheme` | `'tailwind' \| 'shadcn'` | `'tailwind'` | Base theme to use before applying overrides |
375
- | `mergeTheme` | `boolean` | `true` | Whether to merge theme with base theme |
376
- | `shikiTheme` | `BundledTheme` | `'github-light'` | Code highlighting theme |
377
- | `mermaidConfig` | `MermaidConfig` | - | Mermaid diagram configuration |
378
- | `katexConfig` | `KatexOptions \| ((inline: boolean) => KatexOptions)` | - | KaTeX math rendering options |
379
- | `animation` | `AnimationConfig` | - | Animation configuration for streaming content |
380
- | `animation.enabled` | `boolean` | `false` | Enable/disable animations |
381
- | `animation.type` | `'fade' \| 'blur' \| 'typewriter' \| 'slideUp' \| 'slideDown'` | `'blur'` | Animation style for text appearance |
382
- | `animation.duration` | `number` | `500` | Animation duration in milliseconds |
383
- | `animation.timingFunction` | `'ease' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'linear'` | `'ease-in'` | CSS timing function for animations |
384
- | `animation.tokenize` | `'word' \| 'char'` | `'word'` | Tokenization method for text animations |
385
- | `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 |
386
-
387
- ### Custom Component Props
388
-
389
- **Every single markdown element** can be customized with Svelte snippets, giving you
390
- complete control over styling and behavior:
391
-
392
- Each snippet receives `{ children, token }` where `token` is a typed token object containing the parsed markdown token with its properties and children is a snippet to be rendered.
393
-
394
- ```svelte
395
- <script>
396
- import { Streamdown } from 'svelte-streamdown';
397
-
398
- let content = `# Fully Customizable
399
-
400
- This heading uses a custom component with your design system!`;
401
- </script>
402
-
403
- {#snippet customHeading({ children, token })}
404
- <h1
405
- class="text-gradient mb-6 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-4xl font-bold text-transparent"
406
- {...token.props}
407
- >
408
- {@render children()}
409
- </h1>
410
- {/snippet}
411
-
412
- {#snippet customCode({ children, token })}
413
- <code class="rounded bg-gray-100 px-2 py-1 font-mono text-sm dark:bg-gray-800" {...token.props}>
414
- {@render children()}
415
- </code>
416
- {/snippet}
417
-
418
- {#snippet customBlockquote({ children, token })}
419
- <blockquote
420
- class="border-l-4 border-blue-500 pl-4 text-gray-600 italic dark:text-gray-300"
421
- {...token.props}
422
- >
423
- {@render children()}
424
- </blockquote>
425
- {/snippet}
426
-
427
- <Streamdown {content} heading={customHeading} code={customCode} blockquote={customBlockquote} />
428
- ```
369
+ | Prop | Type | Default | Description |
370
+ | -------------------------- | -------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
371
+ | `content` | `string` | - | **Required.** The markdown content to render |
372
+ | `class` | `string` | - | CSS class names for the wrapper element |
373
+ | `parseIncompleteMarkdown` | `boolean` | `true` | Parse and fix incomplete markdown syntax |
374
+ | `defaultOrigin` | `string` | - | Default origin for relative URLs |
375
+ | `allowedLinkPrefixes` | `string[]` | `['*']` | Allowed URL prefixes for links |
376
+ | `allowedImagePrefixes` | `string[]` | `['*']` | Allowed URL prefixes for images |
377
+ | `skipHtml` | `boolean` | - | Skip HTML parsing entirely |
378
+ | `unwrapDisallowed` | `boolean` | - | Unwrap instead of removing disallowed elements |
379
+ | `urlTransform` | `UrlTransform \| null` | - | Custom URL transformation function |
380
+ | `theme` | `DeepPartial<Theme>` | - | Custom theme overrides |
381
+ | `baseTheme` | `'tailwind' \| 'shadcn'` | `'tailwind'` | Base theme to use before applying overrides |
382
+ | `mergeTheme` | `boolean` | `true` | Whether to merge theme with base theme |
383
+ | `shikiTheme` | `BundledTheme` | `'github-light'` | Code highlighting theme |
384
+ | `mermaidConfig` | `MermaidConfig` | - | Mermaid diagram configuration |
385
+ | `katexConfig` | `KatexOptions \| ((inline: boolean) => KatexOptions)` | - | KaTeX math rendering options |
386
+ | `animation` | `AnimationConfig` | - | Animation configuration for streaming content |
387
+ | `animation.enabled` | `boolean` | `false` | Enable/disable animations |
388
+ | `animation.type` | `'fade' \| 'blur' \| 'typewriter' \| 'slideUp' \| 'slideDown'` | `'blur'` | Animation style for text appearance |
389
+ | `animation.duration` | `number` | `500` | Animation duration in milliseconds |
390
+ | `animation.timingFunction` | `'ease' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'linear'` | `'ease-in'` | CSS timing function for animations |
391
+ | `animation.tokenize` | `'word' \| 'char'` | `'word'` | Tokenization method for text animations |
392
+ | `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 |
393
+ | `extensions` | `Array<Extension>` | `[]` | Custom marked tokenizers to render special markdown blocks or inline tokens |
394
+ | `children` | `Snippet<[{token:GenericToken, streamdown: StreamdownContext, children: Snippet` | `undefined` | Snippet used to render element that are not supported by Streamdown and tokenized by your custom extensions |
429
395
 
430
396
  #### All Available Customizable Elements:
431
397
 
@@ -435,15 +401,15 @@ This heading uses a custom component with your design system!`;
435
401
 
436
402
  **Lists**: `ul`, `ol`, `li`
437
403
 
438
- **Code**: `code`, `inlineCode`, `pre`
404
+ **Code**: `code`, `codeSpan`
439
405
 
440
- **Tables**: `table`, `thead`, `tbody`, `tr`, `th`, `td`
406
+ **Tables**: `table`, `thead`, `tbody`, `tr`, `th`, `td`, `tfoot`
441
407
 
442
- **Special Content**: `blockquote`, `hr`, `alert`, `mermaid`, `math`, `inlineMath`
408
+ **Special Content**: `blockquote`, `hr`, `alert`, `mermaid`, `math`, `footnoteRef`
443
409
 
444
410
  **Note**: The above elements are **supported by Streamdown** and should be customized using individual props or the theme system.
445
411
 
446
- ## 🎨 Advanced Theming System
412
+ ## 🎨 Theming System
447
413
 
448
414
  ### Built-in Themes
449
415
 
@@ -548,6 +514,66 @@ Each component supports multiple themeable parts:
548
514
 
549
515
  Themes are intelligently merged using Tailwind's class merging utility, so you only need to override the specific parts you want to customize while keeping the default styling for everything else.
550
516
 
517
+ ## 💉 Extensibility
518
+
519
+ Streamdown is extensible through the use of custom extensions.
520
+
521
+ An extension is an object that has a `name`, a `level` and a `tokenizer` function.
522
+
523
+ - `name`: The name of the extension
524
+ - `level`: The level of the extension, can be `block` or `inline`
525
+ - `tokenizer`: The tokenizer function, see [marked](https://github.com/markedjs/marked) for more information
526
+
527
+ To render the extension custom tokens, you can then simply use the `children` snippet.
528
+
529
+ ### Example
530
+
531
+ ```svelte
532
+ <script lang="ts">
533
+ import { Streamdown, type Extension } from 'svelte-streamdown';
534
+ const markedCollapsible: Extension = {
535
+ name: 'collapsible',
536
+ level: 'block',
537
+ tokenizer(this, src) {
538
+ // Match [detail]...[detail] blocks (case insensitive)
539
+ const detailMatch = src.match(/^\[detail\](.*?)\[detail\]/is);
540
+
541
+ if (detailMatch) {
542
+ const content = detailMatch[1] || '';
543
+ const tokens = this.lexer.blockTokens(content);
544
+
545
+ return {
546
+ type: 'detail',
547
+ raw: detailMatch[0], // The entire matched string including tags
548
+ tokens
549
+ };
550
+ }
551
+
552
+ return undefined;
553
+ }
554
+ };
555
+ </script>
556
+
557
+ <Streamdown
558
+ extensions={[markedCollapsible]}
559
+ content={`
560
+ [detail]
561
+ This is a collapsible **section**
562
+ [detail]`}
563
+ >
564
+ {#snippet children({ token, streamdown, children })}
565
+ {#if token.type === 'detail'}
566
+ <details>
567
+ <summary> Detail </summary>
568
+ <div>
569
+ {@render children()}
570
+ </div>
571
+ </details>
572
+ {/if}
573
+ {/snippet}
574
+ </Streamdown>
575
+ ```
576
+
551
577
  ## 🛠️ Development
552
578
 
553
579
  ### Setup
package/dist/Block.svelte CHANGED
@@ -14,8 +14,8 @@
14
14
  insideFootnote?: boolean;
15
15
  } = $props();
16
16
 
17
- const tokens = $derived(lex(parseIncompleteMarkdown(block.trim())));
18
17
  const streamdown = useStreamdown();
18
+ const tokens = $derived(lex(parseIncompleteMarkdown(block.trim()), streamdown.extensions));
19
19
  </script>
20
20
 
21
21
  {#snippet renderChildren(tokens: StreamdownToken[])}
@@ -223,6 +223,40 @@
223
223
  <Alert {token} {children} />
224
224
  {:else if token.type === 'footnoteRef'}
225
225
  <FootnoteRef {token} />
226
+ {:else if token.type === 'footnote'}
227
+ <!-- TODO Footnotes are rendered inside the FootnoteRef popover -->
228
+ {:else if token.type === 'descriptionList'}
229
+ <Slot props={{ children, token }} render={streamdown.snippets.descriptionList}>
230
+ <dl class={streamdown.theme.descriptionList.base}>
231
+ {@render children()}
232
+ </dl>
233
+ </Slot>
234
+ {:else if token.type === 'description'}
235
+ <Slot props={{ children, token }} render={streamdown.snippets.description}>
236
+ <div class={streamdown.theme.description.base}>
237
+ {@render children()}
238
+ </div>
239
+ </Slot>
240
+ {:else if token.type === 'descriptionTerm'}
241
+ <Slot props={{ children, token }} render={streamdown.snippets.descriptionTerm}>
242
+ <dt class={streamdown.theme.descriptionTerm.base}>
243
+ {@render children()}
244
+ </dt>
245
+ </Slot>
246
+ {:else if token.type === 'descriptionDetail'}
247
+ <Slot props={{ children, token }} render={streamdown.snippets.descriptionDetail}>
248
+ <dd class={streamdown.theme.descriptionDetail.base}>
249
+ {@render children()}
250
+ </dd>
251
+ </Slot>
252
+ {:else if token.type === 'def'}
253
+ <!-- TODO This does not seems to be tokenized for now -->
254
+ {:else if token.type === 'escape'}
255
+ <!-- TODO This does not seems to be tokenized for now -->
256
+ {:else if token.type === 'space'}
257
+ <!-- TODO This does not seems to be tokenized for now -->
258
+ {:else if token.type === 'text'}
259
+ {@render children()}
226
260
  {:else if token.type === 'html'}
227
261
  {#if streamdown.renderHtml}
228
262
  {@const content =
@@ -230,6 +264,6 @@
230
264
  {@html content}
231
265
  {/if}
232
266
  {:else}
233
- <!-- For tokens we don't handle specifically, render children or fallback -->
234
- {@render children?.()}
267
+ <!-- For tokens we don't handle specifically, it may certainely be a custom extension to to the children props to handle -->
268
+ {@render streamdown.children?.({ token, children, streamdown })}
235
269
  {/if}
@@ -6,4 +6,3 @@ export { default as Link } from './Link.svelte';
6
6
  export { default as Math } from './Math.svelte';
7
7
  export { default as Mermaid } from './Mermaid.svelte';
8
8
  export { default as Slot } from './Slot.svelte';
9
- export { default as Table } from './Table.svelte';
@@ -6,4 +6,3 @@ export { default as Link } from './Link.svelte';
6
6
  export { default as Math } from './Math.svelte';
7
7
  export { default as Mermaid } from './Mermaid.svelte';
8
8
  export { default as Slot } from './Slot.svelte';
9
- export { default as Table } from './Table.svelte';
@@ -25,6 +25,8 @@
25
25
  animation,
26
26
  element = $bindable(),
27
27
  icons,
28
+ children,
29
+ extensions,
28
30
  ...snippets
29
31
  }: StreamdownProps = $props();
30
32
 
@@ -99,6 +101,12 @@
99
101
  mermaid: mermaidControls
100
102
  };
101
103
  },
104
+ get children() {
105
+ return children;
106
+ },
107
+ get extensions() {
108
+ return extensions;
109
+ },
102
110
  get icons() {
103
111
  return icons;
104
112
  }
@@ -106,7 +114,7 @@
106
114
 
107
115
  const id = $props.id();
108
116
 
109
- const blocks = $derived(parseBlocks(content));
117
+ const blocks = $derived(parseBlocks(content, streamdown.extensions));
110
118
  </script>
111
119
 
112
120
  <div bind:this={element} class={className}>
@@ -28,10 +28,11 @@ export declare class StreamdownContext {
28
28
  });
29
29
  }
30
30
  export declare const useStreamdown: () => StreamdownContext;
31
- import type { AlertToken, MathToken, SubSupToken, TableToken, THead, TBody, TFoot, THeadRow, TRow, TD, TH } from './marked/index.js';
31
+ import type { AlertToken, MathToken, SubSupToken, TableToken, THead, TBody, TFoot, THeadRow, TRow, TD, TH, Extension, GenericToken } from './marked/index.js';
32
32
  import type { Tokens } from 'marked';
33
33
  import type { ListItemToken, ListToken } from './marked/marked-list.js';
34
34
  import type { Footnote, FootnoteRef, FootnoteToken } from './marked/marked-footnotes.js';
35
+ import type { DescriptionDetailToken, DescriptionListToken, DescriptionTermToken, DescriptionToken } from './marked/marked-dl.js';
35
36
  type TokenSnippet = {
36
37
  heading: Tokens.Heading;
37
38
  paragraph: Tokens.Paragraph;
@@ -62,6 +63,10 @@ type TokenSnippet = {
62
63
  footnotePopover: FootnoteToken;
63
64
  sup: SubSupToken;
64
65
  sub: SubSupToken;
66
+ descriptionList: DescriptionListToken;
67
+ description: DescriptionToken;
68
+ descriptionTerm: DescriptionTermToken;
69
+ descriptionDetail: DescriptionDetailToken;
65
70
  };
66
71
  type PredefinedElements = keyof TokenSnippet;
67
72
  export type Snippets = {
@@ -123,5 +128,11 @@ export type StreamdownProps = {
123
128
  caution?: Snippet;
124
129
  important?: Snippet;
125
130
  };
131
+ extensions?: Extension[];
132
+ children?: Snippet<[{
133
+ streamdown: StreamdownContext;
134
+ token: GenericToken;
135
+ children: Snippet;
136
+ }]>;
126
137
  } & Partial<Snippets>;
127
138
  export {};
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { default as Streamdown } from './Streamdown.svelte';
2
2
  export { useStreamdown, type StreamdownProps } from './context.svelte.js';
3
3
  export { theme, shadcnTheme, mergeTheme, type Theme } from './theme.js';
4
- export { lex, parseBlocks, type StreamdownToken } from './marked/index.js';
4
+ export { type Extension, type StreamdownToken } from './marked/index.js';
5
+ export { lex, parseBlocks } from './marked/index.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { default as Streamdown } from './Streamdown.svelte';
2
2
  export { useStreamdown } from './context.svelte.js';
3
3
  export { theme, shadcnTheme, mergeTheme } from './theme.js';
4
+ export {} from './marked/index.js';
4
5
  export { lex, parseBlocks } from './marked/index.js';
@@ -1,4 +1,4 @@
1
- import { type MarkedToken, type Tokens } from 'marked';
1
+ import { type MarkedToken, type Token, type TokenizerStartFunction, type TokenizerThis, type Tokens, type TokensList } from 'marked';
2
2
  import { type AlertToken } from './marked-alert.js';
3
3
  import { type FootnoteToken } from './marked-footnotes.js';
4
4
  import { type MathToken } from './marked-math.js';
@@ -7,8 +7,21 @@ import { type ListItemToken, type ListToken } from './marked-list.js';
7
7
  import { type BrToken } from './marked-br.js';
8
8
  import { type HrToken } from './marked-hr.js';
9
9
  import { type TableToken, type THead, type TBody, type TFoot, type THeadRow, type TRow, type TH, type TD } from './marked-table.js';
10
- export type StreamdownToken = Exclude<MarkedToken, Tokens.List | Tokens.ListItem> | ListToken | ListItemToken | MathToken | AlertToken | FootnoteToken | SubSupToken | BrToken | HrToken | TableToken | THead | TBody | TFoot | THeadRow | TRow | TH | TD;
10
+ import { type DescriptionDetailToken, type DescriptionListToken, type DescriptionTermToken, type DescriptionToken } from './marked-dl.js';
11
+ export type GenericToken = {
12
+ type: string;
13
+ raw: string;
14
+ tokens?: Token[];
15
+ } & Record<string, any>;
16
+ export type Extension = {
17
+ name: string;
18
+ level: 'block' | 'inline';
19
+ tokenizer: (this: TokenizerThis, src: string, tokens: Token[] | TokensList) => GenericToken | undefined;
20
+ start?: TokenizerStartFunction;
21
+ applyInBlockParsing?: boolean;
22
+ };
23
+ export type StreamdownToken = Exclude<MarkedToken, Tokens.List | Tokens.ListItem> | ListToken | ListItemToken | MathToken | AlertToken | FootnoteToken | SubSupToken | BrToken | HrToken | TableToken | THead | TBody | TFoot | THeadRow | TRow | TH | TD | DescriptionListToken | DescriptionToken | DescriptionDetailToken | DescriptionTermToken;
11
24
  export type { TableToken, THead, TBody, TFoot, THeadRow, TRow, TH, TD } from './marked-table.js';
12
- export declare const lex: (markdown: string) => StreamdownToken[];
13
- export declare const parseBlocks: (markdown: string) => string[];
25
+ export declare const lex: (markdown: string, extensions?: Extension[]) => StreamdownToken[];
26
+ export declare const parseBlocks: (markdown: string, extensions?: Extension[]) => string[];
14
27
  export type { MathToken, AlertToken, FootnoteToken, SubSupToken, BrToken, HrToken };
@@ -2,22 +2,13 @@ import { Lexer } from 'marked';
2
2
  import { markedAlert } from './marked-alert.js';
3
3
  import { markedFootnote } from './marked-footnotes.js';
4
4
  import { markedMath } from './marked-math.js';
5
- import { markedSubSup } from './marked-subsup.js';
5
+ import { markedSub, markedSup } from './marked-subsup.js';
6
6
  import { markedList } from './marked-list.js';
7
7
  import { markedBr } from './marked-br.js';
8
8
  import { markedHr } from './marked-hr.js';
9
9
  import { markedTable } from './marked-table.js';
10
- const extensions = [
11
- markedTable(),
12
- markedFootnote(),
13
- markedAlert(),
14
- markedMath(),
15
- markedSubSup(),
16
- markedList(),
17
- markedBr(),
18
- markedHr()
19
- ];
20
- const parseExtensions = (...ext) => {
10
+ import { markedDl, markedDt } from './marked-dl.js';
11
+ const parseExtensions = (...extensions) => {
21
12
  const options = {
22
13
  gfm: true,
23
14
  extensions: {
@@ -29,35 +20,33 @@ const parseExtensions = (...ext) => {
29
20
  startInline: []
30
21
  }
31
22
  };
32
- ext.forEach(({ extensions }) => {
33
- extensions.forEach(({ level, name, tokenizer, ...rest }) => {
34
- if ('start' in rest && rest.start) {
35
- if (level === 'block') {
36
- options.extensions.startBlock.push(rest.start);
37
- }
38
- else {
39
- options.extensions.startInline.push(rest.start);
40
- }
23
+ extensions.forEach(({ level, name, tokenizer, ...rest }) => {
24
+ if ('start' in rest && rest.start) {
25
+ if (level === 'block') {
26
+ options.extensions.startBlock.push(rest.start);
41
27
  }
42
- if (tokenizer) {
43
- if (level === 'block') {
44
- options.extensions.block.push(tokenizer);
45
- }
46
- else {
47
- options.extensions.inline.push(tokenizer);
48
- }
28
+ else {
29
+ options.extensions.startInline.push(rest.start);
49
30
  }
50
- });
31
+ }
32
+ if (tokenizer) {
33
+ if (level === 'block') {
34
+ options.extensions.block.push(tokenizer);
35
+ }
36
+ else {
37
+ options.extensions.inline.push(tokenizer);
38
+ }
39
+ }
51
40
  });
52
41
  return options;
53
42
  };
54
- const blockLexer = new Lexer(parseExtensions(markedHr(), markedFootnote(), markedTable()));
55
- export const lex = (markdown) => {
56
- return new Lexer(parseExtensions(markedHr(), markedTable(), markedFootnote(), markedAlert(), markedMath(), markedSubSup(), markedList(), markedBr()))
43
+ export const lex = (markdown, extensions = []) => {
44
+ return new Lexer(parseExtensions(markedHr, markedTable, ...markedFootnote(), markedAlert, ...markedMath, markedSub, markedSup, markedList, markedBr, markedDl, markedDt, ...extensions))
57
45
  .lex(markdown)
58
46
  .filter((token) => token.type !== 'space' && token.type !== 'footnote');
59
47
  };
60
- export const parseBlocks = (markdown) => {
48
+ export const parseBlocks = (markdown, extensions = []) => {
49
+ const blockLexer = new Lexer(parseExtensions(markedHr, ...markedFootnote(), markedDl, markedTable, ...extensions.filter(({ level, applyInBlockParsing }) => level === 'block' && applyInBlockParsing)));
61
50
  return blockLexer.blockTokens(markdown, []).reduce((acc, block) => {
62
51
  if (block.type === 'space' || block.type === 'footnote') {
63
52
  return acc;
@@ -1,14 +1,8 @@
1
+ import type { Extension } from './index.js';
1
2
  import type { Tokenizer, Tokens } from 'marked';
2
- import type { TokenizerExtensionFunction } from 'marked';
3
3
  type variantType = 'note' | 'tip' | 'important' | 'warning' | 'caution';
4
4
  export declare function createSyntaxPattern(type: variantType): string;
5
- export declare function markedAlert(): {
6
- extensions: Array<{
7
- name: string;
8
- level: 'block' | 'inline';
9
- tokenizer: TokenizerExtensionFunction;
10
- }>;
11
- };
5
+ export declare const markedAlert: Extension;
12
6
  export declare function processAlertToken(token: Tokens.Blockquote, tokenizer: Tokenizer): void;
13
7
  export type AlertToken = {
14
8
  type: 'alert';
@@ -3,26 +3,20 @@ const variants = ['note', 'tip', 'important', 'warning', 'caution'];
3
3
  export function createSyntaxPattern(type) {
4
4
  return `^\\s*[\\*_]*\\[!${type.toUpperCase()}\\][\\*_]*\\s*`;
5
5
  }
6
- export function markedAlert() {
7
- const defaultLexer = new Lexer({ gfm: true });
8
- const defaultTokenizer = defaultLexer.options.tokenizer;
9
- return {
10
- extensions: [
11
- {
12
- name: 'alert',
13
- level: 'block',
14
- tokenizer(src) {
15
- const cap = defaultTokenizer.rules.block.blockquote.exec(src);
16
- if (cap) {
17
- const blockquoteToken = defaultTokenizer.blockquote(src);
18
- blockquoteToken && processAlertToken(blockquoteToken, this.lexer.options.tokenizer);
19
- return blockquoteToken;
20
- }
21
- }
22
- }
23
- ]
24
- };
25
- }
6
+ const defaultLexer = new Lexer({ gfm: true });
7
+ const defaultTokenizer = defaultLexer.options.tokenizer;
8
+ export const markedAlert = {
9
+ name: 'alert',
10
+ level: 'block',
11
+ tokenizer(src) {
12
+ const cap = defaultTokenizer.rules.block.blockquote.exec(src);
13
+ if (cap) {
14
+ const blockquoteToken = defaultTokenizer.blockquote(src);
15
+ blockquoteToken && processAlertToken(blockquoteToken, this.lexer.options.tokenizer);
16
+ return blockquoteToken;
17
+ }
18
+ }
19
+ };
26
20
  export function processAlertToken(token, tokenizer) {
27
21
  const matchedVariant = variants.find((type) => new RegExp(createSyntaxPattern(type), 'i').test(('text' in token && token.text) || ''));
28
22
  if (!matchedVariant) {
@@ -1,12 +1,6 @@
1
- import type { TokenizerExtensionFunction } from 'marked';
1
+ import type { Extension } from './index.js';
2
2
  export interface BrToken {
3
3
  type: 'br';
4
4
  raw: string;
5
5
  }
6
- export declare function markedBr(): {
7
- extensions: Array<{
8
- name: string;
9
- level: 'inline';
10
- tokenizer: TokenizerExtensionFunction;
11
- }>;
12
- };
6
+ export declare const markedBr: Extension;
@@ -1,21 +1,15 @@
1
- export function markedBr() {
2
- return {
3
- extensions: [
4
- {
5
- name: 'br',
6
- level: 'inline',
7
- tokenizer(src) {
8
- // Match HTML <br> tags (with or without closing slash, case insensitive)
9
- const match = src.match(/^<br\s*\/?>/i);
10
- if (match) {
11
- return {
12
- type: 'br',
13
- raw: match[0]
14
- };
15
- }
16
- return undefined;
17
- }
18
- }
19
- ]
20
- };
21
- }
1
+ export const markedBr = {
2
+ name: 'br',
3
+ level: 'inline',
4
+ tokenizer(src) {
5
+ // Match HTML <br> tags (with or without closing slash, case insensitive)
6
+ const match = src.match(/^<br\s*\/?>/i);
7
+ if (match) {
8
+ return {
9
+ type: 'br',
10
+ raw: match[0]
11
+ };
12
+ }
13
+ return undefined;
14
+ }
15
+ };