# Diagrams and fenced previews

Build Mermaid and ECharts previews with loading, source fallback and resource cleanup.

## Complete interactive example

The flowchart uses built-in `Mermaid`. The chart below uses the custom `echarts` fence extension supplied on this page. Edit source, change values and move the selection away to update the preview. Read Markdown to verify rendering has not reserialized the fence.

### charts/editor.vue

```vue
<script setup lang="ts">
import { StarterKit } from "@quirekit/msfe-starter-kit";
import { Mermaid } from "@quirekit/msfe-markdown-extras";
import { VueComponentViewRenderer } from "@quirekit/msfe-vue";
import EditorExample from "../shared/editor-example.vue";
import type { ExampleLocale } from "../shared/types";
import { Charts } from "./extension";
import PreviewControls from "./preview-controls.vue";
const props = withDefaults(defineProps<{ /** Host UI language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const english = props.locale === "en";
const flow = english ? "Draft --> Review --> Publish" : "Draft[草稿] --> Review[评审] --> Publish[发布]";
const markdown = `${english ? '# Diagrams from source' : '# 源码中的图表'}\n\n~~~mermaid\nflowchart LR\n  ${flow}\n~~~\n\n~~~echarts\n{"type":"bar","labels":["A","B","C"],"values":[12,20,16]}\n~~~\n`;
const mermaid = Mermaid.configure({ controls: VueComponentViewRenderer(PreviewControls) });
</script>
<template><EditorExample :locale="locale" :markdown="markdown" :extensions="[StarterKit, mermaid, Charts]" /></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";

```

### charts/extension.ts

```ts
import { Node, defineSource, componentView } from "@quirekit/msfe-core";
import { EditorSelection } from "@quirekit/cm-state";
import { chartRenderer } from "./renderer";
/** Closed top-level echarts fences, rendered without rewriting their JSON or fence spelling. */
export const Charts = Node.create({ name: "exampleCharts",
  /** @returns Semantic recognition; this example does not provide a JSON field-editing command. */
  addSource() {
    return defineSource({ nodes: ["FencedCode"],
      /** @param document - Canonical source. @param node - Confirmed syntax. @returns Recognized data or a native source fallback. */
      read(document, node) {
        const info = node.child("CodeInfo");
        if (node.parentKind !== "Document" || node.children("CodeMark").length !== 2 || !info
          || document.slice(info.from, info.to).trim().toLowerCase() !== "echarts") return null;
        return { text: node.children("CodeText").map(part => document.slice(part.from, part.to)).join("") };
      },
      /** @returns Read-only projection: edit its canonical JSON in source instead. */
      patch() { return null; },
    });
  },
  /** @returns A direct structural layer, available before viewport layout. */
  addProjectionLayers() {
    const owner = this;
    return [{ key: "exampleCharts.preview", placement: "block",
      /** @param input - Accepted source and current permissions. @returns Declarative replacements; selection reveal stays with core. */
      build(input) {
        return input.query(owner.name).matches.map(match => {
          const from = input.document.lineAt(match.from).from, to = match.to;
          const source = input.document.slice(from, to);
          return { type: "replace" as const, from, to, key: String(match.from),
            view: componentView(chartRenderer, { text: String(match.attrs.text), source, readOnly: input.readOnly,
              /** @returns False if source has changed since these props were accepted. */
              reveal() {
                const editor = owner.editor;
                if (editor.scope.disposed || editor.state.doc.sliceString(from, to) !== source) return false;
                editor.setMode("live");
                editor.setSelection(EditorSelection.single(editor.state.doc.lineAt(from).to + 1));
                editor.focus();
                return true;
              },
            }),
          };
        });
      },
    }];
  },
});

```

### charts/renderer.ts

```ts
import { VueComponentViewRenderer } from "@quirekit/msfe-vue";
import type { ComponentViewRenderer } from "@quirekit/msfe-core";
import ChartView from "./chart-view.vue";
import type { ChartProps, ChartViewProps } from "./types";
const vueRenderer = VueComponentViewRenderer<ChartViewProps>(ChartView);
/** @param mount - Runtime-owned DOM, provider host and lifetime. @returns Retained Vue view with the same ownership across updates. */
export const chartRenderer: ComponentViewRenderer<ChartProps> = mount => {
  const view = vueRenderer({ ...mount, props: { ...mount.props, scope: mount.scope, requestMeasure: mount.requestMeasure } });
  return {
    /** @param props - Newly accepted canonical inputs. */
    update(props) { view.update({ ...props, scope: mount.scope, requestMeasure: mount.requestMeasure }); },
    /** Releases the portal; runtime independently ends the scope. */
    destroy() { view.destroy(); },
  };
};

```

### charts/chart-view.vue

```vue
<script setup lang="ts">
import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue";
import { QuireButton, QuireProseSource } from "@quirekit/ui/vue";
import type { TaskScope } from "@quirekit/msfe-core";
import type { ChartViewProps } from "./types";
import { parseChartData } from "./data";
import { exampleLocaleKey } from "../shared/locale";
import { exampleMessages } from "../shared/messages";
const props = defineProps<{ /** Accepted source and runtime-owned view lifetime. */ view: ChartViewProps }>();
const locale = inject(exampleLocaleKey, computed(() => "zh-CN"));
const english = computed(() => locale.value === "en");
const text = computed(() => exampleMessages[locale.value]);
const surface = shallowRef<HTMLElement | null>(null);
const status = ref<"loading" | "ready" | "error">("loading");
let request: TaskScope | undefined;
/** Cancels previous work before loading another source generation. */
function render(): void {
  request?.dispose();
  status.value = "loading";
  const source = props.view.text;
  const lifetime = props.view.scope.fork(() => { lifetime.dispose(); status.value = "error"; void nextTick(props.view.requestMeasure); });
  request = lifetime;
  lifetime.acquire(async signal => {
    const data = parseChartData(source);
    const echarts = await import("echarts");
    if (signal.aborted || !surface.value) return { value: null };
    const element = surface.value;
    const chart = echarts.init(element, undefined, { renderer: "svg", locale: english.value ? "EN" : "ZH" });
    const releaseChart = lifetime.own(() => chart.dispose());
    // Host text color is resolved from design tokens; JSON never supplies scripts or HTML.
    chart.setOption({ animation: false, grid: { left: 44, right: 16, bottom: 32, top: 24 },
      xAxis: { type: "category", data: [...data.labels] }, yAxis: { type: "value" },
      series: [{ type: data.type, data: [...data.values] }], tooltip: { show: false } });
    /** Updates visual colors without replacing source, chart identity or editor state. */
    function recolor(): void {
      const color = getComputedStyle(element).color;
      chart.setOption({ color: [color], textStyle: { color }, xAxis: { axisLabel: { color } }, yAxis: { axisLabel: { color } } });
    }
    recolor();
    const resize = new ResizeObserver(() => { chart.resize(); props.view.requestMeasure(); });
    lifetime.own(() => resize.disconnect());
    resize.observe(element);
    const theme = new MutationObserver(recolor);
    lifetime.own(() => theme.disconnect());
    theme.observe(document.documentElement, { attributes: true, attributeFilter: ["data-qk-scheme", "style", "class"] });
    return { value: chart, release() { resize.disconnect(); theme.disconnect(); releaseChart(); } };
  }, chart => { if (chart) { status.value = "ready"; void nextTick(props.view.requestMeasure); } });
}
onMounted(render);
watch(() => props.view.text, render);
onBeforeUnmount(() => request?.dispose());
</script>
<template>
  <section class="grid gap-2 rounded-qk-control border border-qk-border-subtle p-qk-surface" :aria-label="english ? 'Chart preview' : '图表预览'" :data-chart-status="status">
    <div class="flex items-center justify-between gap-2">
      <span class="text-qk-text-secondary">ECharts</span>
      <div class="flex gap-2">
        <QuireButton v-if="status === 'error'" size="sm" variant="outline" @click="render">{{ english ? 'Retry' : '重试' }}</QuireButton>
        <QuireButton size="sm" variant="ghost" :disabled="view.readOnly" @click="view.reveal">{{ english ? 'Edit source' : '编辑源码' }}</QuireButton>
      </div>
    </div>
    <p v-if="status !== 'ready'" role="status">{{ status === 'loading' ? (english ? 'Loading chart…' : '正在加载图表…') : (english ? 'Invalid chart data. Edit the JSON and try again.' : '图表数据无效，请修改 JSON 后重试。') }}</p>
    <div ref="surface" class="h-64 min-w-0 w-full text-qk-text-accent" role="img" :aria-label="english ? 'Values by category' : '分类数据图表'" :aria-hidden="status !== 'ready'" />
    <QuireProseSource v-if="status === 'error'" :code="view.source" filename="chart.md" language="markdown" />
  </section>
</template>

```

### charts/types.ts

```ts
import type { TaskScope } from "@quirekit/msfe-core";
/** Immutable source-derived chart inputs; no executable code is accepted. */
export interface ChartProps {
  /** JSON payload read from a confirmed fence. */ readonly text: string;
  /** Exact full fence retained as the fallback. */ readonly source: string;
  /** Effective editing permission. */ readonly readOnly: boolean;
  /** @returns Whether the original source can still be revealed. */ readonly reveal: () => boolean;
}
/** Runtime ownership supplied by the component renderer. */
export interface ChartViewProps extends ChartProps {
  /** View lifetime; each load gets its own child lifetime. */ readonly scope: TaskScope;
  /** Notify CodeMirror after preview geometry changes. */ readonly requestMeasure: () => void;
}
/** Deliberately small, validated chart data contract, serialized as JSON. */
export interface ChartData {
  /** Category labels, at most 50. */ readonly labels: readonly string[];
  /** One finite number per category. */ readonly values: readonly number[];
  /** Supported series family. */ readonly type: "bar" | "line";
}

```

### charts/data.ts

```ts
import type { ChartData } from "./types";
/** @param text - Source-owned JSON. @returns Validated bounded data; arbitrary ECharts options are intentionally unsupported. */
export function parseChartData(text: string): ChartData {
  if (text.length > 16_000) throw new Error("chart-too-large");
  const data: unknown = JSON.parse(text);
  if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("invalid-chart");
  const value = data as Record<string, unknown>;
  if (Object.keys(value).some(key => !["labels", "values", "type"].includes(key))
    || !Array.isArray(value.labels) || !Array.isArray(value.values)
    || value.labels.length < 1 || value.labels.length > 50 || value.values.length !== value.labels.length
    || !value.labels.every(label => typeof label === "string" && label.length <= 80)
    || !value.values.every(number => typeof number === "number" && Number.isFinite(number))
    || (value.type !== "bar" && value.type !== "line")) throw new Error("invalid-chart-data");
  return { labels: value.labels, values: value.values, type: value.type };
}

```

### charts/preview-controls.vue

```vue
<script setup lang="ts">
import { computed, inject } from "vue";
import type { PreviewControlProps } from "@quirekit/msfe-markdown-extras";
import { QuireButton } from "@quirekit/ui/vue";
import { exampleLocaleKey } from "../shared/locale";
defineProps<{ /** Shared built-in preview actions, including failure recovery. */ view: PreviewControlProps }>();
const locale = inject(exampleLocaleKey, computed(() => "zh-CN"));
const english = computed(() => locale.value === "en");
</script>
<template><div class="flex justify-end gap-2">
  <QuireButton v-if="view.status === 'error'" size="sm" variant="outline" @click="view.retry">{{ english ? 'Retry' : '重试' }}</QuireButton>
  <QuireButton size="sm" variant="ghost" :disabled="view.readOnly" @click="view.revealSource">{{ english ? 'Edit diagram source' : '编辑图示源码' }}</QuireButton>
</div></template>

```

## Built-in text diagrams and custom charts

| Scenario | Approach |
| --- | --- |
| Mermaid flowchart, sequenceDiagram and related diagrams | Install `Mermaid`; use a `mermaid` fence |
| PlantUML sequence, class, use case, component, state, deployment and related diagrams | Install `TextDiagrams`; use `plantuml` or `puml` |
| Graphviz relationships, clustered architecture and dependency graphs | Install `TextDiagrams`; use `dot` or `graphviz` |
| An application-specific `echarts` JSON protocol | Use this codec, block layer and controlled renderer |
| `flow`, flowchart.js or another DSL | Write a fence recognizer and engine adapter; no built-in alias is provided |

The flowchart body starts with `flowchart LR`, but its fence language remains `mermaid`. Mermaid flowchart and flowchart.js are different DSLs. The built-in extension does not route `flow` to another engine.

## Install and reuse existing behavior

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

~~~ts
import { Mermaid } from '@quirekit/msfe-markdown-extras'
import { VueComponentViewRenderer } from '@quirekit/msfe-vue'
import PreviewControls from './preview-controls.vue'

const diagrams = Mermaid.configure({
  controls: VueComponentViewRenderer(PreviewControls),
})
~~~

Built-in Mermaid handles closed-fence recognition, asynchronous rendering, source fallback, retry and cleanup. Its public option is currently `controls`; it has no arbitrary engine `render` callback. Create an extension for a different rendering pipeline.

The built-in renderer uses strict security, disables HTML labels and validates SVG output. It rejects document initialization directives and limits source length and edges. Failures retain readable source. Syntax depends on the installed Mermaid version; see [official Mermaid usage](https://mermaid.js.org/config/usage.html).

## PlantUML and Graphviz/DOT

Standalone MSFE hosts install `TextDiagrams`; MD Editor includes it by default. It recognizes different languages from `Mermaid`, without competing for the same fence:

~~~ts
import { Mermaid, TextDiagrams } from '@quirekit/msfe-markdown-extras'

const extensions = [Mermaid, TextDiagrams]
~~~

~~~~markdown
```plantuml
@startuml
Client -> Service: Request
Service --> Client: Response
@enduml
```

```dot
digraph G {
  rankdir=LR;
  Draft -> Review -> Publish;
}
```
~~~~

In [MD Editor](/en/md-editor), Insert → Text diagram opens the shared Classic / Block source workbench. Saving produces standard Markdown fences. Cancellation or rendering failure never commits a draft; existing invalid source remains readable and editable. `features.extras.mermaid` and `features.extras.textDiagram` control Mermaid and PlantUML / DOT separately.

## Standalone text-diagram documentation

The shared workbench, 25 bilingual templates, CodeMirror shortcuts, SVG / PNG downloads, standalone Vue integration and asset deployment are maintained in the [Text Diagram documentation](http://127.0.0.1:3260/en). Open the [playground](http://127.0.0.1:3260/en/playground) or [support boundaries](http://127.0.0.1:3260/en/reference/support) directly. These are fixed-port local preview links; the planned domain is `text-diagram.quirekit.com`.

This page continues to own MSFE fence integration and custom chart extensions. PlantUML uses the local official JavaScript core; syntax and appearance are not guaranteed to match the full Java distribution. Failures retain source.

## 1. Define the data protocol

This example accepts `{ type: 'bar' | 'line', labels: string[], values: number[] }`. Arrays must have matching lengths of 1–50 and numbers must be finite. Source is limited to 16,000 UTF-16 units. `charts/data.ts` rejects unknown fields rather than implying arbitrary ECharts option support.

~~~~markdown
~~~echarts
{"type":"line","labels":["Jan","Feb","Mar"],"values":[12,20,16]}
~~~
~~~~

This is a minimal application protocol, not built-in MSFE ECharts support or a complete `EChartsOption` serializer. Extend validation, mapping and tests together. Never evaluate document text as JavaScript. JSON cannot and should not carry formatter functions.

## 2. Recognize confirmed, closed fences

`charts/extension.ts` uses `defineSource({ nodes: ['FencedCode'] })`, checks `CodeInfo`, two `CodeMark` nodes and allowed parent context, then reads `CodeText`. Return `null` for unclosed, unknown or unsupported structures to retain source.

The custom ECharts example deliberately replaces only top-level fences. ECharts inside quotes or lists remains source; built-in Mermaid has its own container handling. Supporting nested chart blocks requires list markers, quote prefixes, indentation and hit-testing work, not merely expanding `match.from` to a line start.

## 3. Provide a block projection

`addProjectionLayers()` declares `placement: 'block'`, then `componentView(chartRenderer, props)` describes the non-editable preview. Block output is available before viewport layout, avoiding multiline replacement geometry errors.

Keep renderer identity and keys stable and pass changed props through `update()`. A selection entering the source range reveals it. The chart DOM must not become another editor. A projection-only codec may return `null` from `patch()`.

## 4. Own loading, sizing and cleanup

`charts/renderer.ts` passes runtime `scope` and `requestMeasure` to Vue. Source changes cancel the preceding child scope before loading another chart. Unmounts, document replacement and late results require cleanup.

Give the container dimensions before `echarts.init()`. Observe its size with `ResizeObserver` and call `resize()`; disconnect and `dispose()` during cleanup. This example fixes chart height and requests CodeMirror measurement after status changes, following [ECharts sizing and disposal guidance](https://echarts.apache.org/handbook/en/concepts/chart-size/).

Failures show an explanation, the exact fence and a retry action. They never discard source. Readonly disables source editing while preserving viewing and retry.

## 5. Adapt flowchart.js or another engine

Reuse the example structure while replacing these responsibilities:

1. Give the extension and layer unique names and recognize `flow`, without competing with Mermaid.
2. Replace JSON validation with bounded DSL validation.
3. Load the engine in a browser lifetime. flowchart.js exposes `parse(source)` followed by `drawSVG(...)` against a sized target; consult its [official project documentation](https://github.com/adrai/flowchart.js) for parameters.
4. Wrap engine objects, SVG, listeners and global dependencies in an independent resource lease. Release the old view before updating. Do not assume ECharts cancellation or cleanup behavior applies to another engine.
5. Constrain links, HTML, external resources and unsafe SVG. Retain copyable source fallback and verify theme, sizing and selection behavior.

The interactive example actually runs Mermaid and ECharts. flowchart.js is an adapter path, not an engine bundled or verified by MSFE. Worker-based layout remains possible but still needs cancellation/version checks and a controlled DOM output boundary.

## Acceptance criteria

Compare exact source before and after rendering. Cover valid/invalid JSON, rapid changes, document replacement while loading, viewport exit, theme changes, narrow containers, readonly, copying, undo and native source reveal. A visible chart alone is insufficient. Continue with [Resources](/en/extensions/resources) and [Projection layers](/en/extensions/projections).
