markstream-svelte 0.0.4-beta.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -1
- package/dist/components/CodeBlockNode.svelte +195 -19
- package/dist/components/CodeBlockNode.svelte.d.ts +1 -0
- package/dist/components/PreCodeNode.svelte +57 -2
- package/dist/components/PreCodeNode.svelte.d.ts +4 -0
- package/dist/components/shared/node-helpers.d.ts +1 -0
- package/dist/index.css +175 -28
- package/dist/index.px.css +175 -28
- package/dist/index.tailwind.css +175 -28
- package/dist/optional/monaco.js +25 -11
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -27,9 +27,48 @@ Optional heavy renderers stay as peer dependencies, matching the Vue and React p
|
|
|
27
27
|
Plain Markdown does not require these packages:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
pnpm add katex mermaid stream-
|
|
30
|
+
pnpm add katex mermaid stream-diffs @terrastruct/d2 @antv/infographic
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
`stream-diffs` powers the enhanced code blocks (smaller runtime, no `monaco-editor`).
|
|
34
|
+
If you prefer the legacy Monaco-based rendering, install `stream-monaco` instead;
|
|
35
|
+
it is used automatically as a fallback when `stream-diffs` is absent.
|
|
36
|
+
|
|
37
|
+
## Enhanced Code Blocks
|
|
38
|
+
|
|
39
|
+
`CodeBlockNode` renders a single code block with the header, toolbar, and a `stream-diffs` File / FileDiff surface. Inside `MarkdownRender`, code blocks resolve to the same runtime automatically.
|
|
40
|
+
|
|
41
|
+
```svelte
|
|
42
|
+
<script lang="ts">
|
|
43
|
+
import { CodeBlockNode } from 'markstream-svelte'
|
|
44
|
+
import type { CodeBlockMonacoOptions } from 'markstream-svelte'
|
|
45
|
+
|
|
46
|
+
const node = {
|
|
47
|
+
type: 'code_block',
|
|
48
|
+
language: 'ts',
|
|
49
|
+
code: 'const answer = 42',
|
|
50
|
+
raw: 'const answer = 42',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// fontSize / lineHeight / tabSize also drive the streaming <pre> fallback so
|
|
54
|
+
// the enhanced surface swaps in without a visual jump.
|
|
55
|
+
const monacoOptions: CodeBlockMonacoOptions = {
|
|
56
|
+
fontSize: 14,
|
|
57
|
+
lineHeight: 21,
|
|
58
|
+
tabSize: 4,
|
|
59
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
60
|
+
wordWrap: 'off',
|
|
61
|
+
theme: 'vitesse-dark',
|
|
62
|
+
renderSideBySide: true,
|
|
63
|
+
MAX_HEIGHT: 640,
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<CodeBlockNode {node} {monacoOptions} isDark showLineNumbers />
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Component-level options: `isDark`, `showLineNumbers` (default `true`), and `monacoOptions` for both single blocks and diff blocks (`renderSideBySide`, `diffHunkActionsOnHover`, `onDiffHunkAction`). When neither `stream-diffs` nor `stream-monaco` is installed, the block renders as a plain `<pre>`.
|
|
71
|
+
|
|
33
72
|
## Basic Usage
|
|
34
73
|
|
|
35
74
|
```svelte
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
import { hideTooltip, showTooltipForAnchor } from '../tooltip/singletonTooltip'
|
|
8
8
|
import { getLanguageIcon, isLikelyIncompleteLanguageIdentifier, languageMap, normalizeLanguageIdentifier, resolveMonacoLanguageId } from '../utils/languageIcon'
|
|
9
9
|
import HtmlPreviewFrame from './HtmlPreviewFrame.svelte'
|
|
10
|
+
import PreCodeNode from './PreCodeNode.svelte'
|
|
10
11
|
import { copyTextToClipboard, resolveCssSize } from './shared/rich-block-helpers'
|
|
11
|
-
import { getString
|
|
12
|
+
import { getString } from './shared/node-helpers'
|
|
12
13
|
|
|
13
14
|
type Props = {
|
|
14
15
|
node: SvelteRenderableNode
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
showPreviewButton?: boolean
|
|
31
32
|
showCollapseButton?: boolean
|
|
32
33
|
showFontSizeButtons?: boolean
|
|
34
|
+
showLineNumbers?: boolean
|
|
33
35
|
htmlPreviewAllowScripts?: boolean
|
|
34
36
|
htmlPreviewSandbox?: string | undefined
|
|
35
37
|
}
|
|
@@ -54,6 +56,7 @@
|
|
|
54
56
|
showPreviewButton = true,
|
|
55
57
|
showCollapseButton = true,
|
|
56
58
|
showFontSizeButtons = true,
|
|
59
|
+
showLineNumbers = true,
|
|
57
60
|
htmlPreviewAllowScripts = false,
|
|
58
61
|
htmlPreviewSandbox = undefined
|
|
59
62
|
}: Props = $props()
|
|
@@ -72,6 +75,7 @@
|
|
|
72
75
|
revealLineCount: 0,
|
|
73
76
|
})
|
|
74
77
|
const streamingLanguageTokens = ['javascript', 'plaintext', 'shellscript', 'typescript']
|
|
78
|
+
const defaultPreFallbackFontFamily = '"SF Mono", Monaco, Consolas, "Ubuntu Mono", "Liberation Mono", "Courier New", monospace'
|
|
75
79
|
|
|
76
80
|
function resolveRecoverableFallbackLanguage(error: unknown) {
|
|
77
81
|
const message = error instanceof Error ? error.message : String(error ?? '')
|
|
@@ -129,13 +133,16 @@
|
|
|
129
133
|
let useFallback = $state(false)
|
|
130
134
|
let fallbackLanguage = $state('')
|
|
131
135
|
let editorKind: 'single' | 'diff' | null = $state(null)
|
|
136
|
+
let editorStreamMode: boolean | null = $state(null)
|
|
137
|
+
let editorRevealed = $state(false)
|
|
138
|
+
let fallbackRetired = $state(false)
|
|
132
139
|
let createEditorPromise: Promise<void> | null = $state(null)
|
|
133
140
|
let mounted = $state(false)
|
|
134
141
|
let collapsed = $state(false)
|
|
135
142
|
let expanded = $state(false)
|
|
136
143
|
let copied = $state(false)
|
|
137
144
|
let previewOpen = $state(false)
|
|
138
|
-
let codeFontSize = $state(
|
|
145
|
+
let codeFontSize = $state(12)
|
|
139
146
|
let copyTimer: ReturnType<typeof setTimeout> | null = $state(null)
|
|
140
147
|
let lifecycleId = $state(0)
|
|
141
148
|
let heightSyncRaf: number | null = $state(null)
|
|
@@ -171,7 +178,7 @@
|
|
|
171
178
|
: lightTheme ?? resolvedThemes?.lightTheme,
|
|
172
179
|
resolvedIsDark ? 'vitesse-dark' : 'vitesse-light',
|
|
173
180
|
))
|
|
174
|
-
let defaultCodeFontSize = $derived(Number(mergedMonacoOptions.fontSize) ||
|
|
181
|
+
let defaultCodeFontSize = $derived(Number(mergedMonacoOptions.fontSize) || 12)
|
|
175
182
|
let minWidthValue = $derived(resolveCssSize(minWidth ?? resolvedThemes?.minWidth))
|
|
176
183
|
let maxWidthValue = $derived(resolveCssSize(maxWidth ?? resolvedThemes?.maxWidth))
|
|
177
184
|
let containerStyle = $derived([
|
|
@@ -186,9 +193,12 @@
|
|
|
186
193
|
let documentStreaming = $derived(context?.final === false || resolvedLoading)
|
|
187
194
|
let shouldDeferStreamingLanguage = $derived(resolvedStream !== false && documentStreaming && (isLikelyIncompleteLanguageIdentifier(rawLanguage) || isStreamingLanguagePrefix(rawLanguage)))
|
|
188
195
|
let shouldRender = $derived(!(resolvedLoading && !code.trim()))
|
|
189
|
-
let
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
let preFallbackNode = $derived({
|
|
197
|
+
...(node as any),
|
|
198
|
+
code,
|
|
199
|
+
loading: resolvedLoading,
|
|
200
|
+
} as SvelteRenderableNode)
|
|
201
|
+
let preFallbackStyle = $derived(buildPreFallbackStyle())
|
|
192
202
|
let settledRefreshSignature = $derived(diff
|
|
193
203
|
? `${monacoLanguage}\0${originalCode}\0${updatedCode || code}`
|
|
194
204
|
: `${monacoLanguage}\0${code}`)
|
|
@@ -258,6 +268,58 @@
|
|
|
258
268
|
return getString((sourceNode as any)?.code)
|
|
259
269
|
}
|
|
260
270
|
|
|
271
|
+
function readPositiveMetric(value: unknown) {
|
|
272
|
+
const number = Number(value)
|
|
273
|
+
return Number.isFinite(number) && number > 0 ? number : undefined
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function readNonNegativeMetric(value: unknown) {
|
|
277
|
+
const number = Number(value)
|
|
278
|
+
return Number.isFinite(number) && number >= 0 ? number : undefined
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function getCodeLineHeight() {
|
|
282
|
+
return readPositiveMetric(mergedMonacoOptions.lineHeight)
|
|
283
|
+
?? (codeFontSize === 12 ? 18 : Math.max(12, Math.round(codeFontSize * 1.5)))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getCodePadding() {
|
|
287
|
+
const padding = mergedMonacoOptions.padding as Record<string, unknown> | undefined
|
|
288
|
+
const defaultPadding = diff ? 0 : 8
|
|
289
|
+
return {
|
|
290
|
+
top: readNonNegativeMetric(padding?.top) ?? defaultPadding,
|
|
291
|
+
bottom: readNonNegativeMetric(padding?.bottom) ?? defaultPadding,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getCodeFontFamily() {
|
|
296
|
+
return typeof mergedMonacoOptions.fontFamily === 'string' && mergedMonacoOptions.fontFamily.trim()
|
|
297
|
+
? mergedMonacoOptions.fontFamily.trim()
|
|
298
|
+
: defaultPreFallbackFontFamily
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildPreFallbackStyle() {
|
|
302
|
+
const padding = getCodePadding()
|
|
303
|
+
const fontFamily = getCodeFontFamily()
|
|
304
|
+
const tabSize = readPositiveMetric(mergedMonacoOptions.tabSize) ?? 4
|
|
305
|
+
const lineHeight = getCodeLineHeight()
|
|
306
|
+
return [
|
|
307
|
+
`--markstream-code-font-family: ${fontFamily}`,
|
|
308
|
+
`--vscode-editor-font-size: ${codeFontSize}px`,
|
|
309
|
+
`--vscode-editor-line-height: ${lineHeight}px`,
|
|
310
|
+
`--markstream-code-padding-y: ${padding.top}px`,
|
|
311
|
+
`--markstream-pre-line-number-top: ${padding.top}px`,
|
|
312
|
+
`font-family: ${fontFamily}`,
|
|
313
|
+
`font-size: ${codeFontSize}px`,
|
|
314
|
+
`line-height: ${lineHeight}px`,
|
|
315
|
+
`padding-top: ${padding.top}px`,
|
|
316
|
+
`padding-right: var(--markstream-code-padding-x, 12px)`,
|
|
317
|
+
`padding-bottom: ${padding.bottom}px`,
|
|
318
|
+
`padding-left: var(--markstream-code-padding-left, 52px)`,
|
|
319
|
+
`tab-size: ${tabSize}`,
|
|
320
|
+
].join('; ')
|
|
321
|
+
}
|
|
322
|
+
|
|
261
323
|
function getThemeName(theme: CodeBlockMonacoTheme | undefined, fallback: string) {
|
|
262
324
|
if (typeof theme === 'string' && theme)
|
|
263
325
|
return theme
|
|
@@ -306,9 +368,25 @@
|
|
|
306
368
|
wrappingIndent: 'same',
|
|
307
369
|
revealDebounceMs: 75,
|
|
308
370
|
}
|
|
371
|
+
const padding = getCodePadding()
|
|
372
|
+
const configuredUnsafeCSS = typeof raw.unsafeCSS === 'string' ? raw.unsafeCSS : ''
|
|
373
|
+
const unsafeCSS = `[data-file], [data-diff] { --diffs-min-number-column-width-default: 4ch !important; }
|
|
374
|
+
${configuredUnsafeCSS}`.trim()
|
|
309
375
|
const finalOptions = {
|
|
310
376
|
MAX_HEIGHT: maxHeight,
|
|
377
|
+
fontFamily: getCodeFontFamily(),
|
|
311
378
|
fontSize: codeFontSize,
|
|
379
|
+
lineHeight: getCodeLineHeight(),
|
|
380
|
+
// stream-diffs expects a boolean (`disableLineNumbers: options.lineNumbers === false`);
|
|
381
|
+
// passing 'off'/'on' strings would always be truthy and defeat showLineNumbers={false}.
|
|
382
|
+
lineNumbers: showLineNumbers !== false,
|
|
383
|
+
padding,
|
|
384
|
+
unsafeCSS,
|
|
385
|
+
// The component owns the streaming fallback and file header. Initialize
|
|
386
|
+
// stream-diffs in final mode so the revealed surface has highlighting,
|
|
387
|
+
// line numbers, and the same geometry as the fallback.
|
|
388
|
+
disableFileHeader: true,
|
|
389
|
+
stream: false,
|
|
312
390
|
themes: buildThemeList(),
|
|
313
391
|
}
|
|
314
392
|
|
|
@@ -408,6 +486,7 @@
|
|
|
408
486
|
}
|
|
409
487
|
|
|
410
488
|
helpers = mod.useMonaco(syncRuntimeMonacoOptions())
|
|
489
|
+
await Promise.resolve(helpers.setTheme?.(requestedTheme))
|
|
411
490
|
lastThemeRequest = requestedTheme
|
|
412
491
|
})().finally(() => {
|
|
413
492
|
ensureMonacoPromise = null
|
|
@@ -439,8 +518,9 @@
|
|
|
439
518
|
? Boolean(helpers.getDiffEditorView?.())
|
|
440
519
|
: Boolean(helpers.getEditorView?.())
|
|
441
520
|
const hasEditor = hasEditorView && hasRenderedEditorDom(desiredKind)
|
|
521
|
+
const desiredStreamMode = false
|
|
442
522
|
|
|
443
|
-
if (!hasEditor || editorKind !== desiredKind) {
|
|
523
|
+
if (!hasEditor || editorKind !== desiredKind || editorStreamMode !== desiredStreamMode) {
|
|
444
524
|
await recreateEditor(desiredKind)
|
|
445
525
|
if (!mounted || useFallback || !helpers)
|
|
446
526
|
return
|
|
@@ -467,9 +547,72 @@
|
|
|
467
547
|
function hasRenderedEditorDom(kind: 'single' | 'diff') {
|
|
468
548
|
if (!editorHost)
|
|
469
549
|
return false
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
550
|
+
if (kind === 'diff') {
|
|
551
|
+
return Boolean(editorHost.querySelector([
|
|
552
|
+
'.monaco-diff-editor',
|
|
553
|
+
'diffs-container',
|
|
554
|
+
'.stream-diffs-shell',
|
|
555
|
+
'[data-stream-diffs-state]',
|
|
556
|
+
].join(',')))
|
|
557
|
+
}
|
|
558
|
+
return Boolean(editorHost.querySelector([
|
|
559
|
+
'.monaco-editor',
|
|
560
|
+
'diffs-container',
|
|
561
|
+
'.stream-diffs-shell',
|
|
562
|
+
'[data-stream-diffs-state]',
|
|
563
|
+
].join(',')))
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function getVisualEditorSurface() {
|
|
567
|
+
return editorHost?.querySelector<HTMLElement>([
|
|
568
|
+
'.monaco-diff-editor',
|
|
569
|
+
'.monaco-editor',
|
|
570
|
+
'diffs-container',
|
|
571
|
+
'[data-stream-diffs-state]',
|
|
572
|
+
'.stream-diffs-shell',
|
|
573
|
+
].join(',')) ?? null
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function isEditorVisuallyReady(kind: 'single' | 'diff', requireRevealed = false) {
|
|
577
|
+
if (!editorHost || !hasRenderedEditorDom(kind))
|
|
578
|
+
return false
|
|
579
|
+
if (requireRevealed) {
|
|
580
|
+
const hostStyle = window.getComputedStyle(editorHost)
|
|
581
|
+
if (hostStyle.display === 'none' || hostStyle.visibility === 'hidden' || Number.parseFloat(hostStyle.opacity || '1') <= 0.01)
|
|
582
|
+
return false
|
|
583
|
+
}
|
|
584
|
+
const surface = getVisualEditorSurface()
|
|
585
|
+
if (!surface)
|
|
586
|
+
return false
|
|
587
|
+
const rect = surface.getBoundingClientRect()
|
|
588
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
589
|
+
return false
|
|
590
|
+
const style = window.getComputedStyle(surface)
|
|
591
|
+
return style.display !== 'none'
|
|
592
|
+
&& style.visibility !== 'hidden'
|
|
593
|
+
&& Number.parseFloat(style.opacity || '1') > 0.01
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function prepareEditorHandoff(kind: 'single' | 'diff', creationId: number) {
|
|
597
|
+
await tick()
|
|
598
|
+
// Time-box the handoff: if visual readiness can't be confirmed (e.g. a
|
|
599
|
+
// hidden/zero-size container), reveal the editor anyway once its DOM is
|
|
600
|
+
// mounted so the block never strands in the pre-fallback forever.
|
|
601
|
+
const deadline = Date.now() + 1500
|
|
602
|
+
let attempt = 0
|
|
603
|
+
while (Date.now() < deadline && attempt < 30) {
|
|
604
|
+
attempt += 1
|
|
605
|
+
if (!mounted || !editorHost || lifecycleId !== creationId)
|
|
606
|
+
return false
|
|
607
|
+
syncEditorHostHeight(true)
|
|
608
|
+
await nextAnimationFrame()
|
|
609
|
+
if (isEditorVisuallyReady(kind)) {
|
|
610
|
+
syncEditorHostHeight(true)
|
|
611
|
+
await nextAnimationFrame()
|
|
612
|
+
return isEditorVisuallyReady(kind)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return !!(mounted && editorHost && lifecycleId === creationId && hasRenderedEditorDom(kind))
|
|
473
616
|
}
|
|
474
617
|
|
|
475
618
|
async function recreateEditor(kind: 'single' | 'diff') {
|
|
@@ -487,6 +630,7 @@
|
|
|
487
630
|
lastLayoutWidth = null
|
|
488
631
|
lastLayoutHeight = null
|
|
489
632
|
|
|
633
|
+
editorStreamMode = false
|
|
490
634
|
if (kind === 'diff' && typeof helpers.createDiffEditor === 'function') {
|
|
491
635
|
await helpers.createDiffEditor(editorHost, originalCode, updatedCode || code, monacoLanguage)
|
|
492
636
|
await Promise.resolve(helpers.updateDiff?.(originalCode, updatedCode || code, monacoLanguage))
|
|
@@ -498,10 +642,17 @@
|
|
|
498
642
|
editorKind = 'single'
|
|
499
643
|
}
|
|
500
644
|
applyEditorOptions()
|
|
501
|
-
editorReady = true
|
|
502
645
|
bindEditorHeightSync()
|
|
503
|
-
scheduleEditorHeightSync()
|
|
504
646
|
queueThemeSync()
|
|
647
|
+
if (!await prepareEditorHandoff(kind, creationId))
|
|
648
|
+
return
|
|
649
|
+
// Apply the reveal and fallback retirement in one Svelte render. The
|
|
650
|
+
// editor surface has already passed the hidden-host readiness check, so
|
|
651
|
+
// the browser never paints an intermediate frame with neither layer.
|
|
652
|
+
editorRevealed = true
|
|
653
|
+
fallbackRetired = true
|
|
654
|
+
editorReady = true
|
|
655
|
+
scheduleEditorHeightSync()
|
|
505
656
|
}
|
|
506
657
|
catch (error) {
|
|
507
658
|
if (mounted) {
|
|
@@ -583,6 +734,9 @@
|
|
|
583
734
|
}
|
|
584
735
|
catch {}
|
|
585
736
|
editorKind = null
|
|
737
|
+
editorStreamMode = null
|
|
738
|
+
editorRevealed = false
|
|
739
|
+
fallbackRetired = false
|
|
586
740
|
editorReady = false
|
|
587
741
|
lastLayoutWidth = null
|
|
588
742
|
lastLayoutHeight = null
|
|
@@ -597,9 +751,25 @@
|
|
|
597
751
|
function applyEditorOptions() {
|
|
598
752
|
const target = diff ? helpers?.getDiffEditorView?.() : helpers?.getEditorView?.()
|
|
599
753
|
target?.updateOptions?.({ fontSize: codeFontSize, automaticLayout: false })
|
|
754
|
+
syncEditorGeometryVars()
|
|
600
755
|
scheduleEditorHeightSync()
|
|
601
756
|
}
|
|
602
757
|
|
|
758
|
+
// Align the enhanced surface with the pre-fallback geometry (see vue3):
|
|
759
|
+
// stream-diffs/pierre honor these CSS variables on the editor host.
|
|
760
|
+
function syncEditorGeometryVars() {
|
|
761
|
+
if (!editorHost)
|
|
762
|
+
return
|
|
763
|
+
const tabSize = readPositiveMetric(mergedMonacoOptions.tabSize) ?? 4
|
|
764
|
+
editorHost.style.setProperty('--diffs-tab-size', String(tabSize))
|
|
765
|
+
const rawPadding = mergedMonacoOptions.padding
|
|
766
|
+
const hasConfiguredPadding = Boolean(rawPadding && typeof rawPadding === 'object')
|
|
767
|
+
if (hasConfiguredPadding)
|
|
768
|
+
editorHost.style.setProperty('--diffs-gap-block', `${getCodePadding().top}px`)
|
|
769
|
+
else
|
|
770
|
+
editorHost.style.removeProperty('--diffs-gap-block')
|
|
771
|
+
}
|
|
772
|
+
|
|
603
773
|
function getMaxHeightValue() {
|
|
604
774
|
const raw = resolvedMonacoOptions.MAX_HEIGHT
|
|
605
775
|
if (raw === 'none' || raw == null)
|
|
@@ -735,8 +905,8 @@
|
|
|
735
905
|
heightSyncDisposables = []
|
|
736
906
|
}
|
|
737
907
|
|
|
738
|
-
function syncEditorHostHeight() {
|
|
739
|
-
if (!editorHost || !helpers || !editorReady || collapsed)
|
|
908
|
+
function syncEditorHostHeight(preparing = false) {
|
|
909
|
+
if (!editorHost || !helpers || (!editorReady && !preparing) || collapsed)
|
|
740
910
|
return
|
|
741
911
|
|
|
742
912
|
const maxHeight = getMaxHeightValue()
|
|
@@ -805,7 +975,7 @@
|
|
|
805
975
|
const editor = helpers?.getEditorView?.()
|
|
806
976
|
const height = Number(editor?.getContentHeight?.() || 0)
|
|
807
977
|
if (height > 0)
|
|
808
|
-
return Math.ceil(height
|
|
978
|
+
return Math.ceil(height)
|
|
809
979
|
}
|
|
810
980
|
catch {}
|
|
811
981
|
return null
|
|
@@ -960,11 +1130,17 @@
|
|
|
960
1130
|
{#if !collapsed}
|
|
961
1131
|
<div class:code-block-body--expanded={expanded} class="code-block-body">
|
|
962
1132
|
{#if !shouldDelayEditor}
|
|
963
|
-
<div bind:this={editorHost} class:is-hidden={
|
|
964
|
-
{/if}
|
|
965
|
-
{#if showPreFallback}
|
|
966
|
-
<pre class="code-pre-fallback"><code class={preLanguageClass ? `language-${preLanguageClass}` : undefined}>{code}</code></pre>
|
|
1133
|
+
<div bind:this={editorHost} class:is-hidden={!editorRevealed} class="code-editor-container"></div>
|
|
967
1134
|
{/if}
|
|
1135
|
+
<div class:is-hidden={fallbackRetired} class="code-editor-fallback-surface">
|
|
1136
|
+
<PreCodeNode
|
|
1137
|
+
class="code-pre-fallback"
|
|
1138
|
+
enhanceable={false}
|
|
1139
|
+
node={preFallbackNode}
|
|
1140
|
+
showLineNumbers={showLineNumbers !== false}
|
|
1141
|
+
style={preFallbackStyle}
|
|
1142
|
+
/>
|
|
1143
|
+
</div>
|
|
968
1144
|
</div>
|
|
969
1145
|
{/if}
|
|
970
1146
|
|
|
@@ -4,9 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
type Props = {
|
|
6
6
|
node: SvelteRenderableNode
|
|
7
|
+
showLineNumbers?: boolean
|
|
8
|
+
enhanceable?: boolean
|
|
9
|
+
class?: string
|
|
10
|
+
style?: string
|
|
7
11
|
};
|
|
8
12
|
let {
|
|
9
|
-
node
|
|
13
|
+
node,
|
|
14
|
+
showLineNumbers = false,
|
|
15
|
+
enhanceable = true,
|
|
16
|
+
class: className = undefined,
|
|
17
|
+
style = undefined,
|
|
10
18
|
}: Props = $props()
|
|
11
19
|
|
|
12
20
|
let languageRaw = $derived(getString((node as any)?.language).trim())
|
|
@@ -14,8 +22,55 @@
|
|
|
14
22
|
let code = $derived(getString((node as any)?.code))
|
|
15
23
|
let diff = $derived(Boolean((node as any)?.diff))
|
|
16
24
|
let loading = $derived((node as any)?.loading === true)
|
|
25
|
+
let displayCode = $derived(loading ? code : code.replace(/\r\n$|\n$|\r$/, ''))
|
|
26
|
+
|
|
27
|
+
// Non-diff line numbers, aligned with the Vue 3 PreCodeNode.
|
|
28
|
+
let showLineGutter = $derived(showLineNumbers === true && !diff)
|
|
29
|
+
let lineCount = $derived(countCodeLines(displayCode))
|
|
30
|
+
let lineNumbersText = $derived(buildLineNumbersText(lineCount))
|
|
31
|
+
let lineNumberLayoutStyle = $derived(
|
|
32
|
+
showLineGutter
|
|
33
|
+
? `--markstream-pre-line-number-width: ${Math.max(4, String(lineCount).length)}ch; --markstream-pre-diff-line-number-width: ${Math.max(4, String(lineCount).length)}ch; --markstream-code-padding-left: calc(var(--markstream-pre-line-number-padding-left, 2ch) + var(--markstream-pre-line-number-width, 2ch) + var(--markstream-pre-line-number-padding-right, 1ch) + var(--markstream-pre-line-number-separator-width, 2px) + var(--markstream-pre-line-number-gap-to-code, 1ch));`
|
|
34
|
+
: '',
|
|
35
|
+
)
|
|
36
|
+
let mergedStyle = $derived([lineNumberLayoutStyle, style].filter(Boolean).join(' '))
|
|
37
|
+
|
|
38
|
+
function countCodeLines(codeStr: string) {
|
|
39
|
+
let count = 1
|
|
40
|
+
for (let index = 0; index < codeStr.length; index++) {
|
|
41
|
+
if (codeStr[index] === '\n') {
|
|
42
|
+
count++
|
|
43
|
+
}
|
|
44
|
+
else if (codeStr[index] === '\r') {
|
|
45
|
+
count++
|
|
46
|
+
if (codeStr[index + 1] === '\n')
|
|
47
|
+
index++
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return codeStr.length ? count : 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildLineNumbersText(count: number) {
|
|
54
|
+
let out = ''
|
|
55
|
+
for (let line = 1; line <= count; line++)
|
|
56
|
+
out += `${out ? '\n' : ''}${line}`
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
17
59
|
</script>
|
|
18
60
|
|
|
19
61
|
{#if !(loading && !code.trim())}
|
|
20
|
-
<pre
|
|
62
|
+
<pre
|
|
63
|
+
data-markstream-code-block={enhanceable ? '1' : undefined}
|
|
64
|
+
data-markstream-language={languageRaw || undefined}
|
|
65
|
+
data-markstream-loading={loading ? '1' : undefined}
|
|
66
|
+
data-markstream-diff={diff ? '1' : undefined}
|
|
67
|
+
data-markstream-original={diff ? encodeDataPayload(getString((node as any)?.originalCode)) : undefined}
|
|
68
|
+
data-markstream-updated={diff ? encodeDataPayload(getString((node as any)?.updatedCode)) : undefined}
|
|
69
|
+
data-markstream-pre="1"
|
|
70
|
+
data-markstream-line-numbers={showLineGutter ? '1' : undefined}
|
|
71
|
+
aria-busy={loading ? 'true' : undefined}
|
|
72
|
+
class:markstream-pre--line-numbers={showLineGutter}
|
|
73
|
+
class={[language ? `language-${language}` : '', className].filter(Boolean).join(' ') || undefined}
|
|
74
|
+
style={mergedStyle || undefined}
|
|
75
|
+
>{#if showLineGutter}<span class="markstream-pre__line-numbers" aria-hidden="true"><span class="markstream-pre__line-numbers-text">{lineNumbersText}</span></span>{/if}<code class="markstream-pre__code" translate="no">{displayCode}</code></pre>
|
|
21
76
|
{/if}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { SvelteRenderableNode } from './shared/node-helpers';
|
|
2
2
|
type Props = {
|
|
3
3
|
node: SvelteRenderableNode;
|
|
4
|
+
showLineNumbers?: boolean;
|
|
5
|
+
enhanceable?: boolean;
|
|
6
|
+
class?: string;
|
|
7
|
+
style?: string;
|
|
4
8
|
};
|
|
5
9
|
declare const PreCodeNode: import("svelte").Component<Props, {}, "">;
|
|
6
10
|
type PreCodeNode = ReturnType<typeof PreCodeNode>;
|
|
@@ -27,6 +27,7 @@ export type NodeRendererCodeBlockProps = Partial<{
|
|
|
27
27
|
showPreviewButton: boolean;
|
|
28
28
|
showCollapseButton: boolean;
|
|
29
29
|
showFontSizeButtons: boolean;
|
|
30
|
+
showLineNumbers: boolean;
|
|
30
31
|
htmlPreviewAllowScripts: boolean;
|
|
31
32
|
htmlPreviewSandbox: string;
|
|
32
33
|
}> & Record<string, unknown>;
|