import { TSchema } from '@sinclair/typebox'; import { DocxRendererId } from './renderer.js'; import '@json-to-office/shared'; /** * Component Registry - SINGLE SOURCE OF TRUTH * * This is the ONLY place where standard components are defined. * All schema generators MUST use this registry. * * Adding a new component: Add it to STANDARD_COMPONENTS_REGISTRY below. * It will automatically be included in: * - StandardComponentDefinitionSchema (components.ts) * - ComponentDefinitionSchema (components.ts) * - generateUnifiedDocumentSchema (generator.ts) * - Monaco editor autocomplete * - Build-time JSON schemas */ /** * A position under a component's `props` where full component definitions are * embedded outside the shared `children` array. * * Declaring these here is what keeps the deep-validation walk aligned with the * live document schema: the same entry's `createPropsSchema` wires a recursive * component ref into these positions, and the walk visits exactly what is * declared. `embedded-component-regions.test.ts` probes each factory with a * marker schema and fails when a declaration and the factory disagree, so a * new embedded position cannot silently become a walk blind spot (#292). */ interface EmbeddedComponentRegion { /** Path segments under `props`; `'*'` means every element of an array. */ path: readonly string[]; /** * `'component'` — the position holds one component definition; * `'component-array'` — it holds an array of them. Values of another legal * shape at the position (a string cell content, `'linkToPrevious'`) are * simply not walked. */ arity: 'component' | 'component-array'; /** * Whether the walk reports malformed entries (a non-object, a missing * `name`) at this position. True where the static props schema types the * position loosely (`Type.Any()` — the walk is its only structural checker); * false where the props schema already validates structure, so the walk only * adds per-component prop errors and must not double-report. */ reportStructure: boolean; /** * The standard components the position holds, when not every one. The * schema builder hands the factory the union of these branches (and any * plugins) instead of the full recursive ref, so the published schema says * what renders there. Omitted: any component. Every region of one component * that names a list must name the same list — the factory takes one ref. */ components?: readonly string[]; } /** * Component definition with metadata */ interface StandardComponentDefinition { /** Component name identifier (e.g., 'heading', 'text', 'toc') */ name: string; /** TypeBox schema for the component's props */ propsSchema: TSchema; /** Whether this component can contain children */ hasChildren: boolean; /** * Names of standard components allowed as direct children. * Only meaningful when hasChildren is true. * Plugin components are always allowed in addition to these. * Omit to allow the full recursive union (backward-compat). */ allowedChildren?: readonly string[]; /** * Factory that builds props with a live recursive ref (e.g., for section * header/footer, table cell content). When present and a recursive ref is * available, used instead of the static `propsSchema`. */ createPropsSchema?: (recursiveRef: TSchema) => TSchema; /** * Where `createPropsSchema` embeds component definitions inside `props`. * Drives the deep-validation walk; must match the factory (enforced by * `embedded-component-regions.test.ts`). Omit when the component embeds * none. */ embeddedComponents?: readonly EmbeddedComponentRegion[]; /** * The renderers that can draw this component. Omitted means all of them. * * Only for a component whose backend gap is real, not a slice boundary: a * component listed here disappears from the other renderers' schema branch * entirely, so a schema-driven editor stops offering it rather than * offering it and failing at render time. */ renderers?: readonly DocxRendererId[]; /** Component category for organization */ category: 'container' | 'content' | 'layout'; /** Human-readable description */ description: string; /** Special flags for this component */ special?: { /** Has $schema field (only 'docx') */ hasSchemaField?: boolean; }; } /** * Flow content: everything a section body holds. * * The containers that sit in flow — `group`, `columns`, `text-box` — are each * compiled to a table cell or a transparent run of blocks, and a cell holds * whatever a section holds. So they name this same list (`columns` minus * itself), and the schema builder below gives every container that names the * whole list one shared recursive definition instead of an inlined copy: * that is what lets a text-box hold a columns that holds a text-box. */ declare const FLOW_CHILDREN: readonly ["block", "group", "heading", "paragraph", "image", "statistic", "table", "list", "toc", "divider", "highcharts", "chart", "visual", "columns", "text-box"]; /** * SINGLE SOURCE OF TRUTH for all standard components * * This is the ONLY place where standard components are defined. * All schema generators MUST use this registry. * * IMPORTANT: When adding a new component: * 1. Add the component definition to this array * 2. Import its props schema at the top of this file * 3. That's it! The component will automatically appear everywhere. */ declare const STANDARD_COMPONENTS_REGISTRY: readonly StandardComponentDefinition[]; /** * Get a component definition by name */ declare function getStandardComponent(name: string): StandardComponentDefinition | undefined; /** * Get all standard component names */ declare function getAllStandardComponentNames(): readonly string[]; /** * Get components by category */ declare function getComponentsByCategory(category: StandardComponentDefinition['category']): readonly StandardComponentDefinition[]; /** * Get container components (components that can have children) */ declare function getContainerComponents(): readonly StandardComponentDefinition[]; /** * Get content components (components that cannot have children) */ declare function getContentComponents(): readonly StandardComponentDefinition[]; /** * Check if a component name is a standard component */ declare function isStandardComponent(name: string): boolean; /** * The components a component's embedded regions hold, when they name them — * the list its props factory is handed a union of. `undefined` when the * regions take any component (section header and footer) or there are none. * Throws on regions that disagree, since the factory takes a single ref. */ declare function embeddedComponentNames(component: StandardComponentDefinition): readonly string[] | undefined; /** * Generate TypeBox schema object for a component. * * @param component - Component definition from the registry * @param childrenType - Schema for children items. For containers this should be * a narrowed union of allowed children; for leaves omit it. * @returns TypeBox schema object for the component */ declare function createComponentSchemaObject(component: StandardComponentDefinition, childrenType?: TSchema, selfRef?: TSchema, profile?: { renderer: DocxRendererId; requireDiscriminator: boolean; }): TSchema; /** * Generate an array of TypeBox schemas for all standard components. * Uses a flat recursive ref for all containers (legacy behavior). * * @param recursiveRef - Optional recursive reference for children * @returns Array of TypeBox schemas for all components in the registry */ declare function createAllComponentSchemas(recursiveRef?: TSchema): readonly TSchema[]; /** * Build all standard component schemas with per-container narrowed children. * * Flow content is one shared recursive definition (`flow`): every container * whose `allowedChildren` is exactly `FLOW_CHILDREN` — `section`, `group`, * `text-box` — takes it as its children type, which is what lets the containers * in flow nest each other without a cycle in the schema. A container that * narrows the list (`docx` to sections, `columns` to flow minus itself) gets an * inlined union of the branches it names, resolved in dependency order; only * those can still form a cycle, and one is a registry mistake. * * Plugin schemas join the flow definition and every inlined union, so a plugin * is allowed wherever a standard component is. * * @param selfRef - The Type.Recursive self-reference: section header and * footer, plugin children — the positions that take any component at all. * Table cell content takes the narrower union its regions name. * @param pluginSchemas - Plugin component schemas (always allowed in all containers) * @returns every standard branch (`schemas`, by name in `byName`), the flow * definition, and `roots` — the branches outside flow (`docx`, `section`). * A union a TypeBox check can walk on its own is `[...roots, flow]`: the flow * branches reference the definition by `$id`, which only resolves through it. */ declare function createAllComponentSchemasNarrowed(selfRef: TSchema, pluginSchemas?: TSchema[], profile?: { renderer: DocxRendererId; requireDiscriminator: boolean; }): { schemas: TSchema[]; byName: Map; flow: TSchema; roots: TSchema[]; }; export { type EmbeddedComponentRegion, FLOW_CHILDREN, STANDARD_COMPONENTS_REGISTRY, type StandardComponentDefinition, createAllComponentSchemas, createAllComponentSchemasNarrowed, createComponentSchemaObject, embeddedComponentNames, getAllStandardComponentNames, getComponentsByCategory, getContainerComponents, getContentComponents, getStandardComponent, isStandardComponent };