@humanspeak/svelte-markdown 1.0.2 → 1.0.4
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 +129 -0
- package/dist/SvelteMarkdown.svelte +49 -2
- package/dist/extensions/alert/AlertRenderer.svelte +23 -0
- package/dist/extensions/alert/AlertRenderer.svelte.d.ts +8 -0
- package/dist/extensions/alert/index.d.ts +3 -0
- package/dist/extensions/alert/index.js +2 -0
- package/dist/extensions/alert/markedAlert.d.ts +37 -0
- package/dist/extensions/alert/markedAlert.js +62 -0
- package/dist/extensions/footnote/FootnoteRef.svelte +9 -0
- package/dist/extensions/footnote/FootnoteRef.svelte.d.ts +6 -0
- package/dist/extensions/footnote/FootnoteSection.svelte +25 -0
- package/dist/extensions/footnote/FootnoteSection.svelte.d.ts +10 -0
- package/dist/extensions/footnote/index.d.ts +3 -0
- package/dist/extensions/footnote/index.js +3 -0
- package/dist/extensions/footnote/markedFootnote.d.ts +31 -0
- package/dist/extensions/footnote/markedFootnote.js +80 -0
- package/dist/extensions/index.d.ts +17 -0
- package/dist/extensions/index.js +16 -0
- package/dist/extensions/mermaid/MermaidRenderer.svelte +79 -0
- package/dist/extensions/mermaid/MermaidRenderer.svelte.d.ts +8 -0
- package/dist/extensions/mermaid/index.d.ts +2 -0
- package/dist/extensions/mermaid/index.js +2 -0
- package/dist/extensions/mermaid/markedMermaid.d.ts +28 -0
- package/dist/extensions/mermaid/markedMermaid.js +49 -0
- package/dist/renderers/Br.svelte +1 -9
- package/dist/renderers/Br.svelte.d.ts +26 -7
- package/dist/renderers/html/Sup.svelte +1 -1
- package/dist/renderers/html/Sup.svelte.d.ts +1 -1
- package/dist/utils/createFilterUtilities.js +4 -2
- package/dist/utils/parse-and-cache.d.ts +19 -1
- package/dist/utils/parse-and-cache.js +49 -5
- package/package.json +36 -24
package/README.md
CHANGED
|
@@ -438,6 +438,135 @@ Where `KatexRenderer.svelte` is:
|
|
|
438
438
|
</SvelteMarkdown>
|
|
439
439
|
```
|
|
440
440
|
|
|
441
|
+
### Mermaid Diagrams (Async Rendering)
|
|
442
|
+
|
|
443
|
+
The package includes built-in `markedMermaid` and `MermaidRenderer` helpers for Mermaid diagram support. Install mermaid as an optional peer dependency:
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
npm install mermaid
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Then use the built-in helpers — no boilerplate needed:
|
|
450
|
+
|
|
451
|
+
```svelte
|
|
452
|
+
<script lang="ts">
|
|
453
|
+
import SvelteMarkdown from '@humanspeak/svelte-markdown'
|
|
454
|
+
import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
|
|
455
|
+
import { markedMermaid, MermaidRenderer } from '@humanspeak/svelte-markdown/extensions'
|
|
456
|
+
|
|
457
|
+
// markdown containing fenced mermaid code blocks
|
|
458
|
+
let { source } = $props()
|
|
459
|
+
|
|
460
|
+
interface MermaidRenderers extends Renderers {
|
|
461
|
+
mermaid: RendererComponent
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const renderers: Partial<MermaidRenderers> = {
|
|
465
|
+
mermaid: MermaidRenderer
|
|
466
|
+
}
|
|
467
|
+
</script>
|
|
468
|
+
|
|
469
|
+
<SvelteMarkdown {source} extensions={[markedMermaid()]} {renderers} />
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
`markedMermaid()` is a zero-dependency tokenizer that converts ` ```mermaid ` code blocks into custom tokens. `MermaidRenderer` lazy-loads mermaid in the browser, renders SVG asynchronously, and automatically re-renders when dark/light mode changes.
|
|
473
|
+
|
|
474
|
+
You can also use snippet overrides to wrap `MermaidRenderer` with custom markup:
|
|
475
|
+
|
|
476
|
+
```svelte
|
|
477
|
+
<SvelteMarkdown source={markdown} extensions={[markedMermaid()]}>
|
|
478
|
+
{#snippet mermaid(props)}
|
|
479
|
+
<div class="my-diagram-wrapper">
|
|
480
|
+
<MermaidRenderer text={props.text} />
|
|
481
|
+
</div>
|
|
482
|
+
{/snippet}
|
|
483
|
+
</SvelteMarkdown>
|
|
484
|
+
```
|
|
485
|
+
|
|
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
|
+
|
|
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
|
+
|
|
441
570
|
### How It Works
|
|
442
571
|
|
|
443
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**.
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
type Token,
|
|
59
59
|
type TokensList
|
|
60
60
|
} from './utils/markdown-parser.js'
|
|
61
|
-
import { parseAndCacheTokens } from './utils/parse-and-cache.js'
|
|
61
|
+
import { parseAndCacheTokens, parseAndCacheTokensAsync } from './utils/parse-and-cache.js'
|
|
62
62
|
import { rendererKeysInternal } from './utils/rendererKeys.js'
|
|
63
63
|
import { Marked } from 'marked'
|
|
64
64
|
|
|
@@ -87,7 +87,13 @@
|
|
|
87
87
|
const combinedOptions = $derived({ ...defaultOptions, ...extensionDefaults, ...options })
|
|
88
88
|
const slugger = new Slugger()
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
// Detect if any extension requires async processing
|
|
91
|
+
const hasAsyncExtension = $derived(extensions.some((ext) => ext.async === true))
|
|
92
|
+
|
|
93
|
+
// Synchronous token derivation (default fast path)
|
|
94
|
+
const syncTokens = $derived.by(() => {
|
|
95
|
+
if (hasAsyncExtension) return undefined
|
|
96
|
+
|
|
91
97
|
// Pre-parsed tokens - skip caching and parsing
|
|
92
98
|
if (Array.isArray(source)) {
|
|
93
99
|
return source as Token[]
|
|
@@ -102,6 +108,47 @@
|
|
|
102
108
|
return parseAndCacheTokens(source as string, combinedOptions, isInline)
|
|
103
109
|
}) satisfies Token[] | TokensList | undefined
|
|
104
110
|
|
|
111
|
+
// Async token state (used only when extensions require async walkTokens)
|
|
112
|
+
let asyncTokens = $state<Token[] | TokensList | undefined>(undefined)
|
|
113
|
+
let asyncRequestId = 0
|
|
114
|
+
|
|
115
|
+
$effect(() => {
|
|
116
|
+
if (!hasAsyncExtension) return
|
|
117
|
+
|
|
118
|
+
// Pre-parsed tokens - skip caching and parsing
|
|
119
|
+
if (Array.isArray(source)) {
|
|
120
|
+
asyncTokens = source as Token[]
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Empty string - return empty array
|
|
125
|
+
if (source === '') {
|
|
126
|
+
asyncTokens = []
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Async parse with caching
|
|
131
|
+
const currentSource = source as string
|
|
132
|
+
const currentOptions = combinedOptions
|
|
133
|
+
const currentInline = isInline
|
|
134
|
+
const requestId = ++asyncRequestId
|
|
135
|
+
parseAndCacheTokensAsync(currentSource, currentOptions, currentInline)
|
|
136
|
+
.then((result) => {
|
|
137
|
+
if (requestId === asyncRequestId) {
|
|
138
|
+
asyncTokens = result
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
.catch((error) => {
|
|
142
|
+
if (requestId === asyncRequestId) {
|
|
143
|
+
console.error('[svelte-markdown] async walkTokens failed:', error)
|
|
144
|
+
asyncTokens = []
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Unified tokens: prefer sync path, fall back to async
|
|
150
|
+
const tokens = $derived(hasAsyncExtension ? asyncTokens : syncTokens)
|
|
151
|
+
|
|
105
152
|
$effect(() => {
|
|
106
153
|
if (!tokens) return
|
|
107
154
|
parsed(tokens)
|
|
@@ -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,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,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">↩</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,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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension helpers for `@humanspeak/svelte-markdown`.
|
|
3
|
+
*
|
|
4
|
+
* Import from `@humanspeak/svelte-markdown/extensions`:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
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'
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* @module @humanspeak/svelte-markdown/extensions
|
|
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';
|
|
17
|
+
export { MermaidRenderer, markedMermaid } from './mermaid/index.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension helpers for `@humanspeak/svelte-markdown`.
|
|
3
|
+
*
|
|
4
|
+
* Import from `@humanspeak/svelte-markdown/extensions`:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
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'
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* @module @humanspeak/svelte-markdown/extensions
|
|
13
|
+
*/
|
|
14
|
+
export { AlertRenderer, markedAlert } from './alert/index.js';
|
|
15
|
+
export { FootnoteRef, FootnoteSection, markedFootnote } from './footnote/index.js';
|
|
16
|
+
export { MermaidRenderer, markedMermaid } from './mermaid/index.js';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
text: string
|
|
6
|
+
lightTheme?: string
|
|
7
|
+
darkTheme?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { text, lightTheme = 'default', darkTheme = 'dark' }: Props = $props()
|
|
11
|
+
|
|
12
|
+
let svg = $state('')
|
|
13
|
+
let error = $state('')
|
|
14
|
+
let loading = $state(true)
|
|
15
|
+
let mermaidModule: typeof import('mermaid').default | null = $state(null)
|
|
16
|
+
let renderCounter = 0
|
|
17
|
+
|
|
18
|
+
async function renderDiagram(diagramText: string) {
|
|
19
|
+
if (!mermaidModule) return
|
|
20
|
+
const current = ++renderCounter
|
|
21
|
+
try {
|
|
22
|
+
error = ''
|
|
23
|
+
const isDark = document.documentElement.classList.contains('dark')
|
|
24
|
+
const theme = isDark ? darkTheme : lightTheme
|
|
25
|
+
const themed = `%%{init: {'theme': '${theme}'}}%%\n${diagramText}`
|
|
26
|
+
const id = `mermaid-${crypto.randomUUID()}`
|
|
27
|
+
const result = await mermaidModule.render(id, themed)
|
|
28
|
+
if (current !== renderCounter) return
|
|
29
|
+
svg = result.svg
|
|
30
|
+
} catch (e) {
|
|
31
|
+
if (current !== renderCounter) return
|
|
32
|
+
error = e instanceof Error ? e.message : String(e)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
$effect(() => {
|
|
37
|
+
if (mermaidModule) {
|
|
38
|
+
renderDiagram(text)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
onMount(() => {
|
|
43
|
+
let observer: MutationObserver
|
|
44
|
+
;(async () => {
|
|
45
|
+
try {
|
|
46
|
+
const mod = (await import('mermaid')).default
|
|
47
|
+
mod.initialize({ startOnLoad: false, securityLevel: 'strict' })
|
|
48
|
+
mermaidModule = mod
|
|
49
|
+
} catch (e) {
|
|
50
|
+
error = e instanceof Error ? e.message : String(e)
|
|
51
|
+
} finally {
|
|
52
|
+
loading = false
|
|
53
|
+
}
|
|
54
|
+
})()
|
|
55
|
+
|
|
56
|
+
observer = new MutationObserver((mutations) => {
|
|
57
|
+
for (const mutation of mutations) {
|
|
58
|
+
if (mutation.attributeName === 'class') {
|
|
59
|
+
renderDiagram(text)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
|
65
|
+
|
|
66
|
+
return () => observer.disconnect()
|
|
67
|
+
})
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
{#if loading}
|
|
71
|
+
<div class="mermaid-loading" data-testid="mermaid-loading">Loading diagram...</div>
|
|
72
|
+
{:else if error}
|
|
73
|
+
<div class="mermaid-error" data-testid="mermaid-error">Diagram error: {error}</div>
|
|
74
|
+
{:else}
|
|
75
|
+
<div class="mermaid-diagram" data-testid="mermaid-diagram">
|
|
76
|
+
<!-- trunk-ignore(eslint/svelte/no-at-html-tags) -->
|
|
77
|
+
{@html svg}
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { MarkedExtension } from 'marked';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a marked extension that tokenizes fenced ` ```mermaid ` code blocks
|
|
4
|
+
* into custom `mermaid` tokens.
|
|
5
|
+
*
|
|
6
|
+
* The extension produces block-level tokens with `{ type: 'mermaid', raw, text }`
|
|
7
|
+
* where `text` is the trimmed diagram source. It has zero runtime dependencies —
|
|
8
|
+
* pair it with `MermaidRenderer` (or your own component) to render the diagrams.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <script lang="ts">
|
|
13
|
+
* import SvelteMarkdown from '@humanspeak/svelte-markdown'
|
|
14
|
+
* import { markedMermaid, MermaidRenderer } from '@humanspeak/svelte-markdown'
|
|
15
|
+
*
|
|
16
|
+
* const renderers = { mermaid: MermaidRenderer }
|
|
17
|
+
* </script>
|
|
18
|
+
*
|
|
19
|
+
* <SvelteMarkdown
|
|
20
|
+
* source={markdown}
|
|
21
|
+
* extensions={[markedMermaid()]}
|
|
22
|
+
* {renderers}
|
|
23
|
+
* />
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @returns A `MarkedExtension` with a single block-level `mermaid` tokenizer
|
|
27
|
+
*/
|
|
28
|
+
export declare function markedMermaid(): MarkedExtension;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a marked extension that tokenizes fenced ` ```mermaid ` code blocks
|
|
3
|
+
* into custom `mermaid` tokens.
|
|
4
|
+
*
|
|
5
|
+
* The extension produces block-level tokens with `{ type: 'mermaid', raw, text }`
|
|
6
|
+
* where `text` is the trimmed diagram source. It has zero runtime dependencies —
|
|
7
|
+
* pair it with `MermaidRenderer` (or your own component) to render the diagrams.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```svelte
|
|
11
|
+
* <script lang="ts">
|
|
12
|
+
* import SvelteMarkdown from '@humanspeak/svelte-markdown'
|
|
13
|
+
* import { markedMermaid, MermaidRenderer } from '@humanspeak/svelte-markdown'
|
|
14
|
+
*
|
|
15
|
+
* const renderers = { mermaid: MermaidRenderer }
|
|
16
|
+
* </script>
|
|
17
|
+
*
|
|
18
|
+
* <SvelteMarkdown
|
|
19
|
+
* source={markdown}
|
|
20
|
+
* extensions={[markedMermaid()]}
|
|
21
|
+
* {renderers}
|
|
22
|
+
* />
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @returns A `MarkedExtension` with a single block-level `mermaid` tokenizer
|
|
26
|
+
*/
|
|
27
|
+
export function markedMermaid() {
|
|
28
|
+
return {
|
|
29
|
+
extensions: [
|
|
30
|
+
{
|
|
31
|
+
name: 'mermaid',
|
|
32
|
+
level: 'block',
|
|
33
|
+
start(src) {
|
|
34
|
+
return src.match(/```mermaid/)?.index;
|
|
35
|
+
},
|
|
36
|
+
tokenizer(src) {
|
|
37
|
+
const match = src.match(/^```mermaid\n([\s\S]*?)```/);
|
|
38
|
+
if (match) {
|
|
39
|
+
return {
|
|
40
|
+
type: 'mermaid',
|
|
41
|
+
raw: match[0],
|
|
42
|
+
text: match[1].trim()
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/renderers/Br.svelte
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
|
@@ -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,
|
|
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,
|
|
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, {}, "">;
|
|
@@ -45,13 +45,15 @@ export const createFilterUtilities = (keys, unsupportedComponent, defaultsMap) =
|
|
|
45
45
|
const hasKey = (key) => keys.includes(key);
|
|
46
46
|
/**
|
|
47
47
|
* Builds a map where every key is set to the unsupported component.
|
|
48
|
-
*
|
|
48
|
+
* Keys whose default value is null are preserved as null so that
|
|
49
|
+
* fallback chains (e.g. orderedlistitem || listitem) still work.
|
|
49
50
|
*/
|
|
50
51
|
const buildUnsupported = () => {
|
|
51
52
|
const result = {};
|
|
52
53
|
for (const key of keys) {
|
|
53
54
|
;
|
|
54
|
-
result[key] =
|
|
55
|
+
result[key] =
|
|
56
|
+
defaultsMap[key] === null ? null : unsupportedComponent;
|
|
55
57
|
}
|
|
56
58
|
return result;
|
|
57
59
|
};
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { SvelteMarkdownOptions } from '../types.js';
|
|
10
10
|
import type { Token, TokensList } from './markdown-parser.js';
|
|
11
11
|
/**
|
|
12
|
-
* Parses markdown source with caching.
|
|
12
|
+
* Parses markdown source with caching (synchronous path).
|
|
13
13
|
* Checks cache first, parses on miss, stores result, and returns tokens.
|
|
14
14
|
*
|
|
15
15
|
* @param source - Raw markdown string to parse
|
|
@@ -29,3 +29,21 @@ import type { Token, TokensList } from './markdown-parser.js';
|
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
31
|
export declare function parseAndCacheTokens(source: string, options: SvelteMarkdownOptions, isInline: boolean): Token[] | TokensList;
|
|
32
|
+
/**
|
|
33
|
+
* Parses markdown source with caching (async path).
|
|
34
|
+
* Uses Marked's recursive walkTokens with Promise.all to properly
|
|
35
|
+
* handle async walkTokens callbacks (e.g. marked-code-format).
|
|
36
|
+
*
|
|
37
|
+
* @param source - Raw markdown string to parse
|
|
38
|
+
* @param options - Svelte markdown parser options
|
|
39
|
+
* @param isInline - Whether to parse as inline markdown (no block elements)
|
|
40
|
+
* @returns Promise resolving to cleaned and cached token array
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* import { parseAndCacheTokensAsync } from './parse-and-cache.js'
|
|
45
|
+
*
|
|
46
|
+
* const tokens = await parseAndCacheTokensAsync('# Hello', opts, false)
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseAndCacheTokensAsync(source: string, options: SvelteMarkdownOptions, isInline: boolean): Promise<Token[] | TokensList>;
|
|
@@ -8,9 +8,17 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { tokenCache } from './token-cache.js';
|
|
10
10
|
import { shrinkHtmlTokens } from './token-cleanup.js';
|
|
11
|
-
import { Lexer } from 'marked';
|
|
11
|
+
import { Lexer, Marked } from 'marked';
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Lex and clean tokens from markdown source. Shared by sync and async paths.
|
|
14
|
+
*/
|
|
15
|
+
function lexAndClean(source, options, isInline) {
|
|
16
|
+
const lexer = new Lexer(options);
|
|
17
|
+
const parsedTokens = isInline ? lexer.inlineTokens(source) : lexer.lex(source);
|
|
18
|
+
return shrinkHtmlTokens(parsedTokens);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parses markdown source with caching (synchronous path).
|
|
14
22
|
* Checks cache first, parses on miss, stores result, and returns tokens.
|
|
15
23
|
*
|
|
16
24
|
* @param source - Raw markdown string to parse
|
|
@@ -36,9 +44,7 @@ export function parseAndCacheTokens(source, options, isInline) {
|
|
|
36
44
|
return cached;
|
|
37
45
|
}
|
|
38
46
|
// Cache miss - parse and store
|
|
39
|
-
const
|
|
40
|
-
const parsedTokens = isInline ? lexer.inlineTokens(source) : lexer.lex(source);
|
|
41
|
-
const cleanedTokens = shrinkHtmlTokens(parsedTokens);
|
|
47
|
+
const cleanedTokens = lexAndClean(source, options, isInline);
|
|
42
48
|
if (typeof options.walkTokens === 'function') {
|
|
43
49
|
cleanedTokens.forEach(options.walkTokens);
|
|
44
50
|
}
|
|
@@ -46,3 +52,41 @@ export function parseAndCacheTokens(source, options, isInline) {
|
|
|
46
52
|
tokenCache.setTokens(source, options, cleanedTokens);
|
|
47
53
|
return cleanedTokens;
|
|
48
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Parses markdown source with caching (async path).
|
|
57
|
+
* Uses Marked's recursive walkTokens with Promise.all to properly
|
|
58
|
+
* handle async walkTokens callbacks (e.g. marked-code-format).
|
|
59
|
+
*
|
|
60
|
+
* @param source - Raw markdown string to parse
|
|
61
|
+
* @param options - Svelte markdown parser options
|
|
62
|
+
* @param isInline - Whether to parse as inline markdown (no block elements)
|
|
63
|
+
* @returns Promise resolving to cleaned and cached token array
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* import { parseAndCacheTokensAsync } from './parse-and-cache.js'
|
|
68
|
+
*
|
|
69
|
+
* const tokens = await parseAndCacheTokensAsync('# Hello', opts, false)
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export async function parseAndCacheTokensAsync(source, options, isInline) {
|
|
73
|
+
// Check cache first - avoids expensive parsing
|
|
74
|
+
const cached = tokenCache.getTokens(source, options);
|
|
75
|
+
if (cached) {
|
|
76
|
+
return cached;
|
|
77
|
+
}
|
|
78
|
+
// Cache miss - parse and store
|
|
79
|
+
const cleanedTokens = lexAndClean(source, options, isInline);
|
|
80
|
+
if (typeof options.walkTokens === 'function') {
|
|
81
|
+
// Use Marked's recursive walkTokens which handles tables, lists,
|
|
82
|
+
// nested tokens, and extension childTokens. Await all returned
|
|
83
|
+
// promises so async walkTokens callbacks complete before caching.
|
|
84
|
+
const marked = new Marked();
|
|
85
|
+
marked.defaults = { ...marked.defaults, ...options };
|
|
86
|
+
const results = marked.walkTokens(cleanedTokens, options.walkTokens);
|
|
87
|
+
await Promise.all(results);
|
|
88
|
+
}
|
|
89
|
+
// Cache the cleaned tokens for next time
|
|
90
|
+
tokenCache.setTokens(source, options, cleanedTokens);
|
|
91
|
+
return cleanedTokens;
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-markdown",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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",
|
|
@@ -47,6 +47,10 @@
|
|
|
47
47
|
".": {
|
|
48
48
|
"types": "./dist/index.d.ts",
|
|
49
49
|
"svelte": "./dist/index.js"
|
|
50
|
+
},
|
|
51
|
+
"./extensions": {
|
|
52
|
+
"types": "./dist/extensions/index.d.ts",
|
|
53
|
+
"svelte": "./dist/extensions/index.js"
|
|
50
54
|
}
|
|
51
55
|
},
|
|
52
56
|
"svelte": "./dist/index.js",
|
|
@@ -63,56 +67,63 @@
|
|
|
63
67
|
}
|
|
64
68
|
},
|
|
65
69
|
"dependencies": {
|
|
66
|
-
"@humanspeak/memory-cache": "^1.0.
|
|
70
|
+
"@humanspeak/memory-cache": "^1.0.6",
|
|
67
71
|
"github-slugger": "^2.0.0",
|
|
68
72
|
"htmlparser2": "^10.1.0",
|
|
69
|
-
"marked": "^17.0.
|
|
73
|
+
"marked": "^17.0.4"
|
|
70
74
|
},
|
|
71
75
|
"devDependencies": {
|
|
72
|
-
"@eslint/compat": "^2.0.
|
|
76
|
+
"@eslint/compat": "^2.0.3",
|
|
73
77
|
"@eslint/js": "^10.0.1",
|
|
74
78
|
"@playwright/cli": "^0.1.1",
|
|
75
79
|
"@playwright/test": "^1.58.2",
|
|
76
80
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
77
|
-
"@sveltejs/kit": "^2.
|
|
81
|
+
"@sveltejs/kit": "^2.55.0",
|
|
78
82
|
"@sveltejs/package": "^2.5.7",
|
|
79
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
83
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
80
84
|
"@testing-library/jest-dom": "^6.9.1",
|
|
81
85
|
"@testing-library/svelte": "^5.3.1",
|
|
82
86
|
"@testing-library/user-event": "^14.6.1",
|
|
83
87
|
"@types/katex": "^0.16.8",
|
|
84
|
-
"@types/node": "^25.
|
|
85
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
86
|
-
"@typescript-eslint/parser": "^8.
|
|
87
|
-
"@vitest/coverage-v8": "^4.0
|
|
88
|
-
"
|
|
89
|
-
"eslint": "^10.0.0",
|
|
88
|
+
"@types/node": "^25.5.0",
|
|
89
|
+
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
|
90
|
+
"@typescript-eslint/parser": "^8.57.0",
|
|
91
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
92
|
+
"eslint": "^10.0.3",
|
|
90
93
|
"eslint-config-prettier": "^10.1.8",
|
|
91
94
|
"eslint-plugin-import": "^2.32.0",
|
|
92
|
-
"eslint-plugin-svelte": "^3.15.
|
|
95
|
+
"eslint-plugin-svelte": "^3.15.2",
|
|
93
96
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
94
|
-
"globals": "^17.
|
|
97
|
+
"globals": "^17.4.0",
|
|
95
98
|
"husky": "^9.1.7",
|
|
96
99
|
"jsdom": "^28.1.0",
|
|
97
|
-
"katex": "^0.16.
|
|
100
|
+
"katex": "^0.16.38",
|
|
98
101
|
"marked-katex-extension": "^5.1.7",
|
|
102
|
+
"mermaid": "^11.13.0",
|
|
103
|
+
"mprocs": "^0.8.3",
|
|
99
104
|
"prettier": "^3.8.1",
|
|
100
105
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
101
|
-
"prettier-plugin-svelte": "^3.5.
|
|
106
|
+
"prettier-plugin-svelte": "^3.5.1",
|
|
102
107
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
103
|
-
"publint": "^0.3.
|
|
104
|
-
"svelte": "^5.53.
|
|
105
|
-
"svelte-check": "^4.4.
|
|
108
|
+
"publint": "^0.3.18",
|
|
109
|
+
"svelte": "^5.53.11",
|
|
110
|
+
"svelte-check": "^4.4.5",
|
|
106
111
|
"typescript": "^5.9.3",
|
|
107
|
-
"typescript-eslint": "^8.
|
|
108
|
-
"vite": "^
|
|
109
|
-
"vitest": "^4.0
|
|
112
|
+
"typescript-eslint": "^8.57.0",
|
|
113
|
+
"vite": "^8.0.0",
|
|
114
|
+
"vitest": "^4.1.0"
|
|
110
115
|
},
|
|
111
116
|
"peerDependencies": {
|
|
117
|
+
"mermaid": ">=10.0.0",
|
|
112
118
|
"svelte": "^5.0.0"
|
|
113
119
|
},
|
|
120
|
+
"peerDependenciesMeta": {
|
|
121
|
+
"mermaid": {
|
|
122
|
+
"optional": true
|
|
123
|
+
}
|
|
124
|
+
},
|
|
114
125
|
"volta": {
|
|
115
|
-
"node": "24.
|
|
126
|
+
"node": "24.14.0"
|
|
116
127
|
},
|
|
117
128
|
"publishConfig": {
|
|
118
129
|
"access": "public"
|
|
@@ -123,10 +134,11 @@
|
|
|
123
134
|
],
|
|
124
135
|
"scripts": {
|
|
125
136
|
"build": "vite build && npm run package",
|
|
137
|
+
"cf-typegen": "pnpm --filter docs cf-typegen",
|
|
126
138
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
127
139
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
128
140
|
"dev": "vite dev",
|
|
129
|
-
"dev:all": "
|
|
141
|
+
"dev:all": "mprocs",
|
|
130
142
|
"dev:pkg": "svelte-kit sync && svelte-package --watch",
|
|
131
143
|
"format": "prettier --write .",
|
|
132
144
|
"lint": "prettier --check . && eslint .",
|