# 容器与嵌套块

注册业务容器、定义样式、嵌套正文并接入自己的折叠控件。

## 完整示例

此例安装 `decision` 业务容器，在其中嵌套 `warning`，并用 QuireKit 按钮控制 `details`。切换源码模式可以编辑任何正文；展开和收起不会改写 Markdown。接入代码包含注册、样式、控件和挂载。

### containers/editor.vue

```vue
<script setup lang="ts">
import { createMdEditorExtensions } from "@quirekit/md-editor";
import { VueComponentViewRenderer } from "@quirekit/msfe-vue";
import EditorExample from "../shared/editor-example.vue";
import type { ExampleLocale } from "../shared/types";
import Toggle from "./toggle.vue";
const props = withDefaults(defineProps<{ /** Host UI language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const english = props.locale === "en";
const markdown = english
  ? '# Container workshop\n\n:::: decision Release decision\nA **native Markdown** paragraph.\n\n::: warning Before release\n- Check the rollback plan\n- Check the version\n:::\n::::\n\n::: details Evidence\nThese paragraphs remain editable Markdown.\n\n| Check | Result |\n| --- | --- |\n| Source | Unchanged |\n:::\n'
  : '# 容器工作坊\n\n:::: decision 发布决策\n一段 **原生 Markdown** 正文。\n\n::: warning 发布前检查\n- 确认回滚方案\n- 确认版本\n:::\n::::\n\n::: details 验证依据\n这些段落仍然是可以编辑的 Markdown。\n\n| 检查项 | 结果 |\n| --- | --- |\n| 源码 | 保持原样 |\n:::\n';
// Arrays replace defaults: retain standard roles explicitly when adding a business role.
const extensions = createMdEditorExtensions({ containers: {
  types: ["info", "note", "tip", "important", "warning", "danger", "caution", "details", "decision"],
  controls: VueComponentViewRenderer(Toggle), detailsOpen: false,
  /** @param expanded - Current disclosure. @param title - Plain container title. @returns Accessible localized action. */
  toggleLabel: (expanded, title) => `${english ? (expanded ? "Collapse" : "Expand") : (expanded ? "收起" : "展开")} ${title}`,
} });
</script>
<template><EditorExample :locale="locale" :markdown="markdown" :extensions="extensions" prose /></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";

```

### containers/toggle.vue

```vue
<script setup lang="ts">
import type { ContainerControlProps } from "@quirekit/msfe-markdown-extras";
import { QuireIconButton, QuireIcon } from "@quirekit/ui/vue";
defineProps<{ /** Source-neutral disclosure state and action. */ view: ContainerControlProps }>();
</script>
<template><QuireIconButton size="sm" variant="ghost" :aria-expanded="view.expanded" :label="view.label" data-qk-not-prose @click="view.toggle"><QuireIcon :name="view.expanded ? 'chevronDown' : 'chevronRight'" /></QuireIconButton></template>

```

## 安装与注册

~~~sh
pnpm add '@quirekit/msfe-markdown-extras@workspace:*'
~~~

~~~ts
import { CustomContainer } from '@quirekit/msfe-markdown-extras'

const Containers = CustomContainer.configure({
  types: ['info', 'note', 'tip', 'important', 'warning', 'danger', 'caution', 'details', 'decision'],
  detailsOpen: false,
  toggleLabel: (expanded, title) => `${expanded ? '收起' : '展开'} ${title}`,
})
// Include Containers alongside StarterKit in extensions.
~~~

默认支持 `info`、`note`、`tip`、`important`、`warning`、`danger`、`caution`、`details`。`types` 是完整替换的数组，新增业务名时保留需要的标准类型。类型名使用小写字母开头，后续可包含数字和连字符。

## 语法与嵌套规则

~~~~markdown
:::: decision 发布决策
正文可以包含 **强调**、列表、表格和代码块。

::: warning 发布前检查
- 确认回滚方案
- 确认版本
:::
::::
~~~~

开围栏至少三个冒号，随后必须有空白及已注册类型，余下内容作为字面标题。标题不是可执行 HTML，也不按内联 Markdown 重新渲染。

关闭围栏的冒号数量至少与当前打开围栏相同。嵌套时使用更长的外层围栏（外层四个、内层三个），让内层闭合不提前结束外层。围栏的缩进必须符合 Markdown 块上下文；普通正文中四空格缩进是代码，不能当作顶层容器。

未知类型或未闭合容器保留源码。正文由同一个 Markdown 解析器处理，可以嵌套普通块；这不等于支持任意富文本子树、任意 HTML 属性或 `markdown-it-container` 的自定义 HTML renderer。

## 复用 Prose 的完整块样式

这个示例使用 `@quirekit/md-editor` 的 `createMdEditorExtensions()` 组合源码能力与公共 Prose 呈现。编辑区直接消费 `qk-prose`，容器使用与 `QuireProseContainer` 相同的 `qk-prose-container`、`qk-prose-container__header`、`qk-prose-container__body`。背景、完整边框、内边距和嵌套间距都由组件库拥有。

这里的容器是真实的嵌套 DOM 包装，正文仍是同一个 CodeMirror 中的原始行，没有嵌套第二个编辑器。应通过公共 Prose 样式和语义主题定制它，不再给每一行分别涂色，也不用判断一行同时具有多少外层角色。

纯 headless `CustomContainer` 仍提供源码语义、折叠状态和可替换控件；采用 QuireKit 设计的应用可以直接使用产品组合。完整编辑、源码与 Prose 阅读对照见 [MD Editor 独立示例](/md-editor)。

## 自定义折叠控件

`controls` 接受 `ComponentViewRenderer<ContainerControlProps>`，Vue 使用 `VueComponentViewRenderer(Toggle)`，组件接收 `view` 属性。

| 属性 | 用法 |
| --- | --- |
| `expanded` | 显示状态并绑定 `aria-expanded` |
| `label` | 使用 `toggleLabel` 生成的本地化无障碍名称 |
| `toggle()` | 请求源码中立的展开或收起 |

当前该入口用于 `details` 的折叠控制，不能替换容器的可编辑正文。折叠状态在模式切换间保留，换文档时重置；选区和搜索进入折叠内容会按运行时规则显露它。只读阅读允许折叠，因为它不改源码。

## 修改标题、导出和验证

当前内置容器 codec 读取 `name`、`title`，没有通用字段写回：`updateAttributes('customContainer', ...)` 不会自动成为标题编辑器。需要标题表单时，实现自己的精确 header patch，并测试缩进、嵌套、CRLF、撤销和过期意图；不要重建整个容器文本。

保存输出仍为原始冒号语法。HTML 导出由宿主自己的 Markdown 管线解释注册类型，不能假设编辑器组件会自动参与导出。

验收时至少检查：未知/未闭合回退、内层闭合、源码选区、只读折叠、撤销、搜索到隐藏正文、两种主题，以及切换文档后的折叠重置。协议见 [扩展 API](/api/extensions)。
