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.
svelte-kit-vice/src/uix/soma/components/words/engine/blocks/spec.ts

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

Powered by TurnKey Linux.