markstream-svelte 0.0.1-beta.5 → 0.0.1
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 +18 -2
- package/dist/components/ImageNode.svelte +36 -15
- package/dist/components/LinkNode.svelte +4 -2
- package/dist/components/MermaidBlockNode.svelte +34 -8
- package/dist/components/MermaidBlockNode.svelte.d.ts +1 -0
- package/dist/components/NodeRenderer.svelte +4 -15
- package/dist/components/shared/node-helpers.d.ts +1 -0
- package/dist/composables/useSmoothMarkdownStream.svelte.d.ts +1 -1
- package/dist/composables/useSmoothMarkdownStream.svelte.js +2 -1
- package/dist/enhanceRenderedHtml.js +9 -4
- package/dist/renderMarkdownHtml.js +7 -4
- package/package.json +44 -14
package/README.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# markstream-svelte
|
|
2
2
|
|
|
3
|
-
Svelte 5
|
|
3
|
+
Svelte 5 streaming Markdown renderer for AI chat, LLM token streams, SSE/WebSocket output, incomplete Markdown states, long documents, custom components, Mermaid, KaTeX, Monaco, D2, and Infographic.
|
|
4
|
+
|
|
5
|
+
## When to use it
|
|
6
|
+
|
|
7
|
+
Use `markstream-svelte` when Markdown changes while users are reading it:
|
|
8
|
+
LLM output, SSE streams, WebSocket streams, AI chat messages, long generated answers,
|
|
9
|
+
progressive diagrams, math, or code blocks.
|
|
10
|
+
|
|
11
|
+
For normal chat streaming, start with the raw `content` string path. Use pre-parsed
|
|
12
|
+
`nodes` only when another part of your app already owns the parser or AST state.
|
|
13
|
+
|
|
14
|
+
## Known limitations
|
|
15
|
+
|
|
16
|
+
- **Svelte 5 only.** Svelte 4 is not supported.
|
|
17
|
+
- This package is currently beta. Check npm and the [Svelte guide](https://markstream.simonhe.me/guide/svelte) for the latest API maturity.
|
|
18
|
+
- It is not the first choice for short static Markdown or apps that require a fully stable Svelte 4-compatible API.
|
|
4
19
|
|
|
5
20
|
## Install
|
|
6
21
|
|
|
@@ -8,7 +23,8 @@ Svelte 5-only renderer aligned with `markstream-vue`, `markstream-vue2`, and `ma
|
|
|
8
23
|
pnpm add markstream-svelte svelte@^5
|
|
9
24
|
```
|
|
10
25
|
|
|
11
|
-
Optional heavy renderers stay as peer dependencies, matching the Vue and React packages
|
|
26
|
+
Optional heavy renderers stay as peer dependencies, matching the Vue and React packages.
|
|
27
|
+
Plain Markdown does not require these packages:
|
|
12
28
|
|
|
13
29
|
```bash
|
|
14
30
|
pnpm add katex mermaid stream-monaco @terrastruct/d2 @antv/infographic
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SvelteRenderableNode } from './shared/node-helpers'
|
|
3
|
+
import { sanitizeImageSrc } from 'stream-markdown-parser'
|
|
3
4
|
import { useSafeI18n } from '../i18n/useSafeI18n'
|
|
4
5
|
import { getString } from './shared/node-helpers'
|
|
5
6
|
|
|
@@ -21,38 +22,58 @@
|
|
|
21
22
|
|
|
22
23
|
const { t } = useSafeI18n()
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
type ImageStage = 'primary' | 'fallback' | 'failed'
|
|
26
|
+
|
|
27
|
+
function resolveImageState(primarySrc: string, fallbackSrc: string, loading: boolean): { src: string; stage: ImageStage } {
|
|
28
|
+
if (loading || primarySrc)
|
|
29
|
+
return { src: primarySrc, stage: 'primary' }
|
|
30
|
+
if (fallbackSrc)
|
|
31
|
+
return { src: fallbackSrc, stage: 'fallback' }
|
|
32
|
+
return { src: '', stage: 'failed' }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let safeNodeSrc = $derived(sanitizeImageSrc((node as any)?.src))
|
|
36
|
+
let safeFallbackSrc = $derived(sanitizeImageSrc(fallbackSrc))
|
|
25
37
|
let alt = $derived(getString((node as any)?.alt))
|
|
26
38
|
let title = $derived(getString((node as any)?.title))
|
|
27
39
|
let raw = $derived(getString((node as any)?.raw))
|
|
28
40
|
let isLoading = $derived(Boolean((node as any)?.loading))
|
|
29
41
|
let useEagerImagePath = $derived(!lazy)
|
|
30
42
|
|
|
31
|
-
let
|
|
32
|
-
let
|
|
33
|
-
let
|
|
43
|
+
let initialSafeNodeSrc = untrack(() => sanitizeImageSrc((node as any)?.src))
|
|
44
|
+
let initialSafeFallbackSrc = untrack(() => sanitizeImageSrc(fallbackSrc))
|
|
45
|
+
let initialIsLoading = untrack(() => Boolean((node as any)?.loading))
|
|
46
|
+
let initialImageState = resolveImageState(initialSafeNodeSrc, initialSafeFallbackSrc, initialIsLoading)
|
|
47
|
+
let previousSafeNodeSrc = $state(initialSafeNodeSrc)
|
|
48
|
+
let previousSafeFallbackSrc = $state(initialSafeFallbackSrc)
|
|
49
|
+
let previousIsLoading = $state(initialIsLoading)
|
|
50
|
+
let currentSrc = $state(initialImageState.src)
|
|
51
|
+
let imageStage = $state<ImageStage>(initialImageState.stage)
|
|
34
52
|
let imageLoaded = $state(false)
|
|
35
|
-
let hasError = $state(
|
|
36
|
-
let fallbackTried = $state(false)
|
|
53
|
+
let hasError = $state(initialImageState.stage === 'failed')
|
|
37
54
|
|
|
38
55
|
$effect.pre(() => {
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
if (safeNodeSrc !== previousSafeNodeSrc || safeFallbackSrc !== previousSafeFallbackSrc || isLoading !== previousIsLoading) {
|
|
57
|
+
previousSafeNodeSrc = safeNodeSrc
|
|
58
|
+
previousSafeFallbackSrc = safeFallbackSrc
|
|
59
|
+
previousIsLoading = isLoading
|
|
60
|
+
const next = resolveImageState(safeNodeSrc, safeFallbackSrc, isLoading)
|
|
61
|
+
currentSrc = next.src
|
|
62
|
+
imageStage = next.stage
|
|
42
63
|
imageLoaded = false
|
|
43
|
-
hasError =
|
|
44
|
-
fallbackTried = false
|
|
64
|
+
hasError = next.stage === 'failed'
|
|
45
65
|
}
|
|
46
66
|
})
|
|
47
67
|
|
|
48
68
|
function handleImageError() {
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
if (imageStage === 'primary' && safeFallbackSrc && safeFallbackSrc !== currentSrc) {
|
|
70
|
+
currentSrc = safeFallbackSrc
|
|
71
|
+
imageStage = 'fallback'
|
|
52
72
|
imageLoaded = false
|
|
53
73
|
hasError = false
|
|
54
74
|
return
|
|
55
75
|
}
|
|
76
|
+
imageStage = 'failed'
|
|
56
77
|
hasError = true
|
|
57
78
|
imageLoaded = false
|
|
58
79
|
}
|
|
@@ -69,7 +90,7 @@
|
|
|
69
90
|
</script>
|
|
70
91
|
|
|
71
92
|
<span class="image-node-container">
|
|
72
|
-
{#if !isLoading && !hasError}
|
|
93
|
+
{#if !isLoading && !hasError && currentSrc}
|
|
73
94
|
<img
|
|
74
95
|
class="image-node__img"
|
|
75
96
|
class:is-loaded={useEagerImagePath || imageLoaded}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SvelteRenderableNode, SvelteRenderContext } from './shared/node-helpers'
|
|
3
|
+
import { sanitizeHtmlAttrs, shouldOpenLinkInNewTab } from 'stream-markdown-parser'
|
|
3
4
|
import { hideTooltip, showTooltipForAnchor } from '../tooltip/singletonTooltip'
|
|
4
5
|
import RenderChildren from './RenderChildren.svelte'
|
|
5
6
|
import { getNodeList, getString } from './shared/node-helpers'
|
|
@@ -13,11 +14,12 @@
|
|
|
13
14
|
|
|
14
15
|
let { node, context, indexKey, showTooltip }: Props = $props();
|
|
15
16
|
|
|
16
|
-
let href = $derived(getString((node as any)?.href));
|
|
17
|
+
let href = $derived(sanitizeHtmlAttrs({ href: getString((node as any)?.href) }, 'safe', 'a').href ?? '');
|
|
17
18
|
let title = $derived(getString((node as any)?.title || href));
|
|
18
19
|
let children = $derived(getNodeList((node as any)?.children));
|
|
19
20
|
let tooltipEnabled = $derived(showTooltip ?? context?.showTooltips ?? true);
|
|
20
21
|
let isHashLink = $derived(href.startsWith('#') && href.length > 1);
|
|
22
|
+
let openInNewTab = $derived(shouldOpenLinkInNewTab(href));
|
|
21
23
|
|
|
22
24
|
function showLinkTooltip(event: MouseEvent | FocusEvent) {
|
|
23
25
|
if (!tooltipEnabled || !title)
|
|
@@ -44,4 +46,4 @@
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
</script>
|
|
47
|
-
<a class:link-loading={Boolean((node as any)?.loading)} class="link-node" href={href || undefined} title={tooltipEnabled ? undefined : title} onblur={() => hideTooltip()} onclick={scrollToHashTarget} onfocus={showLinkTooltip} onmouseleave={() => hideTooltip()} onmouseenter={showLinkTooltip} target={
|
|
49
|
+
<a class:link-loading={Boolean((node as any)?.loading)} class="link-node" href={href || undefined} title={tooltipEnabled ? undefined : title} onblur={() => hideTooltip()} onclick={scrollToHashTarget} onfocus={showLinkTooltip} onmouseleave={() => hideTooltip()} onmouseenter={showLinkTooltip} target={openInNewTab ? '_blank' : undefined} rel={openInNewTab ? 'noreferrer noopener' : undefined}><span class="link-text-wrapper"><span class="link-text">{#if children.length}<RenderChildren nodes={children} context={context} prefix={String(indexKey ?? 'link') + '-link'} />{:else}{getString((node as any)?.text || href)}{/if}</span>{#if (node as any)?.loading}<span class="link-loading-indicator"></span>{/if}</span></a>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SvelteRenderableNode, SvelteRenderContext } from './shared/node-helpers'
|
|
3
|
-
import { onMount, untrack } from 'svelte'
|
|
3
|
+
import { onMount, tick, untrack } from 'svelte'
|
|
4
|
+
import { toSafeMermaidSvgMarkup } from 'stream-markdown-parser'
|
|
4
5
|
import { useSafeI18n } from '../i18n/useSafeI18n'
|
|
5
6
|
import { getMermaid } from '../optional/mermaid'
|
|
6
|
-
import { toSafeSvgMarkup } from '../sanitizeSvg'
|
|
7
7
|
import { hideTooltip, showTooltipForAnchor, type TooltipPlacement } from '../tooltip/singletonTooltip'
|
|
8
8
|
import { getLanguageIcon } from '../utils/languageIcon'
|
|
9
9
|
import { canParseOffthread, findPrefixOffthread } from '../workers/mermaidWorkerClient'
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
showCollapseButton?: boolean
|
|
34
34
|
showZoomControls?: boolean
|
|
35
35
|
isStrict?: boolean
|
|
36
|
+
enableMermaidInteractions?: boolean
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
let {
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
showCollapseButton = true,
|
|
56
57
|
showZoomControls = true,
|
|
57
58
|
isStrict = true,
|
|
59
|
+
enableMermaidInteractions = false,
|
|
58
60
|
}: Props = $props()
|
|
59
61
|
|
|
60
62
|
const { t } = useSafeI18n()
|
|
@@ -75,8 +77,11 @@
|
|
|
75
77
|
let showSource = $state(false)
|
|
76
78
|
let modalOpen = $state(false)
|
|
77
79
|
let zoom = $state(1)
|
|
80
|
+
let previewHost: HTMLElement | null = $state(null)
|
|
81
|
+
let modalHost: HTMLElement | null = $state(null)
|
|
78
82
|
let renderTimer: ReturnType<typeof setTimeout> | null = $state(null)
|
|
79
83
|
let copyTimer: ReturnType<typeof setTimeout> | null = $state(null)
|
|
84
|
+
let lastMermaidBindFunctions: ((element: Element) => unknown) | null = null
|
|
80
85
|
|
|
81
86
|
let source = $derived(normalizeMermaidSource(getString((node as any)?.code)))
|
|
82
87
|
let nodeLoading = $derived(typeof (node as any)?.loading === 'boolean' ? Boolean((node as any)?.loading) : true)
|
|
@@ -121,6 +126,11 @@
|
|
|
121
126
|
}
|
|
122
127
|
})
|
|
123
128
|
|
|
129
|
+
$effect(() => {
|
|
130
|
+
if (mounted && modalOpen && svgMarkup && modalHost && enableMermaidInteractions)
|
|
131
|
+
untrack(() => void tick().then(() => bindCurrentMermaidInteractions(modalHost)))
|
|
132
|
+
})
|
|
133
|
+
|
|
124
134
|
function clearRenderTimer() {
|
|
125
135
|
if (!renderTimer)
|
|
126
136
|
return
|
|
@@ -239,10 +249,11 @@
|
|
|
239
249
|
return
|
|
240
250
|
|
|
241
251
|
const rawSvg = typeof rendered === 'string' ? rendered : rendered?.svg
|
|
242
|
-
const safeSvg =
|
|
252
|
+
const safeSvg = toSafeMermaidSvgMarkup(rawSvg)
|
|
243
253
|
if (!safeSvg)
|
|
244
254
|
throw new Error('Mermaid rendered empty SVG.')
|
|
245
255
|
svgMarkup = safeSvg
|
|
256
|
+
lastMermaidBindFunctions = typeof rendered === 'string' ? null : rendered?.bindFunctions ?? null
|
|
246
257
|
renderError = ''
|
|
247
258
|
lastProgressiveMissSignature = ''
|
|
248
259
|
if (fullRender) {
|
|
@@ -250,8 +261,14 @@
|
|
|
250
261
|
lastRenderedCode = normalized
|
|
251
262
|
svgCache[theme] = safeSvg
|
|
252
263
|
}
|
|
253
|
-
if (typeof rendered !== 'string')
|
|
254
|
-
|
|
264
|
+
if (enableMermaidInteractions && typeof rendered !== 'string') {
|
|
265
|
+
await tick()
|
|
266
|
+
if (mounted && token === renderToken) {
|
|
267
|
+
bindCurrentMermaidInteractions(previewHost)
|
|
268
|
+
if (modalOpen)
|
|
269
|
+
bindCurrentMermaidInteractions(modalHost)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
255
272
|
}
|
|
256
273
|
catch (error) {
|
|
257
274
|
if (token === renderToken) {
|
|
@@ -385,7 +402,7 @@
|
|
|
385
402
|
}
|
|
386
403
|
|
|
387
404
|
function getRenderSignature() {
|
|
388
|
-
return `${source}\n${theme}\n${isStrict}\n${final}\n${progressivePreview}`
|
|
405
|
+
return `${source}\n${theme}\n${isStrict}\n${enableMermaidInteractions}\n${final}\n${progressivePreview}`
|
|
389
406
|
}
|
|
390
407
|
|
|
391
408
|
function withTimeout<T>(run: () => Promise<T>, timeoutMs: number) {
|
|
@@ -403,6 +420,15 @@
|
|
|
403
420
|
})
|
|
404
421
|
}
|
|
405
422
|
|
|
423
|
+
function bindCurrentMermaidInteractions(element: Element | null | undefined) {
|
|
424
|
+
if (!enableMermaidInteractions || !element?.querySelector('svg'))
|
|
425
|
+
return
|
|
426
|
+
try {
|
|
427
|
+
lastMermaidBindFunctions?.(element)
|
|
428
|
+
}
|
|
429
|
+
catch {}
|
|
430
|
+
}
|
|
431
|
+
|
|
406
432
|
async function copy() {
|
|
407
433
|
await copyTextToClipboard(source)
|
|
408
434
|
context?.events?.onCopy?.(source)
|
|
@@ -543,7 +569,7 @@
|
|
|
543
569
|
<button type="button" class="mermaid-btn mermaid-action-btn mermaid-zoom-reset" aria-label={t('common.resetZoom')} onblur={() => hideTooltip()} onclick={() => (zoom = 1)} onfocus={(event) => showButtonTooltip(event, t('common.resetZoom') || 'Reset zoom')} onmouseleave={() => hideTooltip()} onmouseenter={(event) => showButtonTooltip(event, t('common.resetZoom') || 'Reset zoom')}>{Math.round(zoom * 100)}%</button>
|
|
544
570
|
</div>
|
|
545
571
|
{/if}
|
|
546
|
-
<div class="mermaid-preview markstream-svelte-mermaid" style={previewStyle}>
|
|
572
|
+
<div bind:this={previewHost} class="mermaid-preview markstream-svelte-mermaid" style={previewStyle}>
|
|
547
573
|
{#if svgMarkup}
|
|
548
574
|
{@html svgMarkup}
|
|
549
575
|
{:else if renderError}
|
|
@@ -576,7 +602,7 @@
|
|
|
576
602
|
</button>
|
|
577
603
|
</div>
|
|
578
604
|
<div class="mermaid-modal-body">
|
|
579
|
-
<div class="mermaid-modal-content markstream-svelte-mermaid" style={`transform: scale(${zoom});`}>{@html svgMarkup}</div>
|
|
605
|
+
<div bind:this={modalHost} class="mermaid-modal-content markstream-svelte-mermaid" style={`transform: scale(${zoom});`}>{@html svgMarkup}</div>
|
|
580
606
|
</div>
|
|
581
607
|
</div>
|
|
582
608
|
</div>
|
|
@@ -19,6 +19,7 @@ type Props = {
|
|
|
19
19
|
showCollapseButton?: boolean;
|
|
20
20
|
showZoomControls?: boolean;
|
|
21
21
|
isStrict?: boolean;
|
|
22
|
+
enableMermaidInteractions?: boolean;
|
|
22
23
|
};
|
|
23
24
|
declare const MermaidBlockNode: import("svelte").Component<Props, {}, "">;
|
|
24
25
|
type MermaidBlockNode = ReturnType<typeof MermaidBlockNode>;
|
|
@@ -84,8 +84,9 @@
|
|
|
84
84
|
let renderBatchToken = 0
|
|
85
85
|
const textStreamState = new Map<string, string>()
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
let hasMounted = $state(false)
|
|
88
|
+
const smoothStream = useSmoothMarkdownStream(() => smoothStreamingOptions)
|
|
89
|
+
const hasMountedForSmoothStreaming = $derived(smoothStreaming === true || hasMounted)
|
|
89
90
|
const hasNodes = $derived(Array.isArray(nodes))
|
|
90
91
|
const parentSmoothStreaming = getContext<SmoothStreamingContextValue | undefined>(
|
|
91
92
|
SMOOTH_STREAMING_CONTEXT,
|
|
@@ -107,18 +108,6 @@
|
|
|
107
108
|
const smoothStreamingEnabled = $derived(hasMountedForSmoothStreaming && smoothStreamingEligible)
|
|
108
109
|
setContext(SMOOTH_STREAMING_CONTEXT, () => smoothStreamingEnabled)
|
|
109
110
|
|
|
110
|
-
// Baseline sync: in auto mode with initial static content, ensure the smooth
|
|
111
|
-
// stream source/visible is already synced before smooth streaming activates.
|
|
112
|
-
// This prevents a blank flash when the mount gate opens and renderContent
|
|
113
|
-
// switches from raw content to smoothStream.visible (which would be empty).
|
|
114
|
-
if (
|
|
115
|
-
smoothStreaming !== true
|
|
116
|
-
&& !Array.isArray(nodes)
|
|
117
|
-
&& content
|
|
118
|
-
) {
|
|
119
|
-
smoothStream.reset(content)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
111
|
const renderContent = $derived(smoothStreamingEnabled ? smoothStream.visible : (content ?? ''))
|
|
123
112
|
const rawContent = $derived(content ?? '')
|
|
124
113
|
const smoothSourceSynced = $derived(hasNodes || smoothStream.source === rawContent)
|
|
@@ -133,7 +122,7 @@
|
|
|
133
122
|
})
|
|
134
123
|
|
|
135
124
|
onMount(() => {
|
|
136
|
-
|
|
125
|
+
hasMounted = true
|
|
137
126
|
})
|
|
138
127
|
|
|
139
128
|
$effect(() => {
|
|
@@ -46,6 +46,7 @@ export type NodeRendererMermaidProps = Partial<{
|
|
|
46
46
|
showCollapseButton: boolean;
|
|
47
47
|
showZoomControls: boolean;
|
|
48
48
|
isStrict: boolean;
|
|
49
|
+
enableMermaidInteractions: boolean;
|
|
49
50
|
}> & Record<string, unknown>;
|
|
50
51
|
export type NodeRendererD2Props = Partial<{
|
|
51
52
|
maxHeight: string | null;
|
|
@@ -16,4 +16,4 @@ export interface SmoothMarkdownStreamControllerSvelte {
|
|
|
16
16
|
pause: () => void;
|
|
17
17
|
resume: () => void;
|
|
18
18
|
}
|
|
19
|
-
export declare function useSmoothMarkdownStream(
|
|
19
|
+
export declare function useSmoothMarkdownStream(optionsOrGetter?: SmoothMarkdownStreamOptions | (() => SmoothMarkdownStreamOptions)): SmoothMarkdownStreamControllerSvelte;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createSmoothMarkdownStream } from 'markstream-core';
|
|
2
2
|
import { onDestroy } from 'svelte';
|
|
3
|
-
export function useSmoothMarkdownStream(
|
|
3
|
+
export function useSmoothMarkdownStream(optionsOrGetter = {}) {
|
|
4
|
+
const options = typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
4
5
|
let source = $state('');
|
|
5
6
|
let visible = $state('');
|
|
6
7
|
let done = $state(false);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toSafeMermaidSvgMarkup } from 'stream-markdown-parser';
|
|
1
2
|
import { getD2 } from './optional/d2';
|
|
2
3
|
import { getInfographic } from './optional/infographic';
|
|
3
4
|
import { getKatex } from './optional/katex';
|
|
@@ -212,7 +213,7 @@ async function renderKatexMarkup(source, displayMode) {
|
|
|
212
213
|
return html;
|
|
213
214
|
}
|
|
214
215
|
async function renderMermaid(root, cleanupFns, options, isActive) {
|
|
215
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
216
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
216
217
|
const strictMode = ((_a = options.mermaidProps) === null || _a === void 0 ? void 0 : _a.isStrict) !== false;
|
|
217
218
|
const mermaid = await getMermaid({
|
|
218
219
|
startOnLoad: false,
|
|
@@ -264,14 +265,18 @@ async function renderMermaid(root, cleanupFns, options, isActive) {
|
|
|
264
265
|
const svg = typeof rendered === 'string' ? rendered : rendered === null || rendered === void 0 ? void 0 : rendered.svg;
|
|
265
266
|
if (!svg)
|
|
266
267
|
continue;
|
|
267
|
-
const safeSvg =
|
|
268
|
+
const safeSvg = toSafeMermaidSvgMarkup(svg);
|
|
268
269
|
if (!safeSvg)
|
|
269
270
|
continue;
|
|
270
271
|
shell.body.innerHTML = safeSvg;
|
|
271
272
|
shell.body.classList.add('markstream-svelte-mermaid');
|
|
272
273
|
shell.wrapper.dataset.markstreamMermaid = '1';
|
|
273
|
-
if (typeof rendered !== 'string')
|
|
274
|
-
|
|
274
|
+
if (((_g = options.mermaidProps) === null || _g === void 0 ? void 0 : _g.enableMermaidInteractions) === true && typeof rendered !== 'string') {
|
|
275
|
+
try {
|
|
276
|
+
(_h = rendered === null || rendered === void 0 ? void 0 : rendered.bindFunctions) === null || _h === void 0 ? void 0 : _h.call(rendered, shell.body);
|
|
277
|
+
}
|
|
278
|
+
catch { }
|
|
279
|
+
}
|
|
275
280
|
cleanupFns.push(() => {
|
|
276
281
|
if (shell.wrapper.isConnected)
|
|
277
282
|
shell.wrapper.replaceWith(originalPre.cloneNode(true));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getMarkdown, isHtmlTagBlocked, isUnsafeHtmlUrl, NON_STRUCTURING_HTML_TAGS, normalizeCustomHtmlTagName, normalizeCustomHtmlTags, parseMarkdownToStructure, sanitizeHtmlAttrs, } from 'stream-markdown-parser';
|
|
1
|
+
import { getMarkdown, isHtmlTagBlocked, isUnsafeHtmlUrl, NON_STRUCTURING_HTML_TAGS, normalizeCustomHtmlTagName, normalizeCustomHtmlTags, parseMarkdownToStructure, sanitizeHtmlAttrs, sanitizeImageSrc, shouldOpenLinkInNewTab, } from 'stream-markdown-parser';
|
|
2
2
|
import { hydrateCustomTagContent } from './hydrateCustomTagContent';
|
|
3
3
|
import { sanitizeHtmlContent } from './sanitizeHtmlContent';
|
|
4
4
|
const DEFAULT_CACHE_KEY = 'markstream-svelte-html';
|
|
@@ -224,8 +224,9 @@ function renderLinkNode(node, ctx) {
|
|
|
224
224
|
? renderNodesToHtml(getNodeList(node.children), ctx)
|
|
225
225
|
: escapeHtml(getString(node.text || href));
|
|
226
226
|
const titleAttr = title ? ` title="${escapeAttr(title)}"` : '';
|
|
227
|
-
const
|
|
228
|
-
const
|
|
227
|
+
const safeHref = href && !isUnsafeHtmlUrl(href) ? href : '';
|
|
228
|
+
const hrefAttr = safeHref ? ` href="${escapeAttr(safeHref)}"` : '';
|
|
229
|
+
const externalAttrs = shouldOpenLinkInNewTab(safeHref) ? ' target="_blank" rel="noreferrer noopener"' : '';
|
|
229
230
|
return `<a${hrefAttr}${titleAttr}${externalAttrs}>${content}</a>`;
|
|
230
231
|
}
|
|
231
232
|
function renderMathInlineNode(node) {
|
|
@@ -237,7 +238,9 @@ function renderMathBlockNode(node) {
|
|
|
237
238
|
return `<div class="markstream-nested-math-block"><pre class="markstream-nested-math-block__source"><code>${source}</code></pre><div class="markstream-nested-math-block__render" aria-hidden="true"></div></div>`;
|
|
238
239
|
}
|
|
239
240
|
function renderImageNode(node) {
|
|
240
|
-
const src =
|
|
241
|
+
const src = sanitizeImageSrc(node.src);
|
|
242
|
+
if (!src)
|
|
243
|
+
return '';
|
|
241
244
|
const alt = getString(node.alt);
|
|
242
245
|
const title = getString(node.title);
|
|
243
246
|
const titleAttr = title ? ` title="${escapeAttr(title)}"` : '';
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markstream-svelte",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.1
|
|
5
|
-
"description": "Svelte Markdown renderer for
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Svelte 5 and SvelteKit streaming Markdown renderer for AI chat, LLM token streams, SSE/WebSocket output, incomplete Markdown, Mermaid, KaTeX, Monaco-powered code blocks, and custom Svelte components.",
|
|
6
6
|
"author": "Simon He",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"homepage": "https://
|
|
8
|
+
"homepage": "https://markstream.simonhe.me/frameworks/svelte",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/Simon-He95/markstream-vue.git",
|
|
@@ -16,11 +16,40 @@
|
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"svelte",
|
|
19
|
+
"svelte5",
|
|
20
|
+
"sveltekit",
|
|
19
21
|
"markdown",
|
|
20
22
|
"markdown-renderer",
|
|
23
|
+
"svelte-markdown",
|
|
24
|
+
"svelte-markdown-renderer",
|
|
25
|
+
"svelte5-markdown",
|
|
26
|
+
"sveltekit-markdown",
|
|
27
|
+
"svelte-streaming-markdown",
|
|
28
|
+
"svelte-ai-chat",
|
|
21
29
|
"streaming-markdown",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
30
|
+
"ai-markdown-renderer",
|
|
31
|
+
"ai-chat",
|
|
32
|
+
"llm",
|
|
33
|
+
"llm-markdown",
|
|
34
|
+
"llm-streaming",
|
|
35
|
+
"sse",
|
|
36
|
+
"sse-markdown",
|
|
37
|
+
"websocket",
|
|
38
|
+
"websocket-markdown",
|
|
39
|
+
"incomplete-markdown",
|
|
40
|
+
"mermaid",
|
|
41
|
+
"streaming-mermaid",
|
|
42
|
+
"katex",
|
|
43
|
+
"streaming-katex",
|
|
44
|
+
"streaming-code-blocks",
|
|
45
|
+
"d2",
|
|
46
|
+
"infographic",
|
|
47
|
+
"monaco-editor",
|
|
48
|
+
"large-documents",
|
|
49
|
+
"custom-components",
|
|
50
|
+
"beta",
|
|
51
|
+
"markstream-svelte",
|
|
52
|
+
"markstream"
|
|
24
53
|
],
|
|
25
54
|
"sideEffects": [
|
|
26
55
|
"**/*.css"
|
|
@@ -49,7 +78,7 @@
|
|
|
49
78
|
"@terrastruct/d2": ">=0.1.33",
|
|
50
79
|
"katex": ">=0.16.22",
|
|
51
80
|
"mermaid": ">=11",
|
|
52
|
-
"stream-monaco": ">=0.0.
|
|
81
|
+
"stream-monaco": ">=0.0.45",
|
|
53
82
|
"svelte": ">=5 <6"
|
|
54
83
|
},
|
|
55
84
|
"peerDependenciesMeta": {
|
|
@@ -71,20 +100,20 @@
|
|
|
71
100
|
},
|
|
72
101
|
"dependencies": {
|
|
73
102
|
"@floating-ui/dom": "^1.7.6",
|
|
74
|
-
"markstream-core": "
|
|
75
|
-
"stream-markdown-parser": "
|
|
103
|
+
"markstream-core": "1.0.3",
|
|
104
|
+
"stream-markdown-parser": "1.0.5"
|
|
76
105
|
},
|
|
77
106
|
"devDependencies": {
|
|
78
|
-
"@sveltejs/package": "^2.5.
|
|
107
|
+
"@sveltejs/package": "^2.5.8",
|
|
79
108
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
80
109
|
"@types/node": "^18.19.130",
|
|
81
|
-
"autoprefixer": "^10.
|
|
110
|
+
"autoprefixer": "^10.5.0",
|
|
82
111
|
"postcss-cli": "^11.0.1",
|
|
83
|
-
"svelte": "^5.
|
|
84
|
-
"svelte-check": "^4.
|
|
112
|
+
"svelte": "^5.56.3",
|
|
113
|
+
"svelte-check": "^4.6.0",
|
|
85
114
|
"tailwindcss": "^3.4.19",
|
|
86
115
|
"typescript": "^5.9.3",
|
|
87
|
-
"vite": "^7.3.
|
|
116
|
+
"vite": "^7.3.5"
|
|
88
117
|
},
|
|
89
118
|
"scripts": {
|
|
90
119
|
"build": "pnpm --dir ../markstream-core build && svelte-package -i src -o dist && postcss src/index.css -o dist/index.css && cp dist/index.css dist/index.tailwind.css && node ../../scripts/generate-px-css.mjs",
|
|
@@ -92,6 +121,7 @@
|
|
|
92
121
|
"preview": "vite preview",
|
|
93
122
|
"typecheck": "svelte-check --tsconfig ./tsconfig.json",
|
|
94
123
|
"check:core-published": "node ../../scripts/check-core-published.mjs --package-json package.json --core-package-json ../markstream-core/package.json",
|
|
95
|
-
"
|
|
124
|
+
"check:workspace-deps-published": "node ../../scripts/check-workspace-deps-published.mjs --package-json package.json",
|
|
125
|
+
"release": "pnpm run check:workspace-deps-published && bumpp --commit --no-tag --no-push && pnpm publish --access public && node ../../scripts/tag-package.mjs --package-json package.json --push"
|
|
96
126
|
}
|
|
97
127
|
}
|