You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
226 lines
10 KiB
226 lines
10 KiB
/**
|
|
* Block registry — spec contract.
|
|
*
|
|
* A `WordsBlockSpec` is the single, self-contained definition of one
|
|
* block type. The built-in blocks each ship a spec; third-party blocks
|
|
* register their own. Every surface that needs per-type behaviour — the
|
|
* renderer, the validator, the serializers, the factories and the insert
|
|
* menus — derives from the registry instead of switching over a closed
|
|
* union.
|
|
*
|
|
* Behavioural methods (`render`, then `validate` / `create` /
|
|
* `toHtml` / `toMarkdown` in later phases) and their context types are
|
|
* added to this interface as each engine concern is migrated onto the
|
|
* registry. Each method is optional while its concern is mid-migration so
|
|
* the test suite stays green throughout; the engine falls back to its
|
|
* legacy switch for any block whose spec hasn't yet implemented the
|
|
* method.
|
|
*
|
|
* `render` takes `block` as the base `WordsBlock` and the spec narrows it
|
|
* internally (`block as ParagraphBlock`). This is sound because the
|
|
* registry only ever dispatches a block to the spec whose `type` matches —
|
|
* the same controlled cast ProseMirror node specs use.
|
|
*/
|
|
|
|
import type { WordsPath } from '../path';
|
|
import type { Block, WordsBlock, WordsInline, WordsShadow, WordsText } from '../types';
|
|
import type {
|
|
WordsRenderAttrs,
|
|
WordsRenderDecorations,
|
|
WordsRenderElement,
|
|
WordsRenderNode
|
|
} from '../render';
|
|
import type { ValidationErrorCode } from '../validate';
|
|
|
|
/** Coarse grouping used to organise the insert menus. */
|
|
export type WordsBlockGroup = 'text' | 'list' | 'media' | 'structure';
|
|
|
|
/**
|
|
* One entry in the insert menus (left-gutter "Insert below" + the slash
|
|
* menu). A single spec may contribute several entries — e.g. the heading
|
|
* spec exposes Heading 1/2/3, the list spec exposes bulleted/numbered/
|
|
* check — each with its own factory.
|
|
*/
|
|
export interface WordsBlockMenuEntry {
|
|
/** Stable id (e.g. `'heading-1'`, `'unordered-list'`). Unique per menu. */
|
|
readonly id: string;
|
|
/** Label shown in the menu. */
|
|
readonly label: string;
|
|
/** Optional one-line description (slash menu shows it). */
|
|
readonly description?: string;
|
|
/** Search keywords for the slash menu filter. */
|
|
readonly keywords?: readonly string[];
|
|
/** Grouping for menu organisation. */
|
|
readonly group: WordsBlockGroup;
|
|
/**
|
|
* Whether this entry can be inserted directly. `false` marks an entry
|
|
* that needs a follow-up flow before it is valid (e.g. image → URL /
|
|
* upload); such entries are surfaced but routed through their flow.
|
|
* @default true
|
|
*/
|
|
readonly insertable?: boolean;
|
|
/**
|
|
* Produces the block to insert. Returns a raw block literal — the
|
|
* engine normalises it (assigns ids, fills defaults) on insertion.
|
|
*/
|
|
readonly create: () => Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* The definition of a block type. Built-ins register the canonical specs;
|
|
* apps register more. Behavioural methods are layered on in later phases.
|
|
*/
|
|
export interface WordsBlockSpec {
|
|
/** Discriminant — matches `block.type` and the `WordsBlockMap` key. */
|
|
readonly type: string;
|
|
/** Coarse grouping (menus, defaults). */
|
|
readonly group: WordsBlockGroup;
|
|
/**
|
|
* Core blocks are always present and cannot be unregistered (paragraph
|
|
* is the fallback block). Everything else is optional / plugin.
|
|
* @default false
|
|
*/
|
|
readonly core?: boolean;
|
|
/** Insert-menu entries this spec contributes (none → not insertable). */
|
|
readonly menu?: readonly WordsBlockMenuEntry[];
|
|
/**
|
|
* Renders the block to an abstract render node. Receives the base
|
|
* `WordsBlock` (narrow it to the spec's own block type) and a context
|
|
* exposing the shared render helpers + recursion. Optional while the
|
|
* render concern is mid-migration (R2); blocks without it fall back to
|
|
* the engine's legacy switch.
|
|
*/
|
|
render?(block: WordsBlock, ctx: WordsBlockRenderContext): WordsRenderElement;
|
|
/**
|
|
* Validates the block's TYPE-SPECIFIC shape (children, intrinsic enums,
|
|
* required fields). The common concerns — block-type registration, `id`
|
|
* uniqueness and the base style props — are checked by the validator
|
|
* before dispatch, so a spec only asserts what's unique to it. The block
|
|
* is the RAW, untrusted shape (`Record<string, unknown>`) — not yet a
|
|
* valid `WordsBlock` — so fields are read defensively. Optional while the
|
|
* validate concern is mid-migration (R3); blocks without it fall back to
|
|
* the validator's legacy switch.
|
|
*/
|
|
validate?(block: Record<string, unknown>, ctx: WordsBlockValidateContext): void;
|
|
/**
|
|
* Serialises the block to portable, editor-markup-free HTML (the
|
|
* `exportContent('html')` surface — NOT the editor display). Optional
|
|
* while the serialize concern is mid-migration (R5); blocks without it
|
|
* fall back to the serializer's legacy switch.
|
|
*/
|
|
toHtml?(block: WordsBlock, ctx: WordsHtmlSerializeContext): string;
|
|
/**
|
|
* Serialises the block to (lossy) Markdown — the `exportContent('md')`
|
|
* surface. The context carries the current nesting `depth` for indented
|
|
* lists / callouts. Optional while the serialize concern is
|
|
* mid-migration (R5); blocks without it fall back to the legacy switch.
|
|
*/
|
|
toMarkdown?(block: WordsBlock, ctx: WordsMarkdownSerializeContext): string;
|
|
}
|
|
|
|
/**
|
|
* Helpers + recursion handed to `spec.render`. The engine builds one per
|
|
* block (scoped to that block's `path` + the active decorations) so specs
|
|
* stay pure — they never import the renderer's internals, which avoids an
|
|
* import cycle.
|
|
*/
|
|
export interface WordsBlockRenderContext {
|
|
/** This block's path in the document tree. */
|
|
readonly path: WordsPath;
|
|
/** Transient editor decorations (find matches, selected block, …). */
|
|
readonly decorations?: WordsRenderDecorations;
|
|
/** Render a run of inline content (text + links, with marks). */
|
|
renderInlines(inlines: readonly WordsInline[], parentPath: WordsPath): readonly WordsRenderNode[];
|
|
/** Render a single text inline, optionally tokenised for a code language. */
|
|
renderText(text: WordsText, path: WordsPath, codeLanguage?: string): WordsRenderElement;
|
|
/** Render a child block (recursion — e.g. callout contents). */
|
|
renderBlock(block: WordsBlock, path: WordsPath): WordsRenderElement;
|
|
/** Compose the common block attrs (data-words-node / path / block / id /
|
|
* style). Accepts any `Block`-shaped node — `blockType` is passed
|
|
* explicitly, so plugin blocks (not in the built-in union) work too. */
|
|
composeBlockAttrs(
|
|
block: Block & { shadow?: WordsShadow },
|
|
blockType: string,
|
|
path: WordsPath,
|
|
extra: Record<string, string | undefined>
|
|
): WordsRenderAttrs;
|
|
/** Translate common style props into a CSS declarations object. Accepts
|
|
* any `Block`-shaped node (blocks, table rows, table cells). */
|
|
blockStyle(block: Block & { shadow?: WordsShadow }): Readonly<Record<string, string>>;
|
|
/** Serialise a CSS declarations object to an inline `style` string. */
|
|
stringifyStyle(style: Readonly<Record<string, string>>): string | undefined;
|
|
/** Encode a path into its `data-words-path` attribute value. */
|
|
encodeWordsPath(path: WordsPath): string;
|
|
}
|
|
|
|
/**
|
|
* Helpers handed to `spec.validate`. Paths are JSON-pointer strings
|
|
* (`/children/2/...`). The validator builds one per block, scoped to that
|
|
* block's `path`, closing over the shared error list + id bookkeeping.
|
|
*/
|
|
export interface WordsBlockValidateContext {
|
|
/** JSON-pointer path to this block. */
|
|
readonly path: string;
|
|
/** Record a validation error. */
|
|
error(path: string, code: ValidationErrorCode, message: string): void;
|
|
/** Validate an optional, document-unique `id`. */
|
|
validateId(id: unknown, path: string): void;
|
|
/** Validate the common base style props (align / margin / … / shadow). */
|
|
validateStyle(node: Record<string, unknown>, path: string): void;
|
|
/** Validate a run of inline children (text + links). */
|
|
validateInlineChildren(children: unknown, path: string): void;
|
|
/** Validate children restricted to plain text inlines (code blocks). */
|
|
validateTextOnlyChildren(children: unknown, path: string): void;
|
|
/** Validate an optional (or required) enum value against an allow-list. */
|
|
validateOptionalEnum(
|
|
value: unknown,
|
|
allowed: readonly string[],
|
|
path: string,
|
|
required?: boolean
|
|
): void;
|
|
/** Validate an optional non-negative finite number. */
|
|
validateOptionalNumber(value: unknown, path: string): void;
|
|
/** Validate a nested block (recursion — e.g. callout contents). */
|
|
validateBlock(block: unknown, path: string): void;
|
|
/** Whether a string parses as an absolute or relative URL. */
|
|
isParseableUrl(href: string): boolean;
|
|
/** Whether a value is a plain (non-array) object. */
|
|
isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* Helpers handed to `spec.toHtml`. The serializer builds one (it's
|
|
* stateless) so specs emit portable HTML without importing the
|
|
* serializer's internals.
|
|
*/
|
|
export interface WordsHtmlSerializeContext {
|
|
/** Serialise a run of inline content (text + links + marks) to HTML. */
|
|
inlinesToHtml(inlines: readonly WordsInline[]): string;
|
|
/** Serialise a nested block (recursion — e.g. callout contents). */
|
|
blockToHtml(block: WordsBlock): string;
|
|
/** The block's ` style="…"` attribute (or empty string). */
|
|
styleAttr(block: Block & { shadow?: WordsShadow }): string;
|
|
/** Translate common style props into a CSS declarations object. */
|
|
blockStyle(block: Block & { shadow?: WordsShadow }): Readonly<Record<string, string>>;
|
|
/** Serialise a CSS declarations object to an inline `style` string. */
|
|
stringifyStyle(style: Readonly<Record<string, string>>): string | undefined;
|
|
/** Escape text content (`&`, `<`, `>`). */
|
|
escapeText(text: string): string;
|
|
/** Escape an attribute value (`&`, `<`, `>`, `"`). */
|
|
escapeAttr(value: string): string;
|
|
}
|
|
|
|
/**
|
|
* Helpers handed to `spec.toMarkdown`. Carries the current nesting
|
|
* `depth` (for indented list items / callout bodies) + inline + recursion
|
|
* serialisers.
|
|
*/
|
|
export interface WordsMarkdownSerializeContext {
|
|
/** Current nesting depth (0 at the document root). */
|
|
readonly depth: number;
|
|
/** Serialise a run of inline content (text + links + marks) to Markdown. */
|
|
inlinesToMd(inlines: readonly WordsInline[]): string;
|
|
/** Serialise a nested block at a given depth (recursion — e.g. callout). */
|
|
blockToMd(block: WordsBlock, depth: number): string;
|
|
}
|