# Containers and nested blocks

Register business containers, style native content and provide disclosure controls.

## Complete example

This example registers a `decision` container, nests a `warning`, and supplies a QuireKit disclosure button for `details`. Source mode edits all body content. Folding does not rewrite Markdown. The source tab includes registration, styles, controls and mounting.

### 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>

```

## Install and register

~~~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 ? 'Collapse' : 'Expand'} ${title}`,
})
// Include Containers alongside StarterKit in extensions.
~~~

Defaults include `info`, `note`, `tip`, `important`, `warning`, `danger`, `caution` and `details`. The `types` array replaces the defaults, so retain standard roles you still need. Names start with a lowercase letter and may contain digits and hyphens.

## Syntax and nesting

~~~~markdown
:::: decision Release decision
Content can include **emphasis**, lists, tables and fenced code.

::: warning Before release
- Check the rollback plan
- Check the version
:::
::::
~~~~

An opening fence has at least three colons, whitespace and a registered type. Remaining header text is a literal title, not executable HTML or separately rendered inline Markdown.

A closing fence must contain at least as many colons as its opener. Use a longer outer fence when nesting, such as four outside and three inside. The inner close then cannot terminate the outer block. Indentation follows Markdown block context; four-space indentation in ordinary top-level content is code.

Unknown names and unclosed containers retain source. The same Markdown parser owns nested body blocks. This does not imply arbitrary editable HTML children or compatibility with custom `markdown-it-container` HTML renderers.

## Reuse complete Prose blocks

This example uses `createMdEditorExtensions()` from `@quirekit/md-editor` to combine source capabilities with public Prose presentation. The document uses `qk-prose`; containers share `qk-prose-container`, `qk-prose-container__header` and `qk-prose-container__body` with `QuireProseContainer`. The component library owns their surfaces, full outlines, padding and nested spacing.

Containers are real nested DOM wrappers around the original CodeMirror lines. There is still one editor, selection and source document. Customize public Prose styles and semantic theme tokens instead of painting every line separately or overriding overlapping ancestor classes.

The headless `CustomContainer` continues to own source semantics, disclosure state and replaceable controls. Applications using QuireKit's design can consume the product preset directly. See the [standalone MD Editor example](/en/md-editor) for editing, source and Prose reading views.

## Replace disclosure controls

`controls` accepts `ComponentViewRenderer<ContainerControlProps>`. Vue uses `VueComponentViewRenderer(Toggle)` and the component receives `view`.

| Property | Usage |
| --- | --- |
| `expanded` | Visual state and `aria-expanded` |
| `label` | Localized accessible action from `toggleLabel` |
| `toggle()` | Request source-neutral disclosure |

This entry currently controls `details`; it cannot replace the editable container body. Disclosure survives mode changes and resets on document replacement. Selection and search reveal folded content through runtime behavior. Readonly readers may fold because source stays unchanged.

## Header editing, export and verification

The built-in codec reads `name` and `title` but does not offer a generic header patch. Calling `updateAttributes('customContainer', ...)` does not create a title editor. A custom title form needs a precise header codec with indentation, nesting, CRLF, history and stale-intent tests; do not regenerate the whole body.

Saving preserves colon syntax. Your HTML export pipeline must interpret registered roles independently; editor components do not automatically participate in export.

Verify unknown/unclosed fallback, nested closing, source selection, readonly folding, undo, search into hidden content, both themes and document-reset behavior. See [Extension API](/en/api/extensions).
