# 与 Incremark 比较

为什么我们重新设计了 Incremark，以及新引擎怎样处理流式 Markdown。

**Incremark 是我们开发的第一代流式 Markdown 工具。** QuireKit Markdown 延续了同一个目标：让 AI 一边生成文字，页面一边显示排版好的内容。

这次重新设计，主要解决一个容易被低估的问题：**后面收到的内容，仍然可能改变前面那句话应该怎样显示。** 新引擎在减少重复解析和渲染的同时，让这些后到的定义能够正确更新前文。

## 看一棵树怎样更新

“中间插入”会连续修改同一份原文三次：先在第二段加入加粗，再在第一段加入行内代码，
最后在中间插入两项列表。每次上一轮的结果都成为下一轮的起点。

点击播放，跟随移动片段进入原文落点，再找到橙色的受影响分支、换入蓝色的新节点。
绿色节点保留原树对象。可以暂停、单步、回退，或选择某次插入与某个阶段。
视图固定，不支持拖动或缩放。

第三次插在段落边界：第三段原文虽然没改，解析器仍重建了它的语法分支。
这也说明更新范围要看实际语法与上下文，不能只按改了几个字判断。
“末尾追加”和“后置引用”保留对照；后置定义到达后，前面变成链接的是 **bar**。
树和阅读结果来自真实引擎；移动轨迹与慢放是教学编排，不是底层执行日志。

这段树变化也由我们的流程图库绘制与播放：节点、连线、片段移动及树的替换，都使用同一个公共播放器。

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

```

### 对照旧版的流程

切换“旧 Incremark”和“当前算法”，沿着箭头看一份新片段到达后会经历哪些步骤。图使用我们现有的流程图编辑器渲染，支持缩放和查看大图。

### algorithms/flow.vue

```vue
<script setup lang="ts">
import { computed, ref } from "vue";
import { QuireButton, QuireSpace, QuireTypography } from "@quirekit/ui/vue";
import type { ExampleLocale } from "../shared/types";
import DiagramCode from "../diagrams/code.vue";
import { algorithmFlow } from "./flow";
const props = withDefaults(defineProps<{ /** Diagram content and interface language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const version = ref<"old" | "current">("current");
const code = computed(() => algorithmFlow(props.locale, version.value));
</script>
<template>
  <div class="p-qk-surface" data-algorithm-flow>
    <QuireSpace gap="inline" wrap>
      <QuireButton size="sm" :variant="version === 'old' ? 'solid' : 'outline'" :aria-pressed="version === 'old'" @click="version = 'old'">{{ locale === 'en' ? 'Earlier Incremark' : '旧 Incremark' }}</QuireButton>
      <QuireButton size="sm" :variant="version === 'current' ? 'solid' : 'outline'" :aria-pressed="version === 'current'" @click="version = 'current'">{{ locale === 'en' ? 'Current algorithm' : '当前算法' }}</QuireButton>
    </QuireSpace>
    <DiagramCode :code="code" language="quirekit-flowchart" :locale="locale" />
    <QuireTypography variant="helper" class="text-qk-text-secondary">{{ locale === 'en' ? 'The two earlier parsers share the same streaming strategy. The current engine checks syntax reuse and rendering reuse separately.' : '旧版两种解析引擎共用会话策略。当前算法分别判断语法能否复用、显示内容能否复用。' }}</QuireTypography>
  </div>
</template>

```

### shared/types.ts

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

```

### 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 });
}

```

## 为什么要重新设计

旧 Incremark 把正在生成的回答分成两部分：已经结束的段落，和还在继续写的部分。新文字到来时，尽量保留前面的解析结果，只重新解析末尾。这个思路能减少重复计算。

但 Markdown 不总是写完一段就能确定这一段的意思。例如，AI 可以先写正文，最后才给出链接地址：

```md
更多内容见[文档][api]。

[api]: https://example.com/docs
```

在最后一行到达之前，`[文档][api]` 还没有可用的地址；地址到达以后，前面的“文档”应当变成链接。脚注也是如此：正文先出现，解释可能在整篇回答的末尾才到达。

旧版已经为这些情况做了处理：先记录引用，等定义到达后再补上链接。困难在于，有些后置定义还会改变**哪几个方括号组成一个链接**。这时，只给已有节点补地址就不够了。我们需要重新检查前面的语法，同时让页面更新正确的内容。

## 旧实现的两个具体问题

### 同一个链接地址定义了两次

假设下面的正文和两个地址依次到达：

```md
[文档][api]

[api]: /first

[api]: /second
```

按照 Markdown 的规则，同名定义应使用第一次出现的地址，所以“文档”应当指向 `/first`。旧版在合并定义时覆盖了原来的值，导致前面的链接改为 `/second`。当前引擎保留首次定义。

### 链接落在了错误的文字上

再看一个短例子。先收到第一行，随后收到 `baz` 的定义：

```md
[foo][bar][baz]

[baz]: /target
```

这段文字应该被理解为：保留普通文字 `[foo]`，将 `[bar][baz]` 解释为链接，也就是让 **bar** 可以点击。

旧版提前把 `[foo][bar]` 识别成了一个引用，把 `[baz]` 留作另一个引用。等地址到达后，可点击的变成了 **baz**。前面的分组已经确定，补上地址也不能纠正它。当前引擎会依据实际到达的定义重新判断分组，因此链接落在 **bar** 上。

我们用旧版的两种引擎重新跑了这两个例子，结果如下：

| 情况 | 旧 Marked | 旧 Micromark | 当前 QuireKit Markdown |
| --- | --- | --- | --- |
| `api` 定义了两次 | 链接到 `/second` | 链接到 `/second` | 链接到 `/first` |
| 相邻的三个方括号 | baz 可以点击 | baz 可以点击 | bar 可以点击 |

## 新算法怎样处理流式更新

### 1. 保留实际收到的原文

每次追加都写入同一份 Markdown。停止生成时保留已经收到的文字，不为了显示效果补写围栏、括号或 HTML 结束标签。切换到另一条消息时，再建立新的解析会话。

### 2. 让 Lezer 找出可以继续使用的解析结果

Lezer 把标题、段落、列表和链接等结构记录在语法树里。新文字到来后，它结合语法上下文判断哪些旧结果还能使用，哪些需要重新解析。我们复用这套机制来处理流式追加。

例如，在末尾开始新段落时，前面未受影响的段落可以继续使用。列表、围栏等结构发生变化时，解析器也会按相应的语法处理，而不是仅凭一个空行就断定前面的内容已经结束。

### 3. 后到的定义可以回头修正引用

新引擎先收集当前文档中的链接定义，再检查哪些段落的引用判断受到影响。像上面的 `[foo][bar][baz]`，需要重新判断哪部分是链接；同名定义则按首次出现的值处理。

它也会记录段落查询过哪些引用，包括当时还不存在的引用。有足够依据确认一个新定义没有影响该段落时，就继续使用旧结果。

下面这张白板把依赖画出来：`api` 的定义到达后，需要重新检查用过它的段落 B。普通介绍和只查询 `faq` 的段落，满足复用条件时可以保留。

### algorithms/references.vue

```vue
<script setup lang="ts">
import { computed } from "vue";
import type { ExampleLocale } from "../shared/types";
import DiagramCode from "../diagrams/code.vue";
import { algorithmReferences } from "./references";
const props = withDefaults(defineProps<{ /** Diagram language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const code = computed(() => algorithmReferences(props.locale));
</script>
<template><DiagramCode :code="code" language="quirekit-whiteboard" :locale="locale" /></template>

```

### shared/types.ts

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

```

### 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/references.ts

```ts
import { createWhiteboardDocument, editWhiteboardScene, serializeWhiteboard } from "@quirekit/whiteboard";
import type { ExampleLocale } from "../shared/types";

/** @param locale - Literal diagram language. @returns A native whiteboard describing the dependency from a late definition to earlier text. */
export function algorithmReferences(locale: ExampleLocale): string {
  const zh = locale === "zh-CN", board = createWhiteboardDocument("algorithm-references", zh ? "后置定义：谁需要更新？" : "Late definitions: which blocks change?");
  // Semantic shapes and bindings are authored through the shared API; Excalidraw owns rendering.
  const scene = editWhiteboardScene(board.scene, { nodes: [
    { id: "plain", x: 0, y: 0, width: 300, height: 84, label: zh ? "段落 A：普通介绍\n无引用 → 继续复用" : "A: ordinary introduction\nNo references → reuse", fill: "#eff6ff", color: "#1e40af", fontSize: 19 },
    { id: "api-user", x: 0, y: 160, width: 300, height: 96, label: zh ? "段落 B：[文档][api]\n查询过 api → 重新检查" : "B: [Documentation][api]\nQueried api → recheck", fill: "#fffbeb", color: "#92400e", fontSize: 19 },
    { id: "faq-user", x: 0, y: 330, width: 300, height: 96, label: zh ? "段落 C：[帮助][faq]\n只查询 faq → 继续复用" : "C: [Help][faq]\nOnly queried faq → reuse", fill: "#eff6ff", color: "#1e40af", fontSize: 19 },
    { id: "late", x: 450, y: 160, width: 280, height: 96, label: zh ? "最后收到的定义\n[api]: /docs" : "Definition arrives last\n[api]: /docs", fill: "#f0fdf4", color: "#166534", fontSize: 19 },
    { id: "update", x: 450, y: 0, width: 280, height: 84, label: zh ? "api 从不存在变为存在\n重判 B，再更新链接" : "api now exists\nRefine B, then update its link", fill: "#fffbeb", color: "#92400e", fontSize: 19 },
    { id: "limit", shape: "text", x: 450, y: 345, width: 310, height: 70, label: zh ? "无法证明不受影响时，重新处理。\n依赖检查本身仍有开销。" : "Unproven reuse → process again.\nDependency checks still cost work.", color: "#475569", fontSize: 17 },
  ], edges: [
    { id: "late-to-user", from: "late", to: "api-user", label: zh ? "api 依赖" : "api dependency", color: "#a16207" },
    { id: "late-to-update", from: "late", to: "update", color: "#a16207" },
  ] });
  return serializeWhiteboard({ ...board, scene });
}

```

### 4. 显示内容也要检查，才能复用

语法结构没变，链接地址或脚注编号仍可能改变。因此，渲染缓存还会检查这些依赖，以及组件和 HTML 配置。条件满足时保留已有内容；受影响或无法确认的块重新生成。

这样，AI 继续往下写时，前面可以复用的内容继续保留；后面的定义改变了前文含义时，前面的链接和脚注仍会更新。

### 这些步骤由谁负责

解析、引用判断和缓存证明在公共内核里。Vue 和 React 负责各自的组件与提交生命周期；网络流和可选图表渲染由应用接入。

### algorithms/layers.vue

```vue
<script setup lang="ts">
import { computed } from "vue";
import type { ExampleLocale } from "../shared/types";
import DiagramCode from "../diagrams/code.vue";
import { algorithmLayers } from "./layers";
const props = withDefaults(defineProps<{ /** Diagram language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const code = computed(() => algorithmLayers(props.locale));
</script>
<template><DiagramCode :code="code" language="quirekit-mind-map" :locale="locale" /></template>

```

### shared/types.ts

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

```

### 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/layers.ts

```ts
import { createMindMapDocument, createMindMapEmbed, serializeMindMapEmbed } from "@quirekit/mind-map";
import type { MindMapOutline } from "@quirekit/mind-map";
import type { ExampleLocale } from "../shared/types";

/** @param text - Literal topic title. @param children - Child responsibility topics. @returns A plain outline consumed by the public domain factory. */
function topic(text: string, children: readonly MindMapOutline[] = []): MindMapOutline { return { text, children }; }

/** @param locale - Literal content language. @returns Canonical mind-map source showing public ownership boundaries. */
export function algorithmLayers(locale: ExampleLocale): string {
  const zh = locale === "zh-CN", title = zh ? "解析、渲染和宿主各自负责什么" : "Parsing, rendering and host responsibilities";
  const outline = topic("QuireKit Markdown", [
    topic("incremark-core", [
      topic(zh ? "源码与原生语法树" : "Source and native syntax tree", [topic(zh ? "追加、替换、解析续作" : "Append, replace, resume parsing"), topic(zh ? "引用分组与首次定义" : "Reference grouping; first definition wins")]),
      topic(zh ? "阅读投影与缓存证明" : "Reading projection and cache validation", [topic(zh ? "链接、脚注、HTML 策略" : "Links, notes, HTML policy"), topic(zh ? "块身份与外部依赖" : "Block identity and external inputs")]),
    ]),
    topic(zh ? "框架适配" : "Framework adapters", [topic(zh ? "Vue：VNode 与组件生命周期" : "Vue: VNodes and component lifetime"), topic(zh ? "React：提交后接受缓存" : "React: accept cache after commit")]),
    topic(zh ? "应用宿主" : "Application host", [topic(zh ? "网络流、停止与消息 identity" : "Network stream, stop, message identity"), topic(zh ? "组件替换与可选图表渲染器" : "Components and optional diagrams"), topic(zh ? "资源取消、释放与错误处理" : "Resource cleanup and error handling")]),
  ]);
  return serializeMindMapEmbed({ ...createMindMapEmbed("algorithm-layers", title), document: createMindMapDocument(outline, { layout: "logical-right" }) });
}

```

这三张图都保留了原始图稿。自己的文档也可以接入同样的[流程图、思维导图与白板渲染](/guide/diagrams)。

## 看一次流式更新

点击“开始流式演示”，观察正文先出现、链接和脚注定义随后到达的过程。同一个脚注被引用两次，脚注正文里也有链接。点击“停止”可以保留当前已经收到的内容。

### reading/stream.vue

```vue
<script setup lang="ts">
import { computed, onBeforeUnmount, ref } from "vue";
import { QuireMarkdown, type QuireMarkdownProps } from "@quirekit/incremark/vue";
import { QuireButton, QuireTypography } from "@quirekit/ui/vue";
import type { ExampleLocale } from "../shared/types";
import { readingSample } from "./sample";

const props = withDefaults(defineProps<{ /** Tutorial language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const content = ref(readingSample(props.locale)), identity = ref(0);
const status = ref<NonNullable<QuireMarkdownProps["status"]>>("complete");
const labels = computed(() => props.locale === "zh-CN"
  ? { start: "开始流式演示", stop: "停止", load: "一次性加载", reading: "流式阅读预览", receiving: "正在接收", stopped: "已停止", complete: "已完成" }
  : { start: "Start streaming", stop: "Stop", load: "Load all", reading: "Streaming reading preview", receiving: "Receiving", stopped: "Stopped", complete: "Complete" });
let timer: ReturnType<typeof setTimeout> | undefined;

/** Cancels the producer independently of QuireMarkdown's parser lifecycle. */
function cancel(): void { if (timer !== undefined) clearTimeout(timer); timer = undefined; }
/** Supplies the authoritative full text after each simulated network chunk; no parser APIs are needed. */
function start(): void {
  cancel(); identity.value++; content.value = ""; status.value = "receiving";
  const source = readingSample(props.locale);
  /** Delivers the next 20 UTF-16 code units, including arbitrary syntax boundaries. */
  function append(): void {
    content.value = source.slice(0, content.value.length + 20);
    if (content.value.length < source.length) timer = setTimeout(append, 30);
    else { timer = undefined; status.value = "complete"; }
  }
  append();
}
/** Retains the exact received prefix when the user stops delivery. */
function stop(): void { cancel(); status.value = "stopped"; }
/** Replaces the message with its complete source and a new resource identity. */
function load(): void { cancel(); identity.value++; content.value = readingSample(props.locale); status.value = "complete"; }
onBeforeUnmount(cancel);
</script>
<template>
  <div class="grid gap-4 p-qk-surface">
    <div class="flex flex-wrap items-center gap-2">
      <QuireButton size="sm" variant="outline" @click="start">{{ labels.start }}</QuireButton>
      <QuireButton size="sm" variant="ghost" :disabled="status !== 'receiving'" @click="stop">{{ labels.stop }}</QuireButton>
      <QuireButton size="sm" variant="ghost" @click="load">{{ labels.load }}</QuireButton>
      <QuireTypography as="span" variant="helper">{{ status === 'receiving' ? labels.receiving : status === 'stopped' ? labels.stopped : labels.complete }}</QuireTypography>
    </div>
    <section :aria-label="labels.reading"><QuireMarkdown :content="content" :identity="String(identity)" :status="status" :locale="locale" /></section>
  </div>
</template>

```

### shared/types.ts

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

```

### reading/sample.ts

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

/** @param locale - Tutorial language. @returns One source with late definitions, repeated footnotes and an incrementally displayed code block. */
export function readingSample(locale: ExampleLocale): string {
  const chinese = locale === "zh-CN";
  return [chinese ? "## 流式 Markdown" : "## Streaming Markdown",
    chinese ? "先收到正文和[文档][api]引用，定义会在末尾到达。这里重复引用同一脚注[^note]、[^note]。"
      : "The [documentation][api] reference arrives before its definition. This footnote is used twice[^note], [^note].",
    ...Array.from({ length: 6 }, (_, index) => chinese
      ? `第 ${index + 1} 段保留 **强调**、中文与 emoji 🙂。已收到的正文继续可读，后置定义到达时前面的链接会更新。`
      : `Paragraph ${index + 1} keeps **emphasis** and emoji 🙂. Existing text stays readable while late definitions update earlier links.`),
    "```ts\nconst message = 'hello'\nconsole.log(message)\n```",
    chinese ? "[^note]: 脚注正文也使用[文档][api]链接。" : "[^note]: The footnote also links to the [documentation][api].",
    "[api]: https://example.com/docs", "[api]: https://example.com/ignored",
  ].join("\n\n");
}

````

也可以把上面的两个短例子粘到 [Playground](/playground)，查看新引擎的输出。

## 速度有什么变化

旧版的 Marked 本身很快。换用 Lezer，并不意味着所有 Markdown 都会解析得更快。当前优化主要减少两种重复工作：重新解析没有受影响的文字，以及重新生成可以复用的显示内容。

此前原 demo 的解析测试中，Lezer/MSFE 快于旧 Micromark、慢于旧 Marked。这批数据早于当前渲染缓存，不能当作当前版本的速度排名。

大型列表、表格或很长的单段落，仍可能作为一个整体重新处理。判断流式体验时，需要同时看解析、渲染和浏览器更新，不能只看解析器耗时。测量方法见[性能说明](/guide/performance)。

## 使用与迁移

QuireKit Markdown 支持 Vue 和 React，两者共用同一套解析规则。基本用法仍然是传入 Markdown文字；链接、代码块等可以换成自己的组件，也支持自定义语法和显式开启 HTML。

新包没有沿用旧 Incremark 的 API，旧插件需要适配。可以从 [Vue 快速开始](/guide/getting-started)或 [React 接入](/guide/react)开始，按[自定义语法与节点](/guide/extensions)迁移扩展。当前仍为未发布的预览版；旧 Incremark 与当前的核心、Vue、React 包均使用 MIT 许可。

## 继续了解实现

如果你准备扩展解析器或研究缓存，下面这些细节会更有用：

- **语法复用：** CodeMirror 用文本事务和 `TreeFragment` 映射变化。我们在 Lezer 层提供
  顶层块复用信息，保持原生语法和复用判断；引用处理放在 MSFE 的 parser 包装层。
- **引用判断：** 完整段落记录语法实际查询过的标签及其是否存在。只有这些查询结果未变，
  才复用相应判断；没有完整记录的范围保守检查。地址和 title 的变化另由渲染层检查。
- **渲染缓存：** 检查原生 Tree 身份、源码范围、引用目标、脚注导航和渲染配置。
  自定义节点可以用 `cacheKey(document)` 声明额外依赖；未声明时重新生成。
  具体条件与缓存限额见[性能说明](/guide/performance)。
- **框架与资源：** Vue 和 React 使用各自的组件。React 在提交后接受候选缓存，资源也在
  客户端提交后加载；切换、卸载和迟到结果按取消与释放规则处理，见 [React 接入](/guide/react)。

文中两例于 2026-10-11 验证，旧版为 `@incremark/core` 1.0.2、commit `98f6b95`。旧结果来自两引擎的 AST 与定义表，当前结果来自 React 服务端渲染，并以独立完整 GFM 解析和[CommonMark 引用规则](https://spec.commonmark.org/0.31.2/#links)核对。这两个例子说明了具体问题，版本范围不延伸到后续 Incremark。

对应源码：[旧解析会话](https://github.com/kingshuaishuai/incremark/blob/98f6b95bba58d0c9e4ba1af37a4d52c2f2fae1df/packages/core/src/parser/IncremarkParser.ts)、[旧引用识别](https://github.com/kingshuaishuai/incremark/blob/98f6b95bba58d0c9e4ba1af37a4d52c2f2fae1df/packages/core/src/extensions/marked-extensions/optimisticReferenceExtension.ts)、[旧定义合并](https://github.com/kingshuaishuai/incremark/blob/98f6b95bba58d0c9e4ba1af37a4d52c2f2fae1df/packages/core/src/parser/manager/DefinitionManager.ts)。
