@humanspeak/svelte-markdown 1.0.1 โ†’ 1.0.2

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/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2024-2025 Humanspeak, Inc.
1
+ Copyright (c) 2024-2026 Humanspeak, Inc.
2
2
 
3
3
  Copyright (c) 2020-2024 Pablo Berganza
4
4
 
package/README.md CHANGED
@@ -25,7 +25,7 @@ A powerful, customizable markdown renderer for Svelte with TypeScript support. B
25
25
  - โ™ฟ WCAG 2.1 accessibility compliance
26
26
  - ๐ŸŽฏ GitHub-style slug generation for headers
27
27
  - ๐Ÿงช Comprehensive test coverage (vitest and playwright)
28
- - ๐ŸŽจ Custom Marked extensions support (e.g., GitHub-style alerts)
28
+ - ๐Ÿงฉ First-class marked extensions support via `extensions` prop (e.g., KaTeX math, alerts)
29
29
  - โšก Intelligent token caching (50-200x faster re-renders)
30
30
  - ๐Ÿ–ผ๏ธ Smart image lazy loading with fade-in animation
31
31
 
@@ -72,7 +72,8 @@ import type {
72
72
  Renderers,
73
73
  Token,
74
74
  TokensList,
75
- SvelteMarkdownOptions
75
+ SvelteMarkdownOptions,
76
+ MarkedExtension
76
77
  } from '@humanspeak/svelte-markdown'
77
78
  ```
78
79
 
@@ -352,6 +353,111 @@ You can render arbitrary (non-standard) HTML tags like `<click>`, `<tooltip>`, o
352
353
 
353
354
  Both approaches work for any tag name. Snippet overrides take precedence over component renderers when both are provided.
354
355
 
356
+ ## Marked Extensions
357
+
358
+ Use third-party [marked extensions](https://marked.js.org/using_advanced#extensions) via the `extensions` prop. The component handles registering tokenizers internally โ€” you just provide renderers for the custom token types.
359
+
360
+ ### KaTeX Math Rendering
361
+
362
+ ```bash
363
+ npm install marked-katex-extension katex
364
+ ```
365
+
366
+ **Component renderer approach:**
367
+
368
+ ```svelte
369
+ <script lang="ts">
370
+ import SvelteMarkdown from '@humanspeak/svelte-markdown'
371
+ import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
372
+ import markedKatex from 'marked-katex-extension'
373
+ import KatexRenderer from './KatexRenderer.svelte'
374
+
375
+ interface KatexRenderers extends Renderers {
376
+ inlineKatex: RendererComponent
377
+ blockKatex: RendererComponent
378
+ }
379
+
380
+ const renderers: Partial<KatexRenderers> = {
381
+ inlineKatex: KatexRenderer,
382
+ blockKatex: KatexRenderer
383
+ }
384
+ </script>
385
+
386
+ <svelte:head>
387
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.28/dist/katex.min.css" crossorigin="anonymous" />
388
+ </svelte:head>
389
+
390
+ <SvelteMarkdown
391
+ source="Euler's identity: $e^{{i\pi}} + 1 = 0$"
392
+ extensions={[markedKatex({ throwOnError: false })]}
393
+ {renderers}
394
+ />
395
+ ```
396
+
397
+ Where `KatexRenderer.svelte` is:
398
+
399
+ ```svelte
400
+ <script lang="ts">
401
+ import katex from 'katex'
402
+
403
+ interface Props {
404
+ text: string
405
+ displayMode?: boolean
406
+ }
407
+ const { text, displayMode = false }: Props = $props()
408
+
409
+ const html = $derived(katex.renderToString(text, { throwOnError: false, displayMode }))
410
+ </script>
411
+
412
+ {@html html}
413
+ ```
414
+
415
+ **Snippet override approach** (no separate component file needed):
416
+
417
+ ```svelte
418
+ <script lang="ts">
419
+ import SvelteMarkdown from '@humanspeak/svelte-markdown'
420
+ import katex from 'katex'
421
+ import markedKatex from 'marked-katex-extension'
422
+ </script>
423
+
424
+ <svelte:head>
425
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.28/dist/katex.min.css" crossorigin="anonymous" />
426
+ </svelte:head>
427
+
428
+ <SvelteMarkdown
429
+ source="Euler's identity: $e^{{i\pi}} + 1 = 0$"
430
+ extensions={[markedKatex({ throwOnError: false })]}
431
+ >
432
+ {#snippet inlineKatex(props)}
433
+ {@html katex.renderToString(props.text, { displayMode: false })}
434
+ {/snippet}
435
+ {#snippet blockKatex(props)}
436
+ {@html katex.renderToString(props.text, { displayMode: true })}
437
+ {/snippet}
438
+ </SvelteMarkdown>
439
+ ```
440
+
441
+ ### How It Works
442
+
443
+ Marked extensions define custom token types with a `name` property (e.g., `inlineKatex`, `blockKatex`, `alert`). When you pass extensions via the `extensions` prop, SvelteMarkdown automatically extracts these token type names and makes them available as both **component renderer keys** and **snippet override names**.
444
+
445
+ To find the token type names for any extension, check its source or documentation for the `name` field in its `extensions` array:
446
+
447
+ ```js
448
+ // Example: marked-katex-extension registers tokens named "inlineKatex" and "blockKatex"
449
+ // โ†’ use renderers={{ inlineKatex: ..., blockKatex: ... }}
450
+ // โ†’ or {#snippet inlineKatex(props)} and {#snippet blockKatex(props)}
451
+
452
+ // Example: a custom alert extension registers a token named "alert"
453
+ // โ†’ use renderers={{ alert: AlertComponent }}
454
+ // โ†’ or {#snippet alert(props)}
455
+ ```
456
+
457
+ Each snippet/component receives the token's properties as props (e.g., `text`, `displayMode` for KaTeX; `text`, `level` for alerts).
458
+
459
+ See the [full documentation](https://markdown.svelte.page/docs/advanced/marked-extensions) and [interactive demo](https://markdown.svelte.page/examples/marked-extensions).
460
+
355
461
  ### TypeScript
356
462
 
357
463
  All snippet prop types are exported for use in external components:
@@ -526,12 +632,13 @@ The component emits a `parsed` event when tokens are calculated:
526
632
 
527
633
  ## Props
528
634
 
529
- | Prop | Type | Description |
530
- | --------- | ----------------------- | ------------------------------------- |
531
- | source | `string \| Token[]` | Markdown content or pre-parsed tokens |
532
- | renderers | `Partial<Renderers>` | Custom component overrides |
533
- | options | `SvelteMarkdownOptions` | Marked parser configuration |
534
- | isInline | `boolean` | Toggle inline parsing mode |
635
+ | Prop | Type | Description |
636
+ | ---------- | ----------------------- | ------------------------------------------------ |
637
+ | source | `string \| Token[]` | Markdown content or pre-parsed tokens |
638
+ | renderers | `Partial<Renderers>` | Custom component overrides |
639
+ | options | `SvelteMarkdownOptions` | Marked parser configuration |
640
+ | isInline | `boolean` | Toggle inline parsing mode |
641
+ | extensions | `MarkedExtension[]` | Third-party marked extensions (e.g., KaTeX math) |
535
642
 
536
643
  ## Security
537
644
 
@@ -95,7 +95,7 @@
95
95
  />
96
96
  {/each}
97
97
  {/if}
98
- {:else if type in renderers}
98
+ {:else if type in renderers || type in snippetOverrides}
99
99
  {#if type === 'table'}
100
100
  {#if renderers.table && renderers.tablerow && renderers.tablecell}
101
101
  {@const tableSnippet = snippetOverrides[type]}
@@ -60,6 +60,7 @@
60
60
  } from './utils/markdown-parser.js'
61
61
  import { parseAndCacheTokens } from './utils/parse-and-cache.js'
62
62
  import { rendererKeysInternal } from './utils/rendererKeys.js'
63
+ import { Marked } from 'marked'
63
64
 
64
65
  const {
65
66
  source = [],
@@ -67,12 +68,23 @@
67
68
  options = {},
68
69
  isInline = false,
69
70
  parsed = () => {},
71
+ extensions = [],
70
72
  ...rest
71
73
  }: SvelteMarkdownProps & {
72
74
  [key: string]: unknown
73
75
  } = $props()
74
76
 
75
- const combinedOptions = $derived({ ...defaultOptions, ...options })
77
+ // Extract custom token type names from the extensions array
78
+ const extensionTokenNames = $derived(
79
+ extensions.flatMap((ext) => ext.extensions?.map((e) => e.name) ?? [])
80
+ )
81
+
82
+ // Create a scoped Marked instance and extract its resolved defaults
83
+ const extensionDefaults = $derived(
84
+ extensions.length > 0 ? new Marked(...extensions).defaults : {}
85
+ )
86
+
87
+ const combinedOptions = $derived({ ...defaultOptions, ...extensionDefaults, ...options })
76
88
  const slugger = new Slugger()
77
89
 
78
90
  const tokens = $derived.by(() => {
@@ -106,10 +118,13 @@
106
118
  : defaultRenderers.html
107
119
  })
108
120
 
109
- // Collect markdown snippet overrides (keys matching renderer names)
121
+ // All renderer keys: built-in + extension token names
122
+ const allRendererKeys = $derived([...rendererKeysInternal, ...extensionTokenNames])
123
+
124
+ // Collect markdown snippet overrides (keys matching renderer names or extension token names)
110
125
  const snippetOverrides = $derived(
111
126
  Object.fromEntries(
112
- rendererKeysInternal
127
+ allRendererKeys
113
128
  .filter((key) => key in rest && rest[key] != null)
114
129
  .map((key) => [key, rest[key]])
115
130
  )
@@ -126,10 +141,7 @@
126
141
 
127
142
  // Passthrough: everything that isn't a known snippet override
128
143
  const snippetKeySet = $derived(
129
- new Set([
130
- ...rendererKeysInternal,
131
- ...Object.keys(rest).filter((k) => k.startsWith('html_'))
132
- ])
144
+ new Set([...allRendererKeys, ...Object.keys(rest).filter((k) => k.startsWith('html_'))])
133
145
  )
134
146
  const passThroughProps = $derived(
135
147
  Object.fromEntries(Object.entries(rest).filter(([key]) => !snippetKeySet.has(key)))
package/dist/index.d.ts CHANGED
@@ -59,5 +59,7 @@ export { htmlRendererKeysInternal as htmlRendererKeys, rendererKeysInternal as r
59
59
  */
60
60
  export { MemoryCache } from './utils/cache.js';
61
61
  export { TokenCache, tokenCache } from './utils/token-cache.js';
62
+ /** Re-exported `MarkedExtension` type for the `extensions` prop. */
63
+ export type { MarkedExtension } from 'marked';
62
64
  /** Re-exported types for consumer convenience. */
63
65
  export type { BlockquoteSnippetProps, BrSnippetProps, CodeSnippetProps, CodespanSnippetProps, DelSnippetProps, EmSnippetProps, EscapeSnippetProps, HeadingSnippetProps, HrSnippetProps, HtmlRenderers, HtmlSnippetOverrides, HtmlSnippetProps, ImageSnippetProps, LinkSnippetProps, ListItemSnippetProps, ListSnippetProps, ParagraphSnippetProps, RawTextSnippetProps, RendererComponent, Renderers, SnippetOverrides, StrongSnippetProps, SvelteMarkdownOptions, SvelteMarkdownProps, TableBodySnippetProps, TableCellSnippetProps, TableHeadSnippetProps, TableRowSnippetProps, TableSnippetProps, TextSnippetProps, Token, TokensList };
package/dist/types.d.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * @packageDocumentation
19
19
  */
20
- import type { Token, TokensList } from 'marked';
20
+ import type { MarkedExtension, Token, TokensList } from 'marked';
21
21
  import type { Snippet } from 'svelte';
22
22
  import type { MarkedOptions, Renderers } from './utils/markdown-parser.js';
23
23
  import type { HtmlKey } from './utils/rendererKeys.js';
@@ -152,6 +152,29 @@ export type SvelteMarkdownProps<T extends Renderers = Renderers> = {
152
152
  * generation settings. Merged with {@link defaultOptions}.
153
153
  */
154
154
  options?: Partial<SvelteMarkdownOptions>;
155
+ /**
156
+ * Array of marked extensions to apply when parsing.
157
+ *
158
+ * Internally creates a scoped `Marked` instance, extracts its resolved
159
+ * defaults, and merges them into the parser options so the lexer
160
+ * recognises any custom token types the extensions define.
161
+ *
162
+ * Extension token names are also used to collect snippet overrides,
163
+ * enabling both component-renderer and snippet-based rendering of
164
+ * custom tokens.
165
+ *
166
+ * @defaultValue `[]`
167
+ *
168
+ * @example
169
+ * ```svelte
170
+ * <SvelteMarkdown
171
+ * source={markdown}
172
+ * extensions={[markedKatex({ throwOnError: false })]}
173
+ * renderers={{ inlineKatex: KatexRenderer, blockKatex: KatexRenderer }}
174
+ * />
175
+ * ```
176
+ */
177
+ extensions?: MarkedExtension[];
155
178
  /**
156
179
  * When `true`, the source is parsed with `Lexer.lexInline()` instead of
157
180
  * `Lexer.lex()`, producing only inline tokens (no block elements).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-markdown",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Fast, customizable markdown renderer for Svelte with built-in caching, TypeScript support, and Svelte 5 runes",
5
5
  "keywords": [
6
6
  "svelte",
@@ -80,6 +80,7 @@
80
80
  "@testing-library/jest-dom": "^6.9.1",
81
81
  "@testing-library/svelte": "^5.3.1",
82
82
  "@testing-library/user-event": "^14.6.1",
83
+ "@types/katex": "^0.16.8",
83
84
  "@types/node": "^25.2.3",
84
85
  "@typescript-eslint/eslint-plugin": "^8.56.0",
85
86
  "@typescript-eslint/parser": "^8.56.0",
@@ -93,6 +94,8 @@
93
94
  "globals": "^17.3.0",
94
95
  "husky": "^9.1.7",
95
96
  "jsdom": "^28.1.0",
97
+ "katex": "^0.16.28",
98
+ "marked-katex-extension": "^5.1.7",
96
99
  "prettier": "^3.8.1",
97
100
  "prettier-plugin-organize-imports": "^4.3.0",
98
101
  "prettier-plugin-svelte": "^3.5.0",
@@ -123,7 +126,7 @@
123
126
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
124
127
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
125
128
  "dev": "vite dev",
126
- "dev:all": "concurrently -k -n pkg,docs -c green,cyan \"pnpm -w -r --filter @humanspeak/svelte-motion run dev:pkg\" \"pnpm --filter docs run dev\"",
129
+ "dev:all": "concurrently -k -n pkg,docs -c green,cyan \"pnpm -w -r --filter @humanspeak/svelte-markdown run dev:pkg\" \"pnpm --filter docs run dev\"",
127
130
  "dev:pkg": "svelte-kit sync && svelte-package --watch",
128
131
  "format": "prettier --write .",
129
132
  "lint": "prettier --check . && eslint .",