Skip to content
QuireKit MSFE
Developer docs
v0.0.0 · Experimental

Custom nodes

Follow a clickable status token through syntax, fields, commands, and component integration.

Try a field update

Click Pending to change it to Done; the reading preview updates below. Read the Markdown to confirm that only the value inside [[todo]] changed. Undo and redo update both views. Locked mode prevents field changes.

Preparing the editor…

Define syntax and fields

status.ts uses Node.create() to declare semantics. addAttributes() admits the value field and validates its values. The literal-delimiter syntax in defineSource() is suitable for single-line, non-nested tokens.

read(document, node) reads only the confirmed range. Invalid values return null and keep the source visible. For complex or nested syntax, supply parsing rules through addMarkdownExtensions().

Write precise changes

patch(document, node, patch) returns source edits. This example preserves the surrounding brackets and replaces only the inner value. The editor handles field validation, read-only access, stale targets, and undo centrally.

@quirekit/msfe-core

Parser-neutral semantic codec; rendering is optional and never owns persistence.

SourceDefinitionts

Add commands

addCommands() defines insertStatus(). Module augmentation in types.ts provides type information for editor.commands, editor.can(), and editor.chain(). Commands use the supplied tr instead of dispatching a separate transaction.

ts

Replace the component

The application uses .extend({ addNodeView() { ... } }) to install VueNodeViewRenderer(). status-view.vue receives nodeView.attrs and updateAttributes(), so it does not calculate source offsets itself.

The component tree must include VueNodeViewHost. When moving to React, retain the same syntax extension and use the corresponding renderer and host.

Add static reading

MdProse accepts third-party reading contributions through withMdReading(extension, definition). Finish configure() / extend() first, then wrap and install the extension in the same editor. The public entry is @quirekit/md-editor/vue; no changes to built-in syntax branches are needed.

ts

render(context, resource) receives attrs from the installed extension's original codec, sharing its defaults and validation. Context also provides name, node, document and locale, without editing actions. renderInline(from?, to?) and renderBlocks(omitted?) delegate children to the shared reader. Inline bounds must stay inside the node without cutting a child syntax node; invalid bounds trigger local source fallback.

Pass the original editor's immutable snapshot to MdProse.state. The example uses const state = useMsfeState(editor) with automatic subscription cleanup; see Vue state subscriptions. No Markdown serialization runs on each input. Selection and focus changes do not repeat decoding or loading. Reading depends on source, syntax, installed contributions, presentation configuration and locale; codecs should not derive static content from selection or mutable external state. Document changes may rebuild the reading snapshot conservatively; this API does not promise incremental node updates.

Reading resources and fallback

For IO, declare load(context, signal) returning ResourceLease<Resource> ({ value, release? }) or a Promise of it. render runs when the resource is ready. Snapshot replacement and unmount abort the signal. Late resources still call release() even if the loader ignores cancellation. Reading and editing NodeViews own separate lifetimes; the adapter does not automatically reuse addSource().load.

Pending or failed loads retain exact source, including during server rendering. SSR never starts loading; synchronous renderers can render on the server. Vue children inherit host providers, and client-side child errors stay local to the node. Renderers are trusted host code; raw Markdown is never executed as HTML.

Listen with <MdProse @error="reportReadingError" /> for MdReadingError, including owner, read / load / render / dispose phase, original error and UTF-16 bounds in the captured snapshot. These diagnostic bounds are not safe asynchronous write targets. Without an error listener, the Vue entry preserves visible source; hosts can add logging. Disposal failures are reported without replacing successfully rendered new content.

ScenarioEditing / sourceStatic reading
Valid custom fieldsOriginal patch makes precise, undoable changesSame codec defaults and validation
Invalid fields, unknown or unclosed syntaxOriginal text retainedBuilt-in rendering or source when no contribution claims it
Tokens in inline or fenced codeRemain code textNot parsed again as custom nodes
Multiple semantic candidates for one syntaxExisting installation orderFirst codec returning non-null owns the node
render returns undefined / nullSource unchangedUndefined retains exact source; null explicitly produces no content
Pending/rejected loads or render errorsSource unchanged, surrounding blocks intactOnly the failing node falls back; errors can be observed
Locked modeField and history writes rejectedSame content remains readable
Missing source codec / duplicate reading ownerInstallation explicitly rejectedNo invalid reading contribution
React or Word/PDF/OFD exportFramework-neutral semantics retainedThis adapter serves Vue reading; exports need separate conversion contributions

Lifecycle constraints

A node component may unmount when it leaves the viewport, reveals source, switches documents, or is destroyed. Write persistent fields back to source and choose suitable lifetimes for transient UI state; do not assume the component stays mounted forever.

Read, patch and validation contracts

addAttributes() admits writable fields. Defaults and validation do not replace source encoding. read() returning null means the confirmed syntax is not a supported semantic form; preserve it rather than normalizing it.

A patch receives partial fields, not necessarily every attribute. Return one edit, disjoint edits or null. Replace only intended field ranges and preserve spacing, quote style and delimiters. Encode or reject values containing delimiter syntax instead of interpolating them blindly.

Unknown or invalid fields are rejected. For several source spellings, define reversible writes first. Unsupported writable forms may remain readable; do not claim editing merely because rendering works.

Source fingerprints

source(document, node) may define a smaller exact fingerprint, but it must include every patchable field. A code-language control can fingerprint the header; a body editor cannot use that same narrow fingerprint.

Fingerprints prevent stale components from editing different content. They are not durable business IDs. Neither are viewport view IDs. Persist required identifiers as explicitly validated source fields.

Asynchronous components

defineSource<Attrs, Resource>() can add load(attrs, signal). The view receives loading/ready/error status and an optional resource. Keep source writes behind guarded actions such as updateAttributes(), deleteNode() and revealSource(). Read Resources before adding uploads or server queries.

Choosing block behavior

This example's literal syntax is a single-line, non-nested token. The helper is unsuitable for arbitrary multiline syntax or nested containers. Use native Markdown body ownership for containers, block layers for diagrams, and explicit grammar for parser plugins.

Completion criteria

Cover valid, invalid, unclosed and escaped forms; exact field writes; undo; readonly rejection; obsolete resource cleanup; and actions after unmount. Check component baseline, native selection, focus and both themes at narrow widths.

MSFE · QuireKitYour Markdown source stays yours.