Plugins
Philosophy
Zero coupling — a plugin declares type, version, schema, normalizer, web renderer, serializers, export adapters, commands, migrations and contract tests. If a plugin does not define an output for a format, strict export fails with known code; tolerant mode inserts an accessible placeholder.
Internal vs external
- Internal: bundled in
src/, registered at build time (e.g.equation,variable). Zero-cost. - External: loaded at runtime via
registerPlugin(pluginDef). Validated against schema, sandboxed (no prototype pollution, nojavascript:URLs).
Interface
ts
interface PluginDef {
type: string; version: number;
schema: JSONSchema;
createNode: (idGen) => BlockNode|InlineNode;
normalize?: (node) => void;
renderWeb: (node) => string; // HTML
renderPdf?: (node, ctx) => void;
renderOdt?: (node, ctx) => string;
renderDocx?: (node, ctx) => string;
commands?: CommandDef[];
formatters?: Record<string, FormatterFn>;
migrate?: (doc) => void;
}Registration
ts
import { registerNodeType, registerFormatter, registerCommand, registerPlugin } from "velo-text";
registerNodeType("my-widget", MyWidgetNode);
registerFormatter("myFormat", (v) => String(v));
registerPlugin(myPluginDef); // external
// Validation: unknown props ignored unless explicit compatibility policyExample: equation plugin (internal)
See src/core/equation/index.ts (validate, latexToHtml), src/core/model/types.ts (InlineEquationNode, EquationBlockNode), src/editor-web/view (render), src/export/* (fallback $latex$).
Security
- No HTML interpretation for variables, no
javascript:URLs, asset checks. - Plugin code runs in same isolate — external plugins are audited via
docs/threat-model.md.
See src/core/plugin/index.ts and src/public-api/index.ts.