markstream-svelte 0.0.1-beta.4 → 0.0.1-beta.6
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/dist/components/ImageNode.svelte +36 -15
- package/dist/components/InlineCodeNode.svelte +60 -8
- package/dist/components/InlineCodeNode.svelte.d.ts +3 -1
- 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/NodeOutlet.svelte +2 -1
- package/dist/components/NodeRenderer.svelte +243 -12
- package/dist/components/TextNode.svelte +12 -14
- package/dist/components/shared/node-helpers.d.ts +6 -0
- package/dist/components/shared/node-helpers.js +1 -0
- package/dist/composables/useSmoothMarkdownStream.svelte.d.ts +19 -0
- package/dist/composables/useSmoothMarkdownStream.svelte.js +41 -0
- package/dist/context/smoothStreaming.d.ts +15 -0
- package/dist/context/smoothStreaming.js +14 -0
- package/dist/enhanceRenderedHtml.js +9 -4
- package/dist/index.css +38 -4
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/index.px.css +38 -4
- package/dist/index.tailwind.css +38 -4
- package/dist/renderMarkdownHtml.js +7 -4
- package/package.json +11 -8
|
@@ -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,13 +1,65 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { SvelteRenderableNode } from './shared/node-helpers'
|
|
2
|
+
import type { SvelteRenderableNode, SvelteRenderContext } from './shared/node-helpers'
|
|
3
|
+
import { resolveStreamingTextState } from 'markstream-core'
|
|
3
4
|
import { getString } from './shared/node-helpers'
|
|
4
|
-
|
|
5
|
+
|
|
5
6
|
interface Props {
|
|
6
|
-
node: SvelteRenderableNode
|
|
7
|
+
node: SvelteRenderableNode
|
|
8
|
+
context?: SvelteRenderContext
|
|
9
|
+
indexKey?: string | number
|
|
7
10
|
}
|
|
8
|
-
|
|
9
|
-
let {
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
node,
|
|
14
|
+
context = undefined,
|
|
15
|
+
indexKey = undefined,
|
|
16
|
+
}: Props = $props()
|
|
17
|
+
|
|
18
|
+
let previousKey = ''
|
|
19
|
+
let previousCode = ''
|
|
20
|
+
let deltaClass = 'markstream-svelte-text__stream-delta--a'
|
|
21
|
+
|
|
22
|
+
const code = $derived(getString((node as any)?.code ?? (node as any)?.content ?? (node as any)?.raw))
|
|
23
|
+
const streamKey = $derived(
|
|
24
|
+
`${String(context?.customId ?? 'global')}:${String(context?.streamRenderVersion ?? 0)}:${String(indexKey ?? 'inline-code')}`,
|
|
25
|
+
)
|
|
26
|
+
const fadeEnabled = $derived(context?.fade !== false)
|
|
27
|
+
|
|
28
|
+
const streamInfo = $derived.by(() => {
|
|
29
|
+
const state = context?.textStreamState
|
|
30
|
+
const previous = streamKey === previousKey
|
|
31
|
+
? previousCode
|
|
32
|
+
: (state?.get(streamKey) ?? '')
|
|
33
|
+
|
|
34
|
+
const result = resolveStreamingTextState({
|
|
35
|
+
nextContent: code,
|
|
36
|
+
previousContent: previous,
|
|
37
|
+
typewriterEnabled: fadeEnabled,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (result.appended) {
|
|
41
|
+
deltaClass = deltaClass.endsWith('--a')
|
|
42
|
+
? 'markstream-svelte-text__stream-delta--b'
|
|
43
|
+
: 'markstream-svelte-text__stream-delta--a'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
previousKey = streamKey
|
|
47
|
+
previousCode = code
|
|
48
|
+
state?.set(streamKey, code)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
stableCode: result.settledContent,
|
|
52
|
+
deltaCode: result.streamedDelta,
|
|
53
|
+
deltaClass,
|
|
54
|
+
}
|
|
55
|
+
})
|
|
12
56
|
</script>
|
|
13
|
-
|
|
57
|
+
|
|
58
|
+
<code class="inline-code-node">
|
|
59
|
+
{streamInfo.stableCode}
|
|
60
|
+
{#if streamInfo.deltaCode}
|
|
61
|
+
<span class="markstream-svelte-text__stream-delta text-node-stream-delta {streamInfo.deltaClass}">
|
|
62
|
+
{streamInfo.deltaCode}
|
|
63
|
+
</span>
|
|
64
|
+
{/if}
|
|
65
|
+
</code>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { SvelteRenderableNode } from './shared/node-helpers';
|
|
1
|
+
import type { SvelteRenderableNode, SvelteRenderContext } from './shared/node-helpers';
|
|
2
2
|
interface Props {
|
|
3
3
|
node: SvelteRenderableNode;
|
|
4
|
+
context?: SvelteRenderContext;
|
|
5
|
+
indexKey?: string | number;
|
|
4
6
|
}
|
|
5
7
|
declare const InlineCodeNode: import("svelte").Component<Props, {}, "">;
|
|
6
8
|
type InlineCodeNode = ReturnType<typeof InlineCodeNode>;
|
|
@@ -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>;
|
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
isDark={context?.isDark}
|
|
104
104
|
indexKey={indexKey}
|
|
105
105
|
typewriter={context?.typewriter}
|
|
106
|
+
fade={context?.fade}
|
|
106
107
|
{...customInputs}
|
|
107
108
|
/>
|
|
108
109
|
{:else if resolvedType === 'text' || resolvedType === 'text_special'}
|
|
@@ -136,7 +137,7 @@
|
|
|
136
137
|
{:else if resolvedType === 'image'}
|
|
137
138
|
<ImageNode {node} />
|
|
138
139
|
{:else if resolvedType === 'inline_code'}
|
|
139
|
-
<InlineCodeNode {node} />
|
|
140
|
+
<InlineCodeNode {node} {context} {indexKey} />
|
|
140
141
|
{:else if resolvedType === 'strong'}
|
|
141
142
|
<StrongNode {node} {context} {indexKey} />
|
|
142
143
|
{:else if resolvedType === 'emphasis'}
|
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
SvelteRenderableNode,
|
|
6
6
|
SvelteRenderContext,
|
|
7
7
|
} from './shared/node-helpers'
|
|
8
|
-
import { onDestroy, onMount, tick } from 'svelte'
|
|
8
|
+
import { getContext, onDestroy, onMount, setContext, tick } from 'svelte'
|
|
9
9
|
import { getCustomNodeComponents, subscribeCustomComponents } from '../customComponents'
|
|
10
10
|
import { disposeRenderedHtmlEnhancements, enhanceRenderedHtml } from '../enhanceRenderedHtml'
|
|
11
11
|
import NodeOutlet from './NodeOutlet.svelte'
|
|
12
12
|
import { buildRenderContext, resolveParsedNodes } from './shared/node-helpers'
|
|
13
|
+
import { useSmoothMarkdownStream } from '../composables/useSmoothMarkdownStream.svelte'
|
|
14
|
+
import { SMOOTH_STREAMING_CONTEXT } from '../context/smoothStreaming'
|
|
15
|
+
import type { SmoothStreamingContextValue } from '../context/smoothStreaming'
|
|
13
16
|
|
|
14
17
|
type NodeRendererComponentProps = NodeRendererProps & NodeRendererEvents & {
|
|
15
18
|
className?: string
|
|
@@ -45,7 +48,8 @@
|
|
|
45
48
|
isDark = false,
|
|
46
49
|
customId = undefined,
|
|
47
50
|
indexKey = undefined,
|
|
48
|
-
typewriter =
|
|
51
|
+
typewriter = false,
|
|
52
|
+
fade = true,
|
|
49
53
|
batchRendering = true,
|
|
50
54
|
initialRenderBatchSize = 40,
|
|
51
55
|
renderBatchSize = 80,
|
|
@@ -56,6 +60,8 @@
|
|
|
56
60
|
maxLiveNodes = 320,
|
|
57
61
|
liveNodeBuffer = 60,
|
|
58
62
|
allowHtml = true,
|
|
63
|
+
smoothStreaming = 'auto' as boolean | 'auto',
|
|
64
|
+
smoothStreamingOptions = undefined,
|
|
59
65
|
className = '',
|
|
60
66
|
onCopy = undefined,
|
|
61
67
|
onHandleArtifactClick = undefined,
|
|
@@ -78,10 +84,80 @@
|
|
|
78
84
|
let renderBatchToken = 0
|
|
79
85
|
const textStreamState = new Map<string, string>()
|
|
80
86
|
|
|
87
|
+
let hasMounted = $state(false)
|
|
88
|
+
const smoothStream = useSmoothMarkdownStream(() => smoothStreamingOptions)
|
|
89
|
+
const hasMountedForSmoothStreaming = $derived(smoothStreaming === true || hasMounted)
|
|
90
|
+
const hasNodes = $derived(Array.isArray(nodes))
|
|
91
|
+
const parentSmoothStreaming = getContext<SmoothStreamingContextValue | undefined>(
|
|
92
|
+
SMOOTH_STREAMING_CONTEXT,
|
|
93
|
+
)
|
|
94
|
+
const smoothStreamingEligible = $derived.by(() => {
|
|
95
|
+
if (smoothStreaming === false)
|
|
96
|
+
return false
|
|
97
|
+
if (hasNodes)
|
|
98
|
+
return false
|
|
99
|
+
// When the parent renderer is already pacing content, avoid double-pacing
|
|
100
|
+
// in nested renderers (e.g. thinking blocks, custom tag content).
|
|
101
|
+
// Only applies in 'auto' mode — smoothStreaming === true explicitly opts in.
|
|
102
|
+
if (smoothStreaming !== true && parentSmoothStreaming?.())
|
|
103
|
+
return false
|
|
104
|
+
if (smoothStreaming === true)
|
|
105
|
+
return true
|
|
106
|
+
return typewriter === true || (maxLiveNodes ?? 0) <= 0
|
|
107
|
+
})
|
|
108
|
+
const smoothStreamingEnabled = $derived(hasMountedForSmoothStreaming && smoothStreamingEligible)
|
|
109
|
+
setContext(SMOOTH_STREAMING_CONTEXT, () => smoothStreamingEnabled)
|
|
110
|
+
|
|
111
|
+
const renderContent = $derived(smoothStreamingEnabled ? smoothStream.visible : (content ?? ''))
|
|
112
|
+
const rawContent = $derived(content ?? '')
|
|
113
|
+
const smoothSourceSynced = $derived(hasNodes || smoothStream.source === rawContent)
|
|
114
|
+
const requestedFinal = $derived.by(() => {
|
|
115
|
+
const base = parseOptions ?? {}
|
|
116
|
+
return final ?? (base as any).final
|
|
117
|
+
})
|
|
118
|
+
const effectiveFinal = $derived.by(() => {
|
|
119
|
+
if (smoothStreamingEnabled && requestedFinal != null)
|
|
120
|
+
return Boolean(requestedFinal && smoothSourceSynced && smoothStream.caughtUp)
|
|
121
|
+
return requestedFinal
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
onMount(() => {
|
|
125
|
+
hasMounted = true
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
$effect(() => {
|
|
129
|
+
const nextContent = content ?? ''
|
|
130
|
+
if (hasNodes) {
|
|
131
|
+
smoothStream.reset('')
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (!smoothStreamingEnabled) {
|
|
135
|
+
smoothStream.reset(nextContent)
|
|
136
|
+
if (requestedFinal)
|
|
137
|
+
smoothStream.finish({ flush: true })
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
const source = smoothStream.source
|
|
141
|
+
if (!nextContent) {
|
|
142
|
+
smoothStream.reset('')
|
|
143
|
+
}
|
|
144
|
+
else if (nextContent === source) {
|
|
145
|
+
// no-op
|
|
146
|
+
}
|
|
147
|
+
else if (nextContent.startsWith(source)) {
|
|
148
|
+
smoothStream.enqueue(nextContent.slice(source.length))
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
smoothStream.reset(nextContent)
|
|
152
|
+
}
|
|
153
|
+
if (requestedFinal)
|
|
154
|
+
smoothStream.finish()
|
|
155
|
+
})
|
|
156
|
+
|
|
81
157
|
let rendererProps = $derived({
|
|
82
|
-
content,
|
|
158
|
+
content: renderContent,
|
|
83
159
|
nodes,
|
|
84
|
-
final,
|
|
160
|
+
final: effectiveFinal,
|
|
85
161
|
parseOptions,
|
|
86
162
|
customMarkdownIt,
|
|
87
163
|
debugPerformance,
|
|
@@ -106,6 +182,7 @@
|
|
|
106
182
|
customId,
|
|
107
183
|
indexKey,
|
|
108
184
|
typewriter,
|
|
185
|
+
fade,
|
|
109
186
|
batchRendering,
|
|
110
187
|
initialRenderBatchSize,
|
|
111
188
|
renderBatchSize,
|
|
@@ -116,12 +193,14 @@
|
|
|
116
193
|
maxLiveNodes,
|
|
117
194
|
liveNodeBuffer,
|
|
118
195
|
allowHtml,
|
|
196
|
+
smoothStreaming,
|
|
197
|
+
smoothStreamingOptions,
|
|
119
198
|
} satisfies NodeRendererProps)
|
|
120
199
|
|
|
121
200
|
$effect.pre(() => {
|
|
122
|
-
if (previousContent !==
|
|
201
|
+
if (previousContent !== renderContent || previousNodes !== nodes) {
|
|
123
202
|
streamRenderVersion += 1
|
|
124
|
-
previousContent =
|
|
203
|
+
previousContent = renderContent
|
|
125
204
|
previousNodes = nodes
|
|
126
205
|
}
|
|
127
206
|
})
|
|
@@ -133,7 +212,7 @@
|
|
|
133
212
|
console.info('[markstream-svelte][perf] parse(sync)', {
|
|
134
213
|
ms: Math.round(performance.now() - start),
|
|
135
214
|
nodes: nextParsedNodes.length,
|
|
136
|
-
contentLength:
|
|
215
|
+
contentLength: renderContent?.length ?? 0,
|
|
137
216
|
})
|
|
138
217
|
}
|
|
139
218
|
return nextParsedNodes
|
|
@@ -167,7 +246,7 @@
|
|
|
167
246
|
void renderBatchDelay
|
|
168
247
|
void renderBatchBudgetMs
|
|
169
248
|
void renderBatchIdleTimeoutMs
|
|
170
|
-
void
|
|
249
|
+
void effectiveFinal
|
|
171
250
|
void renderedNodeCount
|
|
172
251
|
syncRenderedNodeWindow()
|
|
173
252
|
})
|
|
@@ -175,7 +254,7 @@
|
|
|
175
254
|
|
|
176
255
|
$effect(() => {
|
|
177
256
|
void rootEl
|
|
178
|
-
void
|
|
257
|
+
void effectiveFinal
|
|
179
258
|
void parsedNodes
|
|
180
259
|
void renderedNodes
|
|
181
260
|
void isDark
|
|
@@ -203,6 +282,7 @@
|
|
|
203
282
|
enhancementHandle?.dispose()
|
|
204
283
|
enhancementHandle = null
|
|
205
284
|
disposeRenderedHtmlEnhancements(rootEl)
|
|
285
|
+
clearTypewriterCursorTimeout()
|
|
206
286
|
})
|
|
207
287
|
|
|
208
288
|
function toPositiveInteger(value: unknown, fallback: number) {
|
|
@@ -213,7 +293,7 @@
|
|
|
213
293
|
function syncRenderedNodeWindow() {
|
|
214
294
|
const total = parsedNodes.length
|
|
215
295
|
const initialCount = Math.min(total, toPositiveInteger(initialRenderBatchSize, 40))
|
|
216
|
-
const shouldBatch =
|
|
296
|
+
const shouldBatch = effectiveFinal !== false && batchRendering !== false && total > initialCount
|
|
217
297
|
|
|
218
298
|
if (!shouldBatch) {
|
|
219
299
|
cancelRenderBatch()
|
|
@@ -296,7 +376,7 @@
|
|
|
296
376
|
async function scheduleEnhancement() {
|
|
297
377
|
if (!rootEl || typeof window === 'undefined')
|
|
298
378
|
return
|
|
299
|
-
const enhancementFinal = typeof
|
|
379
|
+
const enhancementFinal = typeof effectiveFinal === 'boolean' ? effectiveFinal : !hasLoadingNodes(parsedNodes)
|
|
300
380
|
if (!enhancementFinal) {
|
|
301
381
|
enhancementToken += 1
|
|
302
382
|
enhancementHandle?.dispose()
|
|
@@ -335,6 +415,156 @@
|
|
|
335
415
|
return false
|
|
336
416
|
}
|
|
337
417
|
|
|
418
|
+
// ── Typewriter cursor ──
|
|
419
|
+
let typewriterCursorEl: HTMLSpanElement | null = $state(null)
|
|
420
|
+
let showTypewriterCursor = $state(false)
|
|
421
|
+
let typewriterCursorTimeout: ReturnType<typeof setTimeout> | undefined
|
|
422
|
+
let lastTypewriterContentLength = 0
|
|
423
|
+
const TYPEWRITER_CURSOR_EXCLUDED_NODE_TYPES = new Set(['code_block', 'admonition', 'table', 'math_block', 'html_block', 'image'])
|
|
424
|
+
|
|
425
|
+
function shouldSkipTypewriterCursorForNode(node: unknown) {
|
|
426
|
+
if (!node || typeof node !== 'object')
|
|
427
|
+
return false
|
|
428
|
+
const type = (node as Record<string, unknown>).type
|
|
429
|
+
return typeof type === 'string' && TYPEWRITER_CURSOR_EXCLUDED_NODE_TYPES.has(type)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function shouldShowTypewriterCursorForCurrentNodes() {
|
|
433
|
+
const lastNode = parsedNodes[parsedNodes.length - 1]
|
|
434
|
+
return !shouldSkipTypewriterCursorForNode(lastNode)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function getNodeTextLength(node: unknown): number {
|
|
438
|
+
if (!node || typeof node !== 'object')
|
|
439
|
+
return 0
|
|
440
|
+
const record = node as Record<string, unknown>
|
|
441
|
+
const direct = record.raw ?? record.content ?? record.code
|
|
442
|
+
if (typeof direct === 'string')
|
|
443
|
+
return direct.length
|
|
444
|
+
const children = record.children
|
|
445
|
+
if (Array.isArray(children))
|
|
446
|
+
return children.reduce((total: number, child: unknown) => total + getNodeTextLength(child), 0)
|
|
447
|
+
const items = record.items
|
|
448
|
+
if (Array.isArray(items))
|
|
449
|
+
return items.reduce((total: number, item: unknown) => total + getNodeTextLength(item), 0)
|
|
450
|
+
return 0
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function getTypewriterContentLength() {
|
|
454
|
+
if (nodes?.length)
|
|
455
|
+
return nodes.reduce((total: number, node: unknown) => total + getNodeTextLength(node), 0)
|
|
456
|
+
// Use raw content length, not renderContent (which may be the paced-out
|
|
457
|
+
// visible portion when smooth streaming is active).
|
|
458
|
+
return (content ?? '').length
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function clearTypewriterCursorTimeout() {
|
|
462
|
+
if (!typewriterCursorTimeout)
|
|
463
|
+
return
|
|
464
|
+
clearTimeout(typewriterCursorTimeout)
|
|
465
|
+
typewriterCursorTimeout = undefined
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function getLastTextNode(root: HTMLElement) {
|
|
469
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
470
|
+
acceptNode(node) {
|
|
471
|
+
const text = node.textContent ?? ''
|
|
472
|
+
if (!text.trim())
|
|
473
|
+
return NodeFilter.FILTER_REJECT
|
|
474
|
+
const parent = node.parentElement
|
|
475
|
+
if (!parent)
|
|
476
|
+
return NodeFilter.FILTER_REJECT
|
|
477
|
+
if (parent.closest('.typewriter-cursor, .height-estimation-probes, [data-node-type="code_block"], [data-node-type="admonition"], [data-node-type="table"], [data-node-type="math_block"], [data-node-type="html_block"], [data-node-type="image"], script, style'))
|
|
478
|
+
return NodeFilter.FILTER_REJECT
|
|
479
|
+
return NodeFilter.FILTER_ACCEPT
|
|
480
|
+
},
|
|
481
|
+
})
|
|
482
|
+
let last: Text | null = null
|
|
483
|
+
let current = walker.nextNode()
|
|
484
|
+
while (current) {
|
|
485
|
+
last = current as Text
|
|
486
|
+
current = walker.nextNode()
|
|
487
|
+
}
|
|
488
|
+
return last
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function updateTypewriterCursorPosition() {
|
|
492
|
+
if (typeof window === 'undefined' || !showTypewriterCursor || !rootEl || !typewriterCursorEl)
|
|
493
|
+
return
|
|
494
|
+
const root = rootEl
|
|
495
|
+
const cursor = typewriterCursorEl
|
|
496
|
+
const lastText = getLastTextNode(root)
|
|
497
|
+
const rootRect = root.getBoundingClientRect()
|
|
498
|
+
let left = 0
|
|
499
|
+
let top = 0
|
|
500
|
+
let height = 20
|
|
501
|
+
|
|
502
|
+
if (lastText?.textContent) {
|
|
503
|
+
const range = document.createRange()
|
|
504
|
+
const end = lastText.textContent.length
|
|
505
|
+
range.setStart(lastText, Math.max(0, end - 1))
|
|
506
|
+
range.setEnd(lastText, end)
|
|
507
|
+
const rects = typeof range.getClientRects === 'function'
|
|
508
|
+
? range.getClientRects()
|
|
509
|
+
: undefined
|
|
510
|
+
const rect = rects?.[rects.length - 1] ?? lastText.parentElement?.getBoundingClientRect()
|
|
511
|
+
if (rect) {
|
|
512
|
+
left = rect.right - rootRect.left + root.scrollLeft
|
|
513
|
+
top = rect.top - rootRect.top + root.scrollTop
|
|
514
|
+
height = rect.height || height
|
|
515
|
+
}
|
|
516
|
+
range.detach()
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
cursor.style.transform = `translate(${Math.max(0, left)}px, ${Math.max(0, top)}px)`
|
|
520
|
+
cursor.style.height = `${height}px`
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
$effect(() => {
|
|
524
|
+
void renderContent
|
|
525
|
+
void nodes
|
|
526
|
+
void typewriter
|
|
527
|
+
void parsedNodes.length
|
|
528
|
+
void effectiveFinal
|
|
529
|
+
if (typeof window === 'undefined' || hasNodes)
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
// When the stream is final (and effective — smooth streaming has caught up),
|
|
533
|
+
// hide the cursor immediately.
|
|
534
|
+
if (effectiveFinal) {
|
|
535
|
+
showTypewriterCursor = false
|
|
536
|
+
clearTypewriterCursorTimeout()
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const nextLength = getTypewriterContentLength()
|
|
541
|
+
const cursorAllowed = shouldShowTypewriterCursorForCurrentNodes()
|
|
542
|
+
if (typewriter === false || !cursorAllowed || nextLength <= lastTypewriterContentLength) {
|
|
543
|
+
if (typewriter === false || !cursorAllowed)
|
|
544
|
+
showTypewriterCursor = false
|
|
545
|
+
lastTypewriterContentLength = nextLength
|
|
546
|
+
return
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
lastTypewriterContentLength = nextLength
|
|
550
|
+
showTypewriterCursor = true
|
|
551
|
+
clearTypewriterCursorTimeout()
|
|
552
|
+
tick().then(() => {
|
|
553
|
+
updateTypewriterCursorPosition()
|
|
554
|
+
})
|
|
555
|
+
typewriterCursorTimeout = setTimeout(() => {
|
|
556
|
+
showTypewriterCursor = false
|
|
557
|
+
}, 3000)
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
$effect(() => {
|
|
561
|
+
if (!showTypewriterCursor)
|
|
562
|
+
return
|
|
563
|
+
tick().then(() => {
|
|
564
|
+
updateTypewriterCursorPosition()
|
|
565
|
+
})
|
|
566
|
+
})
|
|
567
|
+
|
|
338
568
|
function handleMouseover(event: MouseEvent) {
|
|
339
569
|
const target = event.target as HTMLElement | null
|
|
340
570
|
if (target?.closest('[data-node-index]'))
|
|
@@ -362,9 +592,10 @@
|
|
|
362
592
|
>
|
|
363
593
|
{#each renderedNodes as node, index ((indexKey != null ? String(indexKey) : 'markdown-renderer') + '-' + index)}
|
|
364
594
|
<div class="node-slot" data-node-index={index} data-node-type={(node as any)?.type}>
|
|
365
|
-
<div class:typewriter-node={
|
|
595
|
+
<div class:fade-node={fade !== false && String((node as any)?.type || '') !== 'code_block'} class:typewriter-node={fade !== false && String((node as any)?.type || '') !== 'code_block'} class="node-content" data-node-index={index}>
|
|
366
596
|
<NodeOutlet node={node} context={renderContext} indexKey={(indexKey != null ? String(indexKey) : 'markdown-renderer') + '-' + index} />
|
|
367
597
|
</div>
|
|
368
598
|
</div>
|
|
369
599
|
{/each}
|
|
600
|
+
{#if showTypewriterCursor}<span bind:this={typewriterCursorEl} class="typewriter-cursor" aria-hidden="true"></span>{/if}
|
|
370
601
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SvelteRenderableNode, SvelteRenderContext } from './shared/node-helpers'
|
|
3
|
+
import { resolveStreamingTextState } from 'markstream-core'
|
|
3
4
|
import { getString } from './shared/node-helpers'
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
@@ -23,30 +24,27 @@
|
|
|
23
24
|
const content = $derived(getString((node as any)?.content ?? (node as any)?.raw))
|
|
24
25
|
const centered = $derived(Boolean((node as any)?.center))
|
|
25
26
|
const streamKey = $derived(String(context?.customId ?? 'global') + ':' + String(context?.streamRenderVersion ?? 0) + ':' + String(indexKey ?? 'node'))
|
|
27
|
+
const fadeEnabled = $derived(context?.fade !== false)
|
|
26
28
|
|
|
27
29
|
const streamInfo = $derived.by(() => {
|
|
28
30
|
const state = context?.textStreamState
|
|
29
31
|
const previous = streamKey === previousKey ? previousContent : (state?.get(streamKey) ?? '')
|
|
30
|
-
|
|
31
|
-
let stableContent = ''
|
|
32
|
-
let deltaContent = ''
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const result = resolveStreamingTextState({
|
|
34
|
+
nextContent: content,
|
|
35
|
+
previousContent: previous,
|
|
36
|
+
typewriterEnabled: fadeEnabled,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
if (result.appended)
|
|
37
40
|
deltaClass = deltaClass.endsWith('--a') ? 'markstream-svelte-text__stream-delta--b' : 'markstream-svelte-text__stream-delta--a'
|
|
38
|
-
|
|
39
|
-
else {
|
|
40
|
-
stableContent = content
|
|
41
|
-
deltaContent = ''
|
|
42
|
-
}
|
|
43
|
-
|
|
41
|
+
|
|
44
42
|
previousKey = streamKey
|
|
45
43
|
previousContent = content
|
|
46
44
|
state?.set(streamKey, content)
|
|
47
45
|
|
|
48
|
-
return { stableContent, deltaContent, deltaClass }
|
|
46
|
+
return { stableContent: result.settledContent, deltaContent: result.streamedDelta, deltaClass }
|
|
49
47
|
})
|
|
50
48
|
</script>
|
|
51
49
|
|
|
52
|
-
<span data-typewriter={typewriter
|
|
50
|
+
<span data-typewriter={context?.typewriter === true ? '1' : undefined} class:markstream-svelte-text--centered={centered} class="markstream-svelte-text-node text-node">{streamInfo.stableContent}{#if streamInfo.deltaContent}<span class="markstream-svelte-text__stream-delta text-node-stream-delta {streamInfo.deltaClass}">{streamInfo.deltaContent}</span>{/if}</span>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SmoothMarkdownStreamOptions } from 'markstream-core';
|
|
1
2
|
import type { BaseNode, HtmlPolicy, MarkdownIt, ParsedNode, ParseOptions } from 'stream-markdown-parser';
|
|
2
3
|
import type { CustomComponentMap } from '../../customComponents';
|
|
3
4
|
import type { CodeBlockMonacoOptions, CodeBlockMonacoTheme } from '../../types/monaco';
|
|
@@ -45,6 +46,7 @@ export type NodeRendererMermaidProps = Partial<{
|
|
|
45
46
|
showCollapseButton: boolean;
|
|
46
47
|
showZoomControls: boolean;
|
|
47
48
|
isStrict: boolean;
|
|
49
|
+
enableMermaidInteractions: boolean;
|
|
48
50
|
}> & Record<string, unknown>;
|
|
49
51
|
export type NodeRendererD2Props = Partial<{
|
|
50
52
|
maxHeight: string | null;
|
|
@@ -99,6 +101,7 @@ export interface NodeRendererProps {
|
|
|
99
101
|
customId?: string;
|
|
100
102
|
indexKey?: number | string;
|
|
101
103
|
typewriter?: boolean;
|
|
104
|
+
fade?: boolean;
|
|
102
105
|
batchRendering?: boolean;
|
|
103
106
|
initialRenderBatchSize?: number;
|
|
104
107
|
renderBatchSize?: number;
|
|
@@ -109,6 +112,8 @@ export interface NodeRendererProps {
|
|
|
109
112
|
maxLiveNodes?: number;
|
|
110
113
|
liveNodeBuffer?: number;
|
|
111
114
|
allowHtml?: boolean;
|
|
115
|
+
smoothStreaming?: boolean | 'auto';
|
|
116
|
+
smoothStreamingOptions?: SmoothMarkdownStreamOptions;
|
|
112
117
|
}
|
|
113
118
|
export interface SvelteRenderContext {
|
|
114
119
|
customId?: string;
|
|
@@ -116,6 +121,7 @@ export interface SvelteRenderContext {
|
|
|
116
121
|
indexKey?: string;
|
|
117
122
|
final?: boolean;
|
|
118
123
|
typewriter?: boolean;
|
|
124
|
+
fade?: boolean;
|
|
119
125
|
textStreamState?: Map<string, string>;
|
|
120
126
|
streamRenderVersion?: number;
|
|
121
127
|
showTooltips?: boolean;
|
|
@@ -29,6 +29,7 @@ export function buildRenderContext(props, events = {}, textStreamState, streamRe
|
|
|
29
29
|
indexKey: props.indexKey != null ? String(props.indexKey) : undefined,
|
|
30
30
|
final: props.final,
|
|
31
31
|
typewriter: props.typewriter,
|
|
32
|
+
fade: props.fade,
|
|
32
33
|
textStreamState,
|
|
33
34
|
streamRenderVersion,
|
|
34
35
|
showTooltips: props.showTooltips,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SmoothMarkdownStreamOptions } from 'markstream-core';
|
|
2
|
+
export type { SmoothMarkdownStreamOptions };
|
|
3
|
+
export interface SmoothMarkdownStreamControllerSvelte {
|
|
4
|
+
source: string;
|
|
5
|
+
visible: string;
|
|
6
|
+
done: boolean;
|
|
7
|
+
caughtUp: boolean;
|
|
8
|
+
final: boolean;
|
|
9
|
+
pendingChars: number;
|
|
10
|
+
enqueue: (chunk: string) => void;
|
|
11
|
+
finish: (options?: {
|
|
12
|
+
flush?: boolean;
|
|
13
|
+
}) => void;
|
|
14
|
+
flush: () => void;
|
|
15
|
+
reset: (initialMarkdown?: string) => void;
|
|
16
|
+
pause: () => void;
|
|
17
|
+
resume: () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function useSmoothMarkdownStream(optionsOrGetter?: SmoothMarkdownStreamOptions | (() => SmoothMarkdownStreamOptions)): SmoothMarkdownStreamControllerSvelte;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createSmoothMarkdownStream } from 'markstream-core';
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
export function useSmoothMarkdownStream(optionsOrGetter = {}) {
|
|
4
|
+
const options = typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
5
|
+
let source = $state('');
|
|
6
|
+
let visible = $state('');
|
|
7
|
+
let done = $state(false);
|
|
8
|
+
let pendingChars = $state(0);
|
|
9
|
+
let caughtUp = $state(false);
|
|
10
|
+
let final = $state(false);
|
|
11
|
+
const controller = createSmoothMarkdownStream(options);
|
|
12
|
+
const sync = () => {
|
|
13
|
+
const snapshot = controller.getSnapshot();
|
|
14
|
+
source = snapshot.source;
|
|
15
|
+
visible = snapshot.visible;
|
|
16
|
+
done = snapshot.done;
|
|
17
|
+
pendingChars = snapshot.pendingChars;
|
|
18
|
+
caughtUp = snapshot.caughtUp;
|
|
19
|
+
final = snapshot.final;
|
|
20
|
+
};
|
|
21
|
+
const unsubscribe = controller.subscribe(sync);
|
|
22
|
+
sync();
|
|
23
|
+
onDestroy(() => {
|
|
24
|
+
unsubscribe();
|
|
25
|
+
controller.destroy();
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
get source() { return source; },
|
|
29
|
+
get visible() { return visible; },
|
|
30
|
+
get done() { return done; },
|
|
31
|
+
get caughtUp() { return caughtUp; },
|
|
32
|
+
get final() { return final; },
|
|
33
|
+
get pendingChars() { return pendingChars; },
|
|
34
|
+
enqueue: chunk => controller.enqueue(chunk),
|
|
35
|
+
finish: opts => controller.finish(opts),
|
|
36
|
+
flush: () => controller.flush(),
|
|
37
|
+
reset: initialMarkdown => controller.reset(initialMarkdown),
|
|
38
|
+
pause: () => controller.pause(),
|
|
39
|
+
resume: () => controller.resume(),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context key and types for Svelte smooth streaming parent suppression.
|
|
3
|
+
*
|
|
4
|
+
* When a parent NodeRenderer is already pacing content via smooth streaming,
|
|
5
|
+
* nested renderers (e.g. thinking blocks, custom tag content) should suppress
|
|
6
|
+
* their own smooth streaming to avoid double-pacing.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const parentSmoothStreaming = getContext<SmoothStreamingContextValue | undefined>(
|
|
10
|
+
* SMOOTH_STREAMING_CONTEXT,
|
|
11
|
+
* )
|
|
12
|
+
* setContext(SMOOTH_STREAMING_CONTEXT, () => smoothStreamingEnabled)
|
|
13
|
+
*/
|
|
14
|
+
export declare const SMOOTH_STREAMING_CONTEXT = "markstreamSmoothStreaming";
|
|
15
|
+
export type SmoothStreamingContextValue = () => boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context key and types for Svelte smooth streaming parent suppression.
|
|
3
|
+
*
|
|
4
|
+
* When a parent NodeRenderer is already pacing content via smooth streaming,
|
|
5
|
+
* nested renderers (e.g. thinking blocks, custom tag content) should suppress
|
|
6
|
+
* their own smooth streaming to avoid double-pacing.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const parentSmoothStreaming = getContext<SmoothStreamingContextValue | undefined>(
|
|
10
|
+
* SMOOTH_STREAMING_CONTEXT,
|
|
11
|
+
* )
|
|
12
|
+
* setContext(SMOOTH_STREAMING_CONTEXT, () => smoothStreamingEnabled)
|
|
13
|
+
*/
|
|
14
|
+
export const SMOOTH_STREAMING_CONTEXT = 'markstreamSmoothStreaming';
|
|
@@ -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));
|
package/dist/index.css
CHANGED
|
@@ -130,9 +130,10 @@ markstream-svelte {
|
|
|
130
130
|
width: 100%;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
.markstream-svelte .typewriter-node
|
|
133
|
+
.markstream-svelte .typewriter-node,
|
|
134
|
+
.markstream-svelte .fade-node {
|
|
134
135
|
opacity: 0;
|
|
135
|
-
animation: typewriter-fade var(--typewriter-fade-duration,
|
|
136
|
+
animation: typewriter-fade var(--fade-duration, var(--typewriter-fade-duration, 280ms)) var(--fade-ease, var(--typewriter-fade-ease, cubic-bezier(0.33, 0, 0.67, 1))) both;
|
|
136
137
|
will-change: opacity;
|
|
137
138
|
}
|
|
138
139
|
|
|
@@ -151,8 +152,8 @@ markstream-svelte {
|
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
.markstream-svelte .markstream-svelte-text__stream-delta {
|
|
154
|
-
animation-duration: var(--stream-update-fade-duration, var(--typewriter-fade-duration,
|
|
155
|
-
animation-timing-function: var(--stream-update-fade-ease, var(--typewriter-fade-ease,
|
|
155
|
+
animation-duration: var(--stream-update-fade-duration, var(--fade-duration, var(--typewriter-fade-duration, 280ms)));
|
|
156
|
+
animation-timing-function: var(--stream-update-fade-ease, var(--fade-ease, var(--typewriter-fade-ease, cubic-bezier(0.33, 0, 0.67, 1))));
|
|
156
157
|
animation-fill-mode: both;
|
|
157
158
|
will-change: opacity;
|
|
158
159
|
}
|
|
@@ -278,6 +279,29 @@ markstream-svelte {
|
|
|
278
279
|
to { opacity: 1; }
|
|
279
280
|
}
|
|
280
281
|
|
|
282
|
+
.markstream-svelte .typewriter-cursor {
|
|
283
|
+
position: absolute;
|
|
284
|
+
left: 0;
|
|
285
|
+
top: 0;
|
|
286
|
+
display: inline-block;
|
|
287
|
+
width: 0.55em;
|
|
288
|
+
height: 1em;
|
|
289
|
+
margin-left: 0.08em;
|
|
290
|
+
vertical-align: -0.12em;
|
|
291
|
+
border-right: 2px solid currentColor;
|
|
292
|
+
pointer-events: none;
|
|
293
|
+
animation: typewriter-cursor-blink 1s steps(1, end) infinite;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@keyframes typewriter-cursor-blink {
|
|
297
|
+
0%, 49% {
|
|
298
|
+
opacity: 1;
|
|
299
|
+
}
|
|
300
|
+
50%, 100% {
|
|
301
|
+
opacity: 0;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
281
305
|
@media (prefers-reduced-motion: reduce) {
|
|
282
306
|
.markstream-svelte .link-loading-indicator {
|
|
283
307
|
animation: none !important;
|
|
@@ -287,6 +311,16 @@ markstream-svelte {
|
|
|
287
311
|
.markstream-svelte .markstream-svelte-text__stream-delta {
|
|
288
312
|
animation: none !important;
|
|
289
313
|
}
|
|
314
|
+
|
|
315
|
+
.markstream-svelte .typewriter-node,
|
|
316
|
+
.markstream-svelte .fade-node {
|
|
317
|
+
animation: none !important;
|
|
318
|
+
opacity: 1;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.markstream-svelte .typewriter-cursor {
|
|
322
|
+
animation: none !important;
|
|
323
|
+
}
|
|
290
324
|
}
|
|
291
325
|
|
|
292
326
|
.markstream-svelte strong {
|
package/dist/index.d.ts
CHANGED
|
@@ -49,6 +49,10 @@ export { default as TextNode } from './components/TextNode.svelte';
|
|
|
49
49
|
export { default as ThematicBreakNode } from './components/ThematicBreakNode.svelte';
|
|
50
50
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
51
51
|
export { default as VmrContainerNode } from './components/VmrContainerNode.svelte';
|
|
52
|
+
export type { SmoothMarkdownStreamControllerSvelte, SmoothMarkdownStreamOptions, } from './composables/useSmoothMarkdownStream.svelte';
|
|
53
|
+
export { useSmoothMarkdownStream, } from './composables/useSmoothMarkdownStream.svelte';
|
|
54
|
+
export { SMOOTH_STREAMING_CONTEXT, } from './context/smoothStreaming';
|
|
55
|
+
export type { SmoothStreamingContextValue, } from './context/smoothStreaming';
|
|
52
56
|
export { clearGlobalCustomComponents, getCustomComponentsRevision, getCustomNodeComponents, removeCustomComponents, setCustomComponents, subscribeCustomComponents, } from './customComponents';
|
|
53
57
|
export type { CustomComponentMap, MarkstreamSvelteComponent } from './customComponents';
|
|
54
58
|
export { disposeRenderedHtmlEnhancements, enhanceRenderedHtml, } from './enhanceRenderedHtml';
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,8 @@ export { default as TextNode } from './components/TextNode.svelte';
|
|
|
48
48
|
export { default as ThematicBreakNode } from './components/ThematicBreakNode.svelte';
|
|
49
49
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
50
50
|
export { default as VmrContainerNode } from './components/VmrContainerNode.svelte';
|
|
51
|
+
export { useSmoothMarkdownStream, } from './composables/useSmoothMarkdownStream.svelte';
|
|
52
|
+
export { SMOOTH_STREAMING_CONTEXT, } from './context/smoothStreaming';
|
|
51
53
|
export { clearGlobalCustomComponents, getCustomComponentsRevision, getCustomNodeComponents, removeCustomComponents, setCustomComponents, subscribeCustomComponents, } from './customComponents';
|
|
52
54
|
export { disposeRenderedHtmlEnhancements, enhanceRenderedHtml, } from './enhanceRenderedHtml';
|
|
53
55
|
export { setDefaultI18nMap, useSafeI18n } from './i18n/useSafeI18n';
|
package/dist/index.px.css
CHANGED
|
@@ -130,9 +130,10 @@ markstream-svelte {
|
|
|
130
130
|
width: 100%;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
.markstream-svelte .typewriter-node
|
|
133
|
+
.markstream-svelte .typewriter-node,
|
|
134
|
+
.markstream-svelte .fade-node {
|
|
134
135
|
opacity: 0;
|
|
135
|
-
animation: typewriter-fade var(--typewriter-fade-duration,
|
|
136
|
+
animation: typewriter-fade var(--fade-duration, var(--typewriter-fade-duration, 280ms)) var(--fade-ease, var(--typewriter-fade-ease, cubic-bezier(0.33, 0, 0.67, 1))) both;
|
|
136
137
|
will-change: opacity;
|
|
137
138
|
}
|
|
138
139
|
|
|
@@ -151,8 +152,8 @@ markstream-svelte {
|
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
.markstream-svelte .markstream-svelte-text__stream-delta {
|
|
154
|
-
animation-duration: var(--stream-update-fade-duration, var(--typewriter-fade-duration,
|
|
155
|
-
animation-timing-function: var(--stream-update-fade-ease, var(--typewriter-fade-ease,
|
|
155
|
+
animation-duration: var(--stream-update-fade-duration, var(--fade-duration, var(--typewriter-fade-duration, 280ms)));
|
|
156
|
+
animation-timing-function: var(--stream-update-fade-ease, var(--fade-ease, var(--typewriter-fade-ease, cubic-bezier(0.33, 0, 0.67, 1))));
|
|
156
157
|
animation-fill-mode: both;
|
|
157
158
|
will-change: opacity;
|
|
158
159
|
}
|
|
@@ -278,6 +279,29 @@ markstream-svelte {
|
|
|
278
279
|
to { opacity: 1; }
|
|
279
280
|
}
|
|
280
281
|
|
|
282
|
+
.markstream-svelte .typewriter-cursor {
|
|
283
|
+
position: absolute;
|
|
284
|
+
left: 0;
|
|
285
|
+
top: 0;
|
|
286
|
+
display: inline-block;
|
|
287
|
+
width: 0.55em;
|
|
288
|
+
height: 1em;
|
|
289
|
+
margin-left: 0.08em;
|
|
290
|
+
vertical-align: -0.12em;
|
|
291
|
+
border-right: 2px solid currentColor;
|
|
292
|
+
pointer-events: none;
|
|
293
|
+
animation: typewriter-cursor-blink 1s steps(1, end) infinite;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@keyframes typewriter-cursor-blink {
|
|
297
|
+
0%, 49% {
|
|
298
|
+
opacity: 1;
|
|
299
|
+
}
|
|
300
|
+
50%, 100% {
|
|
301
|
+
opacity: 0;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
281
305
|
@media (prefers-reduced-motion: reduce) {
|
|
282
306
|
.markstream-svelte .link-loading-indicator {
|
|
283
307
|
animation: none !important;
|
|
@@ -287,6 +311,16 @@ markstream-svelte {
|
|
|
287
311
|
.markstream-svelte .markstream-svelte-text__stream-delta {
|
|
288
312
|
animation: none !important;
|
|
289
313
|
}
|
|
314
|
+
|
|
315
|
+
.markstream-svelte .typewriter-node,
|
|
316
|
+
.markstream-svelte .fade-node {
|
|
317
|
+
animation: none !important;
|
|
318
|
+
opacity: 1;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.markstream-svelte .typewriter-cursor {
|
|
322
|
+
animation: none !important;
|
|
323
|
+
}
|
|
290
324
|
}
|
|
291
325
|
|
|
292
326
|
.markstream-svelte strong {
|
package/dist/index.tailwind.css
CHANGED
|
@@ -130,9 +130,10 @@ markstream-svelte {
|
|
|
130
130
|
width: 100%;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
.markstream-svelte .typewriter-node
|
|
133
|
+
.markstream-svelte .typewriter-node,
|
|
134
|
+
.markstream-svelte .fade-node {
|
|
134
135
|
opacity: 0;
|
|
135
|
-
animation: typewriter-fade var(--typewriter-fade-duration,
|
|
136
|
+
animation: typewriter-fade var(--fade-duration, var(--typewriter-fade-duration, 280ms)) var(--fade-ease, var(--typewriter-fade-ease, cubic-bezier(0.33, 0, 0.67, 1))) both;
|
|
136
137
|
will-change: opacity;
|
|
137
138
|
}
|
|
138
139
|
|
|
@@ -151,8 +152,8 @@ markstream-svelte {
|
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
.markstream-svelte .markstream-svelte-text__stream-delta {
|
|
154
|
-
animation-duration: var(--stream-update-fade-duration, var(--typewriter-fade-duration,
|
|
155
|
-
animation-timing-function: var(--stream-update-fade-ease, var(--typewriter-fade-ease,
|
|
155
|
+
animation-duration: var(--stream-update-fade-duration, var(--fade-duration, var(--typewriter-fade-duration, 280ms)));
|
|
156
|
+
animation-timing-function: var(--stream-update-fade-ease, var(--fade-ease, var(--typewriter-fade-ease, cubic-bezier(0.33, 0, 0.67, 1))));
|
|
156
157
|
animation-fill-mode: both;
|
|
157
158
|
will-change: opacity;
|
|
158
159
|
}
|
|
@@ -278,6 +279,29 @@ markstream-svelte {
|
|
|
278
279
|
to { opacity: 1; }
|
|
279
280
|
}
|
|
280
281
|
|
|
282
|
+
.markstream-svelte .typewriter-cursor {
|
|
283
|
+
position: absolute;
|
|
284
|
+
left: 0;
|
|
285
|
+
top: 0;
|
|
286
|
+
display: inline-block;
|
|
287
|
+
width: 0.55em;
|
|
288
|
+
height: 1em;
|
|
289
|
+
margin-left: 0.08em;
|
|
290
|
+
vertical-align: -0.12em;
|
|
291
|
+
border-right: 2px solid currentColor;
|
|
292
|
+
pointer-events: none;
|
|
293
|
+
animation: typewriter-cursor-blink 1s steps(1, end) infinite;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@keyframes typewriter-cursor-blink {
|
|
297
|
+
0%, 49% {
|
|
298
|
+
opacity: 1;
|
|
299
|
+
}
|
|
300
|
+
50%, 100% {
|
|
301
|
+
opacity: 0;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
281
305
|
@media (prefers-reduced-motion: reduce) {
|
|
282
306
|
.markstream-svelte .link-loading-indicator {
|
|
283
307
|
animation: none !important;
|
|
@@ -287,6 +311,16 @@ markstream-svelte {
|
|
|
287
311
|
.markstream-svelte .markstream-svelte-text__stream-delta {
|
|
288
312
|
animation: none !important;
|
|
289
313
|
}
|
|
314
|
+
|
|
315
|
+
.markstream-svelte .typewriter-node,
|
|
316
|
+
.markstream-svelte .fade-node {
|
|
317
|
+
animation: none !important;
|
|
318
|
+
opacity: 1;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.markstream-svelte .typewriter-cursor {
|
|
322
|
+
animation: none !important;
|
|
323
|
+
}
|
|
290
324
|
}
|
|
291
325
|
|
|
292
326
|
.markstream-svelte strong {
|
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markstream-svelte",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.1-beta.
|
|
4
|
+
"version": "0.0.1-beta.6",
|
|
5
5
|
"description": "Svelte Markdown renderer for Markstream, aligned with markstream-vue and markstream-react.",
|
|
6
6
|
"author": "Simon He",
|
|
7
7
|
"license": "MIT",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@terrastruct/d2": ">=0.1.33",
|
|
50
50
|
"katex": ">=0.16.22",
|
|
51
51
|
"mermaid": ">=11",
|
|
52
|
-
"stream-monaco": ">=0.0.
|
|
52
|
+
"stream-monaco": ">=0.0.41",
|
|
53
53
|
"svelte": ">=5 <6"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
@@ -71,25 +71,28 @@
|
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@floating-ui/dom": "^1.7.6",
|
|
74
|
-
"
|
|
74
|
+
"markstream-core": "1.0.3",
|
|
75
|
+
"stream-markdown-parser": "1.0.5"
|
|
75
76
|
},
|
|
76
77
|
"devDependencies": {
|
|
77
78
|
"@sveltejs/package": "^2.5.7",
|
|
78
79
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
79
80
|
"@types/node": "^18.19.130",
|
|
80
|
-
"autoprefixer": "^10.
|
|
81
|
+
"autoprefixer": "^10.5.0",
|
|
81
82
|
"postcss-cli": "^11.0.1",
|
|
82
|
-
"svelte": "^5.55.
|
|
83
|
+
"svelte": "^5.55.7",
|
|
83
84
|
"svelte-check": "^4.4.8",
|
|
84
85
|
"tailwindcss": "^3.4.19",
|
|
85
86
|
"typescript": "^5.9.3",
|
|
86
|
-
"vite": "^7.3.
|
|
87
|
+
"vite": "^7.3.3"
|
|
87
88
|
},
|
|
88
89
|
"scripts": {
|
|
89
|
-
"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",
|
|
90
|
+
"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",
|
|
90
91
|
"dev": "vite dev",
|
|
91
92
|
"preview": "vite preview",
|
|
92
93
|
"typecheck": "svelte-check --tsconfig ./tsconfig.json",
|
|
93
|
-
"
|
|
94
|
+
"check:core-published": "node ../../scripts/check-core-published.mjs --package-json package.json --core-package-json ../markstream-core/package.json",
|
|
95
|
+
"check:workspace-deps-published": "node ../../scripts/check-workspace-deps-published.mjs --package-json package.json",
|
|
96
|
+
"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"
|
|
94
97
|
}
|
|
95
98
|
}
|