# 自定义语法与节点

为业务语法增加解析和阅读显示，以待办状态标记为例。

如果你希望 `[[done]]` 显示成“已完成”，需要解决两件事：识别这段语法并读出状态值，再把状态显示出来。解析扩展负责前者，阅读贡献负责后者。

## 1. 复用解析扩展

已有 MSFE `Node` / `Mark` 可直接通过 `options.extensions` 安装。
`withMarkdownReading(extension, { render, load?, cacheKey? })` 给同一个 source codec 添加阅读能力：
字段继续经过 `addAttributes` 的默认值和校验，非法值或未闭合语法保留原文。先完成
extension 的 configure/extend，再包装阅读贡献。

## 2. 为状态值提供显示

示例的 `status/reading.ts` 将 `Status` 交给 `withMarkdownReading()`，根据 `attrs.value` 返回“待办”或“已完成”。读者看到的是标签，保存的仍是 `[[todo]]` 或 `[[done]]`。

下面使用 `[[todo]]` / `[[done]]` 语法，同时替换链接并开启 HTML。
点击逐字追加，可以观察未闭合节点、HTML 和后置引用的更新。

### reading/custom.vue

```vue
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from "vue";
import { QuireMarkdown } from "@quirekit/incremark/vue";
import { QuireButton } from "@quirekit/ui/vue";
import type { ExampleLocale } from "../shared/types";
import { statusOptions } from "../status/reading";
import ReadingLink from "./link.vue";

const props = withDefaults(defineProps<{ /** Tutorial language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const options = statusOptions;
const components = { a: ReadingLink };
const labels = computed(() => props.locale === "zh-CN"
  ? { stream: "逐字追加", load: "恢复全文", html: "切换 HTML", region: "自定义阅读预览" }
  : { stream: "Stream characters", load: "Restore text", html: "Toggle HTML", region: "Custom reading preview" });
const source = computed(() => [
  props.locale === "zh-CN" ? "任务 [[done]]，无效字段 [[unknown]] 保留原文。[查看文档][docs]"
    : "Task [[done]]; invalid fields [[unknown]] retain their source. [Documentation][docs]",
  '<div><strong>HTML</strong><script>window.__incremarkUnsafe = true</scr' + 'ipt></div>',
  props.locale === "zh-CN" ? '行内 <span title="safe">**强调** 与 `<T>`</span>。' : 'Inline <span title="safe">**emphasis** and `<T>`</span>.',
  "`[[done]]`", "[docs]: https://example.com/docs",
].join("\n\n"));
const content = ref(source.value), receiving = ref(false), html = ref(true), identity = ref(0);
let timer: ReturnType<typeof setTimeout> | undefined;
/** Releases the simulated producer; the reader independently owns its parsing work. */
function cancel(): void { if (timer !== undefined) clearTimeout(timer); timer = undefined; receiving.value = false; }
/** Exercises incomplete node delimiters, HTML and a late definition in the same session. */
function start(): void {
  cancel(); identity.value++; content.value = ""; receiving.value = true;
  const text = source.value;
  /** Supplies one UTF-16 unit at a time; no synthetic closing markers are supplied. */
  function append(): void {
    content.value = text.slice(0, content.value.length + 1);
    if (content.value.length < text.length) timer = setTimeout(append, 12);
    else cancel();
  }
  append();
}
/** Starts a new document identity and restores its complete authoritative source. */
function restore(): void { cancel(); identity.value++; content.value = source.value; }
watch(source, restore);
onBeforeUnmount(cancel);
</script>
<template>
  <div class="grid gap-4 p-qk-surface">
    <div class="flex flex-wrap gap-2">
      <QuireButton size="sm" variant="outline" @click="start">{{ labels.stream }}</QuireButton>
      <QuireButton size="sm" variant="ghost" @click="restore">{{ labels.load }}</QuireButton>
      <QuireButton size="sm" variant="ghost" :aria-pressed="html" @click="html = !html">{{ labels.html }}</QuireButton>
    </div>
    <section :aria-label="labels.region"><QuireMarkdown :content="content" :identity="String(identity)" :options="options"
      :components="components" :html="html" :locale="locale" :status="receiving ? 'receiving' : 'complete'" /></section>
  </div>
</template>

```

### shared/types.ts

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

```

### status/reading.ts

```ts
import { h } from "vue";
import { withMarkdownReading } from "@quirekit/incremark/vue";
import type { MarkdownOptions } from "@quirekit/incremark";
import { Status } from "./status";
import type { StatusAttributes } from "./types";

/** One validated source codec shared by the custom-node guide and playground. */
export const statusOptions: MarkdownOptions = { extensions: [withMarkdownReading<StatusAttributes>(Status, {
  // The codec/render only read local attrs and locale; no external dependency is hidden.
  /** @returns No dependencies outside this node and the reader configuration. */
  cacheKey: () => null,
  /** @param context - Validated source fields and host language. @returns Semantic trusted output. */
  render: ({ attrs, locale }) => h("mark", { "data-status-token": attrs.value },
    locale === "zh-CN" ? attrs.value === "done" ? "已完成" : "待办" : attrs.value === "done" ? "Done" : "To do"),
})] };

```

### status/status.ts

```ts
import { Node, defineSource } from "@quirekit/msfe-core";
import type { StatusAttributes, StatusValue } from "./types";

/** @param value - Source or requested attribute. @returns Whether it has an exact source spelling. */
function validStatus(value: unknown): value is StatusValue { return value === "todo" || value === "done"; }

/** Grammar, fields and commands are independent of the consumer's component framework. */
export const Status = Node.create({ name: "status",
  /** @returns The one admitted, persistable field. */
  addAttributes() { return { value: { validate: validStatus } }; },
  /** @returns Bounded parsing and exact field updates, without a second document. */
  addSource() {
    return defineSource<StatusAttributes>({ nodes: ["StatusToken"], syntax: { name: "StatusToken", open: "[[", close: "]]" }, placement: "replace",
      /** @param document - Canonical Markdown. @param node - Confirmed token. @returns Attributes or a source fallback. */
      read(document, node) {
        const value = document.slice(node.from + 2, node.to - 2);
        return validStatus(value) ? { value } : null;
      },
      /** @param _document - Canonical source. @param node - Current valid range. @param patch - Requested fields. @returns A precise patch or refusal. */
      patch(_document, node, patch) {
        return validStatus(patch.value) ? { from: node.from + 2, to: node.to - 2, insert: patch.value } : null;
      },
    });
  },
  /** @returns Typed commands sharing the editor's transaction and history. */
  addCommands() { return { insertStatus: (value: StatusValue) => ({ tr }) => validStatus(value) && tr.insertText(`[[${value}]]`) }; },
});

```

### status/types.ts

```ts
/** Source-persisted value for the custom status syntax. */
export type StatusValue = "todo" | "done";

/** Attributes decoded from the canonical Markdown token. */
export interface StatusAttributes {
  /** Current task status. */
  readonly value: StatusValue;
}

declare module "@quirekit/msfe-core" {
  /** Editor commands contributed by the same source extension. */
  interface Commands<ReturnType> {
    /** Commands installed by this consumer extension. */
    status: {
      /** @param value - Persisted status. @returns Command outcome. */
      insertStatus: (value: StatusValue) => ReturnType;
    };
  }
}

```

### reading/link.vue

```vue
<script setup lang="ts">
import type { MarkdownNodeInfo } from "@quirekit/incremark/vue";
defineProps<{
  /** Validated link destination, omitted when HTML policy rejects it. */ href?: string;
  /** Source metadata is explicitly consumed rather than forwarded to the DOM. */ node?: MarkdownNodeInfo;
}>();
</script>
<template><a :href="href" class="qk-prose-node" data-custom-link><slot /> ↗</a></template>

```

完整示例按文件查阅：`status/status.ts` 定义语法和字段；`status/types.ts` 定义状态值类型；`status/reading.ts` 定义阅读外观；`reading/custom.vue` 负责将这些配置传给阅读器。链接替换由独立的 `reading/link.vue` 负责，见[替换渲染组件](/guide/components)。

## 3. 处理子内容与异步资源

`render` 获得只读 `document`、`node`、`attrs`、`locale`，以及有范围校验的
`renderInline()` / `renderBlocks()`。自定义块可以递归呈现其已解析子节点。
`load(context, signal)` 可返回 `{ value, release? }` 或相应 Promise；更换快照、消息或卸载
会取消旧任务，迟到结果也会释放；满足[缓存依赖合同](/guide/performance)时，未改变的块保留资源。
渲染、读取、加载失败通过 `error` 事件上报并保留该节点源码。

## 更复杂的语法

复杂语法可用 `options.syntax` 安装原生 Lezer `MarkdownExtension`（包括块与行内 parser），
再由 source codec 解释字段。仅做只读投影时也可用 `renderNode(context, node)` 处理命名
语法节点，未处理时返回 `undefined` 交给内置分派。节点的 grammar 与字段归解析扩展，
外观归阅读贡献；不对最终 HTML 字符串做正则替换。
