svelte-streamdown 4.1.1 → 4.2.0
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 +146 -47
- package/dist/Block.svelte +9 -2
- package/dist/Block.svelte.d.ts +2 -0
- package/dist/Elements/Code.svelte +9 -1
- package/dist/Elements/Element.svelte +20 -5
- package/dist/Elements/Mermaid.svelte +1 -9
- package/dist/Elements/TableDownload.svelte +44 -3
- package/dist/Elements/TableDownload.svelte.d.ts +3 -1
- package/dist/Elements/fallbacks/CodeFallback.svelte +11 -3
- package/dist/Elements/fallbacks/CodeFallback.svelte.d.ts +2 -2
- package/dist/Elements/icons.d.ts +1 -0
- package/dist/Elements/icons.js +8 -0
- package/dist/Streamdown.svelte +58 -3
- package/dist/context.svelte.d.ts +32 -1
- package/dist/context.svelte.js +3 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/marked/index.d.ts +5 -4
- package/dist/marked/index.js +33 -44
- package/dist/marked/marked-mdx.d.ts +27 -1
- package/dist/marked/marked-mdx.js +37 -8
- package/dist/theme.d.ts +6 -0
- package/dist/theme.js +12 -4
- package/dist/utils/expand.svelte.d.ts +21 -0
- package/dist/utils/expand.svelte.js +46 -0
- package/dist/utils/line-numbers.d.ts +23 -0
- package/dist/utils/line-numbers.js +30 -0
- package/dist/utils/normalize-html-indentation.d.ts +10 -0
- package/dist/utils/normalize-html-indentation.js +52 -0
- package/dist/utils/parse-incomplete-markdown.d.ts +20 -6
- package/dist/utils/parse-incomplete-markdown.js +167 -65
- package/package.json +1 -1
package/dist/Streamdown.svelte
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
type StreamdownProps
|
|
7
7
|
} from './context.svelte.js';
|
|
8
8
|
import { mergeTheme, shadcnTheme } from './theme.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
parseBlocks,
|
|
11
|
+
createParseBlocksCache,
|
|
12
|
+
compileTags,
|
|
13
|
+
DEFAULT_TAGS
|
|
14
|
+
} from './marked/index.js';
|
|
15
|
+
import { normalizeHtmlIndentation as dedentHtml } from './utils/normalize-html-indentation.js';
|
|
10
16
|
import { hasUnclosedFence } from './utils/fence.js';
|
|
11
17
|
|
|
12
18
|
let {
|
|
@@ -30,6 +36,7 @@
|
|
|
30
36
|
controls,
|
|
31
37
|
codeBlockMaxHeight,
|
|
32
38
|
tableMaxHeight,
|
|
39
|
+
lineNumbers = false,
|
|
33
40
|
animation,
|
|
34
41
|
element = $bindable(),
|
|
35
42
|
icons,
|
|
@@ -38,6 +45,9 @@
|
|
|
38
45
|
sources,
|
|
39
46
|
inlineCitationsMode = 'carousel',
|
|
40
47
|
mdxComponents,
|
|
48
|
+
customTags,
|
|
49
|
+
literalTagContent,
|
|
50
|
+
normalizeHtmlIndentation = false,
|
|
41
51
|
components,
|
|
42
52
|
static: isStatic,
|
|
43
53
|
...snippets
|
|
@@ -59,6 +69,20 @@
|
|
|
59
69
|
// the last couple of blocks instead of the whole document.
|
|
60
70
|
const blocksCache = createParseBlocksCache();
|
|
61
71
|
|
|
72
|
+
// One compile per allowlist identity, shared by the lexer and the streaming
|
|
73
|
+
// completer so the two can never disagree about what counts as a tag.
|
|
74
|
+
const tags = $derived.by(() => {
|
|
75
|
+
const names = [...(customTags ?? []), ...Object.keys(mdxComponents ?? {})];
|
|
76
|
+
// With nothing to compile, share DEFAULT_TAGS: the lexer options cache is
|
|
77
|
+
// keyed on this object, so a private copy per instance would rebuild and
|
|
78
|
+
// retain its own ~20 tokenizer registrations for no reason.
|
|
79
|
+
return names.length === 0 && !literalTagContent?.length
|
|
80
|
+
? DEFAULT_TAGS
|
|
81
|
+
: compileTags(names, literalTagContent);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const source = $derived(normalizeHtmlIndentation ? dedentHtml(content) : content);
|
|
85
|
+
|
|
62
86
|
streamdown = new StreamdownContext({
|
|
63
87
|
get element() {
|
|
64
88
|
return element;
|
|
@@ -176,6 +200,7 @@
|
|
|
176
200
|
tableCopy: table.copy,
|
|
177
201
|
tableDownload: table.download,
|
|
178
202
|
tableDownloadFilename: table.filename ?? 'table',
|
|
203
|
+
tableFullscreen: table.enabled && tableSection.fullscreen !== false,
|
|
179
204
|
tableCsvSeparator: tableSection.csvSeparator ?? ',',
|
|
180
205
|
mermaid: mermaid.enabled,
|
|
181
206
|
mermaidDownload: mermaid.download,
|
|
@@ -188,6 +213,9 @@
|
|
|
188
213
|
get codeBlockMaxHeight() {
|
|
189
214
|
return codeBlockMaxHeight;
|
|
190
215
|
},
|
|
216
|
+
get lineNumbers() {
|
|
217
|
+
return lineNumbers;
|
|
218
|
+
},
|
|
191
219
|
get tableMaxHeight() {
|
|
192
220
|
return tableMaxHeight;
|
|
193
221
|
},
|
|
@@ -203,6 +231,9 @@
|
|
|
203
231
|
get mdxComponents() {
|
|
204
232
|
return mdxComponents;
|
|
205
233
|
},
|
|
234
|
+
get tags() {
|
|
235
|
+
return tags;
|
|
236
|
+
},
|
|
206
237
|
get components() {
|
|
207
238
|
return components;
|
|
208
239
|
}
|
|
@@ -211,7 +242,7 @@
|
|
|
211
242
|
const id = $props.id();
|
|
212
243
|
|
|
213
244
|
const blocks = $derived(
|
|
214
|
-
isStatic ?
|
|
245
|
+
isStatic ? source : parseBlocks(source, streamdown.extensions, blocksCache, tags)
|
|
215
246
|
);
|
|
216
247
|
|
|
217
248
|
// Only the tail of a live stream can be mid-fence — a static render is finished
|
|
@@ -223,12 +254,13 @@
|
|
|
223
254
|
|
|
224
255
|
<div bind:this={element} class={className}>
|
|
225
256
|
{#if isStatic}
|
|
226
|
-
<Block static={isStatic} block={
|
|
257
|
+
<Block static={isStatic} block={source} />
|
|
227
258
|
{:else}
|
|
228
259
|
{#each blocks as block, index (`${id}-block-${index}`)}
|
|
229
260
|
<Block
|
|
230
261
|
static={isStatic}
|
|
231
262
|
{block}
|
|
263
|
+
live={index === blocks.length - 1}
|
|
232
264
|
incomplete={lastBlockIncomplete && index === blocks.length - 1}
|
|
233
265
|
/>
|
|
234
266
|
{/each}
|
|
@@ -237,6 +269,29 @@
|
|
|
237
269
|
|
|
238
270
|
<style global>
|
|
239
271
|
:global {
|
|
272
|
+
/* Line numbers: a counter on the line span that already exists (why it is
|
|
273
|
+
here and not in the theme: see resolveLineNumbers). CSS ships as a string
|
|
274
|
+
at runtime, so keep this short. */
|
|
275
|
+
[data-streamdown-code] pre[data-line-numbers] > code > span {
|
|
276
|
+
counter-increment: sd-line;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
[data-streamdown-code] pre[data-line-numbers] > code > span::before {
|
|
280
|
+
content: counter(sd-line);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* Lives here, not in Mermaid.svelte: the table overlay needs it on pages
|
|
284
|
+
that never render a diagram. */
|
|
285
|
+
[data-expanded='true'] {
|
|
286
|
+
position: fixed;
|
|
287
|
+
top: 16px;
|
|
288
|
+
left: 16px;
|
|
289
|
+
width: calc(100vw - 32px);
|
|
290
|
+
height: calc(100vh - 32px);
|
|
291
|
+
z-index: 2147483647;
|
|
292
|
+
margin: 0px;
|
|
293
|
+
}
|
|
294
|
+
|
|
240
295
|
@keyframes sd-fade {
|
|
241
296
|
from {
|
|
242
297
|
opacity: 0;
|
package/dist/context.svelte.d.ts
CHANGED
|
@@ -28,6 +28,9 @@ export type Translations = {
|
|
|
28
28
|
tableFormatHtml: string;
|
|
29
29
|
tableFormatCsv: string;
|
|
30
30
|
tableFormatTsv: string;
|
|
31
|
+
tableFullscreen: string;
|
|
32
|
+
exitTableFullscreen: string;
|
|
33
|
+
table: string;
|
|
31
34
|
downloadDiagram: string;
|
|
32
35
|
downloadDiagramPng: string;
|
|
33
36
|
downloadDiagramSvg: string;
|
|
@@ -48,6 +51,8 @@ export type Translations = {
|
|
|
48
51
|
};
|
|
49
52
|
export declare const defaultTranslations: Translations;
|
|
50
53
|
export interface StreamdownContext extends Omit<StreamdownProps, keyof Snippets | 'class' | 'theme' | 'highlightTheme' | 'inlineCitationsMode'> {
|
|
54
|
+
/** `customTags` + `mdxComponents` keys compiled once; lexer and completer share it. */
|
|
55
|
+
tags: TagMatchers;
|
|
51
56
|
snippets: Snippets;
|
|
52
57
|
highlightTheme: HighlightTheme;
|
|
53
58
|
/** False while rendering a bulk update (a replacement or a paste-sized append); Block captures it per update. */
|
|
@@ -72,10 +77,11 @@ export declare class StreamdownContext<Source extends Record<string, any> = Reco
|
|
|
72
77
|
snippets: Snippets<Source>;
|
|
73
78
|
highlightTheme: HighlightTheme;
|
|
74
79
|
animateUpdate: boolean;
|
|
80
|
+
tags: TagMatchers;
|
|
75
81
|
});
|
|
76
82
|
}
|
|
77
83
|
export declare const useStreamdown: () => StreamdownContext<Record<string, any>>;
|
|
78
|
-
import type { AlertToken, CodeToken, MathToken, SubSupToken, TableToken, THead, TBody, TFoot, THeadRow, TRow, TD, TH, Extension, GenericToken, CitationToken, MdxToken } from './marked/index.js';
|
|
84
|
+
import type { AlertToken, CodeToken, MathToken, SubSupToken, TableToken, THead, TBody, TFoot, THeadRow, TRow, TD, TH, Extension, GenericToken, CitationToken, MdxToken, TagMatchers } from './marked/index.js';
|
|
79
85
|
import type { Tokens } from 'marked';
|
|
80
86
|
import type { CsvSeparator } from './utils/table-export.js';
|
|
81
87
|
import type { ListItemToken, ListToken } from './marked/marked-list.js';
|
|
@@ -151,6 +157,7 @@ export type TableControls = boolean | {
|
|
|
151
157
|
enabled?: boolean;
|
|
152
158
|
copy?: boolean;
|
|
153
159
|
download?: DownloadControl<TableToken>;
|
|
160
|
+
fullscreen?: boolean;
|
|
154
161
|
csvSeparator?: CsvSeparator;
|
|
155
162
|
};
|
|
156
163
|
export type MermaidControls = boolean | {
|
|
@@ -173,6 +180,7 @@ export type ResolvedControls = {
|
|
|
173
180
|
tableCopy: boolean;
|
|
174
181
|
tableDownload: boolean;
|
|
175
182
|
tableDownloadFilename: string | ((token: TableToken) => string);
|
|
183
|
+
tableFullscreen: boolean;
|
|
176
184
|
tableCsvSeparator: CsvSeparator;
|
|
177
185
|
mermaid: boolean;
|
|
178
186
|
mermaidDownload: boolean;
|
|
@@ -206,6 +214,13 @@ export type StreamdownProps<Source extends Record<string, any> = Record<string,
|
|
|
206
214
|
[K in keyof Translations]?: Partial<Translations[K]>;
|
|
207
215
|
};
|
|
208
216
|
controls?: Controls;
|
|
217
|
+
/**
|
|
218
|
+
* Number the lines of every code block. Default false — upstream defaults it on,
|
|
219
|
+
* but turning it on here would change every existing render. A fence can flip it
|
|
220
|
+
* either way with a `lineNumbers` / `noLineNumbers` meta word, and start at N with
|
|
221
|
+
* `startLine=N`.
|
|
222
|
+
*/
|
|
223
|
+
lineNumbers?: boolean;
|
|
209
224
|
codeBlockMaxHeight?: string;
|
|
210
225
|
tableMaxHeight?: string;
|
|
211
226
|
renderHtml?: boolean | ((token: Tokens.HTML | Tokens.Tag) => string);
|
|
@@ -221,6 +236,7 @@ export type StreamdownProps<Source extends Record<string, any> = Record<string,
|
|
|
221
236
|
copy?: Snippet;
|
|
222
237
|
download?: Snippet;
|
|
223
238
|
fullscreen?: Snippet;
|
|
239
|
+
close?: Snippet;
|
|
224
240
|
zoomIn?: Snippet;
|
|
225
241
|
zoomOut?: Snippet;
|
|
226
242
|
fitView?: Snippet;
|
|
@@ -234,6 +250,21 @@ export type StreamdownProps<Source extends Record<string, any> = Record<string,
|
|
|
234
250
|
check?: Snippet;
|
|
235
251
|
};
|
|
236
252
|
extensions?: Extension[];
|
|
253
|
+
/**
|
|
254
|
+
* Extra tag names the MDX tokenizer accepts on top of PascalCase, so
|
|
255
|
+
* `<ai-thinking>**bold**</ai-thinking>` becomes a component with parsed
|
|
256
|
+
* children instead of a literal html block. Keys of `mdxComponents` are
|
|
257
|
+
* allowed automatically; nothing else lowercase is, so real HTML is untouched.
|
|
258
|
+
*/
|
|
259
|
+
customTags?: string[];
|
|
260
|
+
/** Tags whose children render verbatim — no Markdown, `**` and `_` left alone. */
|
|
261
|
+
literalTagContent?: string[];
|
|
262
|
+
/**
|
|
263
|
+
* Dedent pretty-printed HTML before parsing, so a nested `<div>` indented four
|
|
264
|
+
* spaces after a blank line is not read as an indented code block. Off by
|
|
265
|
+
* default: dedenting is lossy, and `<pre>`/`<code>` bodies are never touched.
|
|
266
|
+
*/
|
|
267
|
+
normalizeHtmlIndentation?: boolean;
|
|
237
268
|
children?: Snippet<[{
|
|
238
269
|
streamdown: StreamdownContext;
|
|
239
270
|
token: GenericToken;
|
package/dist/context.svelte.js
CHANGED
|
@@ -19,6 +19,9 @@ export const defaultTranslations = {
|
|
|
19
19
|
tableFormatHtml: 'HTML',
|
|
20
20
|
tableFormatCsv: 'CSV',
|
|
21
21
|
tableFormatTsv: 'TSV',
|
|
22
|
+
tableFullscreen: 'Expand table',
|
|
23
|
+
exitTableFullscreen: 'Collapse table',
|
|
24
|
+
table: 'Table',
|
|
22
25
|
downloadDiagram: 'Download diagram',
|
|
23
26
|
downloadDiagramPng: 'PNG',
|
|
24
27
|
downloadDiagramSvg: 'SVG',
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ export { default as Streamdown } from './Streamdown.svelte';
|
|
|
2
2
|
export { useStreamdown, defaultTranslations, type StreamdownProps, type Translations, type Controls, type CodeControls, type TableControls, type MermaidControls, type ResolvedControls } from './context.svelte.js';
|
|
3
3
|
export { extractTableData, tableDataToCSV, tableDataToTSV, tableDataToMarkdown, tableDataToHTML, type TableData, type CsvSeparator } from './utils/table-export.js';
|
|
4
4
|
export { theme, shadcnTheme, mergeTheme, type Theme } from './theme.js';
|
|
5
|
-
export { type CodeToken, type Extension, type StreamdownToken, lex, parseBlocks } from './marked/index.js';
|
|
6
|
-
export {
|
|
5
|
+
export { type CodeToken, type Extension, type StreamdownToken, type TagMatchers, compileTags, lex, parseBlocks } from './marked/index.js';
|
|
6
|
+
export { normalizeHtmlIndentation } from './utils/normalize-html-indentation.js';
|
|
7
|
+
export { parseIncompleteMarkdown, type Plugin, type CompleterOptions, IncompleteMarkdownParser } from './utils/parse-incomplete-markdown.js';
|
|
7
8
|
export { defineLanguage, type LanguageDefinition } from '@tanstack/highlight';
|
|
8
9
|
export type { HighlightTheme } from '@tanstack/highlight/theme';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { default as Streamdown } from './Streamdown.svelte';
|
|
|
2
2
|
export { useStreamdown, defaultTranslations } from './context.svelte.js';
|
|
3
3
|
export { extractTableData, tableDataToCSV, tableDataToTSV, tableDataToMarkdown, tableDataToHTML } from './utils/table-export.js';
|
|
4
4
|
export { theme, shadcnTheme, mergeTheme } from './theme.js';
|
|
5
|
-
export { lex, parseBlocks } from './marked/index.js';
|
|
5
|
+
export { compileTags, lex, parseBlocks } from './marked/index.js';
|
|
6
|
+
export { normalizeHtmlIndentation } from './utils/normalize-html-indentation.js';
|
|
6
7
|
export { parseIncompleteMarkdown, IncompleteMarkdownParser } from './utils/parse-incomplete-markdown.js';
|
|
7
8
|
export { defineLanguage } from '@tanstack/highlight';
|
package/dist/marked/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { type TableToken, type THead, type TBody, type TFoot, type THeadRow, typ
|
|
|
10
10
|
import { type DescriptionDetailToken, type DescriptionListToken, type DescriptionTermToken, type DescriptionToken } from './marked-dl.js';
|
|
11
11
|
import { type AlignToken } from './marked-align.js';
|
|
12
12
|
import { type CitationToken } from './marked-citations.js';
|
|
13
|
-
import { type MdxToken } from './marked-mdx.js';
|
|
13
|
+
import { type MdxToken, type TagMatchers } from './marked-mdx.js';
|
|
14
14
|
export type GenericToken = {
|
|
15
15
|
type: string;
|
|
16
16
|
raw: string;
|
|
@@ -32,7 +32,7 @@ export type CodeToken = Tokens.Code & {
|
|
|
32
32
|
};
|
|
33
33
|
export type StreamdownToken = Exclude<MarkedToken, Tokens.List | Tokens.ListItem | Tokens.Table | Tokens.Code> | CodeToken | ListToken | ListItemToken | MathToken | AlertToken | FootnoteToken | SubSupToken | BrToken | HrToken | TableToken | THead | TBody | TFoot | THeadRow | TRow | TH | TD | DescriptionListToken | DescriptionToken | DescriptionDetailToken | DescriptionTermToken | AlignToken | CitationToken | MdxToken;
|
|
34
34
|
export type { TableToken, THead, TBody, TFoot, THeadRow, TRow, TH, TD } from './marked-table.js';
|
|
35
|
-
export declare const lex: (markdown: string, extensions?: Extension[]) => StreamdownToken[];
|
|
35
|
+
export declare const lex: (markdown: string, extensions?: Extension[], tags?: TagMatchers) => StreamdownToken[];
|
|
36
36
|
/**
|
|
37
37
|
* Opaque incremental state for `parseBlocks`. Create one per Streamdown
|
|
38
38
|
* instance (or per simulated stream) and pass it on every call: append-only
|
|
@@ -65,5 +65,6 @@ export type ParseBlocksCache = {
|
|
|
65
65
|
lastUpdate: 'first' | 'stream' | 'bulk';
|
|
66
66
|
};
|
|
67
67
|
export declare const createParseBlocksCache: () => ParseBlocksCache;
|
|
68
|
-
export declare const parseBlocks: (markdown: string, extensions?: Extension[], cache?: ParseBlocksCache) => string[];
|
|
69
|
-
export
|
|
68
|
+
export declare const parseBlocks: (markdown: string, extensions?: Extension[], cache?: ParseBlocksCache, tags?: TagMatchers) => string[];
|
|
69
|
+
export { compileTags, DEFAULT_TAGS } from './marked-mdx.js';
|
|
70
|
+
export type { TagMatchers, MathToken, AlertToken, FootnoteToken, SubSupToken, BrToken, HrToken, AlignToken, CitationToken, MdxToken };
|
package/dist/marked/index.js
CHANGED
|
@@ -10,13 +10,14 @@ import { markedTable } from './marked-table.js';
|
|
|
10
10
|
import { markedDl } from './marked-dl.js';
|
|
11
11
|
import { markedAlign } from './marked-align.js';
|
|
12
12
|
import { markedCitations } from './marked-citations.js';
|
|
13
|
-
import { markedMdx } from './marked-mdx.js';
|
|
14
|
-
// Default plugin sets, in registration order.
|
|
15
|
-
// (and the regexes/closures inside each plugin) is
|
|
16
|
-
//
|
|
17
|
-
// the Lexer instance (e.g. footnotes
|
|
18
|
-
// Lexer per call keeps documents isolated
|
|
19
|
-
|
|
13
|
+
import { markedMdx, DEFAULT_TAGS } from './marked-mdx.js';
|
|
14
|
+
// Default plugin sets, in registration order. Built behind the options cache
|
|
15
|
+
// below, so the options object (and the regexes/closures inside each plugin) is
|
|
16
|
+
// built once per allowlist, not per chunk. The tokenizers are stateless at
|
|
17
|
+
// creation time — per-document state lives on the Lexer instance (e.g. footnotes
|
|
18
|
+
// maps, reference-link defs), so a fresh Lexer per call keeps documents isolated
|
|
19
|
+
// while the options are shared.
|
|
20
|
+
const defaultLexExtensions = (tags) => [
|
|
20
21
|
markedHr,
|
|
21
22
|
markedTable,
|
|
22
23
|
...markedFootnote(),
|
|
@@ -29,15 +30,15 @@ const DEFAULT_LEX_EXTENSIONS = [
|
|
|
29
30
|
markedDl,
|
|
30
31
|
markedAlign,
|
|
31
32
|
markedCitations,
|
|
32
|
-
markedMdx
|
|
33
|
+
markedMdx(tags)
|
|
33
34
|
];
|
|
34
|
-
const
|
|
35
|
+
const defaultBlockExtensions = (tags) => [
|
|
35
36
|
markedHr,
|
|
36
37
|
...markedFootnote(),
|
|
37
38
|
markedDl,
|
|
38
39
|
markedTable,
|
|
39
40
|
markedAlign,
|
|
40
|
-
markedMdx
|
|
41
|
+
markedMdx(tags)
|
|
41
42
|
];
|
|
42
43
|
class StreamdownTokenizer extends Tokenizer {
|
|
43
44
|
/**
|
|
@@ -104,38 +105,25 @@ const parseExtensions = (...extensions) => {
|
|
|
104
105
|
});
|
|
105
106
|
return options;
|
|
106
107
|
};
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (extensions.length === 0)
|
|
119
|
-
return DEFAULT_LEX_OPTIONS;
|
|
120
|
-
let options = lexOptionsCache.get(extensions);
|
|
121
|
-
if (!options) {
|
|
122
|
-
options = parseExtensions(...DEFAULT_LEX_EXTENSIONS, ...extensions);
|
|
123
|
-
lexOptionsCache.set(extensions, options);
|
|
124
|
-
}
|
|
125
|
-
return options;
|
|
126
|
-
};
|
|
127
|
-
const getBlockOptions = (extensions) => {
|
|
128
|
-
if (extensions.length === 0)
|
|
129
|
-
return DEFAULT_BLOCK_OPTIONS;
|
|
130
|
-
let options = blockOptionsCache.get(extensions);
|
|
131
|
-
if (!options) {
|
|
132
|
-
options = parseExtensions(...DEFAULT_BLOCK_EXTENSIONS, ...extensions.filter(({ level, applyInBlockParsing }) => level === 'block' && applyInBlockParsing));
|
|
133
|
-
blockOptionsCache.set(extensions, options);
|
|
134
|
-
}
|
|
108
|
+
// Stand-in key for "no user extensions" so both cache levels stay WeakMaps —
|
|
109
|
+
// the `extensions = []` default allocates a fresh array on every call.
|
|
110
|
+
const NO_EXTENSIONS = [];
|
|
111
|
+
const cached = (cache, tags, extensions, build) => {
|
|
112
|
+
let byExtensions = cache.get(tags);
|
|
113
|
+
if (!byExtensions)
|
|
114
|
+
cache.set(tags, (byExtensions = new WeakMap()));
|
|
115
|
+
const key = extensions.length === 0 ? NO_EXTENSIONS : extensions;
|
|
116
|
+
let options = byExtensions.get(key);
|
|
117
|
+
if (!options)
|
|
118
|
+
byExtensions.set(key, (options = build()));
|
|
135
119
|
return options;
|
|
136
120
|
};
|
|
137
|
-
|
|
138
|
-
|
|
121
|
+
const lexOptionsCache = new WeakMap();
|
|
122
|
+
const blockOptionsCache = new WeakMap();
|
|
123
|
+
const getLexOptions = (extensions, tags) => cached(lexOptionsCache, tags, extensions, () => parseExtensions(...defaultLexExtensions(tags), ...extensions));
|
|
124
|
+
const getBlockOptions = (extensions, tags) => cached(blockOptionsCache, tags, extensions, () => parseExtensions(...defaultBlockExtensions(tags), ...extensions.filter(({ level, applyInBlockParsing }) => level === 'block' && applyInBlockParsing)));
|
|
125
|
+
export const lex = (markdown, extensions = [], tags = DEFAULT_TAGS) => {
|
|
126
|
+
return new Lexer(getLexOptions(extensions, tags))
|
|
139
127
|
.lex(markdown)
|
|
140
128
|
.filter((token) => token.type !== 'space' && token.type !== 'footnote');
|
|
141
129
|
};
|
|
@@ -182,7 +170,7 @@ class SplitLexer extends Lexer {
|
|
|
182
170
|
return tokens;
|
|
183
171
|
}
|
|
184
172
|
}
|
|
185
|
-
const blockTokensOf = (markdown, extensions) => new SplitLexer(getBlockOptions(extensions)).blockTokens(markdown, []);
|
|
173
|
+
const blockTokensOf = (markdown, extensions, tags) => new SplitLexer(getBlockOptions(extensions, tags)).blockTokens(markdown, []);
|
|
186
174
|
/**
|
|
187
175
|
* Is `markdown` an append to `cache.content`?
|
|
188
176
|
*
|
|
@@ -237,7 +225,7 @@ const updateKind = (isAppend, previousLength, length) => {
|
|
|
237
225
|
return previousLength === 0 ? 'first' : 'bulk';
|
|
238
226
|
return length - previousLength > BULK_APPEND_CHARS ? 'bulk' : 'stream';
|
|
239
227
|
};
|
|
240
|
-
export const parseBlocks = (markdown, extensions = [], cache) => {
|
|
228
|
+
export const parseBlocks = (markdown, extensions = [], cache, tags = DEFAULT_TAGS) => {
|
|
241
229
|
// Whether this call extends the content the cache already described — decided
|
|
242
230
|
// by the same probe the fast path uses, so the contiguity fallback below still
|
|
243
231
|
// counts as an append for the animation's purposes.
|
|
@@ -257,7 +245,7 @@ export const parseBlocks = (markdown, extensions = [], cache) => {
|
|
|
257
245
|
const offset = cache.offsets[cut];
|
|
258
246
|
if (appendable(markdown, cache, cut, offset)) {
|
|
259
247
|
isAppend = true;
|
|
260
|
-
const tailTokens = blockTokensOf(markdown.slice(offset), extensions);
|
|
248
|
+
const tailTokens = blockTokensOf(markdown.slice(offset), extensions, tags);
|
|
261
249
|
let tailLength = 0;
|
|
262
250
|
for (const token of tailTokens)
|
|
263
251
|
tailLength += token.raw.length;
|
|
@@ -295,7 +283,7 @@ export const parseBlocks = (markdown, extensions = [], cache) => {
|
|
|
295
283
|
}
|
|
296
284
|
}
|
|
297
285
|
// Full parse (first call, non-append update, or contiguity fallback).
|
|
298
|
-
const tokens = blockTokensOf(markdown, extensions);
|
|
286
|
+
const tokens = blockTokensOf(markdown, extensions, tags);
|
|
299
287
|
if (cache) {
|
|
300
288
|
cache.raws.length = 0;
|
|
301
289
|
cache.keep.length = 0;
|
|
@@ -329,3 +317,4 @@ export const parseBlocks = (markdown, extensions = [], cache) => {
|
|
|
329
317
|
}
|
|
330
318
|
return blocks;
|
|
331
319
|
};
|
|
320
|
+
export { compileTags, DEFAULT_TAGS } from './marked-mdx.js';
|
|
@@ -9,4 +9,30 @@ export type MdxToken = {
|
|
|
9
9
|
tokens?: Token[];
|
|
10
10
|
text?: string;
|
|
11
11
|
};
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The tag vocabulary the mdx tokenizer and the streaming completer share,
|
|
14
|
+
* compiled once per allowlist (upstream fb9f97c/b392fbe). PascalCase is always
|
|
15
|
+
* in it; lowercase/hyphenated names only ever come from `customTags` /
|
|
16
|
+
* `mdxComponents`, so this can never swallow a real HTML tag the way a blanket
|
|
17
|
+
* `/^<([a-zA-Z][\w-]*)/` would.
|
|
18
|
+
*/
|
|
19
|
+
export type TagMatchers = {
|
|
20
|
+
/** `<Tag …/>` */
|
|
21
|
+
selfClosing: RegExp;
|
|
22
|
+
/** `<Tag …>` */
|
|
23
|
+
openTag: RegExp;
|
|
24
|
+
/** `</Tag>` */
|
|
25
|
+
closeTag: RegExp;
|
|
26
|
+
/** `<Tag …>…</Tag>`, both ends on one line */
|
|
27
|
+
complete: RegExp;
|
|
28
|
+
/** a bare allowed tag name, anchored */
|
|
29
|
+
name: RegExp;
|
|
30
|
+
/** the non-PascalCase names, kept as strings so a half-typed one can be prefix-matched */
|
|
31
|
+
names: string[];
|
|
32
|
+
/** tags whose children are emitted as one literal text token */
|
|
33
|
+
literal: Set<string>;
|
|
34
|
+
};
|
|
35
|
+
export declare const compileTags: (customTags?: string[], literalTagContent?: string[]) => TagMatchers;
|
|
36
|
+
/** PascalCase only — what every call that passes no allowlist gets. */
|
|
37
|
+
export declare const DEFAULT_TAGS: TagMatchers;
|
|
38
|
+
export declare const markedMdx: (tags?: TagMatchers) => Extension;
|
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import { Lexer } from 'marked';
|
|
2
|
+
const escapeName = (name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3
|
+
// Attribute names take hyphens (`data-id`), not just word characters.
|
|
4
|
+
const ATTRS = '((?:\\s+[\\w-]+=(?:"[^"]*"|{[^}]*}))*)';
|
|
5
|
+
export const compileTags = (customTags, literalTagContent) => {
|
|
6
|
+
// A tag whose content is literal is a custom tag by definition: listing it
|
|
7
|
+
// once in `literalTagContent` must be enough, without repeating it in
|
|
8
|
+
// `customTags`.
|
|
9
|
+
const allowed = [...new Set([...(customTags ?? []), ...(literalTagContent ?? [])])];
|
|
10
|
+
const names = ['[A-Z][a-zA-Z0-9]*', ...allowed.map(escapeName)].join('|');
|
|
11
|
+
const tag = `(${names})`;
|
|
12
|
+
return {
|
|
13
|
+
selfClosing: new RegExp(`^<${tag}${ATTRS}\\s*/>`),
|
|
14
|
+
openTag: new RegExp(`^<${tag}${ATTRS}\\s*>`),
|
|
15
|
+
closeTag: new RegExp(`^</${tag}>`),
|
|
16
|
+
complete: new RegExp(`^<${tag}${ATTRS}\\s*>.*?</\\1>`),
|
|
17
|
+
name: new RegExp(`^(?:${names})$`),
|
|
18
|
+
names: allowed,
|
|
19
|
+
literal: new Set(literalTagContent ?? [])
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/** PascalCase only — what every call that passes no allowlist gets. */
|
|
23
|
+
export const DEFAULT_TAGS = compileTags();
|
|
2
24
|
const defaultLexer = new Lexer({ gfm: true });
|
|
3
25
|
const defaultTokenizer = defaultLexer.options.tokenizer;
|
|
4
26
|
/**
|
|
@@ -8,7 +30,7 @@ const defaultTokenizer = defaultLexer.options.tokenizer;
|
|
|
8
30
|
function parseAttributes(attributeString) {
|
|
9
31
|
const attributes = {};
|
|
10
32
|
// Pattern: attr="value" or attr={value}
|
|
11
|
-
const attrPattern = /(\w+)=(?:"([^"]*)"|{([^}]*)})/g;
|
|
33
|
+
const attrPattern = /([\w-]+)=(?:"([^"]*)"|{([^}]*)})/g;
|
|
12
34
|
let match;
|
|
13
35
|
while ((match = attrPattern.exec(attributeString)) !== null) {
|
|
14
36
|
const [, name, stringValue, expressionValue] = match;
|
|
@@ -38,16 +60,17 @@ function parseAttributes(attributeString) {
|
|
|
38
60
|
}
|
|
39
61
|
return attributes;
|
|
40
62
|
}
|
|
41
|
-
export const markedMdx = {
|
|
63
|
+
export const markedMdx = (tags = DEFAULT_TAGS) => ({
|
|
42
64
|
name: 'mdx',
|
|
43
65
|
level: 'block',
|
|
44
66
|
applyInBlockParsing: true,
|
|
45
67
|
tokenizer(src) {
|
|
46
|
-
// Match
|
|
68
|
+
// Match a tag from the allowlist (PascalCase plus anything `customTags` /
|
|
69
|
+
// `mdxComponents` added).
|
|
47
70
|
// Self-closing: <Component attr="value" />
|
|
48
71
|
// With children: <Component attr="value">content</Component>
|
|
49
72
|
// First try self-closing tag
|
|
50
|
-
const selfClosingMatch = src.match(
|
|
73
|
+
const selfClosingMatch = src.match(tags.selfClosing);
|
|
51
74
|
if (selfClosingMatch) {
|
|
52
75
|
const [raw, tagName, attributeString] = selfClosingMatch;
|
|
53
76
|
const attributes = parseAttributes(attributeString);
|
|
@@ -60,7 +83,7 @@ export const markedMdx = {
|
|
|
60
83
|
};
|
|
61
84
|
}
|
|
62
85
|
// Try paired tag with children
|
|
63
|
-
const openTagMatch = src.match(
|
|
86
|
+
const openTagMatch = src.match(tags.openTag);
|
|
64
87
|
if (openTagMatch) {
|
|
65
88
|
const [openTag, tagName, attributeString] = openTagMatch;
|
|
66
89
|
const attributes = parseAttributes(attributeString);
|
|
@@ -106,8 +129,14 @@ export const markedMdx = {
|
|
|
106
129
|
const contentStart = openTag.length;
|
|
107
130
|
const content = src.substring(contentStart, closingIndex);
|
|
108
131
|
const raw = src.substring(0, closingIndex + closingTag.length);
|
|
109
|
-
// Parse children as markdown
|
|
110
|
-
|
|
132
|
+
// Parse children as markdown — unless the tag is in `literalTagContent`,
|
|
133
|
+
// where the inner text is data, not Markdown (upstream b392fbe): one
|
|
134
|
+
// text token, nothing escaped, `**` and `_` left alone.
|
|
135
|
+
const tokens = tags.literal.has(tagName)
|
|
136
|
+
? [{ type: 'text', raw: content, text: content }]
|
|
137
|
+
: content.trim()
|
|
138
|
+
? this.lexer.blockTokens(content.trim(), [])
|
|
139
|
+
: [];
|
|
111
140
|
return {
|
|
112
141
|
type: 'mdx',
|
|
113
142
|
raw,
|
|
@@ -121,4 +150,4 @@ export const markedMdx = {
|
|
|
121
150
|
}
|
|
122
151
|
return undefined;
|
|
123
152
|
}
|
|
124
|
-
};
|
|
153
|
+
});
|
package/dist/theme.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export declare const theme: {
|
|
|
44
44
|
language: string;
|
|
45
45
|
pre: string;
|
|
46
46
|
line: string;
|
|
47
|
+
lineNumber: string;
|
|
47
48
|
};
|
|
48
49
|
codespan: {
|
|
49
50
|
base: string;
|
|
@@ -68,6 +69,7 @@ export declare const theme: {
|
|
|
68
69
|
table: {
|
|
69
70
|
base: string;
|
|
70
71
|
table: string;
|
|
72
|
+
expanded: string;
|
|
71
73
|
};
|
|
72
74
|
thead: {
|
|
73
75
|
base: string;
|
|
@@ -196,6 +198,7 @@ export declare const shadcnTheme: {
|
|
|
196
198
|
language: string;
|
|
197
199
|
pre: string;
|
|
198
200
|
line: string;
|
|
201
|
+
lineNumber: string;
|
|
199
202
|
};
|
|
200
203
|
codespan: {
|
|
201
204
|
base: string;
|
|
@@ -220,6 +223,7 @@ export declare const shadcnTheme: {
|
|
|
220
223
|
table: {
|
|
221
224
|
base: string;
|
|
222
225
|
table: string;
|
|
226
|
+
expanded: string;
|
|
223
227
|
};
|
|
224
228
|
thead: {
|
|
225
229
|
base: string;
|
|
@@ -353,6 +357,7 @@ export declare const mergeTheme: (customTheme?: DeepPartialTheme, baseTheme?: "t
|
|
|
353
357
|
language: string;
|
|
354
358
|
pre: string;
|
|
355
359
|
line: string;
|
|
360
|
+
lineNumber: string;
|
|
356
361
|
};
|
|
357
362
|
codespan: {
|
|
358
363
|
base: string;
|
|
@@ -377,6 +382,7 @@ export declare const mergeTheme: (customTheme?: DeepPartialTheme, baseTheme?: "t
|
|
|
377
382
|
table: {
|
|
378
383
|
base: string;
|
|
379
384
|
table: string;
|
|
385
|
+
expanded: string;
|
|
380
386
|
};
|
|
381
387
|
thead: {
|
|
382
388
|
base: string;
|
package/dist/theme.js
CHANGED
|
@@ -44,7 +44,10 @@ export const theme = {
|
|
|
44
44
|
buttons: 'flex items-center gap-2',
|
|
45
45
|
language: 'ml-1 font-mono lowercase',
|
|
46
46
|
pre: 'overflow-x-auto font-mono p-0 bg-gray-100/40',
|
|
47
|
-
line: 'block'
|
|
47
|
+
line: 'block',
|
|
48
|
+
// Gutter appearance only; the counter itself is a global rule in
|
|
49
|
+
// Streamdown.svelte (see the comment there) so numbers work without Tailwind.
|
|
50
|
+
lineNumber: 'before:inline-block before:w-8 before:pr-3 before:text-right before:text-gray-400 before:select-none'
|
|
48
51
|
},
|
|
49
52
|
codespan: {
|
|
50
53
|
base: 'bg-gray-100 rounded px-1.5 py-0.5 font-mono text-[0.9em]'
|
|
@@ -68,7 +71,10 @@ export const theme = {
|
|
|
68
71
|
},
|
|
69
72
|
table: {
|
|
70
73
|
base: 'overflow-x-auto max-w-full my-4 border border-gray-200 rounded-lg',
|
|
71
|
-
table: 'w-full border-collapse min-w-full'
|
|
74
|
+
table: 'w-full border-collapse min-w-full',
|
|
75
|
+
// Added to `base` while fullscreen; the fixed positioning itself comes from
|
|
76
|
+
// the global [data-expanded='true'] rule.
|
|
77
|
+
expanded: 'overflow-auto bg-white p-2 pt-10 shadow-2xl'
|
|
72
78
|
},
|
|
73
79
|
thead: {
|
|
74
80
|
base: 'bg-gray-200/80'
|
|
@@ -196,7 +202,8 @@ export const shadcnTheme = {
|
|
|
196
202
|
buttons: 'flex items-center gap-2',
|
|
197
203
|
language: 'ml-1 font-mono lowercase',
|
|
198
204
|
pre: 'overflow-x-auto font-mono p-0 bg-muted/40',
|
|
199
|
-
line: 'block '
|
|
205
|
+
line: 'block ',
|
|
206
|
+
lineNumber: 'before:inline-block before:w-8 before:pr-3 before:text-right before:text-muted-foreground before:select-none'
|
|
200
207
|
},
|
|
201
208
|
codespan: {
|
|
202
209
|
base: 'bg-muted rounded px-1.5 py-0.5 font-mono text-foreground text-[0.9em]'
|
|
@@ -220,7 +227,8 @@ export const shadcnTheme = {
|
|
|
220
227
|
},
|
|
221
228
|
table: {
|
|
222
229
|
base: 'overflow-x-auto max-w-full my-4 rounded-lg border border-border',
|
|
223
|
-
table: 'w-full border-collapse min-w-full'
|
|
230
|
+
table: 'w-full border-collapse min-w-full',
|
|
231
|
+
expanded: 'overflow-auto bg-background p-2 pt-10 shadow-2xl'
|
|
224
232
|
},
|
|
225
233
|
thead: {
|
|
226
234
|
base: 'bg-muted/80'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fullscreen half of the mermaid expand behaviour (Mermaid.svelte:264-267)
|
|
3
|
+
* without panzoom's `expand()`, which is entangled with `zoomToFit` and a FLIP
|
|
4
|
+
* animation a table has no use for: state, Escape and focus only. The
|
|
5
|
+
* `position: fixed` comes from the global `[data-expanded='true']` rule in
|
|
6
|
+
* Streamdown.svelte.
|
|
7
|
+
*
|
|
8
|
+
* The flag lives in the caller (so the markup can drive `role`/`aria-*`/the
|
|
9
|
+
* expanded class from it); this only drives it.
|
|
10
|
+
*
|
|
11
|
+
* // ponytail: no open/close animation. Upgrade path: measure the target rect
|
|
12
|
+
* // either side of `set()` and run the same first/last invert panzoom does,
|
|
13
|
+
* // once that block is untangled from the zoom state it reads today.
|
|
14
|
+
*/
|
|
15
|
+
export declare const useExpand: (opts: {
|
|
16
|
+
expanded: boolean;
|
|
17
|
+
getTarget: () => HTMLElement | null | undefined;
|
|
18
|
+
}) => {
|
|
19
|
+
readonly expanded: boolean;
|
|
20
|
+
toggle: (from?: HTMLElement | null) => void;
|
|
21
|
+
};
|