@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.
- package/README.md +82 -0
- package/dist/Parser.svelte +11 -6
- package/dist/Parser.svelte.d.ts +3 -2
- package/dist/SvelteMarkdown.svelte +6 -3
- 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 +5 -0
- package/dist/extensions/index.js +4 -0
- package/dist/renderers/Br.svelte +1 -9
- package/dist/renderers/Br.svelte.d.ts +26 -7
- package/dist/renderers/TableCell.svelte +2 -2
- package/dist/renderers/TableCell.svelte.d.ts +2 -2
- package/dist/renderers/html/Sup.svelte +1 -1
- package/dist/renderers/html/Sup.svelte.d.ts +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/utils/parse-and-cache.d.ts +2 -2
- package/dist/utils/parse-and-cache.js +14 -7
- package/dist/utils/token-cache.d.ts +1 -1
- package/dist/utils/token-cache.js +4 -4
- package/dist/utils/token-cleanup.d.ts +0 -45
- package/dist/utils/token-cleanup.js +26 -18
- 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**.
|
package/dist/Parser.svelte
CHANGED
|
@@ -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,
|
|
67
|
-
htmlSnippetOverrides?: Record<string,
|
|
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:
|
|
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>
|
package/dist/Parser.svelte.d.ts
CHANGED
|
@@ -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,
|
|
57
|
-
htmlSnippetOverrides?: Record<string,
|
|
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 =>
|
|
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,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
|
+
}
|
|
@@ -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';
|
package/dist/extensions/index.js
CHANGED
|
@@ -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';
|
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
|
+
}
|
|
@@ -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'|
|
|
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' |
|
|
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' |
|
|
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'|
|
|
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,
|
|
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, {}, "">;
|
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' |
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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, '"')}"`)
|
|
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}${
|
|
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}${
|
|
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:
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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 (
|
|
302
|
-
|
|
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
|
|
405
|
+
// If we have unclosed tags, return partial result (better than discarding all work)
|
|
398
406
|
if (stack.length > 0) {
|
|
399
|
-
return
|
|
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
|
+
"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.
|
|
70
|
+
"@humanspeak/memory-cache": "^1.0.6",
|
|
71
71
|
"github-slugger": "^2.0.0",
|
|
72
72
|
"htmlparser2": "^10.1.0",
|
|
73
|
-
"marked": "^17.0.
|
|
73
|
+
"marked": "^17.0.4"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@eslint/compat": "^2.0.
|
|
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.
|
|
81
|
+
"@sveltejs/kit": "^2.55.0",
|
|
82
82
|
"@sveltejs/package": "^2.5.7",
|
|
83
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
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.
|
|
89
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
90
|
-
"@typescript-eslint/parser": "^8.
|
|
91
|
-
"@vitest/coverage-v8": "^4.0
|
|
92
|
-
"eslint": "^10.0.
|
|
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.
|
|
95
|
+
"eslint-plugin-svelte": "^3.15.2",
|
|
96
96
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
97
|
-
"globals": "^17.
|
|
97
|
+
"globals": "^17.4.0",
|
|
98
98
|
"husky": "^9.1.7",
|
|
99
|
-
"jsdom": "^
|
|
100
|
-
"katex": "^0.16.
|
|
99
|
+
"jsdom": "^29.0.0",
|
|
100
|
+
"katex": "^0.16.38",
|
|
101
101
|
"marked-katex-extension": "^5.1.7",
|
|
102
|
-
"mermaid": "^11.
|
|
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.
|
|
106
|
+
"prettier-plugin-svelte": "^3.5.1",
|
|
107
107
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
108
|
-
"publint": "^0.3.
|
|
109
|
-
"svelte": "^5.53.
|
|
110
|
-
"svelte-check": "^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.
|
|
113
|
-
"vite": "^
|
|
114
|
-
"vitest": "^4.0
|
|
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": "
|
|
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",
|