# 代码块定制

定制工具栏、语言高亮和样式，理解与整块预览的边界。

## 完整代码块工具栏

示例给代码围栏增加语言选择器。修改语言仅改变 header 的语言字段；代码正文、围栏字符和其他内容保留。切换只读后语言选择器禁用，撤销恢复原语言。

### code-block/editor.vue

```vue
<script setup lang="ts">
import { StarterKit } from "@quirekit/msfe-starter-kit";
import { CodeBlock } from "@quirekit/msfe-code-block";
import { VueNodeViewRenderer } from "@quirekit/msfe-vue";
import EditorExample from "../shared/editor-example.vue";
import type { ExampleLocale } from "../shared/types";
import Toolbar from "./toolbar.vue";
import { codeLanguages } from "./languages";
const props = withDefaults(defineProps<{ /** Host UI language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const markdown = `${props.locale === "en" ? '# Code block workshop' : '# 代码块工作坊'}\n\n~~~typescript\nconst answer: number = 42\nconsole.log(answer)\n~~~\n`;
const CustomCodeBlock = CodeBlock.extend({
  /** @returns A header control; the code body remains in CodeMirror. */
  addNodeView() { return VueNodeViewRenderer(Toolbar); },
});
// Replace the preset's code-block entry, so there is still only one owner with this name.
const extensions = [StarterKit.configure({ codeBlock: CustomCodeBlock })];
</script>
<template><EditorExample :locale="locale" :markdown="markdown" :extensions="extensions" :code-languages="codeLanguages" /></template>

```

### shared/editor-example.vue

```vue
<script setup lang="ts">
import { computed, provide, ref, shallowRef } from "vue";
import { useMsfeEditor, VueNodeViewHost } from "@quirekit/msfe-vue";
import type { AnyExtension, CodeLanguage } from "@quirekit/msfe-core";
import { QuireButton, QuireSegmented, QuireProseSource } from "@quirekit/ui/vue";
import { Appearance } from "./appearance";
import { exampleMessages } from "./messages";
import { exampleLocaleKey } from "./locale";
import type { ExampleLocale } from "./types";
const props = defineProps<{
  /** Initial canonical source; changing this prop does not replace an open document. */ markdown: string;
  /** Explicitly installed feature descriptors. */ extensions: readonly AnyExtension[];
  /** Language of host controls. */ locale: ExampleLocale;
  /** Optional exact lazy code-language registry. */ codeLanguages?: readonly CodeLanguage[];
  /** A product preset already supplies the public Prose presentation. */ prose?: boolean;
}>();
const text = computed(() => exampleMessages[props.locale]);
provide(exampleLocaleKey, computed(() => props.locale));
const parent = shallowRef<HTMLElement | null>(null);
const snapshot = ref<string>();
const mode = ref("live");
const items = computed(() => [{ value: "live", label: text.value.live }, { value: "source", label: text.value.source }, { value: "locked", label: text.value.locked }]);
const { editor, host } = useMsfeEditor(parent, () => ({ markdown: props.markdown, mode: "live", label: text.value.editor,
  extensions: [...props.extensions, ...(props.prose ? [] : [Appearance])], codeLanguages: props.codeLanguages }));
/** @param value - Requested display mode. */
function changeMode(value: string | null | undefined): void {
  if (value === "live" || value === "source" || value === "locked") { mode.value = value; editor.value?.setMode(value); }
}
</script>
<template>
  <div class="grid gap-4 p-qk-surface">
    <div class="flex flex-wrap items-center justify-between gap-3">
      <QuireSegmented :model-value="mode" :items="items" :label="text.modes" size="sm" @update:model-value="changeMode" />
      <div class="flex gap-2">
        <QuireButton size="sm" variant="ghost" :disabled="mode === 'locked'" @click="editor?.undo()">{{ text.undo }}</QuireButton>
        <QuireButton size="sm" variant="outline" @click="snapshot = editor?.getMarkdown()">{{ text.readMarkdown }}</QuireButton>
      </div>
    </div>
    <slot :editor="editor" />
    <div ref="parent" class="overflow-hidden rounded-qk-control border border-qk-border-subtle" />
    <VueNodeViewHost :host="host" />
    <QuireProseSource v-if="snapshot !== undefined" :code="snapshot" language="markdown" filename="document.md" />
  </div>
</template>

```

### shared/appearance.ts

```ts
import { Extension } from "@quirekit/msfe-core";
import { EditorView } from "@quirekit/cm-view";
import { Prec } from "@quirekit/cm-state";
import { HighlightStyle, syntaxHighlighting } from "@quirekit/cm-language";
import { tags } from "@lezer/highlight";
import { themeVariableReferences as token } from "@quirekit/ui";

/** Consumer-owned theme; source semantics stay in the installed feature packages. */
export const Appearance = Extension.create({ name: "documentationAppearance",
  /** @returns Scoped typography, surfaces and syntax colors from the host design system. */
  addCodeMirrorExtensions() {
    return Prec.high([
      EditorView.theme({
        "&": { ...token.typography.body, color: token.color.text.primary, backgroundColor: token.color.surface.page, minHeight: "180px" },
        ".cm-scroller": { fontFamily: "inherit", lineHeight: "1.8" },
        ".cm-content": { padding: token.spacing.surface, caretColor: token.color.text.primary },
        ".cm-line": { padding: "0" },
        ".cm-gutters": { color: token.color.text.tertiary, backgroundColor: token.color.surface.subtle, border: "none" },
        ".cm-activeLine, .cm-activeLineGutter": { backgroundColor: "transparent" },
        ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": { backgroundColor: token.color.selection.document },
        ".cm-cursor": { borderLeftColor: token.color.text.primary },
        ".msfe-h1": { ...token.typography.pageTitle },
        ".msfe-h2, .msfe-h3": { ...token.typography.sectionTitle },
        ".msfe-heading": { paddingBlock: token.spacing.inline },
        ".msfe-link": { color: token.color.text.accent },
        ".msfe-code-line, .msfe-inline-code": { ...token.typography.code },
        ".consumer-status": { verticalAlign: "middle" },
        "&.cm-focused": { outline: "none" },
      }),
      syntaxHighlighting(HighlightStyle.define([
        { tag: tags.heading, color: token.color.text.primary, fontWeight: "600" },
        { tag: tags.strong, fontWeight: "700" },
        { tag: tags.emphasis, fontStyle: "italic" },
        { tag: [tags.link, tags.url], color: token.color.text.accent },
        { tag: tags.processingInstruction, color: token.color.text.tertiary },
        { tag: tags.monospace, color: token.color.text.secondary },
      ])),
    ]);
  },
});

```

### shared/messages.ts

```ts
const chinese = {
  editor: "Markdown 示例编辑器", modes: "编辑器模式", source: "源码", live: "实时预览", locked: "只读",
  undo: "撤销", redo: "重做", reset: "重置", readMarkdown: "读取 Markdown", readingPreview: "阅读预览", insertStatus: "插入状态", done: "已完成", todo: "待处理",
  nativeEditor: "原生 DOM 示例编辑器", statusEditor: "自定义状态编辑器",
  initial: "## 把想法写下来\n\n在这里编辑 **Markdown**，也可以切换到源码。\n\n- [x] 保留 Markdown 原文\n- [ ] 试试修改这一行\n",
  nativeInitial: "## 原生 DOM 接入\n\n这个实例直接使用 `createMsfeEditor()`。\n",
  statusInitial: "## 自定义状态\n\n设计评审：[[todo]]\n\n点击状态按钮，然后读取 Markdown 或撤销。\n",
};
const english: { [Key in keyof typeof chinese]: typeof chinese[Key] } = {
  editor: "Markdown example editor", modes: "Editor mode", source: "Source", live: "Live", locked: "Locked",
  undo: "Undo", redo: "Redo", reset: "Reset", readMarkdown: "Read Markdown", readingPreview: "Reading preview", insertStatus: "Insert status", done: "Done", todo: "Pending",
  nativeEditor: "Native DOM example editor", statusEditor: "Custom status editor",
  initial: "## Write your ideas\n\nEdit **Markdown** here, or switch to source.\n\n- [x] Preserve the Markdown source\n- [ ] Try editing this line\n",
  nativeInitial: "## Native DOM integration\n\nThis instance uses `createMsfeEditor()` directly.\n",
  statusInitial: "## Custom status\n\nDesign review: [[todo]]\n\nClick the status, then read the Markdown or undo.\n",
};
export const exampleMessages = { "zh-CN": chinese, en: english };

```

### shared/locale.ts

```ts
import type { ComputedRef, InjectionKey } from "vue";
import type { ExampleLocale } from "./types";

/** Host UI language shared through the existing Vue provider tree, never stored in Markdown. */
export const exampleLocaleKey: InjectionKey<ComputedRef<ExampleLocale>> = Symbol("example-locale");

```

### shared/types.ts

```ts
/** Example host language; independent of the documentation router. */
export type ExampleLocale = "zh-CN" | "en";

```

### code-block/toolbar.vue

```vue
<script setup lang="ts">
import { computed, inject } from "vue";
import type { MsfeNodeViewProps } from "@quirekit/msfe-core";
import type { CodeBlockAttributes } from "@quirekit/msfe-code-block";
import { QuireButton, QuireSelect } from "@quirekit/ui/vue";
import { exampleLocaleKey } from "../shared/locale";
const props = defineProps<{ /** Current header fields and guarded edits. */ nodeView: MsfeNodeViewProps<CodeBlockAttributes> }>();
const locale = inject(exampleLocaleKey, computed(() => "zh-CN"));
const english = computed(() => locale.value === "en");
const items = [{ value: "typescript", label: "TypeScript" }, { value: "javascript", label: "JavaScript" }, { value: "json", label: "JSON" }];
/** @param value - Selected exact language name. */
function change(value: string | null | undefined): void { if (value) props.nodeView.updateAttributes({ language: value }); }
</script>
<template>
  <span class="inline-flex items-center gap-2 py-2">
    <QuireSelect :model-value="nodeView.attrs.language" :items="items" :label="english ? 'Code language' : '代码语言'" size="sm" :disabled="nodeView.readOnly" @update:model-value="change" />
    <QuireButton size="sm" variant="ghost" @click="nodeView.revealSource()">{{ english ? 'Show source' : '显示源码' }}</QuireButton>
  </span>
</template>

```

### code-block/languages.ts

```ts
import type { CodeLanguage } from "@quirekit/msfe-core";
/** Exact opt-in names; unknown languages stay plain text and do not trigger guessed imports. */
export const codeLanguages: readonly CodeLanguage[] = [
  { name: "typescript", aliases: ["ts"],
    /** @returns TypeScript support, loaded only when encountered. */
    async load() { return (await import("@quirekit/cm-lang-javascript")).javascript({ typescript: true }); } },
  { name: "javascript", aliases: ["js"],
    /** @returns JavaScript support. */
    async load() { return (await import("@quirekit/cm-lang-javascript")).javascript(); } },
  { name: "json",
    /** @returns JSON support. */
    async load() { return (await import("@quirekit/cm-lang-json")).json(); } },
];

```

## 先选择定制层

| 需求 | 入口 |
| --- | --- |
| 改代码字体、背景、间距 | `EditorView.theme()` 和语法高亮 |
| 增加语言、复制、菜单控件 | `CodeBlock.extend({ addNodeView() {} })` |
| 支持 TypeScript、JSON 等语法着色 | 创建选项 `codeLanguages` |
| Mermaid、ECharts 或 flow DSL 图形输出 | 独立语义扩展 + 块级投影，见 [图表预览](/extensions/diagrams) |

`CodeBlock` 的 NodeView 位于围栏前方，属于 `placement: 'before'`。它不是整个代码正文的替换入口，也没有 `contentDOM` 供你接管输入。

## 覆盖预设中的一个功能

~~~ts
import { StarterKit } from '@quirekit/msfe-starter-kit'
import { CodeBlock } from '@quirekit/msfe-code-block'
import { VueNodeViewRenderer } from '@quirekit/msfe-vue'
import Toolbar from './toolbar.vue'

const CustomCodeBlock = CodeBlock.extend({
  addNodeView() { return VueNodeViewRenderer(Toolbar) },
})
const extensions = [StarterKit.configure({ codeBlock: CustomCodeBlock })]
~~~

不要同时安装 `StarterKit` 和同名 `CustomCodeBlock`。如果使用更小的按需预设，也可以禁用预设中的 `codeBlock` 后单独安装一个实现。

## 控件可读写什么

组件接收 `nodeView: MsfeNodeViewProps<CodeBlockAttributes>`。目前持久属性只有 `language`，可调用 `nodeView.updateAttributes({ language: 'json' })`。合法语言 token 长度不超过 40，允许字母、数字、下划线及 `+ # . -`，不允许换行和任意 header 注入。

也可在当前代码块选区使用 `editor.commands.setCodeLanguage('typescript')`。它依赖已确认的围栏语法和适用选区；返回 false 时检查结果。语言高亮目录和语言字段校验分别负责不同的事，某个字段合法不代表安装了对应高亮器。

复制源码属于读取动作：由宿主读取当前已确认的 `CodeText` 范围，保持换行和缩进。NodeView 的 `getRange()` 只提供范围，不承诺给你一份可直接当作代码正文的字符串。跨弹窗动作使用 [锚点](/guide/selection)。

## 按需加载语言

示例 `code-block/languages.ts` 提供 TypeScript、JavaScript 和 JSON，相关依赖为 `@quirekit/cm-lang-javascript` 与 `@quirekit/cm-lang-json`。创建编辑器时传 `codeLanguages`。

`name` 与 `aliases` 精确匹配且大小写不敏感。`load(signal)` 返回 `LanguageSupport`；未知语言保持纯文本，不推测文件名或动态下载任意包。动态导入不能真正中断，宿主资源需要观察信号，过期结果不能覆盖新的编辑器状态。

## 没有正文行的空围栏

`CodeBlock` 默认给相邻的开闭围栏提供「输入代码」占位行。点击或用 Enter / Space 激活后，
通过普通编辑命令插入一行，保留引用和列表缩进，并把光标放入代码正文。显示占位、滚动和
切换模式不改变 Markdown；已有真实空行不重复占位。只读不能激活，插入支持撤销和重做。

使用 `CodeBlock.configure({ emptyLine: false })` 可关闭；`emptyLineLabel` 设置可访问的提示文字。
MD Editor 通过 `features.codeBlock: ProseCodeBlock.configure({ emptyLine: false })` 配置，默认按界面语言显示提示。
源码模式始终展示真实围栏和换行，不显示占位。

MD Editor 默认提供 `mdCodeLanguages`：TypeScript、JavaScript / JSX、TSX、JSON、CSS、HTML、
Python、Bash 和 Markdown，按需加载；传 `codeLanguages` 会替换整份目录，传 `[]` 禁用默认目录。
自定义主题和语法包必须共享同一版本的 `@lezer/highlight`，否则标记身份不一致会导致已解析却无颜色。

## 样式和源码活动状态

可从 `.msfe-code-line`、`.msfe-code-start`、`.msfe-code-body-start`、`.msfe-code-end`、`.msfe-code-indented` 等语义类设置外观，并由 `HighlightStyle` 定义代码 token。语言 token 隐藏与编辑器行布局应由运行时处理；不要隐藏所有 `.cm-line` 或把代码正文移到另一个 `contenteditable`。

进入围栏源码范围时，活动源码会显露。中文组合输入、跨块拖选、复制和撤销都应继续走原生编辑链路。高度变化使用组件测量入口，详见 [投影层](/extensions/projections)。

## 验证范围

至少测试：切换语言、撤销、空/未知语言、长代码、未闭合围栏、反引号和波浪线围栏、只读、Tab 缩进，以及主题切换后代码仍可编辑。完整选项和组件协议见 [渲染 API](/api/rendering)。
