@humanspeak/svelte-markdown 1.0.3 → 1.0.5

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.
Files changed (34) hide show
  1. package/README.md +82 -0
  2. package/dist/Parser.svelte +11 -6
  3. package/dist/Parser.svelte.d.ts +3 -2
  4. package/dist/SvelteMarkdown.svelte +6 -3
  5. package/dist/extensions/alert/AlertRenderer.svelte +23 -0
  6. package/dist/extensions/alert/AlertRenderer.svelte.d.ts +8 -0
  7. package/dist/extensions/alert/index.d.ts +3 -0
  8. package/dist/extensions/alert/index.js +2 -0
  9. package/dist/extensions/alert/markedAlert.d.ts +37 -0
  10. package/dist/extensions/alert/markedAlert.js +62 -0
  11. package/dist/extensions/footnote/FootnoteRef.svelte +9 -0
  12. package/dist/extensions/footnote/FootnoteRef.svelte.d.ts +6 -0
  13. package/dist/extensions/footnote/FootnoteSection.svelte +25 -0
  14. package/dist/extensions/footnote/FootnoteSection.svelte.d.ts +10 -0
  15. package/dist/extensions/footnote/index.d.ts +3 -0
  16. package/dist/extensions/footnote/index.js +3 -0
  17. package/dist/extensions/footnote/markedFootnote.d.ts +31 -0
  18. package/dist/extensions/footnote/markedFootnote.js +80 -0
  19. package/dist/extensions/index.d.ts +5 -0
  20. package/dist/extensions/index.js +4 -0
  21. package/dist/renderers/Br.svelte +1 -9
  22. package/dist/renderers/Br.svelte.d.ts +26 -7
  23. package/dist/renderers/TableCell.svelte +2 -2
  24. package/dist/renderers/TableCell.svelte.d.ts +2 -2
  25. package/dist/renderers/html/Sup.svelte +1 -1
  26. package/dist/renderers/html/Sup.svelte.d.ts +1 -1
  27. package/dist/types.d.ts +10 -2
  28. package/dist/utils/parse-and-cache.d.ts +2 -2
  29. package/dist/utils/parse-and-cache.js +14 -7
  30. package/dist/utils/token-cache.d.ts +1 -1
  31. package/dist/utils/token-cache.js +4 -4
  32. package/dist/utils/token-cleanup.d.ts +0 -45
  33. package/dist/utils/token-cleanup.js +26 -18
  34. package/package.json +25 -24
package/README.md CHANGED
@@ -485,6 +485,88 @@ You can also use snippet overrides to wrap `MermaidRenderer` with custom markup:
485
485
 
486
486
  Since Mermaid rendering is async, the snippet delegates to `MermaidRenderer` rather than calling `mermaid.render()` directly. This pattern works for any async extension — keep the async logic in a component and use the snippet for layout customization.
487
487
 
488
+ ### GitHub Alerts
489
+
490
+ Built-in support for [GitHub-style alerts/admonitions](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts). Five alert types are supported: `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, and `CAUTION`.
491
+
492
+ ```svelte
493
+ <script lang="ts">
494
+ import SvelteMarkdown from '@humanspeak/svelte-markdown'
495
+ import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
496
+ import { markedAlert, AlertRenderer } from '@humanspeak/svelte-markdown/extensions'
497
+
498
+ const source = `
499
+ > [!NOTE]
500
+ > Useful information that users should know.
501
+
502
+ > [!WARNING]
503
+ > Urgent info that needs immediate attention.
504
+ `
505
+
506
+ interface AlertRenderers extends Renderers {
507
+ alert: RendererComponent
508
+ }
509
+
510
+ const renderers: Partial<AlertRenderers> = {
511
+ alert: AlertRenderer
512
+ }
513
+ </script>
514
+
515
+ <SvelteMarkdown {source} extensions={[markedAlert()]} {renderers} />
516
+ ```
517
+
518
+ `AlertRenderer` renders a `<div class="markdown-alert markdown-alert-{type}">` with a title — no inline styles, so you can theme it with your own CSS. You can also use snippet overrides:
519
+
520
+ ```svelte
521
+ <SvelteMarkdown source={markdown} extensions={[markedAlert()]}>
522
+ {#snippet alert(props)}
523
+ <div class="my-alert my-alert-{props.alertType}">
524
+ <strong>{props.alertType}</strong>
525
+ <p>{props.text}</p>
526
+ </div>
527
+ {/snippet}
528
+ </SvelteMarkdown>
529
+ ```
530
+
531
+ ### Footnotes
532
+
533
+ Built-in support for footnote references and definitions. Footnote references (`[^id]`) render as superscript links, and definitions (`[^id]: content`) render as a numbered list at the end of the document with back-links.
534
+
535
+ ```svelte
536
+ <script lang="ts">
537
+ import SvelteMarkdown from '@humanspeak/svelte-markdown'
538
+ import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
539
+ import {
540
+ markedFootnote,
541
+ FootnoteRef,
542
+ FootnoteSection
543
+ } from '@humanspeak/svelte-markdown/extensions'
544
+
545
+ const source = `
546
+ Here is a statement[^1] with a footnote.
547
+
548
+ Another claim[^note] that needs a source.
549
+
550
+ [^1]: This is the first footnote.
551
+ [^note]: This is a named footnote.
552
+ `
553
+
554
+ interface FootnoteRenderers extends Renderers {
555
+ footnoteRef: RendererComponent
556
+ footnoteSection: RendererComponent
557
+ }
558
+
559
+ const renderers: Partial<FootnoteRenderers> = {
560
+ footnoteRef: FootnoteRef,
561
+ footnoteSection: FootnoteSection
562
+ }
563
+ </script>
564
+
565
+ <SvelteMarkdown {source} extensions={[markedFootnote()]} {renderers} />
566
+ ```
567
+
568
+ `FootnoteRef` renders `<sup><a href="#fn-{id}">{id}</a></sup>` and `FootnoteSection` renders an `<ol>` with bidirectional links (ref to definition and back). You can also use snippet overrides for custom rendering.
569
+
488
570
  ### How It Works
489
571
 
490
572
  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**.
@@ -56,6 +56,9 @@
56
56
  RendererComponent
57
57
  } from './utils/markdown-parser.js'
58
58
 
59
+ // trunk-ignore(eslint/@typescript-eslint/no-explicit-any)
60
+ type AnySnippet = (..._args: any[]) => any
61
+
59
62
  interface Props<T extends Renderers = Renderers> {
60
63
  type?: string
61
64
  tokens?: Token[] | TokensList
@@ -63,8 +66,8 @@
63
66
  rows?: Tokens.TableCell[][]
64
67
  ordered?: boolean
65
68
  renderers: T
66
- snippetOverrides?: Record<string, any> // trunk-ignore(eslint/@typescript-eslint/no-explicit-any)
67
- htmlSnippetOverrides?: Record<string, any> // trunk-ignore(eslint/@typescript-eslint/no-explicit-any)
69
+ snippetOverrides?: Record<string, AnySnippet>
70
+ htmlSnippetOverrides?: Record<string, AnySnippet>
68
71
  }
69
72
 
70
73
  const {
@@ -121,14 +124,14 @@
121
124
  {#if cellSnippet}
122
125
  {@render cellSnippet({
123
126
  header: true,
124
- align: (rest.align as string[])[i],
127
+ align: (rest.align as string[] | undefined)?.[i] ?? null,
125
128
  ...cellRest,
126
129
  children: headerCellContent
127
130
  })}
128
131
  {:else}
129
132
  <renderers.tablecell
130
133
  header={true}
131
- align={(rest.align as string[])[i]}
134
+ align={(rest.align as string[] | undefined)?.[i] ?? null}
132
135
  {...cellRest}
133
136
  >
134
137
  {@render headerCellContent()}
@@ -172,7 +175,8 @@
172
175
  {#if cellSnippet}
173
176
  {@render cellSnippet({
174
177
  header: false,
175
- align: (rest.align as string[])[j],
178
+ align:
179
+ (rest.align as string[] | undefined)?.[j] ?? null,
176
180
  ...cellRest,
177
181
  children: bodyCellContent
178
182
  })}
@@ -180,7 +184,8 @@
180
184
  <renderers.tablecell
181
185
  {...cellRest}
182
186
  header={false}
183
- align={(rest.align as string[])[j]}
187
+ align={(rest.align as string[] | undefined)?.[j] ??
188
+ null}
184
189
  >
185
190
  {@render bodyCellContent()}
186
191
  </renderers.tablecell>
@@ -46,6 +46,7 @@
46
46
  */
47
47
  import Parser from './Parser.svelte';
48
48
  import type { Renderers, Token, TokensList, Tokens } from './utils/markdown-parser.js';
49
+ type AnySnippet = (..._args: any[]) => any;
49
50
  interface Props<T extends Renderers = Renderers> {
50
51
  type?: string;
51
52
  tokens?: Token[] | TokensList;
@@ -53,8 +54,8 @@ interface Props<T extends Renderers = Renderers> {
53
54
  rows?: Tokens.TableCell[][];
54
55
  ordered?: boolean;
55
56
  renderers: T;
56
- snippetOverrides?: Record<string, any>;
57
- htmlSnippetOverrides?: Record<string, any>;
57
+ snippetOverrides?: Record<string, AnySnippet>;
58
+ htmlSnippetOverrides?: Record<string, AnySnippet>;
58
59
  }
59
60
  type $$ComponentProps = Props & {
60
61
  [key: string]: unknown;
@@ -62,6 +62,9 @@
62
62
  import { rendererKeysInternal } from './utils/rendererKeys.js'
63
63
  import { Marked } from 'marked'
64
64
 
65
+ // trunk-ignore(eslint/@typescript-eslint/no-explicit-any)
66
+ type AnySnippet = (..._args: any[]) => any
67
+
65
68
  const {
66
69
  source = [],
67
70
  renderers = {},
@@ -174,7 +177,7 @@
174
177
  allRendererKeys
175
178
  .filter((key) => key in rest && rest[key] != null)
176
179
  .map((key) => [key, rest[key]])
177
- )
180
+ ) as Record<string, AnySnippet>
178
181
  )
179
182
 
180
183
  // Collect HTML snippet overrides (keys matching html_<tag>)
@@ -183,7 +186,7 @@
183
186
  Object.entries(rest)
184
187
  .filter(([key, val]) => key.startsWith('html_') && val != null)
185
188
  .map(([key, val]) => [key.slice(5), val])
186
- )
189
+ ) as Record<string, AnySnippet>
187
190
  )
188
191
 
189
192
  // Passthrough: everything that isn't a known snippet override
@@ -199,7 +202,7 @@
199
202
  {tokens}
200
203
  {...passThroughProps}
201
204
  options={combinedOptions}
202
- slug={(val: string): string => (slugger ? slugger.slug(val) : '')}
205
+ slug={(val: string): string => slugger.slug(val)}
203
206
  renderers={combinedRenderers}
204
207
  {snippetOverrides}
205
208
  {htmlSnippetOverrides}
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import type { AlertType } from './markedAlert.js'
3
+
4
+ interface Props {
5
+ text: string
6
+ alertType: AlertType
7
+ }
8
+
9
+ const { text, alertType }: Props = $props()
10
+
11
+ const titles: Record<AlertType, string> = {
12
+ note: 'Note',
13
+ tip: 'Tip',
14
+ important: 'Important',
15
+ warning: 'Warning',
16
+ caution: 'Caution'
17
+ }
18
+ </script>
19
+
20
+ <div class="markdown-alert markdown-alert-{alertType}" role="note">
21
+ <p class="markdown-alert-title">{titles[alertType]}</p>
22
+ <p>{text}</p>
23
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { AlertType } from './markedAlert.js';
2
+ interface Props {
3
+ text: string;
4
+ alertType: AlertType;
5
+ }
6
+ declare const AlertRenderer: import("svelte").Component<Props, {}, "">;
7
+ type AlertRenderer = ReturnType<typeof AlertRenderer>;
8
+ export default AlertRenderer;
@@ -0,0 +1,3 @@
1
+ export { default as AlertRenderer } from './AlertRenderer.svelte';
2
+ export { markedAlert } from './markedAlert.js';
3
+ export type { AlertType } from './markedAlert.js';
@@ -0,0 +1,2 @@
1
+ export { default as AlertRenderer } from './AlertRenderer.svelte';
2
+ export { markedAlert } from './markedAlert.js';
@@ -0,0 +1,37 @@
1
+ import type { MarkedExtension } from 'marked';
2
+ /**
3
+ * Valid GitHub-style alert types.
4
+ *
5
+ * @see https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
6
+ */
7
+ export type AlertType = 'note' | 'tip' | 'important' | 'warning' | 'caution';
8
+ /**
9
+ * Creates a marked extension that tokenizes GitHub-style alert blockquotes
10
+ * into custom `alert` tokens.
11
+ *
12
+ * The extension produces block-level tokens with
13
+ * `{ type: 'alert', raw, text, alertType }` where `alertType` is one of
14
+ * `note`, `tip`, `important`, `warning`, or `caution`, and `text` is the
15
+ * alert body with leading `> ` stripped.
16
+ *
17
+ * Pair it with `AlertRenderer` (or your own component) to render the alerts.
18
+ *
19
+ * @example
20
+ * ```svelte
21
+ * <script lang="ts">
22
+ * import SvelteMarkdown from '@humanspeak/svelte-markdown'
23
+ * import { markedAlert, AlertRenderer } from '@humanspeak/svelte-markdown/extensions'
24
+ *
25
+ * const renderers = { alert: AlertRenderer }
26
+ * </script>
27
+ *
28
+ * <SvelteMarkdown
29
+ * source={markdown}
30
+ * extensions={[markedAlert()]}
31
+ * {renderers}
32
+ * />
33
+ * ```
34
+ *
35
+ * @returns A `MarkedExtension` with a single block-level `alert` tokenizer
36
+ */
37
+ export declare function markedAlert(): MarkedExtension;
@@ -0,0 +1,62 @@
1
+ const ALERT_TYPES = new Set(['note', 'tip', 'important', 'warning', 'caution']);
2
+ /**
3
+ * Creates a marked extension that tokenizes GitHub-style alert blockquotes
4
+ * into custom `alert` tokens.
5
+ *
6
+ * The extension produces block-level tokens with
7
+ * `{ type: 'alert', raw, text, alertType }` where `alertType` is one of
8
+ * `note`, `tip`, `important`, `warning`, or `caution`, and `text` is the
9
+ * alert body with leading `> ` stripped.
10
+ *
11
+ * Pair it with `AlertRenderer` (or your own component) to render the alerts.
12
+ *
13
+ * @example
14
+ * ```svelte
15
+ * <script lang="ts">
16
+ * import SvelteMarkdown from '@humanspeak/svelte-markdown'
17
+ * import { markedAlert, AlertRenderer } from '@humanspeak/svelte-markdown/extensions'
18
+ *
19
+ * const renderers = { alert: AlertRenderer }
20
+ * </script>
21
+ *
22
+ * <SvelteMarkdown
23
+ * source={markdown}
24
+ * extensions={[markedAlert()]}
25
+ * {renderers}
26
+ * />
27
+ * ```
28
+ *
29
+ * @returns A `MarkedExtension` with a single block-level `alert` tokenizer
30
+ */
31
+ export function markedAlert() {
32
+ return {
33
+ extensions: [
34
+ {
35
+ name: 'alert',
36
+ level: 'block',
37
+ start(src) {
38
+ return src.match(/>\s*\[!/)?.index;
39
+ },
40
+ tokenizer(src) {
41
+ const match = src.match(/^(?:>\s*\[!(\w+)\]\n)((?:[^\n]*(?:\n(?:>\s?)[^\n]*)*)?)(?:\n|$)/);
42
+ if (match) {
43
+ const alertType = match[1].toLowerCase();
44
+ if (!ALERT_TYPES.has(alertType))
45
+ return;
46
+ const text = match[2]
47
+ .split('\n')
48
+ .map((line) => line.replace(/^>\s?/, ''))
49
+ .join('\n')
50
+ .trim();
51
+ return {
52
+ type: 'alert',
53
+ raw: match[0],
54
+ text,
55
+ alertType: alertType
56
+ };
57
+ }
58
+ }
59
+ }
60
+ ]
61
+ };
62
+ }
@@ -0,0 +1,9 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ id: string
4
+ }
5
+
6
+ const { id }: Props = $props()
7
+ </script>
8
+
9
+ <sup class="footnote-ref"><a href="#fn-{id}" id="fnref-{id}">{id}</a></sup>
@@ -0,0 +1,6 @@
1
+ interface Props {
2
+ id: string;
3
+ }
4
+ declare const FootnoteRef: import("svelte").Component<Props, {}, "">;
5
+ type FootnoteRef = ReturnType<typeof FootnoteRef>;
6
+ export default FootnoteRef;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ interface Footnote {
3
+ id: string
4
+ text: string
5
+ }
6
+
7
+ interface Props {
8
+ footnotes: Footnote[]
9
+ }
10
+
11
+ const { footnotes }: Props = $props()
12
+ </script>
13
+
14
+ <section class="footnotes" role="doc-endnotes">
15
+ <ol>
16
+ {#each footnotes as { id, text } (id)}
17
+ <li id="fn-{id}">
18
+ <p>
19
+ {text}
20
+ <a href="#fnref-{id}" class="footnote-backref" role="doc-backlink">&#8617;</a>
21
+ </p>
22
+ </li>
23
+ {/each}
24
+ </ol>
25
+ </section>
@@ -0,0 +1,10 @@
1
+ interface Footnote {
2
+ id: string;
3
+ text: string;
4
+ }
5
+ interface Props {
6
+ footnotes: Footnote[];
7
+ }
8
+ declare const FootnoteSection: import("svelte").Component<Props, {}, "">;
9
+ type FootnoteSection = ReturnType<typeof FootnoteSection>;
10
+ export default FootnoteSection;
@@ -0,0 +1,3 @@
1
+ export { default as FootnoteRef } from './FootnoteRef.svelte';
2
+ export { default as FootnoteSection } from './FootnoteSection.svelte';
3
+ export { markedFootnote } from './markedFootnote.js';
@@ -0,0 +1,3 @@
1
+ export { default as FootnoteRef } from './FootnoteRef.svelte';
2
+ export { default as FootnoteSection } from './FootnoteSection.svelte';
3
+ export { markedFootnote } from './markedFootnote.js';
@@ -0,0 +1,31 @@
1
+ import type { MarkedExtension } from 'marked';
2
+ /**
3
+ * Creates a marked extension that tokenizes footnote references (`[^id]`) and
4
+ * footnote definitions (`[^id]: content`) into custom tokens.
5
+ *
6
+ * The extension produces:
7
+ * - **Inline** `footnoteRef` tokens: `{ type: 'footnoteRef', raw, id }`
8
+ * - **Block** `footnoteSection` tokens: `{ type: 'footnoteSection', raw, footnotes }` where
9
+ * `footnotes` is an array of `{ id, text }` objects
10
+ *
11
+ * Pair with `FootnoteRef` and `FootnoteSection` components (or your own) for rendering.
12
+ *
13
+ * @example
14
+ * ```svelte
15
+ * <script lang="ts">
16
+ * import SvelteMarkdown from '@humanspeak/svelte-markdown'
17
+ * import { markedFootnote, FootnoteRef, FootnoteSection } from '@humanspeak/svelte-markdown/extensions'
18
+ *
19
+ * const renderers = { footnoteRef: FootnoteRef, footnoteSection: FootnoteSection }
20
+ * </script>
21
+ *
22
+ * <SvelteMarkdown
23
+ * source={markdown}
24
+ * extensions={[markedFootnote()]}
25
+ * {renderers}
26
+ * />
27
+ * ```
28
+ *
29
+ * @returns A `MarkedExtension` with inline `footnoteRef` and block `footnoteSection` tokenizers
30
+ */
31
+ export declare function markedFootnote(): MarkedExtension;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Creates a marked extension that tokenizes footnote references (`[^id]`) and
3
+ * footnote definitions (`[^id]: content`) into custom tokens.
4
+ *
5
+ * The extension produces:
6
+ * - **Inline** `footnoteRef` tokens: `{ type: 'footnoteRef', raw, id }`
7
+ * - **Block** `footnoteSection` tokens: `{ type: 'footnoteSection', raw, footnotes }` where
8
+ * `footnotes` is an array of `{ id, text }` objects
9
+ *
10
+ * Pair with `FootnoteRef` and `FootnoteSection` components (or your own) for rendering.
11
+ *
12
+ * @example
13
+ * ```svelte
14
+ * <script lang="ts">
15
+ * import SvelteMarkdown from '@humanspeak/svelte-markdown'
16
+ * import { markedFootnote, FootnoteRef, FootnoteSection } from '@humanspeak/svelte-markdown/extensions'
17
+ *
18
+ * const renderers = { footnoteRef: FootnoteRef, footnoteSection: FootnoteSection }
19
+ * </script>
20
+ *
21
+ * <SvelteMarkdown
22
+ * source={markdown}
23
+ * extensions={[markedFootnote()]}
24
+ * {renderers}
25
+ * />
26
+ * ```
27
+ *
28
+ * @returns A `MarkedExtension` with inline `footnoteRef` and block `footnoteSection` tokenizers
29
+ */
30
+ export function markedFootnote() {
31
+ return {
32
+ extensions: [
33
+ {
34
+ name: 'footnoteRef',
35
+ level: 'inline',
36
+ start(src) {
37
+ return src.match(/\[\^/)?.index;
38
+ },
39
+ tokenizer(src) {
40
+ const match = src.match(/^\[\^([^\]\s]+)\](?!:)/);
41
+ if (match) {
42
+ return {
43
+ type: 'footnoteRef',
44
+ raw: match[0],
45
+ id: match[1]
46
+ };
47
+ }
48
+ }
49
+ },
50
+ {
51
+ name: 'footnoteSection',
52
+ level: 'block',
53
+ start(src) {
54
+ return src.match(/\[\^[^\]\s]+\]:/)?.index;
55
+ },
56
+ tokenizer(src) {
57
+ const match = src.match(/^(?:\[\^([^\]\s]+)\]:\s*([^\n]*(?:\n(?!\[\^)[^\n]*)*)(?:\n|$))+/);
58
+ if (match) {
59
+ const footnotes = [];
60
+ const defRegex = /\[\^([^\]\s]+)\]:\s*([^\n]*(?:\n(?!\[\^|\n)[^\n]*)*)/g;
61
+ let defMatch;
62
+ while ((defMatch = defRegex.exec(match[0])) !== null) {
63
+ footnotes.push({
64
+ id: defMatch[1],
65
+ text: defMatch[2].trim()
66
+ });
67
+ }
68
+ if (footnotes.length > 0) {
69
+ return {
70
+ type: 'footnoteSection',
71
+ raw: match[0],
72
+ footnotes
73
+ };
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ]
79
+ };
80
+ }
@@ -5,8 +5,13 @@
5
5
  *
6
6
  * ```ts
7
7
  * import { markedMermaid, MermaidRenderer } from '@humanspeak/svelte-markdown/extensions'
8
+ * import { markedAlert, AlertRenderer } from '@humanspeak/svelte-markdown/extensions'
9
+ * import { markedFootnote, FootnoteRef, FootnoteSection } from '@humanspeak/svelte-markdown/extensions'
8
10
  * ```
9
11
  *
10
12
  * @module @humanspeak/svelte-markdown/extensions
11
13
  */
14
+ export { AlertRenderer, markedAlert } from './alert/index.js';
15
+ export type { AlertType } from './alert/index.js';
16
+ export { FootnoteRef, FootnoteSection, markedFootnote } from './footnote/index.js';
12
17
  export { MermaidRenderer, markedMermaid } from './mermaid/index.js';
@@ -5,8 +5,12 @@
5
5
  *
6
6
  * ```ts
7
7
  * import { markedMermaid, MermaidRenderer } from '@humanspeak/svelte-markdown/extensions'
8
+ * import { markedAlert, AlertRenderer } from '@humanspeak/svelte-markdown/extensions'
9
+ * import { markedFootnote, FootnoteRef, FootnoteSection } from '@humanspeak/svelte-markdown/extensions'
8
10
  * ```
9
11
  *
10
12
  * @module @humanspeak/svelte-markdown/extensions
11
13
  */
14
+ export { AlertRenderer, markedAlert } from './alert/index.js';
15
+ export { FootnoteRef, FootnoteSection, markedFootnote } from './footnote/index.js';
12
16
  export { MermaidRenderer, markedMermaid } from './mermaid/index.js';
@@ -2,13 +2,5 @@
2
2
  @component
3
3
  Renders a markdown line break as a `<br />` element.
4
4
  -->
5
- <script lang="ts">
6
- import type { Snippet } from 'svelte'
7
5
 
8
- interface Props {
9
- children?: Snippet
10
- }
11
- const { children }: Props = $props()
12
- </script>
13
-
14
- <br />{@render children?.()}
6
+ <br />
@@ -1,8 +1,27 @@
1
- import type { Snippet } from 'svelte';
2
- interface Props {
3
- children?: Snippet;
4
- }
5
- /** Renders a markdown line break as a `<br />` element. */
6
- declare const Br: import("svelte").Component<Props, {}, "">;
7
- type Br = ReturnType<typeof Br>;
8
1
  export default Br;
2
+ type Br = SvelteComponent<{
3
+ [x: string]: never;
4
+ }, {
5
+ [evt: string]: CustomEvent<any>;
6
+ }, {}> & {
7
+ $$bindings?: string | undefined;
8
+ };
9
+ /** Renders a markdown line break as a `<br />` element. */
10
+ declare const Br: $$__sveltets_2_IsomorphicComponent<{
11
+ [x: string]: never;
12
+ }, {
13
+ [evt: string]: CustomEvent<any>;
14
+ }, {}, {}, string>;
15
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
16
+ new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
17
+ $$bindings?: Bindings;
18
+ } & Exports;
19
+ (internal: unknown, props: {
20
+ $$events?: Events;
21
+ $$slots?: Slots;
22
+ }): Exports & {
23
+ $set?: any;
24
+ $on?: any;
25
+ };
26
+ z_$$bindings?: Bindings;
27
+ }
@@ -4,7 +4,7 @@ Renders a table cell as either `<th>` (header) or `<td>` (data), with optional
4
4
  text alignment applied as an inline `text-align` style.
5
5
 
6
6
  @prop {boolean} header - When `true`, renders a `<th>`; otherwise renders a `<td>`.
7
- @prop {'left'|'center'|'right'|'justify'|'char'|null|undefined} align - Column alignment.
7
+ @prop {'left'|'center'|'right'|null} align - Column alignment.
8
8
  @prop {Snippet} [children] - Cell content.
9
9
  -->
10
10
  <script lang="ts">
@@ -12,7 +12,7 @@ text alignment applied as an inline `text-align` style.
12
12
 
13
13
  interface Props {
14
14
  header: boolean
15
- align: 'left' | 'center' | 'right' | 'justify' | 'char' | null | undefined
15
+ align: 'left' | 'center' | 'right' | null
16
16
  children?: Snippet
17
17
  }
18
18
 
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  interface Props {
3
3
  header: boolean;
4
- align: 'left' | 'center' | 'right' | 'justify' | 'char' | null | undefined;
4
+ align: 'left' | 'center' | 'right' | null;
5
5
  children?: Snippet;
6
6
  }
7
7
  /**
@@ -9,7 +9,7 @@ interface Props {
9
9
  * text alignment applied as an inline `text-align` style.
10
10
  *
11
11
  * @prop {boolean} header - When `true`, renders a `<th>`; otherwise renders a `<td>`.
12
- * @prop {'left'|'center'|'right'|'justify'|'char'|null|undefined} align - Column alignment.
12
+ * @prop {'left'|'center'|'right'|null} align - Column alignment.
13
13
  * @prop {Snippet} [children] - Cell content.
14
14
  */
15
15
  declare const TableCell: import("svelte").Component<Props, {}, "">;
@@ -7,7 +7,7 @@ Renders an HTML `<sup>` element. Accepts optional attributes and child content.
7
7
 
8
8
  interface Props {
9
9
  children?: Snippet
10
- attributes?: Record<string, unknown>
10
+ attributes?: Record<string, any> // trunk-ignore(eslint/@typescript-eslint/no-explicit-any)
11
11
  }
12
12
 
13
13
  const { children, attributes }: Props = $props()
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  interface Props {
3
3
  children?: Snippet;
4
- attributes?: Record<string, unknown>;
4
+ attributes?: Record<string, any>;
5
5
  }
6
6
  /** Renders an HTML `<sup>` element. Accepts optional attributes and child content. */
7
7
  declare const Sup: import("svelte").Component<Props, {}, "">;
package/dist/types.d.ts CHANGED
@@ -75,7 +75,7 @@ export interface TableRowSnippetProps {
75
75
  }
76
76
  export interface TableCellSnippetProps {
77
77
  header: boolean;
78
- align: 'left' | 'center' | 'right' | 'justify' | 'char' | null | undefined;
78
+ align: 'left' | 'center' | 'right' | null;
79
79
  children?: Snippet;
80
80
  }
81
81
  export interface EmSnippetProps {
@@ -124,8 +124,16 @@ export type SnippetOverrides = {
124
124
  rawtext?: Snippet<[RawTextSnippetProps]>;
125
125
  escape?: Snippet<[EscapeSnippetProps]>;
126
126
  };
127
+ /**
128
+ * Props passed to HTML snippet overrides.
129
+ *
130
+ * **Security note:** `attributes` are spread directly onto the rendered HTML element.
131
+ * This includes any attribute from the source markdown, such as `onclick` or `onerror`.
132
+ * If rendering untrusted markdown, use `allowHtmlOnly`/`excludeHtmlOnly` to restrict
133
+ * allowed tags, or integrate your own sanitizer to strip dangerous attributes.
134
+ */
127
135
  export interface HtmlSnippetProps {
128
- attributes?: Record<string, any>;
136
+ attributes?: Record<string, string | number | boolean | undefined>;
129
137
  children?: Snippet;
130
138
  }
131
139
  export type HtmlSnippetOverrides = {
@@ -28,7 +28,7 @@ import type { Token, TokensList } from './markdown-parser.js';
28
28
  * const cachedTokens = parseAndCacheTokens('# Hello World', { gfm: true }, false)
29
29
  * ```
30
30
  */
31
- export declare function parseAndCacheTokens(source: string, options: SvelteMarkdownOptions, isInline: boolean): Token[] | TokensList;
31
+ export declare const parseAndCacheTokens: (source: string, options: SvelteMarkdownOptions, isInline: boolean) => Token[] | TokensList;
32
32
  /**
33
33
  * Parses markdown source with caching (async path).
34
34
  * Uses Marked's recursive walkTokens with Promise.all to properly
@@ -46,4 +46,4 @@ export declare function parseAndCacheTokens(source: string, options: SvelteMarkd
46
46
  * const tokens = await parseAndCacheTokensAsync('# Hello', opts, false)
47
47
  * ```
48
48
  */
49
- export declare function parseAndCacheTokensAsync(source: string, options: SvelteMarkdownOptions, isInline: boolean): Promise<Token[] | TokensList>;
49
+ export declare const parseAndCacheTokensAsync: (source: string, options: SvelteMarkdownOptions, isInline: boolean) => Promise<Token[] | TokensList>;
@@ -10,13 +10,20 @@ import { tokenCache } from './token-cache.js';
10
10
  import { shrinkHtmlTokens } from './token-cleanup.js';
11
11
  import { Lexer, Marked } from 'marked';
12
12
  /**
13
- * Lex and clean tokens from markdown source. Shared by sync and async paths.
13
+ * Lexes markdown source and cleans the resulting tokens. Shared by sync and async paths.
14
+ *
15
+ * @param source - Raw markdown string to lex
16
+ * @param options - Parser options forwarded to the Marked lexer
17
+ * @param isInline - When true, uses inline tokenization (no block elements)
18
+ * @returns Cleaned token array with HTML tokens properly nested
19
+ *
20
+ * @internal
14
21
  */
15
- function lexAndClean(source, options, isInline) {
22
+ const lexAndClean = (source, options, isInline) => {
16
23
  const lexer = new Lexer(options);
17
24
  const parsedTokens = isInline ? lexer.inlineTokens(source) : lexer.lex(source);
18
25
  return shrinkHtmlTokens(parsedTokens);
19
- }
26
+ };
20
27
  /**
21
28
  * Parses markdown source with caching (synchronous path).
22
29
  * Checks cache first, parses on miss, stores result, and returns tokens.
@@ -37,7 +44,7 @@ function lexAndClean(source, options, isInline) {
37
44
  * const cachedTokens = parseAndCacheTokens('# Hello World', { gfm: true }, false)
38
45
  * ```
39
46
  */
40
- export function parseAndCacheTokens(source, options, isInline) {
47
+ export const parseAndCacheTokens = (source, options, isInline) => {
41
48
  // Check cache first - avoids expensive parsing
42
49
  const cached = tokenCache.getTokens(source, options);
43
50
  if (cached) {
@@ -51,7 +58,7 @@ export function parseAndCacheTokens(source, options, isInline) {
51
58
  // Cache the cleaned tokens for next time
52
59
  tokenCache.setTokens(source, options, cleanedTokens);
53
60
  return cleanedTokens;
54
- }
61
+ };
55
62
  /**
56
63
  * Parses markdown source with caching (async path).
57
64
  * Uses Marked's recursive walkTokens with Promise.all to properly
@@ -69,7 +76,7 @@ export function parseAndCacheTokens(source, options, isInline) {
69
76
  * const tokens = await parseAndCacheTokensAsync('# Hello', opts, false)
70
77
  * ```
71
78
  */
72
- export async function parseAndCacheTokensAsync(source, options, isInline) {
79
+ export const parseAndCacheTokensAsync = async (source, options, isInline) => {
73
80
  // Check cache first - avoids expensive parsing
74
81
  const cached = tokenCache.getTokens(source, options);
75
82
  if (cached) {
@@ -89,4 +96,4 @@ export async function parseAndCacheTokensAsync(source, options, isInline) {
89
96
  // Cache the cleaned tokens for next time
90
97
  tokenCache.setTokens(source, options, cleanedTokens);
91
98
  return cleanedTokens;
92
- }
99
+ };
@@ -35,7 +35,7 @@ import type { Token, TokensList } from './markdown-parser.js';
35
35
  * console.log(hash1 !== hash2) // true - different content = different hash
36
36
  * ```
37
37
  */
38
- declare function hashString(str: string): string;
38
+ declare const hashString: (str: string) => string;
39
39
  /**
40
40
  * Specialized cache for markdown token storage.
41
41
  * Extends MemoryCache with markdown-specific convenience methods.
@@ -33,7 +33,7 @@ import { MemoryCache } from './cache.js';
33
33
  * console.log(hash1 !== hash2) // true - different content = different hash
34
34
  * ```
35
35
  */
36
- function hashString(str) {
36
+ const hashString = (str) => {
37
37
  let hash = 2166136261; // FNV offset basis (32-bit)
38
38
  for (let i = 0; i < str.length; i++) {
39
39
  hash ^= str.charCodeAt(i);
@@ -42,7 +42,7 @@ function hashString(str) {
42
42
  }
43
43
  // Convert to unsigned 32-bit integer and base36 string
44
44
  return (hash >>> 0).toString(36);
45
- }
45
+ };
46
46
  /**
47
47
  * Generates a cache key from markdown source and parser options.
48
48
  * Combines hashes of both source content and options to ensure
@@ -71,7 +71,7 @@ function hashString(str) {
71
71
  // reactivity system creates new objects on prop changes ($derived), so
72
72
  // this is safe for all internal usage paths.
73
73
  const optionsHashCache = new WeakMap();
74
- function getCacheKey(source, options) {
74
+ const getCacheKey = (source, options) => {
75
75
  const sourceHash = hashString(source);
76
76
  let optionsHash = optionsHashCache.get(options);
77
77
  if (!optionsHash) {
@@ -90,7 +90,7 @@ function getCacheKey(source, options) {
90
90
  optionsHashCache.set(options, optionsHash);
91
91
  }
92
92
  return `${sourceHash}:${optionsHash}`;
93
- }
93
+ };
94
94
  /**
95
95
  * Specialized cache for markdown token storage.
96
96
  * Extends MemoryCache with markdown-specific convenience methods.
@@ -32,51 +32,6 @@ export declare const isHtmlOpenTag: (raw: string) => {
32
32
  * @internal
33
33
  */
34
34
  export declare const extractAttributes: (raw: string) => Record<string, string>;
35
- /**
36
- * Converts an HTML string into a sequence of tokens using htmlparser2.
37
- * Handles complex nested structures while maintaining proper order and relationships.
38
- *
39
- * Key features:
40
- * - Preserves original HTML structure without automatic tag closing
41
- * - Handles self-closing tags with proper XML syntax (e.g., <br/> instead of <br>)
42
- * - Gracefully handles malformed HTML by preserving the original structure
43
- * - Maintains attribute information in opening tags
44
- * - Processes text content between tags
45
- *
46
- * @param {string} html - HTML string to be parsed
47
- * @returns {Token[]} Array of tokens representing the HTML structure
48
- *
49
- * @example
50
- * // Well-formed HTML
51
- * parseHtmlBlock('<div>Hello <span>world</span></div>')
52
- * // Returns [
53
- * // { type: 'html', raw: '<div>', ... },
54
- * // { type: 'text', raw: 'Hello ', ... },
55
- * // { type: 'html', raw: '<span>', ... },
56
- * // { type: 'text', raw: 'world', ... },
57
- * // { type: 'html', raw: '</span>', ... },
58
- * // { type: 'html', raw: '</div>', ... }
59
- * // ]
60
- *
61
- * // Self-closing tags
62
- * parseHtmlBlock('<div>Before<br/>After</div>')
63
- * // Returns [
64
- * // { type: 'html', raw: '<div>', ... },
65
- * // { type: 'text', raw: 'Before', ... },
66
- * // { type: 'html', raw: '<br/>', ... },
67
- * // { type: 'text', raw: 'After', ... },
68
- * // { type: 'html', raw: '</div>', ... }
69
- * // ]
70
- *
71
- * // Malformed HTML
72
- * parseHtmlBlock('<div>Unclosed')
73
- * // Returns [
74
- * // { type: 'html', raw: '<div>', ... },
75
- * // { type: 'text', raw: 'Unclosed', ... }
76
- * // ]
77
- *
78
- * @internal
79
- */
80
35
  export declare const parseHtmlBlock: (html: string) => Token[];
81
36
  export declare const containsMultipleTags: (html: string) => boolean;
82
37
  /**
@@ -141,6 +141,22 @@ export const extractAttributes = (raw) => {
141
141
  *
142
142
  * @internal
143
143
  */
144
+ /**
145
+ * Serializes an HTML attribute map into a string for tag construction.
146
+ * Escapes double quotes in values to prevent attribute injection.
147
+ *
148
+ * @param {Record<string, string>} attributes - Map of attribute names to values
149
+ * @returns {string} Serialized attributes string with leading spaces
150
+ *
151
+ * @example
152
+ * serializeAttributes({ class: 'foo', id: 'bar' })
153
+ * // Returns ' class="foo" id="bar"'
154
+ *
155
+ * @internal
156
+ */
157
+ const serializeAttributes = (attributes) => Object.entries(attributes)
158
+ .map(([key, value]) => ` ${key}="${value.replace(/"/g, '&quot;')}"`)
159
+ .join('');
144
160
  export const parseHtmlBlock = (html) => {
145
161
  const tokens = [];
146
162
  let currentText = '';
@@ -158,9 +174,7 @@ export const parseHtmlBlock = (html) => {
158
174
  if (SELF_CLOSING_TAGS.test(name)) {
159
175
  tokens.push({
160
176
  type: 'html',
161
- raw: `<${name}${Object.entries(attributes)
162
- .map(([key, value]) => ` ${key}="${value}"`)
163
- .join('')}/>`,
177
+ raw: `<${name}${serializeAttributes(attributes)}/>`,
164
178
  tag: name,
165
179
  attributes
166
180
  });
@@ -169,9 +183,7 @@ export const parseHtmlBlock = (html) => {
169
183
  openTags.push(name);
170
184
  tokens.push({
171
185
  type: 'html',
172
- raw: `<${name}${Object.entries(attributes)
173
- .map(([key, value]) => ` ${key}="${value}"`)
174
- .join('')}>`,
186
+ raw: `<${name}${serializeAttributes(attributes)}>`,
175
187
  tag: name,
176
188
  attributes
177
189
  });
@@ -203,8 +215,7 @@ export const parseHtmlBlock = (html) => {
203
215
  }
204
216
  }
205
217
  }, {
206
- xmlMode: true,
207
- // Add this to prevent automatic tag closing
218
+ xmlMode: false,
208
219
  recognizeSelfClosing: true
209
220
  });
210
221
  parser.write(html);
@@ -290,19 +301,16 @@ export const shrinkHtmlTokens = (tokens) => {
290
301
  }
291
302
  else if (token.type === 'table') {
292
303
  // Process header cells
293
- if (token.header) {
294
- // @ts-expect-error: expected any
295
- token.header = token.header.map((cell) => ({
304
+ const tableToken = token;
305
+ if (tableToken.header) {
306
+ tableToken.header = tableToken.header.map((cell) => ({
296
307
  ...cell,
297
308
  tokens: cell.tokens ? shrinkHtmlTokens(cell.tokens) : []
298
309
  }));
299
310
  }
300
311
  // Process row cells
301
- if (token.rows) {
302
- // @ts-expect-error: expected any
303
- token.rows = token.rows.map((row) =>
304
- // @ts-expect-error: expected any
305
- row.map((cell) => ({
312
+ if (tableToken.rows) {
313
+ tableToken.rows = tableToken.rows.map((row) => row.map((cell) => ({
306
314
  ...cell,
307
315
  tokens: cell.tokens ? shrinkHtmlTokens(cell.tokens) : []
308
316
  })));
@@ -394,9 +402,9 @@ export const processHtmlTokens = (tokens) => {
394
402
  result.push(token);
395
403
  }
396
404
  }
397
- // If we have unclosed tags, return original tokens
405
+ // If we have unclosed tags, return partial result (better than discarding all work)
398
406
  if (stack.length > 0) {
399
- return tokens;
407
+ return result;
400
408
  }
401
409
  return result;
402
410
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-markdown",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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",
@@ -67,51 +67,51 @@
67
67
  }
68
68
  },
69
69
  "dependencies": {
70
- "@humanspeak/memory-cache": "^1.0.5",
70
+ "@humanspeak/memory-cache": "^1.0.6",
71
71
  "github-slugger": "^2.0.0",
72
72
  "htmlparser2": "^10.1.0",
73
- "marked": "^17.0.3"
73
+ "marked": "^17.0.4"
74
74
  },
75
75
  "devDependencies": {
76
- "@eslint/compat": "^2.0.2",
76
+ "@eslint/compat": "^2.0.3",
77
77
  "@eslint/js": "^10.0.1",
78
78
  "@playwright/cli": "^0.1.1",
79
79
  "@playwright/test": "^1.58.2",
80
80
  "@sveltejs/adapter-auto": "^7.0.1",
81
- "@sveltejs/kit": "^2.53.3",
81
+ "@sveltejs/kit": "^2.55.0",
82
82
  "@sveltejs/package": "^2.5.7",
83
- "@sveltejs/vite-plugin-svelte": "^6.2.4",
83
+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
84
84
  "@testing-library/jest-dom": "^6.9.1",
85
85
  "@testing-library/svelte": "^5.3.1",
86
86
  "@testing-library/user-event": "^14.6.1",
87
87
  "@types/katex": "^0.16.8",
88
- "@types/node": "^25.3.2",
89
- "@typescript-eslint/eslint-plugin": "^8.56.1",
90
- "@typescript-eslint/parser": "^8.56.1",
91
- "@vitest/coverage-v8": "^4.0.18",
92
- "eslint": "^10.0.2",
88
+ "@types/node": "^25.5.0",
89
+ "@typescript-eslint/eslint-plugin": "^8.57.1",
90
+ "@typescript-eslint/parser": "^8.57.1",
91
+ "@vitest/coverage-v8": "^4.1.0",
92
+ "eslint": "^10.0.3",
93
93
  "eslint-config-prettier": "^10.1.8",
94
94
  "eslint-plugin-import": "^2.32.0",
95
- "eslint-plugin-svelte": "^3.15.0",
95
+ "eslint-plugin-svelte": "^3.15.2",
96
96
  "eslint-plugin-unused-imports": "^4.4.1",
97
- "globals": "^17.3.0",
97
+ "globals": "^17.4.0",
98
98
  "husky": "^9.1.7",
99
- "jsdom": "^28.1.0",
100
- "katex": "^0.16.33",
99
+ "jsdom": "^29.0.0",
100
+ "katex": "^0.16.38",
101
101
  "marked-katex-extension": "^5.1.7",
102
- "mermaid": "^11.12.3",
102
+ "mermaid": "^11.13.0",
103
103
  "mprocs": "^0.8.3",
104
104
  "prettier": "^3.8.1",
105
105
  "prettier-plugin-organize-imports": "^4.3.0",
106
- "prettier-plugin-svelte": "^3.5.0",
106
+ "prettier-plugin-svelte": "^3.5.1",
107
107
  "prettier-plugin-tailwindcss": "^0.7.2",
108
- "publint": "^0.3.17",
109
- "svelte": "^5.53.5",
110
- "svelte-check": "^4.4.4",
108
+ "publint": "^0.3.18",
109
+ "svelte": "^5.53.13",
110
+ "svelte-check": "^4.4.5",
111
111
  "typescript": "^5.9.3",
112
- "typescript-eslint": "^8.56.1",
113
- "vite": "^7.3.1",
114
- "vitest": "^4.0.18"
112
+ "typescript-eslint": "^8.57.1",
113
+ "vite": "^8.0.0",
114
+ "vitest": "^4.1.0"
115
115
  },
116
116
  "peerDependencies": {
117
117
  "mermaid": ">=10.0.0",
@@ -123,7 +123,7 @@
123
123
  }
124
124
  },
125
125
  "volta": {
126
- "node": "22.16.0"
126
+ "node": "24.14.0"
127
127
  },
128
128
  "publishConfig": {
129
129
  "access": "public"
@@ -134,6 +134,7 @@
134
134
  ],
135
135
  "scripts": {
136
136
  "build": "vite build && npm run package",
137
+ "cf-typegen": "pnpm --filter docs cf-typegen",
137
138
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
138
139
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
139
140
  "dev": "vite dev",