CodeBlockNode (Component)
CodeBlockNode 是库中用于渲染富交互代码块的组件。对于需要高亮、File/Diff surface 与交互的场景,推荐安装 stream-diffs。Vue 组件负责流式结束、可见性和卸载;stream-diffs 根 runtime 只负责 framework-agnostic DOM surface。
Quick summary
- Enhanced mode (install
stream-diffs) — finalized File/FileDiff surface with syntax highlighting and diff interactions - Fallback — plain
<pre><code>whenstream-diffsis not installed
Props
Refer to src/types/component-props.ts for full signature. Key props:
node— code_block node (required)loading,stream,isShowPreviewcodeBlockOptions— renderer-neutral typography, layout, File/FileDiff, interaction, annotation, and callback options. The same prop is also available at theNodeRenderer/MarkdownRendertop level.theme— a registered string name or{ dark, light };themesis the[dark, light]name pair to load.- Header controls:
showHeader,showCollapseButton,showCopyButton,showExpandButton,showPreviewButton,showFontSizeButtons,showTooltips - HTML preview sandbox:
htmlPreviewAllowScriptsdefaults tofalse, andhtmlPreviewSandboxlets you override the iframe sandbox tokens directly
Built-in inline HTML preview uses sandbox="" by default so untrusted preview documents do not run scripts or inherit the host origin. htmlPreviewSandbox takes precedence over htmlPreviewAllowScripts; passing htmlPreviewSandbox="" keeps the iframe fully sandboxed, omitting htmlPreviewSandbox leaves htmlPreviewAllowScripts in control, and invalid non-string overrides such as null fall back to the safe default. Only opt into htmlPreviewAllowScripts for trusted demos, and avoid combining allow-scripts with allow-same-origin for untrusted preview content.
Default finalized diff UX when no codeBlockOptions override is supplied:
diffStyle: 'split'expandUnchanged: falsecollapsedContextThreshold: 5hunkSeparators: 'line-info'parseDiffOptions: { context: 2 }
The same folding options apply to the fallback and finalized surface; streaming state does not override the consumer's expandUnchanged choice. Theme, content/language, header, mount/reveal timing, and disposal remain host-owned even when codeBlockOptions is present.
overflow controls both the fallback <pre> and the enhanced surface. The default is 'wrap'; use 'scroll' for non-wrapping content with horizontal scrolling. Visual wrapping never creates extra logical diff rows or changes the built-in - / + counts.
Diff blocks also show - / + line counts in the built-in header.
Slots
header-left— replace left headerheader-right— replace right headerloading— customize placeholder when streaming is disabled
Emits
copy(text: string)— when copy pressedpreviewCode(payload)— only emitted when you attach a@preview-codelistener; payload is{ node, artifactType, artifactTitle, id }
Examples
Install and run
pnpm add stream-diffsBasic example
<script setup lang="ts">
import type { CodeBlockNodeProps } from 'markstream-vue'
import { CodeBlockNode } from 'markstream-vue'
const node = {
type: 'code_block',
language: 'js',
code: 'console.log(1)',
raw: 'console.log(1)',
} satisfies CodeBlockNodeProps['node']
</script>
<template>
<CodeBlockNode :node="node" />
</template>Replace header and hide copy button
<script setup lang="ts">
import type { CodeBlockNodeProps } from 'markstream-vue'
import { CodeBlockNode } from 'markstream-vue'
const node = {
type: 'code_block',
language: 'js',
code: 'console.log(1)',
raw: 'console.log(1)',
} satisfies CodeBlockNodeProps['node']
function runSnippet() {}
</script>
<template>
<CodeBlockNode :node="node" :show-copy-button="false">
<template #header-left>
<div class="flex items-center">
Custom left
</div>
</template>
<template #header-right>
<button @click="runSnippet">
Run
</button>
</template>
</CodeBlockNode>
</template>Configure the code surface
<script setup lang="ts">
import type { CodeBlockNodeProps, CodeBlockOptions } from 'markstream-vue'
import { CodeBlockNode } from 'markstream-vue'
const node = {
type: 'code_block',
language: 'ts',
code: 'const answer = 42',
raw: 'const answer = 42',
} satisfies CodeBlockNodeProps['node']
const codeBlockOptions: CodeBlockOptions = {
fontSize: 13,
lineHeight: 20,
overflow: 'wrap',
disableLineNumbers: true,
enableLineSelection: true,
}
</script>
<template>
<CodeBlockNode :node="node" :code-block-options="codeBlockOptions" />
</template>fontSize, lineHeight, fontFamily, numeric-pixel maxHeight, numeric-pixel symmetric padding, and tabSize are host-managed so the fallback and finalized surface share geometry. Other supported fields are forwarded to stream-diffs. Use the separate component props for header and toolbar controls.
Custom loading placeholder
<script setup lang="ts">
import type { CodeBlockNodeProps } from 'markstream-vue'
import { CodeBlockNode } from 'markstream-vue'
const node = {
type: 'code_block',
language: 'ts',
code: 'console.log("loading")',
raw: 'console.log("loading")',
} satisfies CodeBlockNodeProps['node']
</script>
<template>
<CodeBlockNode :node="node" :stream="false" :loading="true">
<template #loading="{ loading, stream }">
<div v-if="loading && !stream">
Loading editor assets…
</div>
</template>
</CodeBlockNode>
</template>Theme Switching
CodeBlockNode supports automatic theme switching based on dark/light mode. Use @vueuse/core's useDark composable to track the theme state and pass theme names to MarkdownRender or CodeBlockNode.
Using @vueuse/core in standalone Vue apps
<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'
import MarkdownRender from 'markstream-vue'
const isDark = useDark() // Ref<boolean> reactive to system/theme preference
const toggleDark = useToggle(isDark)
const content = '# Example\n\n```js\nconsole.log("dark mode")\n```'
// Dark/light theme pair
const themes = [
'vitesse-dark',
'vitesse-light',
] as const
</script>
<template>
<div>
<button @click="toggleDark()">
Toggle Theme
</button>
<MarkdownRender
:is-dark="isDark"
:code-block-props="{ theme: { dark: 'vitesse-dark', light: 'vitesse-light' } }"
:themes="themes"
:content="content"
/>
</div>
</template>VitePress integration
For VitePress, use the built-in isDark from VitePress's useData():
// docs/.vitepress/theme/composables/useDark.ts
import { useData } from 'vitepress'
/**
* VitePress theme composable for dark mode
* Uses VitePress's built-in isDark from useData()
*/
export function useDark() {
const { isDark } = useData()
return isDark
}<!-- In any .md file or component -->
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
import { useDark } from '../../.vitepress/theme'
const isDark = useDark()
const content = '# Example\n\n```js\nconsole.log("dark mode")\n```'
const themes = [
'vitesse-dark',
'vitesse-light',
] as const
</script>
<template>
<MarkdownRender
:is-dark="isDark"
:code-block-props="{ theme: { dark: 'vitesse-dark', light: 'vitesse-light' } }"
:themes="themes"
:content="content"
/>
</template>How it works:
The theme prop accepts either a fixed theme or a light/dark pair:
<!-- Auto-switch between light and dark (recommended) -->
<CodeBlockNode :theme="{ light: 'vitesse-light', dark: 'vitesse-dark' }" />
<!-- Fixed theme (ignores isDark) -->
<CodeBlockNode theme="monokai" />When using a { dark, light } pair, the component automatically switches based on the isDark prop.
The themes prop accepts exactly a [dark, light] pair. A former Monaco JSON theme object is not a valid theme value in 2.0; use registerCustomTheme from stream-diffs/pierre, then pass its registered name.
Backward compatibility:
darkTheme/lightThemeprops still work but are deprecated. Prefer the unifiedthemeprop.
Key differences for CodeBlockNode:
| Prop | Direct CodeBlockNode | Via MarkdownRender |
|---|---|---|
isDark | Passed directly to <CodeBlockNode :is-dark="isDark" /> | Passed via <MarkdownRender :is-dark="isDark" /> and automatically forwarded |
| Theme | :theme="{ dark: 'vitesse-dark', light: 'vitesse-light' }" | :code-block-props="{ theme: { dark: 'vitesse-dark', light: 'vitesse-light' } }" |
| Themes pair | :themes="['vitesse-dark', 'vitesse-light']" | :themes="['vitesse-dark', 'vitesse-light']" |
Notes
- The CodeBlock header API is documented in
docs/guide/codeblock-header.md(examples for replacing header and custom loading placeholder). CodeBlockNodeandMermaidBlockNodeintentionally use differentcopyevent payloads:CodeBlockNodeemitscopy(text: string), whileMermaidBlockNodeemitscopy(ev: MermaidBlockEvent<{ type: 'copy'; text: string }>)(supportspreventDefault()).- For large code blocks (tens of thousands of lines), Shiki tokenization runs on the main thread by default. To offload it to Web Workers, inject an upstream
@pierre/diffsWorkerPoolManagerviasetStreamDiffsWorkerPool(...);CodeBlockNodeforwards it as theworkerManageroption and keeps the active theme in sync. Without an injected pool (or when the pool reports itself unavailable) the surface highlights on the main thread. See Code Block Runtime for the setup.
Try this — simple snapshot example (inline usage):
<script setup lang="ts">
import type { CodeBlockNodeProps } from 'markstream-vue'
import { CodeBlockNode } from 'markstream-vue'
const node = { type: 'code_block', language: 'js', code: 'console.log("hello")', raw: 'console.log("hello")' } satisfies CodeBlockNodeProps['node']
</script>
<template>
<CodeBlockNode :node="node" />
</template>