# 流程图、思维导图与白板

用现有编辑器的图稿渲染文档，并在 Markdown 阅读组件中接入同样的能力。

文档中的[算法图](/guide/comparison)由 QuireKit 的流程图、思维导图和白板渲染器绘制。
图里的节点、文字和连线保存在原生图稿中；点击“查看图稿”可以取得 JSON，点击“查看大图”可以放大阅读。

## 在本站文档里放一张图

本站已经为下面三种代码围栏注册了渲染组件。把编辑器导出的完整图稿放在对应围栏里，正文就会显示图表。

| 围栏语言 | 图稿 | 渲染器 |
| --- | --- | --- |
| `quirekit-flowchart` | `FlowchartEmbed` | Flow Editor 使用的 X6 适配器 |
| `quirekit-mind-map` | `MindMapEmbed` | Mind Editor 使用的 SimpleMindMap 适配器 |
| `quirekit-whiteboard` | `WhiteboardDocument` | 已有 Excalidraw 白板适配器 |

例如，下面是一个可以直接复制的流程图图稿。这段围栏由文档的代码组件自动显示为图表：

```quirekit-flowchart
{"type":"quirekit-flowchart-embed","version":1,"id":"docs-minimal-flow","title":"从片段到解析","preview":null,"document":{"format":"quirekit-flowchart","version":1,"revision":0,"nodes":[{"id":"chunk","shape":"terminator","x":0,"y":0,"width":180,"height":72,"text":"收到片段","style":{}},{"id":"parse","shape":"process","x":280,"y":0,"width":180,"height":72,"text":"继续解析","style":{}}],"edges":[{"id":"chunk-parse","source":{"node":"chunk","port":"right"},"target":{"node":"parse","port":"left"},"vertices":[],"routing":"orthogonal","label":"","labelPosition":0.5,"style":{}}]}}
```

这里只读取图稿，不会改写文档。缩放和“适应画布”只改变查看范围。

## 看原文与语法树怎样变化

下面由 QuireKit 流程图绘制并播放的场景，展示实际 Markdown 原文和 Lezer 语法树。默认连续插入
加粗文字、行内代码与两项列表；跟随移动片段进入源落点，定位旧分支，再换入新节点。
可以播放、暂停、单步、回退和重播，不能拖动、平移或缩放视图。

### algorithms/playback.vue

```vue
<script setup lang="ts">
import { computed, ref } from "vue";
import { QuireSegmented, QuireTypography } from "@quirekit/ui/vue";
import type { ExampleLocale } from "../shared/types";
import type { AlgorithmScenario } from "./playback/types";
import { createAlgorithmPlayback } from "./playback/plan";
import AlgorithmPlayer from "./playback/player.vue";

const props = withDefaults(defineProps<{ /** Tutorial and source language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const scenario = ref<AlgorithmScenario>("insert");
const plan = computed(() => createAlgorithmPlayback(props.locale, scenario.value));
const items = computed(() => [
  { value: "insert", label: props.locale === "en" ? "Middle insert" : "中间插入" },
  { value: "append", label: props.locale === "en" ? "Tail append" : "末尾追加" },
  { value: "reference", label: props.locale === "en" ? "Late reference" : "后置引用" },
]);
/** @param value - Public segmented selection. Starts a fresh recorded run for a documented operation. */
function select(value: string | null | undefined): void { if (value === "insert" || value === "append" || value === "reference") scenario.value = value; }
</script>
<template>
  <div class="algorithm-demo" data-algorithm-demo>
    <div class="algorithm-demo-heading">
      <QuireSegmented :model-value="scenario" :items="items" size="sm" :label="locale === 'en' ? 'Source operation' : '源码操作'" @update:model-value="select" />
      <QuireTypography variant="helper">{{ locale === 'en' ? 'Actual source · actual Lezer trees' : '真实原文 · 真实 Lezer 树' }}</QuireTypography>
    </div>
    <AlgorithmPlayer :key="`${locale}-${scenario}`" :plan="plan" :locale="locale" />
  </div>
</template>
<style scoped>
.algorithm-demo { display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0; gap: 16px; padding: 20px; }
.algorithm-demo-heading { display: flex; align-items: center; flex-wrap: wrap; min-width: 0; gap: 12px; }
.algorithm-demo-heading > :first-child { max-width: 100%; overflow-x: auto; }
.algorithm-demo-heading p { color: var(--qk-color-text-tertiary); }
@media (max-width: 640px) { .algorithm-demo { padding: 12px; } }
</style>

```

### shared/types.ts

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

```

### algorithms/playback/types.ts

```ts
import type { syntaxTree } from "@quirekit/cm-language";
import type { MarkdownRenderWork, MarkdownStream } from "@quirekit/incremark-core";
import type { VNodeChild } from "vue";

/** Native parser identities, independent of a framework's rendered nodes. */
export type NativeTree = ReturnType<typeof syntaxTree>;
/** Actual syntax-node cursor view, including buffered inline nodes. */
export type NativeNode = NativeTree["topNode"];
/** Editable source cases with distinct syntax and dependency effects. */
export type AlgorithmScenario = "insert" | "append" | "reference";

/** Exact source transaction used by this example. */
export interface AlgorithmEdit {
  /** Insertion position in UTF-16 source units. */ readonly from: number;
  /** Unmodified source fragment, including Markdown delimiters. */ readonly insert: string;
}
/** A real syntax node, rather than an inferred semantic text node. */
export interface AlgorithmNode {
  /** Local display key; top-level keys retain native Tree identity across snapshots. */ readonly id: string;
  /** Native Lezer node type. */ readonly name: string;
  /** Inclusive absolute source start. */ readonly from: number;
  /** Exclusive absolute source end. */ readonly to: number;
  /** Exact source slice covered by the node. */ readonly text: string;
  /** Native children in document order. */ readonly children: readonly AlgorithmNode[];
}
/** One top-level syntax branch in its document slot. */
export interface AlgorithmBlock extends AlgorithmNode {
  /** Source-order position used to keep unrelated branches stationary during playback. */ readonly slot: number;
}
/** One actual, accepted parser and Vue projection result. */
export interface AlgorithmSnapshot {
  /** Exact Markdown, without synthetic repair. */ readonly source: string;
  /** Immutable source state with completed native syntax. */ readonly state: MarkdownStream["state"];
  /** Native root's identity key; a new document root can contain retained branches. */ readonly rootId: string;
  /** Top-level nodes extracted from the actual Lezer tree. */ readonly blocks: readonly AlgorithmBlock[];
  /** Projection used only by semantic tests, never remounted as historical VNodes. */ readonly output: VNodeChild;
  /** Detached projection counters, excluding parsing and painting. */ readonly work: Readonly<MarkdownRenderWork>;
}
/** One real source edit in a continuous parser and reader lifetime. */
export interface AlgorithmOperation {
  /** Source-order ordinal within the recorded run. */ readonly index: number;
  /** Short human name of this insertion. */ readonly title: string;
  /** Literal source mutation. */ readonly edit: AlgorithmEdit;
  /** Before and after states, sharing the same parser and projection lifetime. */ readonly snapshots: readonly [AlgorithmSnapshot, AlgorithmSnapshot];
  /** Native old branches actually replaced by this operation. */ readonly affected: readonly AlgorithmBlock[];
  /** New or replaced branches in the accepted result. */ readonly changed: readonly AlgorithmBlock[];
  /** Actual shared top-level Tree objects, including objects whose offsets moved. */ readonly reusedTrees: number;
}
/** A teaching boundary whose data is selected without rerunning the edit. */
export type AlgorithmPhase = "source" | "parse" | "chunk" | "insert" | "locate" | "commit" | "render";
/** One bounded interval in the authored slow-motion sequence. */
export interface AlgorithmStep {
  /** Source operation shown at this boundary. */ readonly operation: number;
  /** Data boundary, rather than a fake graph-processing node. */ readonly phase: AlgorithmPhase;
  /** Authored viewing duration in milliseconds, not parser execution time. */ readonly duration: number;
}
/** Continuous source mutations over one native parser and one reader cache. */
export interface AlgorithmPlaybackPlan {
  /** Selected family of source operations. */ readonly scenario: AlgorithmScenario;
  /** All committed snapshots, including the initial source. */ readonly snapshots: readonly AlgorithmSnapshot[];
  /** Exact sequential edits with their native reuse evidence. */ readonly operations: readonly AlgorithmOperation[];
  /** Authored playback boundaries over those immutable states. */ readonly steps: readonly AlgorithmStep[];
}
/** Recorded source, tree and output at a teaching boundary. */
export interface AlgorithmPlaybackFrame {
  /** Selected actual source operation. */ readonly operation: AlgorithmOperation;
  /** Selected teaching phase. */ readonly phase: AlgorithmPhase;
  /** Source currently available to the demonstration. */ readonly received: string;
  /** Tree visible at this boundary. */ readonly tree: AlgorithmSnapshot;
  /** Display frame accepted at this boundary. */ readonly snapshot: AlgorithmSnapshot;
  /** Whether the initial tree has been revealed. */ readonly treeVisible: boolean;
  /** Whether the source mutation is visible. */ readonly edited: boolean;
  /** Whether the affected range is highlighted. */ readonly located: boolean;
  /** Whether the new syntax tree has been accepted. */ readonly committed: boolean;
  /** Whether the new reading output is visible. */ readonly finished: boolean;
}

```

### algorithms/playback/plan.ts

```ts
import type { ExampleLocale } from "../../shared/types";
import type { AlgorithmOperation, AlgorithmPhase, AlgorithmPlaybackFrame, AlgorithmPlaybackPlan, AlgorithmScenario, AlgorithmStep } from "./types.ts";
import { algorithmSnapshots } from "./recording/snapshots.ts";

/** Shared teaching playback rate for phase timing and node animations. */
export const algorithmPlaybackRate = 1.5;

/** @param locale - Source and interface language. @param scenario - Selected literal mutation. @returns A recorded operation with real native Tree identity evidence. */
export function createAlgorithmPlayback(locale: ExampleLocale, scenario: AlgorithmScenario): AlgorithmPlaybackPlan {
  const { insertions, snapshots } = algorithmSnapshots(locale, scenario);
  const operations: AlgorithmOperation[] = insertions.map(({ edit, title }, index) => {
    const before = snapshots[index]!, after = snapshots[index + 1]!;
    const previous = new Set(before.blocks.map(block => block.id)), current = new Set(after.blocks.map(block => block.id));
    return Object.freeze({ index, title, edit, snapshots: Object.freeze([before, after]) as readonly [typeof before, typeof after],
      affected: Object.freeze(before.blocks.filter(block => !current.has(block.id))), changed: Object.freeze(after.blocks.filter(block => !previous.has(block.id))),
      reusedTrees: after.blocks.filter(block => previous.has(block.id)).length });
  });
  const steps: AlgorithmStep[] = [{ operation: 0, phase: "source", duration: 1200 }, { operation: 0, phase: "parse", duration: 1800 }];
  for (const operation of operations) for (const [phase, duration] of [["chunk", 1800], ["insert", 2600], ["locate", 3000], ["commit", 3200], ["render", 2000]] as const) {
    steps.push(Object.freeze({ operation: operation.index, phase, duration }));
  }
  return Object.freeze({ scenario, snapshots, operations: Object.freeze(operations), steps: Object.freeze(steps) });
}

/** @param plan - Recorded run. @param step - Teaching boundary from source through display. @returns Immutable data selections; seeking never repeats the source mutation. */
export function algorithmFrame(plan: AlgorithmPlaybackPlan, step = 0): AlgorithmPlaybackFrame {
  const boundary = plan.steps[Math.max(0, Math.min(plan.steps.length - 1, Math.trunc(step)))]!;
  const operation = plan.operations[boundary.operation]!, phase = boundary.phase, [before, after] = operation.snapshots;
  const edited = ["insert", "locate", "commit", "render"].includes(phase), committed = phase === "commit" || phase === "render", finished = phase === "render";
  return { operation, phase, received: (edited ? after : before).source, tree: committed ? after : before, snapshot: finished ? after : before,
    treeVisible: phase !== "source", edited, located: ["locate", "commit", "render"].includes(phase), committed, finished };
}

/** @param plan - Recorded run. @param operation - Selected insertion ordinal. @param phase - Selected data boundary. @returns Its stable seek position in the recording. */
export function algorithmStepIndex(plan: AlgorithmPlaybackPlan, operation: number, phase: AlgorithmPhase): number {
  const index = plan.steps.findIndex(step => step.operation === operation && step.phase === phase);
  if (index < 0) throw new Error("Unknown algorithm playback boundary");
  return index;
}

```

### algorithms/playback/recording/snapshots.ts

```ts
import { ensureSyntaxTree, syntaxTree } from "@quirekit/cm-language";
import { MarkdownStream, parseMarkdown, type MarkdownReadingError } from "@quirekit/incremark-core";
import { MarkdownRenderer } from "@quirekit/incremark/vue";
import type { ExampleLocale } from "../../../shared/types";
import type { AlgorithmScenario, AlgorithmSnapshot } from "../types.ts";
import { algorithmFixture, algorithmInsertion } from "./fixture.ts";
import { createTreeRecorder } from "./tree.ts";

/** @param error - Reader failure. Fixtures must not silently accept fallback output. */
function reject(error: MarkdownReadingError): void { throw error; }

/** @param state - Native source transaction result. @returns The accepted completed syntax, without reparsing through a replacement stream. */
function complete(state: AlgorithmSnapshot["state"]): AlgorithmSnapshot["state"] {
  const tree = ensureSyntaxTree(state, state.doc.length, 1000);
  if (!tree) throw new Error("Algorithm example did not finish parsing");
  return tree === syntaxTree(state) ? state : state.update({ filter: false }).state;
}

/** @param locale - Literal source language. @param scenario - Middle edit, append or late definition. @returns Exact source mutation and actual before/after snapshots from one lifetime. */
export function algorithmSnapshots(locale: ExampleLocale, scenario: AlgorithmScenario) {
  const { source } = algorithmFixture(locale, scenario), renderer = new MarkdownRenderer(), record = createTreeRecorder();
  /** @param state - Completed native state. @returns A detached syntax and reading snapshot, with historical VNodes reserved for tests. */
  function capture(state: AlgorithmSnapshot["state"]): AlgorithmSnapshot {
    const output = renderer.render(state, locale, "algorithm", reject);
    return Object.freeze({ source: state.doc.toString(), state, ...record(state), output, work: Object.freeze({ ...renderer.work }) });
  }
  const stream = scenario === "insert" ? undefined : new MarkdownStream();
  if (stream) {
    stream.reset(source);
    if (stream.pending && !stream.advance(1000)) throw new Error("Initial algorithm source did not finish parsing");
  }
  const snapshots = [capture(stream?.state ?? parseMarkdown(source))], insertions = [];
  // All edits share one identity recorder and reader cache. Navigation never reaches this recording phase.
  for (let index = 0; index < (scenario === "insert" ? 3 : 1); index++) {
    const before = snapshots[index]!;
    const insertion = algorithmInsertion(locale, scenario, before.source, index);
    if (stream) {
      stream.append(insertion.edit.insert);
      if (stream.pending && !stream.advance(1000)) throw new Error("Appended algorithm source did not finish parsing");
      snapshots.push(capture(stream.state));
    } else {
      // Non-prefix stream.update would reset; arbitrary insertions use the native source transaction.
      snapshots.push(capture(complete(before.state.update({ changes: insertion.edit, filter: false }).state)));
    }
    insertions.push(insertion);
  }
  return { insertions: Object.freeze(insertions), snapshots: Object.freeze(snapshots) };
}

```

### algorithms/playback/recording/fixture.ts

```ts
import type { ExampleLocale } from "../../../shared/types";
import type { AlgorithmEdit, AlgorithmScenario } from "../types.ts";

/** @param locale - Literal source language. @param scenario - Requested mutation. @returns A bounded source and one exact transaction, with unchanged context on both sides. */
export function algorithmFixture(locale: ExampleLocale, scenario: AlgorithmScenario): { source: string; edit: AlgorithmEdit } {
  const zh = locale === "zh-CN";
  const heading = zh ? "## 一份旅行计划" : "## A travel plan";
  const first = zh
    ? "第一段：周六上午，我们从车站出发，沿着河边步道走到老街。路上会经过一座石桥、几家早餐店和一片开阔的草地。大家可以慢慢走，顺便拍照，不必赶时间。到达老街后先找一家小店吃早餐，再去旁边的书店看看。这里记录的是已经写好的行程，接下来的修改发生在第二段，不会改变这一段的文字或格式。"
    : "First paragraph: On Saturday morning we leave the station and follow the riverside path to the old town. We pass a stone bridge, a few breakfast shops and an open meadow. After breakfast we visit the bookshop. This part of the plan is already written; the next edit happens in the second paragraph.";
  const middle = scenario === "reference" ? "[foo][bar][baz]"
    : zh ? "第二段：午后去公园散步，然后回到河边喝杯咖啡。" : "Second paragraph: Visit the park in the afternoon, then have coffee by the river.";
  const last = zh
    ? "第三段：傍晚从河边回到车站，途中可以买一点面包带回家。如果还有时间，就在广场上坐一会儿，看看远处的钟楼和来往的人。晚餐暂时没有安排，我们可以根据当天的天气与心情决定。这一段位于修改位置之后，它的文字没有变化；在前面插入内容时，起止位置会向后移动，但仍然可能使用同一个语法树对象，不必重新解析这段行程。"
    : "Third paragraph: In the evening we return to the station, perhaps buying some bread on the way. If there is time we sit in the square and watch the clock tower. Dinner is still open. This paragraph follows the edit: its text stays the same while its source positions move forward, and the parser may retain the same tree object.";
  const source = `${heading}\n\n${first}\n\n${middle}\n\n${last}\n\n`;
  const edit = scenario === "insert" ? { from: source.indexOf(zh ? "午后" : "Visit"), insert: zh ? "**新增内容**，" : "**new stop**, " }
    : { from: source.length, insert: scenario === "reference" ? `[baz]: ${zh ? "" : "/en"}/guide/getting-started\n`
      : zh ? "新增一段：记得带上相机。\n\n" : "New paragraph: Remember to bring a camera.\n\n" };
  return { source, edit };
}

/** @param locale - Literal source language. @param scenario - Selected operation family. @param source - Accepted source after the preceding edit. @param index - Operation ordinal. @returns One exact insertion and its human name; positions are resolved in the current source. */
export function algorithmInsertion(locale: ExampleLocale, scenario: AlgorithmScenario, source: string, index: number) {
  const zh = locale === "zh-CN";
  if (scenario !== "insert") return { edit: { from: source.length, insert: algorithmFixture(locale, scenario).edit.insert }, title: zh ? scenario === "reference" ? "后置链接定义" : "追加一个段落" : scenario === "reference" ? "Late link definition" : "Append a paragraph" };
  const title = zh ? ["第二段加入加粗", "第一段加入代码", "中间插入列表"][index] : ["Bold in paragraph 2", "Code in paragraph 1", "Insert a list between paragraphs"][index];
  const edit = index === 0 ? { from: source.indexOf(zh ? "午后" : "Visit"), insert: zh ? "**新增内容**，" : "**new stop**, " }
    : index === 1 ? { from: source.indexOf(zh ? "文字或格式" : "next edit"), insert: zh ? "`地图`、" : "`map` and the " }
      : { from: source.indexOf(zh ? "第三段：" : "Third paragraph:"), insert: zh ? "- 带上相机\n- 带上一把雨伞\n\n" : "- Bring a camera\n- Bring an umbrella\n\n" };
  if (!title || edit.from < 0) throw new Error("Invalid algorithm insertion fixture");
  return { edit, title };
}

```

### algorithms/playback/recording/tree.ts

```ts
import { syntaxTree } from "@quirekit/cm-language";
import type { AlgorithmBlock, AlgorithmNode, AlgorithmSnapshot, NativeNode, NativeTree } from "../types.ts";

/** @returns A run-owned identity recorder, comparing native Tree objects rather than their text or ranges. */
export function createTreeRecorder() {
  const identities = new WeakMap<NativeTree, string>();
  let next = 0;
  /** @param tree - Native tree object. @returns The same local key whenever the native parser retains this object. */
  function identity(tree: NativeTree): string {
    let key = identities.get(tree);
    if (!key) { key = `T${++next}`; identities.set(tree, key); }
    return key;
  }
  /** @param node - Real native node. @param source - Matching source. @param key - Buffered nodes get a path key, without claiming native Tree identity. @returns A detached syntax-node description. */
  function read(node: NativeNode, source: string, key: string): AlgorithmNode {
    const children: AlgorithmNode[] = [];
    for (let child = node.firstChild, index = 0; child; child = child.nextSibling, index++) {
      children.push(read(child, source, `${key}.${index}`));
    }
    return Object.freeze({ id: key, name: node.name, from: node.from, to: node.to, text: source.slice(node.from, node.to), children: Object.freeze(children) });
  }
  /** @param state - Completed parser state. @returns The actual root key and all its top-level branches. */
  return function record(state: AlgorithmSnapshot["state"]): Pick<AlgorithmSnapshot, "rootId" | "blocks"> {
    const tree = syntaxTree(state), rootId = identity(tree), source = state.doc.toString(), blocks: AlgorithmBlock[] = [];
    for (let node = tree.topNode.firstChild; node; node = node.nextSibling) {
      const key = node.tree ? identity(node.tree) : `${rootId}.${blocks.length}`;
      blocks.push(Object.freeze({ ...read(node, source, key), slot: blocks.length }));
    }
    return { rootId, blocks: Object.freeze(blocks) };
  };
}

```

### algorithms/playback/player.vue

```vue
<script setup lang="ts">
import { computed, ref } from "vue";
import { QuireButton, QuireIcon, QuireIconButton, QuireTypography } from "@quirekit/ui/vue";
import { QuireMarkdown } from "@quirekit/incremark/vue";
import { syntaxTree } from "@quirekit/cm-language";
import type { ExampleLocale } from "../../shared/types";
import type { AlgorithmPhase, AlgorithmPlaybackPlan } from "./types";
import { algorithmFrame, algorithmStepIndex } from "./plan";
import { algorithmCopy } from "./copy";
import { algorithmGraph } from "./graph/trace";
import { useAlgorithmPlayer } from "./graph/use-player";
import AlgorithmCanvas from "./graph/canvas.vue";

const props = defineProps<{
  /** Recorded continuous source session. A new scenario remounts this owner. */ plan: AlgorithmPlaybackPlan;
  /** Source and interface language. */ locale: ExampleLocale;
}>();
const compact = ref(false);
const model = computed(() => algorithmGraph(props.plan, props.locale, compact.value));
const { step, progress, playing, ready, attach, retire, toggle, seek, replay } = useAlgorithmPlayer(model);
const frame = computed(() => algorithmFrame(props.plan, step.value));
const copy = computed(() => algorithmCopy(props.locale, props.plan, frame.value));
const work = computed(() => frame.value.operation.snapshots[1].work);
const phases: readonly AlgorithmPhase[] = ["parse", "chunk", "insert", "locate", "commit", "render"];
const motion = computed(() => playing.value ? "running" : progress.value < 1 ? "paused" : "settled");
/** @param operation - Actual insertion ordinal. @param phase - Selected data boundary. Navigates only the recording. */
function select(operation: number, phase: AlgorithmPhase): void { seek(algorithmStepIndex(props.plan, operation, phase)); }
</script>
<template>
  <div class="algorithm-run" data-algorithm-playback data-renderer="quirekit-flowchart-x6" :data-step="step" :data-operation="frame.operation.index" :data-phase="frame.phase" :data-progress="progress.toFixed(4)" :data-playing="playing" :data-motion="motion">
    <div class="algorithm-controls">
      <QuireButton size="sm" variant="solid" color="primary" :disabled="!ready" @click="toggle"><template #leading><QuireIcon :name="playing ? 'pause' : 'play'" /></template>{{ playing ? copy.pause : copy.play }}</QuireButton>
      <QuireIconButton size="sm" variant="ghost" :label="copy.back" :disabled="!ready || step === 0" @click="seek(step - 1)"><QuireIcon name="skipBack" /></QuireIconButton>
      <QuireIconButton size="sm" variant="ghost" :label="copy.forward" :disabled="!ready || step === plan.steps.length - 1" @click="seek(step + 1)"><QuireIcon name="skipForward" /></QuireIconButton>
      <QuireIconButton size="sm" variant="ghost" :label="copy.replay" :disabled="!ready" @click="replay"><QuireIcon name="rotateCcw" /></QuireIconButton>
      <QuireTypography as="span" variant="helper" class="algorithm-fixed">{{ copy.fixed }}</QuireTypography>
    </div>
    <nav v-if="plan.operations.length > 1" class="algorithm-operations" :aria-label="copy.operations">
      <QuireButton v-for="operation in plan.operations" :key="operation.index" size="sm" :disabled="!ready" :variant="frame.operation.index === operation.index ? 'solid' : 'outline'" :color="frame.operation.index === operation.index ? 'primary' : 'neutral'" :aria-current="frame.operation.index === operation.index ? 'step' : undefined" :data-current="frame.operation.index === operation.index" @click="select(operation.index, 'chunk')"><template #leading><span>{{ operation.index + 1 }}</span></template>{{ operation.title }}</QuireButton>
    </nav>
    <nav class="algorithm-timeline" :aria-label="copy.timeline">
      <QuireButton v-for="(phase, index) in phases" :key="phase" size="sm" :disabled="!ready" :variant="frame.phase === phase ? 'soft' : 'ghost'" :color="frame.phase === phase ? 'primary' : 'neutral'" :aria-current="frame.phase === phase ? 'step' : undefined" :data-current="frame.phase === phase" :data-phase-button="phase" @click="select(phase === 'parse' ? 0 : frame.operation.index, phase)">{{ copy.phases[index] }}</QuireButton>
    </nav>
    <div class="algorithm-progress" aria-hidden="true"><span :style="{ width: `${(step + progress) / plan.steps.length * 100}%` }"></span></div>
    <AlgorithmCanvas :model="model" :locale="locale" @ready="attach" @retired="retire" @compact="compact = $event" />
    <QuireTypography variant="helper">{{ locale === 'en' ? 'Drawn and animated by QuireKit Flowchart · 1.5× · fixed view' : '由 QuireKit 流程图绘制与播放 · 1.5 倍 · 固定视图' }}</QuireTypography>
    <div class="algorithm-explanation" aria-live="polite" aria-atomic="true">
      <span class="algorithm-step-number">{{ frame.operation.index + 1 }}</span>
      <div><QuireTypography as="h3" variant="control">{{ copy.title }}</QuireTypography><QuireTypography variant="helper">{{ copy.description }}</QuireTypography></div>
    </div>
    <section class="algorithm-reader" :aria-label="copy.output">
      <div class="algorithm-reader-heading"><QuireTypography as="h3" variant="control">{{ copy.output }}</QuireTypography><span>{{ frame.finished ? copy.newTree : copy.oldTree }} · {{ copy.ordinal }}</span></div>
      <QuireMarkdown class="algorithm-output" :state="frame.snapshot.state" :locale="locale" />
    </section>
    <div class="algorithm-measurements" :data-complete="frame.finished">
      <div><strong>{{ frame.committed ? frame.operation.reusedTrees : '—' }}</strong><span>{{ copy.syntaxReuse }}</span></div>
      <div><strong>{{ frame.finished ? work.reusedBlocks : '—' }}</strong><span>{{ copy.displayReuse }}</span></div>
      <QuireTypography variant="helper">{{ locale === 'en' ? 'Work for the current edit; replay and painting are excluded.' : '计数对应当前这次更新，不含回放与绘制。' }}</QuireTypography>
    </div>
    <details class="algorithm-inspect"><summary>{{ copy.full }}</summary><pre class="algorithm-source">{{ frame.received }}</pre><pre>{{ syntaxTree(frame.tree.state).toString() }}</pre></details>
  </div>
</template>
<style scoped src="./player.css"></style>

```

### algorithms/playback/copy.ts

```ts
import type { ExampleLocale } from "../../shared/types";
import type { AlgorithmPlaybackFrame, AlgorithmPlaybackPlan } from "./types.ts";

/** @param locale - Interface language. @param plan - Continuous recorded run. @param frame - Current actual operation and data boundary. @returns Copy reflecting native evidence, including structural-context rebuilds. */
export function algorithmCopy(locale: ExampleLocale, plan: AlgorithmPlaybackPlan, frame: AlgorithmPlaybackFrame) {
  const zh = locale === "zh-CN", { operation, phase } = frame, [before, after] = operation.snapshots;
  const old = operation.affected[0], next = operation.changed[0], delta = operation.edit.insert.length;
  const structural = plan.scenario === "insert" && operation.index === 2;
  const target = old ? `${old.name} ${old.id} · [${old.from}, ${old.to})` : zh ? "新增片段的空位" : "space for new source";
  const changes = operation.changed.map(block => `${block.name} ${block.id}`).join(" · ");
  const retained = after.blocks.filter(block => before.blocks.some(previous => previous.id === block.id)).map(block => block.id).join(" · ");
  return zh ? {
    play: "播放变化", pause: "暂停", back: "上一步", forward: "下一步", replay: "重播", timeline: "变化阶段", operations: "选择连续插入",
    source: "Markdown 原文", tree: "Lezer 语法树", output: "阅读结果", incoming: "待插入的片段", original: "原文", newSource: "已插入", oldTree: "更新前", newTree: "已更新",
    waiting: "原文即将展开为一棵 Lezer 树。", retained: "保留对象", changed: "换入新对象", added: "新增分支", pending: "新分支的空位", range: "这次受影响的范围",
    snippet: "受影响的原文显示插入点附近；其他长段落显示摘要。", full: "查看完整原文与语法树", identity: "绿色保留 · 橙色定位 · 蓝色更新；T 编号来自真实树对象。",
    syntaxReuse: "保留的语法树", displayReuse: "复用的显示块", fixed: "固定视图", phases: ["原始树", "收到片段", "插入原文", "定位范围", "换入新树", "更新显示"],
    ordinal: `第 ${operation.index + 1} / ${plan.operations.length} 次插入`,
    title: phase === "source" ? "从同一份原文开始" : phase === "parse" ? "原文展开为 Lezer 树" : phase === "chunk" ? operation.title
      : phase === "insert" ? `把 ${delta} 个字符放到位置 ${operation.edit.from}` : phase === "locate" ? structural ? "插入位于段落边界，检查相邻分支" : `找到 ${target}`
        : phase === "commit" ? `换入 ${changes}` : "这次变化进入阅读结果",
    description: phase === "source" || phase === "parse" ? "一个标题、三个段落。接下来在同一份原文中连续插入，上一轮的结果就是下一轮的起点。"
      : phase === "chunk" ? "片段先停在上方。跟随它进入原文，再沿着高亮轨迹找到受影响的语法分支。"
        : phase === "insert" ? `新增片段已经进入原文。此时树和阅读结果先保留上一轮，接下来定位 ${target}。`
          : phase === "locate" ? structural ? "这里插入两项列表。原第三段虽没有改字，解析器仍需重新确认它的边界；不能把它直接标为保留。" : plan.scenario === "reference" ? "定义位于末尾，baz 被前面这段使用。沿引用依赖找到旧分支，更新它的链接分组。" : `插入点 ${operation.edit.from} 落入这段原文的范围。橙色框标出旧分支，其他分支保持原对象。`
            : phase === "commit" ? `蓝色分支进入树，${retained} 保留。原文位置后移 +${delta}；树对象的保留与替换分别判断。`
              : `这次保留 ${operation.reusedTrees} 个树对象，复用 ${after.work.reusedBlocks} 个显示块。${operation.index < plan.operations.length - 1 ? "继续播放，下一次插入接着这份结果更新。" : "本次演示已完成。"}`,
  } : {
    play: "Play changes", pause: "Pause", back: "Previous", forward: "Next", replay: "Replay", timeline: "Change phase", operations: "Choose an insertion",
    source: "Markdown source", tree: "Lezer syntax tree", output: "Reader output", incoming: "Incoming fragment", original: "source", newSource: "inserted", oldTree: "before update", newTree: "updated",
    waiting: "The source is about to unfold into a Lezer tree.", retained: "retained object", changed: "new object", added: "new branch", pending: "space for a new branch", range: "Affected range",
    snippet: "Affected source shows the insertion context; other long paragraphs show excerpts.", full: "Inspect complete source and syntax", identity: "Green: retained · amber: located · blue: updated. T labels identify native tree objects.",
    syntaxReuse: "syntax trees retained", displayReuse: "display blocks reused", fixed: "Fixed view", phases: ["Original tree", "Receive text", "Insert source", "Find range", "Accept tree", "Update output"],
    ordinal: `Insertion ${operation.index + 1} / ${plan.operations.length}`,
    title: phase === "source" ? "Start with one source document" : phase === "parse" ? "Source unfolds into a Lezer tree" : phase === "chunk" ? operation.title
      : phase === "insert" ? `Insert ${delta} characters at ${operation.edit.from}` : phase === "locate" ? structural ? "Check the neighboring paragraph boundary" : `Locate ${target}`
        : phase === "commit" ? `Attach ${changes}` : "The change reaches the reader",
    description: phase === "source" || phase === "parse" ? "A heading and three paragraphs. Each insertion builds on the preceding result, using the same source and reader lifetime."
      : phase === "chunk" ? "Follow this fragment into the source and along the highlighted path to the affected syntax branch."
        : phase === "insert" ? `The fragment has entered the source; syntax and output still show the previous result. Next, locate ${target}.`
          : phase === "locate" ? structural ? "A two-item list is inserted at a paragraph boundary. The old final paragraph has unchanged text but its native branch is still rebuilt." : plan.scenario === "reference" ? "The definition arrives at the end; the earlier paragraph uses baz. Follow its reference dependency to update the old link grouping." : `Position ${operation.edit.from} falls inside the highlighted source range. The amber box marks the previous affected branch.`
            : phase === "commit" ? `The blue branches enter the tree; ${retained} remain. Source positions move by +${delta}, separately from native identity reuse.`
              : `This edit retains ${operation.reusedTrees} trees and reuses ${after.work.reusedBlocks} display blocks. ${operation.index < plan.operations.length - 1 ? "The next insertion continues from this result." : "The recorded run is complete."}`,
  };
}

```

### algorithms/playback/graph/trace.ts

```ts
import type { FlowchartMotionEffect, FlowchartPlaybackStep } from "@quirekit/flowchart";
import type { ExampleLocale } from "../../../shared/types";
import type { AlgorithmPlaybackPlan } from "../types.ts";
import { algorithmFrame } from "../plan.ts";
import { algorithmCopy } from "../copy.ts";
import { algorithmScene, algorithmSlots } from "./scene.ts";
import type { AlgorithmGraphPlan } from "./types.ts";

/** @param plan - Already-recorded parser transactions. @param locale - Source/caption language. @param compact - Vertical presentation. @returns Portable native-flowchart scenes and public playback cues; no animation implementation lives here. */
export function algorithmGraph(plan: AlgorithmPlaybackPlan, locale: ExampleLocale, compact: boolean): AlgorithmGraphPlan {
  const slots = algorithmSlots(plan);
  const steps: FlowchartPlaybackStep[] = plan.steps.map((step, index) => {
    const frame = algorithmFrame(plan, index), copy = algorithmCopy(locale, plan, frame), scene = algorithmScene(frame, locale, slots, compact);
    const effects: FlowchartMotionEffect[] = [];
    if (["chunk", "insert", "locate", "commit"].includes(frame.phase)) {
      const id = frame.phase === "chunk" || frame.phase === "insert" ? "transfer-source" : frame.phase === "locate" ? "transfer-tree" : "transfer-new";
      effects.push({ id, kind: "travel", payload: "token", label: frame.operation.edit.insert.trim().replace(/\n/gu, " ↵ ").slice(0, 80), duration: Math.min(1800, step.duration), easing: "ease-in-out", tone: frame.phase === "locate" ? "running" : "completed" });
      if (frame.phase === "commit" && frame.operation.changed.length > 1) effects.push({ id: "transfer-other", kind: "travel", payload: "token", label: locale === "en" ? "Recheck boundary" : "重新确认边界", duration: Math.min(1800, step.duration), easing: "ease-in-out", tone: "completed" });
      const targets = frame.committed ? frame.operation.changed : frame.operation.affected;
      if (frame.phase === "locate" || frame.phase === "commit") for (const target of targets) effects.push({ id: `tree-${target.id}`, kind: "pulse", duration: Math.min(1800, step.duration), tone: frame.phase === "locate" ? "running" : "completed" });
    }
    return { id: `step-${index}`, title: copy.title, description: copy.description, duration: step.duration,
      scene: scene.document, sceneDuration: Math.min(step.duration, frame.phase === "commit" ? 1400 : 800), changes: scene.changes, effects };
  });
  const documents = steps.map(step => step.scene!);
  const width = Math.max(...documents.flatMap(document => document.nodes.map(node => node.x + node.width))) + 24;
  const height = Math.max(...documents.flatMap(document => document.nodes.map(node => node.y + node.height))) + 24;
  return { document: steps[0]!.scene!, trace: { title: locale === "en" ? "Markdown tree evolution" : "Markdown 树的变化", presentation: "status", hidden: ["transfer-source", "transfer-tree", "transfer-new", "transfer-other"], steps }, width, height };
}

```

### algorithms/playback/graph/scene.ts

```ts
import { createFlowchart, createFlowchartNode, readFlowchart } from "@quirekit/flowchart";
import type { FlowchartDocument, FlowchartEdge, FlowchartNode, FlowchartRunChange, FlowchartRunState } from "@quirekit/flowchart";
import type { ExampleLocale } from "../../../shared/types";
import type { AlgorithmNode, AlgorithmPlaybackFrame, AlgorithmPlaybackPlan } from "../types.ts";
import { algorithmPairs, sourceExcerpt, sourceTarget } from "./pairs.ts";

/** @param plan - Continuous real parse snapshots. @returns Stable positions including space reserved for genuinely new branches. */
export function algorithmSlots(plan: AlgorithmPlaybackPlan): ReadonlyMap<string, number> {
  const slots = new Map(plan.snapshots.at(-1)!.blocks.map((block, index) => [block.id, index]));
  for (const operation of [...plan.operations].reverse()) for (const pair of algorithmPairs(operation)) if (pair.before) slots.set(pair.before.id, slots.get(pair.after.id)!);
  return slots;
}

/** @param frame - Source/tree boundary. @param locale - Caption language. @param slots - Stable branch positions. @param compact - Vertical readable layout. @returns One portable actual-data scene and its target states. */
export function algorithmScene(frame: AlgorithmPlaybackFrame, locale: ExampleLocale, slots: ReadonlyMap<string, number>, compact: boolean): { document: FlowchartDocument; changes: readonly FlowchartRunChange[] } {
  const nodes: FlowchartNode[] = [], edges: FlowchartEdge[] = [], changes: FlowchartRunChange[] = [];
  const zh = locale === "zh-CN", columns = Math.max(...slots.values()) + 1, width = compact ? 240 : columns * 242 + 24;
  const source = frame.edited ? frame.operation.snapshots[1] : frame.operation.snapshots[0];
  const sourceFocus = sourceTarget(frame.operation), targetSlot = slots.get(sourceFocus.id) ?? columns - 1;
  /** @param id - Graph identity. @param text - Literal caption. @param x - World x. @param y - World y. @param w - Width. @param h - Height. @param heading - Larger label. @returns Canonical node with readable text. */
  function box(id: string, text: string, x: number, y: number, w: number, h: number, heading = false): FlowchartNode {
    const node = { ...createFlowchartNode(id, "process", { x, y }, text), width: w, height: h, style: { fontSize: compact ? heading ? 17 : 15 : heading && h > 64 ? 26 : 22, padding: heading && h <= 64 ? 4 : 10, textAlign: "left" as const, ...(heading ? { bold: true } : {}) } };
    nodes.push(node); return node;
  }
  /** @param parent - Real syntax parent. @param child - Real syntax child. Adds a normal native diagram connector. */
  function connect(parent: FlowchartNode, child: FlowchartNode): void {
    edges.push({ id: `link-${child.id}`, source: { node: parent.id, anchor: { x: .5, y: 1 } }, target: { node: child.id, anchor: { x: .5, y: 0 } },
      vertices: [], routing: "orthogonal", connector: "rounded", label: "", labelPosition: .5, style: { targetArrow: "none" } });
  }
  /** @param syntax - Real inline syntax. @param parent - Diagram parent. @param x - Branch x. @param y - Next y. @param level - Native nesting depth. @param state - Parent branch status. @returns Next available y in the branch. */
  function children(syntax: readonly AlgorithmNode[], parent: FlowchartNode, x: number, y: number, level: number, state?: FlowchartRunState): number {
    for (const child of syntax) {
      const node = box(`tree-${child.id}`, `${child.name}\n[${child.from}, ${child.to})\n${child.text.slice(0, 24).replace(/\n/gu, " ↵ ")}`, x + level * 12, y, (compact ? 216 : 218) - level * 12, compact ? 84 : 128);
      connect(parent, node); if (state) changes.push({ id: node.id, state });
      y = children(child.children, node, x, y + (compact ? 104 : 148), level + 1, state);
    }
    return y;
  }
  box("incoming", `${zh ? "插入" : "Insert"} ${frame.operation.index + 1} · ${frame.operation.title}\n${frame.operation.edit.insert.trim().replace(/\n/gu, " ↵ ")}\n+${frame.operation.edit.insert.length} UTF-16 @ ${frame.operation.edit.from}`, compact ? 12 : width / 2 - 280, 12, compact ? 216 : 560, compact || !zh ? 200 : 134, true);
  const sourceY = compact || !zh ? 240 : 156;
  box("source-title", zh ? "Markdown 原文" : "Markdown source", 12, sourceY, compact ? 216 : width - 24, 44, true);
  for (const block of source.blocks) {
    const slot = slots.get(block.id) ?? block.slot, focus = slot === targetSlot;
    box(`source-${slot}`, `${String(block.slot + 1).padStart(2, "0")}  [${block.from}, ${block.to})\n${sourceExcerpt(block, frame.operation, focus)}`, compact ? 12 : 12 + slot * 242, sourceY + 64 + (compact ? block.slot * 158 : 0), compact ? 216 : 218, compact ? 140 : 216);
    changes.push({ id: `source-${slot}`, state: focus && frame.edited ? "running" : "reused" });
  }
  const treeY = sourceY + (compact ? source.blocks.length * 158 + 84 : 320);
  box("tree-title", zh ? "Lezer 语法树" : "Lezer syntax tree", 12, treeY, compact ? 216 : width - 24, 44, true);
  const root = frame.treeVisible ? box(`root-${frame.tree.rootId}`, `Document  ${frame.tree.rootId}\n[0, ${frame.tree.source.length})`, compact ? 12 : width / 2 - 135, treeY + 68, compact ? 216 : 270, compact ? 80 : 100, true) : undefined;
  let bottom = treeY + 196;
  if (root) for (const [index, block] of frame.tree.blocks.entries()) {
    const slot = slots.get(block.id) ?? block.slot, previous = frame.operation.snapshots[0].blocks.find(old => old.id === block.id);
    const pair = algorithmPairs(frame.operation).find(item => (frame.committed ? item.after : item.before)?.id === block.id);
    const replaced = frame.committed && pair?.before && pair.before.id !== block.id ? `${pair.before.id} → ` : "";
    const delta = frame.committed && previous ? block.from - previous.from : 0;
    const state: FlowchartRunState | undefined = !frame.located ? undefined : frame.committed ? frame.operation.changed.some(item => item.id === block.id) ? "completed" : "reused" : frame.operation.affected.some(item => item.id === block.id) ? "running" : "reused";
    const status = state === "reused" ? zh ? "保留对象" : "Retained object" : state === "running" ? zh ? "定位受影响分支" : "Affected branch" : state === "completed" ? zh ? "换入新对象" : "New object" : "";
    const x = compact ? 12 : 12 + slot * 242, y = compact ? bottom : treeY + 196;
    const node = box(`tree-${block.id}`, `${block.name}  ${replaced}${block.id}\n[${block.from}, ${block.to})${delta ? `  +${delta}` : ""}\n${status ? `${status}\n` : ""}${block.text.length > 24 ? `${block.text.slice(0, 24)}…` : block.text}`, x, y, compact ? 216 : 218, compact ? 174 : 220);
    connect(root, node);
    if (state) changes.push({ id: node.id, state });
    const focus = frame.operation.affected.some(item => item.id === block.id) || frame.operation.changed.some(item => item.id === block.id);
    const next = !compact || focus ? children(block.children, node, x, y + (compact ? 196 : 244), 0, state) : y + 194;
    bottom = Math.max(bottom, next + (compact ? 20 : 0));
  }
  // Travel paths belong to the graph, using real diagram coordinates. Keeping them
  // hidden preserves the tree's visual structure while the library moves payloads.
  const sourceNode = nodes.find(node => node.id === `source-${targetSlot}`);
  const affected = frame.committed ? frame.operation.changed[0] : frame.operation.affected[0];
  const treeNode = nodes.find(node => node.id === `tree-${affected?.id}`);
  const incoming = nodes[0]!;
  const from = { x: incoming.x + incoming.width / 2, y: incoming.y + incoming.height };
  const drop = sourceNode ? { x: sourceNode.x + sourceNode.width / 2, y: sourceNode.y + sourceNode.height / 2 } : { x: compact ? 120 : 12 + targetSlot * 242 + 109, y: sourceY + 140 };
  const tree = treeNode ? { x: treeNode.x + treeNode.width / 2, y: treeNode.y + treeNode.height / 2 } : { x: drop.x, y: treeY + 240 };
  const neighbor = nodes.find(node => node.id === `tree-${frame.operation.changed[1]?.id}`);
  const other = neighbor ? { x: neighbor.x + neighbor.width / 2, y: neighbor.y + neighbor.height / 2 } : tree;
  for (const [id, a, b] of [["transfer-source", from, drop], ["transfer-tree", drop, tree], ["transfer-new", { x: tree.x, y: tree.y - 48 }, tree], ["transfer-other", { x: tree.x, y: tree.y - 48 }, other]] as const) {
    const lane = Math.max(a.x, b.x, id === "transfer-tree" && root ? root.x + root.width : 0) + 32;
    edges.push({ id, source: a, target: b, vertices: [{ x: compact ? a.x : lane, y: a.y + 24 }, { x: compact ? b.x : lane, y: b.y - 24 }], routing: "straight", connector: "smooth", label: "", labelPosition: .5, style: { targetArrow: "none" } });
  }
  return { document: readFlowchart({ ...createFlowchart(), nodes, edges }), changes };
}

```

### algorithms/playback/graph/pairs.ts

```ts
import type { AlgorithmBlock, AlgorithmOperation, AlgorithmPlaybackFrame } from "../types.ts";
import type { AlgorithmPair } from "./types.ts";

/** @param operation - Actual sequential edit. @returns Visual alignment, without equating text similarity with native reuse. */
export function algorithmPairs(operation: AlgorithmOperation): readonly AlgorithmPair[] {
  const [before, after] = operation.snapshots, { from, insert } = operation.edit;
  return after.blocks.map(block => {
    const previous = before.blocks.find(old => old.id === block.id) ?? before.blocks.find(old => old.name === block.name && old.from + (old.from >= from ? insert.length : 0) === block.from);
    return { key: block.id, before: previous, after: block };
  });
}
/** @param operation - Accepted edit. @returns Real accepted syntax containing the insertion. */
export function sourceTarget(operation: AlgorithmOperation): AlgorithmBlock {
  const block = operation.snapshots[1].blocks.find(item => item.from <= operation.edit.from && item.to > operation.edit.from) ?? operation.changed[0];
  if (!block) throw new Error("No source target for recorded edit");
  return block;
}
/** @param pair - Presentation alignment. @param frame - Current accepted boundary. @returns Status based only on recorded native identities. */
export function branchStatus(pair: AlgorithmPair, frame: AlgorithmPlaybackFrame): string {
  if (!frame.located) return "original";
  if (!frame.committed) return pair.before ? frame.operation.affected.some(block => block.id === pair.before!.id) ? "affected" : "retained" : "pending";
  return pair.before?.id === pair.after.id ? "retained" : pair.before ? "changed" : "added";
}
/** @param block - Actual source slice. @param operation - Current edit. @param focus - Reveal its insertion context. @returns Exact source with explicit omissions. */
export function sourceExcerpt(block: AlgorithmBlock, operation: AlgorithmOperation, focus = false): string {
  const local = operation.edit.from - block.from, start = focus && local >= 0 && local < block.text.length ? Math.max(0, local - 24) : 0;
  const end = Math.min(block.text.length, start + (focus ? 68 : 36));
  return `${start ? "…" : ""}${block.text.slice(start, end)}${end < block.text.length ? "…" : ""}`;
}

```

### algorithms/playback/graph/types.ts

```ts
import type { FlowchartDocument, FlowchartPlaybackTrace } from "@quirekit/flowchart";
import type { AlgorithmBlock } from "../types";

/** Presentation alignment; matching ranges never claims native identity reuse. */
export interface AlgorithmPair {
  /** Previous native branch when this teaching slot existed. */ readonly before?: AlgorithmBlock;
  /** Accepted native branch at this slot. */ readonly after: AlgorithmBlock;
  /** Accepted identity used for presentation alignment only. */ readonly key: string;
}

/** Portable graph and teaching trace over the recorded parser snapshots. */
export interface AlgorithmGraphPlan {
  /** Static source graph preserved by the flowchart editor. */ readonly document: FlowchartDocument;
  /** Actual snapshot-derived scenes and public motion effects. */ readonly trace: FlowchartPlaybackTrace;
  /** Stable world width for responsive presentation. */ readonly width: number;
  /** Stable world height across all steps. */ readonly height: number;
}

```

### algorithms/playback/graph/use-player.ts

```ts
import { computed, onBeforeUnmount, ref, shallowRef, watch, type Ref } from "vue";
import type { FlowchartEditor } from "@quirekit/flowchart-x6";
import type { FlowchartPlaybackState } from "@quirekit/flowchart";
import type { AlgorithmGraphPlan } from "./types.ts";
import { algorithmPlaybackRate } from "../plan.ts";

/** @param model - Immutable diagram and trace. @returns Public flowchart controls and a read-only progress sampler; the library owns all time and motion. */
export function useAlgorithmPlayer(model: Ref<AlgorithmGraphPlan>) {
  const editor = shallowRef<FlowchartEditor>(), state = shallowRef<FlowchartPlaybackState>(), progress = ref(1);
  let unsubscribe: (() => void) | undefined, sample = 0, started = false;
  const step = computed(() => Math.max(0, (state.value?.index ?? 2) - 1)), playing = computed(() => state.value?.status === "playing");
  /** Samples only the public constant-time progress getter, never advancing time or reading source. */
  function paint(): void { progress.value = editor.value?.playback.getProgress() ?? 1; if (playing.value) sample = requestAnimationFrame(paint); }
  /** @param value - Actual flowchart semantic boundary. */
  function changed(value: FlowchartPlaybackState): void {
    state.value = value; cancelAnimationFrame(sample); progress.value = editor.value?.playback.getProgress() ?? 1;
    if (value.status === "playing") sample = requestAnimationFrame(paint);
  }
  /** @param value - Accepted native editor. Loads the portable trace exactly once at its mount boundary. */
  function attach(value: FlowchartEditor): void {
    editor.value = value; value.playback.load(model.value.trace); value.playback.setRate(algorithmPlaybackRate);
    unsubscribe = value.playback.subscribe(changed); value.playback.seek(2);
  }
  /** @param value - Retired native editor. Releases only its own subscription and progress sampling. */
  function retire(value: FlowchartEditor): void { if (editor.value !== value) return; unsubscribe?.(); unsubscribe = undefined; cancelAnimationFrame(sample); editor.value = undefined; state.value = undefined; }
  /** Toggles the library's logical player; the first run begins with source materialization. */
  function toggle(): void {
    const player = editor.value?.playback; if (!player) return;
    if (playing.value) player.pause(); else if (!started || state.value?.status === "completed") { started = true; player.replay(); } else player.play();
  }
  /** @param boundary - Recorded teaching step. Public prefix seeking never repeats the actual parser transaction. */
  function seek(boundary: number): void { started = true; editor.value?.playback.seek(Math.max(0, Math.min(model.value.trace.steps.length, boundary + 1))); }
  /** Replays the library's accepted trace without rerunning source edits. */
  function replay(): void { started = true; editor.value?.playback.replay(); }
  watch(model, value => {
    const player = editor.value?.playback; if (!player) return;
    const before = player.getState(), position = player.getProgress();
    player.load(value.trace); player.setRate(before.rate); player.seek(Math.min(before.index, value.trace.steps.length));
    if (position < 1) player.seekProgress(position);
    if (before.status === "playing") player.play();
  });
  onBeforeUnmount(() => { unsubscribe?.(); cancelAnimationFrame(sample); });
  return { step, progress, playing, ready: computed(() => !!editor.value), attach, retire, toggle, seek, replay };
}

```

### algorithms/playback/graph/canvas.vue

```vue
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import type { FlowchartEditor } from "@quirekit/flowchart-x6";
import type { AlgorithmGraphPlan } from "./types";
import type { ExampleLocale } from "../../../shared/types";
const props = defineProps<{
  /** Actual-data scenes; playback owns every visual transition. */ model: AlgorithmGraphPlan;
  /** Native control and accessible caption language. */ locale: ExampleLocale;
}>();
const emit = defineEmits<{
  /** Accepted native renderer lifetime. */ ready: [editor: FlowchartEditor];
  /** Retired native renderer lifetime. */ retired: [editor: FlowchartEditor];
  /** Readable vertical layout chosen from this actual host width. */ compact: [value: boolean];
}>();
const parent = ref<HTMLDivElement>(), width = ref(0), failure = ref(false), ready = ref(false);
const height = computed(() => Math.ceil((props.model.height + 40) * Math.min(1, Math.max(220, width.value - 40) / props.model.width)) + 40);
const lifetime = new AbortController();
let instance: FlowchartEditor | undefined, observer: ResizeObserver | undefined;
/** @returns Completion of the current, abortable native renderer initialization. */
async function initialize(): Promise<void> {
  try {
    const { createX6FlowchartEditor } = await import("@quirekit/flowchart-x6");
    if (lifetime.signal.aborted || !parent.value) return;
    const fontFamily = getComputedStyle(parent.value).fontFamily;
    const editor = await createX6FlowchartEditor({ container: parent.value, document: props.model.document, locale: props.locale, readOnly: true, signal: lifetime.signal,
      label: props.locale === "en" ? "Markdown source and syntax tree" : "Markdown 原文与语法树",
      appearance: { surface: "var(--qk-color-surface-page)", text: "var(--qk-color-text-primary)", line: "var(--qk-color-border-default)", primary: "var(--qk-color-border-default)", fontFamily,
        runColors: { pending: "var(--qk-color-text-tertiary)", running: "var(--qk-color-warning-solid)", completed: "var(--qk-color-action-primary)", reused: "var(--qk-color-success-solid)", invalid: "var(--qk-color-text-tertiary)" } },
    });
    if (lifetime.signal.aborted) { await editor.destroy(); return; }
    instance = editor; emit("ready", editor); ready.value = true;
  } catch (error) { if (!lifetime.signal.aborted) { failure.value = true; console.error(error); } }
}
onMounted(() => {
  if (!parent.value) return;
  observer = new ResizeObserver(() => { if (parent.value) { width.value = parent.value.clientWidth; emit("compact", width.value < 720); } });
  observer.observe(parent.value); width.value = parent.value.clientWidth; emit("compact", width.value < 720); void initialize();
});
onBeforeUnmount(() => { lifetime.abort(); observer?.disconnect(); if (instance) { emit("retired", instance); void instance.destroy(); } });
</script>
<template>
  <div class="algorithm-flow-host" data-algorithm-flow-canvas :data-flow-ready="ready" :style="{ height: `${height}px` }">
    <div ref="parent" class="algorithm-flow-mount" />
    <p v-if="!ready" class="algorithm-flow-placeholder" role="status">{{ failure ? locale === 'en' ? 'The diagram could not load.' : '流程图未能载入。' : locale === 'en' ? 'Loading the flowchart…' : '正在载入流程图…' }}</p>
  </div>
</template>
<style scoped>
.algorithm-flow-host { position: relative; width: 100%; min-width: 0; border: 1px solid var(--qk-color-border-default); border-radius: 6px; background: var(--qk-color-surface-page); overflow: hidden; }
.algorithm-flow-mount { width: 100%; height: 100%; pointer-events: none; }
.algorithm-flow-placeholder { position: absolute; inset: 32px 0 auto; text-align: center; color: var(--qk-color-text-secondary); }
</style>

```

`recording/fixture.ts` 定义原文与连续源码修改，`recording/snapshots.ts` 记录真正的增量解析和公共阅读投影。
中间插入使用 EditorState 源码事务；末尾追加与后置引用实际调用 `MarkdownStream.append`。
`recording/tree.ts` 从原生树读取类型、子节点、范围和对象编号，没有按文本相似度猜测复用。

回放只选择不可变的旧、新快照，再交给正常的 `QuireMarkdown` 显示。不会重复插入片段，
也不会重挂历史 Vue 节点。语法树复用与阅读缓存分别展示：即使后续段落的 Tree 对象保留，
绝对位置变化也可能要求重新投影。本例的慢放是教学编排，不是解析器内部日志或性能测速。

`graph/scene.ts` 把这些数据变成 `FlowchartDocument`，`graph/trace.ts` 编排步骤。
每一步的 `scene` 描述接受后的完整图形：相同 ID 保留展示对象，新 ID 表示新节点。
`sceneDuration` 决定位置、文字和节点进出的过渡时长；`travel` 负责片段沿线移动，
`pulse` 标出受影响分支。第三次插入同时展示新列表和相邻段落的变化。

图形、过渡和播放时钟均由 `@quirekit/flowchart-x6` 的公共 `playback` 提供。
本站只把真实快照映射为图稿、操作公共播放器并显示阅读结果。播放使用临时展示图，
不会修改作者图源、编辑历史或静态导出。流程图、思维导图和白板也继续用于静态关系图。

## 在 QuireMarkdown 中接入

Markdown 引擎允许用 `components.pre` 替换代码块的显示。下面的完整示例只为三种原生图稿启用渲染，普通代码仍保留代码查看与复制。

### reading/diagrams.vue

```vue
<script setup lang="ts">
import { computed, defineComponent, h } from "vue";
import { QuireMarkdown } from "@quirekit/incremark/vue";
import type { ExampleLocale } from "../shared/types";
import type { DocsCodeProps } from "@quirekit/docs-kit/runtime";
import NativeCode from "../diagrams/reading.vue";
import { algorithmFlow } from "../algorithms/flow";
const props = withDefaults(defineProps<{ /** Tutorial language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
// The Markdown engine still owns parsing; only the ordinary pre renderer is replaced.
const components = { pre: defineComponent({ props: ["code", "language"],
  /** @param source - Exact code-fence inputs. @returns A local component honoring the application's locale. */
  setup: (source: DocsCodeProps) => () => h(NativeCode, { ...source, locale: props.locale }),
}) };
const content = computed(() => `\`\`\`quirekit-flowchart\n${algorithmFlow(props.locale, "current")}\n\`\`\`\n\n\`\`\`ts\nconst message = "ordinary code stays code";\n\`\`\``);
</script>
<template><QuireMarkdown :content="content" :components="components" :locale="locale" /></template>

```

### shared/types.ts

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

```

### diagrams/reading.vue

```vue
<script setup lang="ts">
import { computed } from "vue";
import { QuireProseSource } from "@quirekit/ui/vue";
import type { DocsCodeProps } from "@quirekit/docs-kit/runtime";
import DiagramCode from "./code.vue";
const props = defineProps<DocsCodeProps & {
  /** Interface language supplied by the application. */ locale?: string;
}>();
const native = computed(() => ["quirekit-flowchart", "quirekit-mind-map", "quirekit-whiteboard"].includes(props.language?.trim() ?? ""));
</script>
<template>
  <DiagramCode v-if="native" v-bind="props" />
  <QuireProseSource v-else :code="code ?? ''" :language="language" :filename="filename" />
</template>

```

### diagrams/code.vue

```vue
<script setup lang="ts">
import { computed, ref } from "vue";
import { QuireDialogRoot, QuireDialogTrigger, QuireDialogContent, QuireProseSource, QuireTypography } from "@quirekit/ui/vue";
import type { DocsCodeProps } from "@quirekit/docs-kit/runtime";
import { readDiagram } from "./document";
import DiagramView from "./view.vue";
const props = defineProps<DocsCodeProps & {
  /** Interface language, provided by examples or the documentation route. */ locale?: string;
}>();
const locale = computed(() => props.locale ?? "zh-CN");
const open = ref(false);
const diagram = computed(() => {
  try { return readDiagram(props.language?.trim() ?? "", props.code ?? ""); }
  catch { return null; }
});
</script>
<template>
  <figure class="docs-native-diagram" data-qk-not-prose>
    <template v-if="diagram">
      <figcaption class="docs-diagram-caption">
        <QuireTypography as="span" variant="control">{{ diagram.value.title }}</QuireTypography>
        <QuireDialogRoot v-model:open="open" lazy-mount unmount-on-exit>
          <QuireDialogTrigger class="qk-button qk-button--neutral-text qk-button--sm">{{ locale === 'en' ? 'Full view' : '查看大图' }}</QuireDialogTrigger>
          <QuireDialogContent :title="diagram.value.title" size="large">
            <DiagramView v-if="open" :diagram="diagram" :locale="locale" expanded />
          </QuireDialogContent>
        </QuireDialogRoot>
      </figcaption>
      <DiagramView :diagram="diagram" :locale="locale" />
      <details class="docs-diagram-source"><summary>{{ locale === 'en' ? 'Diagram source' : '查看图稿' }}</summary>
        <QuireProseSource :code="code ?? ''" language="json" :filename="filename" :collapsible="true" :max-height="320" />
      </details>
    </template>
    <template v-else>
      <QuireTypography variant="helper" role="status">{{ locale === 'en' ? 'Invalid or oversized diagram; original source is preserved.' : '图稿格式不完整或超出预览范围，已保留原始代码。' }}</QuireTypography>
      <QuireProseSource :code="code ?? ''" :language="language" :filename="filename" />
    </template>
  </figure>
</template>
<style>
.docs-native-diagram { margin: 16px 0; border: 1px solid var(--qk-color-border-subtle); border-radius: var(--qk-radius-group); overflow: hidden; min-width: 0; }
.docs-diagram-caption { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; padding: 12px 16px; }
.docs-diagram-source { border-top: 1px solid var(--qk-color-border-subtle); padding: 10px 16px; }
.docs-diagram-source summary { cursor: pointer; font-size: 13px; color: var(--qk-color-text-secondary); }
</style>

```

### diagrams/document.ts

```ts
import { parseFlowchartEmbed } from "@quirekit/flowchart";
import { parseMindMapEmbed } from "@quirekit/mind-map";
import { parseWhiteboard } from "@quirekit/whiteboard";
import type { DiagramDocument } from "./types";

/** @param language - Explicit native fenced format. @param source - Exact JSON body. @returns Validated source; malformed or over-budget input throws. */
export function readDiagram(language: string, source: string): DiagramDocument {
  if (source.length > 4_194_304) throw new RangeError("Diagram preview source exceeds 4,194,304 UTF-16 code units");
  if (language === "quirekit-flowchart") {
    const value = parseFlowchartEmbed(source);
    if (value.document.nodes.length > 200 || value.document.edges.length > 400) throw new RangeError("Diagram preview exceeds 200 shapes or 400 edges");
    return { kind: "flowchart", value };
  }
  if (language === "quirekit-mind-map") {
    const value = parseMindMapEmbed(source);
    let count = 0;
    const pending = [value.document.root, ...(value.document.floatingTopics?.map(topic => topic.root) ?? [])];
    while (pending.length) {
      if (++count > 500) throw new RangeError("Diagram preview exceeds 500 topics");
      pending.push(...pending.pop()!.children);
    }
    return { kind: "mind-map", value };
  }
  if (language === "quirekit-whiteboard") {
    const value = parseWhiteboard(source);
    if (value.scene.elements.length > 500) throw new RangeError("Diagram preview exceeds 500 elements");
    return { kind: "whiteboard", value };
  }
  throw new TypeError("Unsupported diagram language");
}

```

### diagrams/types.ts

```ts
import type { FlowchartEmbed } from "@quirekit/flowchart";
import type { MindMapEmbed } from "@quirekit/mind-map";
import type { WhiteboardDocument } from "@quirekit/whiteboard";

/** Canonical source admitted by its own domain validator. */
export type DiagramDocument = { kind: "flowchart"; value: FlowchartEmbed }
  | { kind: "mind-map"; value: MindMapEmbed } | { kind: "whiteboard"; value: WhiteboardDocument };

/** Viewport-only public adapter; source operations are deliberately absent. */
export interface DiagramView {
  /** @returns Completion of fitting the immutable source to the host. */ fit(): void | Promise<void>;
  /** @param factor - Relative zoom multiplier; source is never read. */ zoom(factor: number): void;
  /** @returns Completion of adapter resource release. */ destroy(): void | Promise<void>;
}

```

### diagrams/view.vue

```vue
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref, shallowRef, watchEffect } from "vue";
import { QuireButton, QuireSpace, QuireTypography } from "@quirekit/ui/vue";
import type { DiagramDocument, DiagramView } from "./types";
import { mountDiagram } from "./mount";
const props = defineProps<{
  /** Validated immutable source. */ diagram: DiagramDocument;
  /** Interface language. */ locale?: string;
  /** Larger viewport inside the full-view dialog. */ expanded?: boolean;
}>();
const parent = shallowRef<HTMLElement>();
const view = shallowRef<DiagramView>();
const active = ref(false), ready = ref(false), failed = ref(false);
let visibility: IntersectionObserver | undefined;
onMounted(() => {
  if (!parent.value || typeof IntersectionObserver === "undefined") { active.value = true; return; }
  visibility = new IntersectionObserver(entries => {
    if (entries.some(entry => entry.isIntersecting)) { active.value = true; visibility?.disconnect(); }
  }, { rootMargin: "160px" });
  visibility.observe(parent.value);
});
onBeforeUnmount(() => visibility?.disconnect());
watchEffect(onCleanup => {
  if (!active.value || !parent.value) return;
  const source = props.diagram, locale = props.locale ?? "zh-CN", host = parent.value;
  const lifetime = new AbortController();
  // Every source revision owns its mount, including any asynchronous native layout.
  const mount = host.ownerDocument.createElement("div");
  mount.className = "docs-diagram-mount";
  host.append(mount);
  ready.value = false;
  failed.value = false;
  let instance: DiagramView | undefined;

  /** Shows failure only while this source revision still owns the view. */
  function showFailure(): void {
    if (lifetime.signal.aborted) return;
    ready.value = false;
    failed.value = true;
  }

  const resize = new ResizeObserver(() => {
    if (ready.value && instance) void Promise.resolve().then(() => instance?.fit()).catch(showFailure);
  });
  resize.observe(host);
  onCleanup(() => {
    lifetime.abort();
    resize.disconnect();
    view.value = undefined;
    ready.value = false;
    void instance?.destroy();
    mount.remove();
  });

  void mountDiagram(source, mount, locale, lifetime.signal).then(async result => {
    // A superseded loader can only own its detached mount, never the next graph's host.
    if (lifetime.signal.aborted) { await result.destroy(); return; }
    instance = result;
    view.value = result;
    await result.fit();
    if (!lifetime.signal.aborted) ready.value = true;
  }).catch(showFailure);
});
/** @param factor - Relative zoom or zero to fit the current viewport. @returns Completion without publishing a failure from an old view. */
async function navigate(factor: number): Promise<void> {
  const current = view.value;
  if (!current) return;
  try { if (factor) current.zoom(factor); else await current.fit(); }
  catch { if (view.value === current) { ready.value = false; failed.value = true; } }
}
</script>
<template>
  <div class="docs-diagram-view" :data-diagram-kind="diagram.kind" :data-diagram-ready="ready || undefined" data-qk-not-prose>
    <QuireSpace justify="end" gap="inline" class="docs-diagram-navigation">
      <QuireButton size="sm" variant="text" :disabled="!ready" :aria-label="locale === 'en' ? 'Zoom out' : '缩小'" @click="navigate(0.8)">−</QuireButton>
      <QuireButton size="sm" variant="text" :disabled="!ready" :aria-label="locale === 'en' ? 'Zoom in' : '放大'" @click="navigate(1.25)">+</QuireButton>
      <QuireButton size="sm" variant="text" :disabled="!ready" @click="navigate(0)">{{ locale === 'en' ? 'Fit' : '适应画布' }}</QuireButton>
    </QuireSpace>
    <div ref="parent" class="docs-diagram-canvas" :class="{ 'docs-diagram-canvas--expanded': expanded }" :aria-label="diagram.value.title" />
    <QuireTypography v-if="!ready" variant="helper" role="status" class="docs-diagram-status">
      {{ failed ? (locale === 'en' ? 'The diagram could not load. Its source is available below.' : '图表加载失败，可以查看原始图稿。') : (locale === 'en' ? 'Preparing diagram…' : '正在准备图表…') }}
    </QuireTypography>
  </div>
</template>
<style>
.docs-diagram-view { min-width: 0; position: relative; }
.docs-diagram-navigation { padding: 4px 8px; background: var(--qk-color-surface-subtle); }
.docs-diagram-canvas { height: 400px; position: relative; background: #ffffff; color: #243247; overflow: hidden; }
.docs-diagram-canvas--expanded { height: min(68vh, 740px); }
.docs-diagram-mount { position: absolute; inset: 0; }
.docs-diagram-status { position: absolute; inset: 46% 16px auto; text-align: center; color: #475569; pointer-events: none; }
.docs-diagram-canvas .excalidraw { --color-primary: #2563eb; }
@media (max-width: 600px) { .docs-diagram-canvas { height: 340px; } .docs-diagram-canvas--expanded { height: 62vh; } }
</style>

```

### diagrams/mount.ts

```ts
import type { DiagramDocument, DiagramView } from "./types";
import { whiteboardView } from "./whiteboard";

/** @param source - Validated immutable diagram. @param container - Instance-owned, connected mount. @param locale - Interface language. @param signal - Initialization and mount lifetime. @returns A viewport-only adapter over the existing editor. */
export async function mountDiagram(source: DiagramDocument, container: HTMLElement, locale: string, signal: AbortSignal): Promise<DiagramView> {
  // Source semantics remain in their domain; only client resources and viewport controls live here.
  signal.throwIfAborted();
  if (source.kind === "flowchart") {
    const { createX6FlowchartEditor } = await import("@quirekit/flowchart-x6");
    signal.throwIfAborted();
    const editor = await createX6FlowchartEditor({ container, document: source.value.document, locale, signal, readOnly: true, label: source.value.title });
    return { fit: () => editor.fit(), zoom: factor => editor.setZoom(editor.getState().scale * factor), destroy: () => editor.destroy() };
  }
  if (source.kind === "mind-map") {
    const { createSimpleMindMapEditor } = await import("@quirekit/mind-map-simple");
    signal.throwIfAborted();
    const editor = await createSimpleMindMapEditor({ container, document: source.value.document, locale, signal, readOnly: true, label: source.value.title });
    return { fit: () => editor.fit(), zoom: factor => editor.setZoom(editor.getState().scale * factor), destroy: () => editor.destroy() };
  }
  const { createExcalidrawWhiteboard } = await import("@quirekit/whiteboard-excalidraw");
  signal.throwIfAborted();
  const editor = await createExcalidrawWhiteboard({ container, document: source.value, locale, signal, readOnly: true, theme: "light" });
  return whiteboardView(editor, container.ownerDocument.defaultView!, signal);
}

```

### diagrams/whiteboard.ts

```ts
import type { WhiteboardEditor } from "@quirekit/whiteboard-excalidraw";
import type { DiagramView } from "./types";

/**
 * Waits for a native viewport commit, with cancellation on detached mounts.
 * @param window - Owning browsing context.
 * @param signal - Diagram lifetime.
 * @returns Completion of the next animation frame.
 */
function viewportFrame(window: Window, signal: AbortSignal): Promise<void> {
  signal.throwIfAborted();
  return new Promise((resolve, reject) => {
    /** Cancels the pending frame when the owning view closes. */
    function abort(): void {
      window.cancelAnimationFrame(frame);
      reject(signal.reason);
    }
    const frame = window.requestAnimationFrame(() => {
      signal.removeEventListener("abort", abort);
      resolve();
    });
    signal.addEventListener("abort", abort, { once: true });
  });
}

/**
 * Reserves space for native read-only chrome without serializing the scene.
 * @param editor - Existing public Excalidraw adapter.
 * @param window - Owning browsing context.
 * @param signal - Diagram lifetime.
 * @returns Coalesced viewport controls and the original disposal boundary.
 */
export function whiteboardView(editor: WhiteboardEditor, window: Window, signal: AbortSignal): DiagramView {
  let fitting: Promise<void> | undefined;

  /** @returns Completion after initial scene restore and the fitted viewport commit. */
  async function fitViewport(): Promise<void> {
    await viewportFrame(window, signal);
    editor.fit();
    await viewportFrame(window, signal);
    // Native fitting uses the full host; reserve the footer's space in this reader.
    editor.zoom(0.8);
  }

  /** @returns One shared fit operation while the native viewport is settling. */
  function fit(): Promise<void> {
    return fitting ??= fitViewport().finally(() => { fitting = undefined; });
  }

  return { fit, zoom: factor => editor.zoom(factor), destroy: () => editor.destroy() };
}

```

### algorithms/flow.ts

```ts
import { createFlowchart, createFlowchartEmbed, createFlowchartNode, readFlowchart, serializeFlowchartEmbed } from "@quirekit/flowchart";
import type { FlowchartEdge, FlowchartNode, FlowchartPort, FlowchartShape } from "@quirekit/flowchart";
import type { ExampleLocale } from "../shared/types";

/** @param id - Stable graph identity. @param x - World left. @param y - World top. @param text - Literal caption. @param shape - Native semantic shape. @returns A readable, validated graph node. */
function node(id: string, x: number, y: number, text: string, shape: FlowchartShape = "process"): FlowchartNode {
  return { ...createFlowchartNode(id, shape, { x, y }, text), width: 220, height: 110,
    style: { fontSize: 17, padding: 8, fill: "#f8fafc", stroke: "#94a3b8", color: "#1e293b" } };
}
/** @param from - Source identity. @param to - Target identity. @param label - Literal relation label. @param sourcePort - Source side. @param targetPort - Target side. @param vertices - Explicit world bends avoiding intervening nodes. @returns A canonical connector, independent of the renderer. */
function edge(from: string, to: string, label = "", sourcePort: FlowchartPort = "right", targetPort: FlowchartPort = "left", vertices: readonly { x: number; y: number }[] = []): FlowchartEdge {
  return { id: `${from}-${to}`, source: { node: from, port: sourcePort }, target: { node: to, port: targetPort },
    routing: "straight", connector: "rounded", vertices, label, labelPosition: 0.5, style: { stroke: "#64748b", width: 2 } };
}

/** @param locale - Literal diagram language. @param version - Earlier session or current implementation. @returns Portable graph source for the existing Flow Editor renderer. */
export function algorithmFlow(locale: ExampleLocale, version: "old" | "current"): string {
  const zh = locale === "zh-CN";
  const title = version === "old" ? (zh ? "旧 Incremark：稳定块 + 尾部解析" : "Earlier Incremark: blocks and tail")
    : (zh ? "当前算法：语法复用 + 依赖校验" : "Current algorithm: reuse and validation");
  const nodes = version === "old" ? [
    node("receive", 0, 0, zh ? "收到新片段" : "Receive a chunk", "terminator"),
    node("split", 260, 0, zh ? "划分稳定块\n与未完成尾部" : "Find stable blocks\nand unfinished tail"),
    node("keep", 520, 0, zh ? "保留已有块\n及其解析节点" : "Retain earlier blocks\nand parsed nodes"),
    node("tail", 260, 160, zh ? "Marked / Micromark\n解析尾部" : "Marked / Micromark\nparse the tail"),
    node("append", 520, 160, zh ? "合并新节点\n和引用定义" : "Merge new nodes\nand definitions"),
    node("patch", 520, 320, zh ? "定义到达后\n补已有节点的地址" : "Fill destinations\non existing nodes"),
    { ...node("risk", 0, 320, zh ? "风险：补地址无法\n修正旧的链接分组" : "Risk: filling a URL\ncannot fix grouping", "note"), style: { fill: "#fff1f2", stroke: "#fb7185", color: "#9f1239", fontSize: 17 } },
  ] : [
    node("receive", 0, 0, zh ? "追加真实源码\n建立文本事务" : "Append actual source\nmap text changes", "terminator"),
    node("structure", 260, 0, zh ? "Lezer 结构解析\n复用有效片段" : "Lezer structural pass\nreuse valid fragments"),
    node("definitions", 520, 0, zh ? "收集当前定义\n同名取首次定义" : "Collect definitions\nfirst definition wins"),
    node("syntax-check", 520, 160, zh ? "引用分组\n受到影响？" : "Reference grouping\naffected?", "decision"),
    node("refine", 260, 160, zh ? "重判受影响段落\n保留无关语法片段" : "Refine affected blocks\nretain other fragments"),
    node("accept", 0, 160, zh ? "接受语义一致\n的原生语法树" : "Accept the native tree\nwith correct semantics"),
    node("render-check", 0, 350, zh ? "缓存条件\n都满足？" : "Cache inputs\nunchanged?", "decision"),
    { ...node("reuse", 260, 350, zh ? "复用显示内容\n保留组件状态" : "Reuse display output\nretain component state"), style: { fill: "#eff6ff", stroke: "#60a5fa", color: "#1e40af", fontSize: 17 } },
    { ...node("rebuild", 520, 350, zh ? "重新生成受影响块\n更新链接、脚注等" : "Project affected blocks\nupdate links and notes"), style: { fill: "#fffbeb", stroke: "#fbbf24", color: "#92400e", fontSize: 17 } },
  ];
  const edges = version === "old" ? [edge("receive", "split"), edge("split", "keep"), edge("split", "tail", "", "bottom", "top"),
    edge("tail", "append"), edge("keep", "append", "", "bottom", "top"), edge("append", "patch", "", "bottom", "top"), edge("patch", "risk", "", "left", "right")]
    : [edge("receive", "structure"), edge("structure", "definitions"), edge("definitions", "syntax-check", "", "bottom", "top"),
      edge("syntax-check", "refine", zh ? "是" : "Yes", "left", "right"), edge("refine", "accept", "", "left", "right"),
      edge("syntax-check", "accept", zh ? "否" : "No", "bottom", "bottom", [{ x: 630, y: 305 }, { x: 110, y: 305 }]),
      edge("accept", "render-check", "", "bottom", "top"), edge("render-check", "reuse", zh ? "是" : "Yes"),
      edge("render-check", "rebuild", zh ? "否 / 无法证明" : "No / unproven", "bottom", "bottom", [{ x: 110, y: 510 }, { x: 630, y: 510 }])];
  // The domain validator owns IDs, geometry and source encoding; the example only describes the algorithm.
  const document = readFlowchart({ ...createFlowchart(), nodes, edges });
  return serializeFlowchartEmbed({ ...createFlowchartEmbed(`algorithm-${version}`, title), document });
}

```

打开示例源码，可以看到原生图稿生成、格式校验、三个渲染器的加载和挂载释放。
这些渲染器由宿主选择；公共 Markdown 内核没有新增图表编辑器依赖。React 宿主可用同样的图稿及浏览器适配器，另行实现自己的组件挂载。

## 接入 docs-kit 的站点

使用共享文档层时，可以在 Nuxt plugin 中通过 `docsCodeRenderersKey` 注册围栏语言到组件的映射。
该入口来自 `@quirekit/docs-kit/runtime`。组件接收原始 `code`、`language`、可选 `filename` 和 `presentation`；图稿不能被解释为 Vue 模板。

未注册的语言继续显示为代码，示例的源码面板也继续显示原始文件。各站自行选择渲染器，docs-kit 不安装图表编辑器。

## 加载与错误处理

图表资源在客户端、图进入可视范围时才加载。服务端保留可读取的图稿；浏览器初始化失败时，可以继续查看和复制原稿。
切换图稿、关闭大图或离开页面，会取消旧加载并销毁渲染实例。本站采用只读查看；编辑及保存仍由已有编辑器和应用负责。

图稿先通过各领域的格式校验。格式不完整时显示原始代码。本站自动预览还限制为 200 个流程图节点与 400 条连线、500 个思维导图主题或白板元素，源码最多 4 Mi 个 UTF-16 单元；超出时保留原稿。这个限制属于文档预览，不是各编辑器协议的容量上限。
