# Custom syntax and nodes

Add parsing and reading support for application syntax, using task status markers.

To display `[[done]]` as “Done”, first recognize the syntax and read its status, then render that status. A parsing extension handles recognition; a reading contribution handles presentation.

## 1. Reuse a parsing extension

Install existing MSFE `Node` / `Mark` definitions through `options.extensions`.
`withMarkdownReading(extension, { render, load?, cacheKey? })` adds reading to the same source codec.
Fields keep their `addAttributes` defaults and validation; invalid values and incomplete
syntax retain source. Configure or extend the extension before wrapping its reading contribution.

## 2. Display the status

The example's `status/reading.ts` passes `Status` to `withMarkdownReading()` and returns “To do” or “Done” from `attrs.value`. Readers see a label while the saved source remains `[[todo]]` or `[[done]]`.

This example defines `[[todo]]` / `[[done]]` grammar, replaces links and enables
HTML. Stream characters to observe incomplete nodes, HTML and late reference updates together.

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

```

Read the complete example by file: `status/status.ts` defines syntax and fields, `status/types.ts` defines the status value type, `status/reading.ts` defines reading output, and `reading/custom.vue` supplies the reader configuration. A separate `reading/link.vue` replaces links, as explained in [Custom components](/en/guide/components).

## 3. Handle children and asynchronous resources

`render` receives read-only `document`, `node`, `attrs`, `locale` and range-checked
`renderInline()` / `renderBlocks()` helpers. Custom blocks can recursively render their
parsed children. `load(context, signal)` returns `{ value, release? }` or a Promise for it.
Replacing the snapshot/message or unmounting cancels old work; late results are released too.
Unchanged blocks retain resources when they satisfy the [cache dependency contract](/en/guide/performance).
Reading, rendering and loading failures emit `error` and preserve that node's source.

## More complex syntax

For more complex grammar, install native Lezer `MarkdownExtension` values, including block
and inline parsers, through `options.syntax`, then interpret fields with a source codec.
For direct read-only projection, `renderNode(context, node)` can handle named syntax nodes;
return `undefined` to use built-in dispatch. Grammar and fields belong to source extensions;
presentation belongs to reading contributions, without regex replacements on rendered HTML.
