{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.fablepool.org/v1/content-document.json", "title": "FablePool Content Document", "description": "Structured, renderer-agnostic rich content format used for problem statements, hints, solutions, lesson prose, and review-anchored text. Designed to be compiled to MDX/HTML on the frontend, to degrade gracefully on low bandwidth (every widget carries a static fallback), and to be safely sanitised (no raw HTML).", "type": "object", "required": ["version", "blocks"], "additionalProperties": false, "properties": { "version": { "description": "Content document format version.", "const": 1 }, "blocks": { "$ref": "#/$defs/blockArray" } }, "$defs": { "blockArray": { "type": "array", "maxItems": 500, "items": { "$ref": "#/$defs/block" } }, "inlineArray": { "type": "array", "maxItems": 500, "items": { "$ref": "#/$defs/inline" } }, "block": { "oneOf": [ { "$ref": "#/$defs/paragraph" }, { "$ref": "#/$defs/heading" }, { "$ref": "#/$defs/mathBlock" }, { "$ref": "#/$defs/codeBlock" }, { "$ref": "#/$defs/image" }, { "$ref": "#/$defs/list" }, { "$ref": "#/$defs/table" }, { "$ref": "#/$defs/callout" }, { "$ref": "#/$defs/quote" }, { "$ref": "#/$defs/widgetEmbed" }, { "$ref": "#/$defs/divider" } ] }, "paragraph": { "type": "object", "required": ["type", "children"], "additionalProperties": false, "properties": { "type": { "const": "paragraph" }, "children": { "$ref": "#/$defs/inlineArray" } } }, "heading": { "type": "object", "required": ["type", "level", "children"], "additionalProperties": false, "properties": { "type": { "const": "heading" }, "level": { "type": "integer", "minimum": 1, "maximum": 4 }, "children": { "$ref": "#/$defs/inlineArray" } } }, "mathBlock": { "type": "object", "required": ["type", "latex"], "additionalProperties": false, "properties": { "type": { "const": "math" }, "latex": { "type": "string", "minLength": 1, "maxLength": 5000 }, "numbered": { "type": "boolean", "default": false }, "label": { "type": "string", "maxLength": 64, "description": "Anchor label for cross-references, e.g. 'eq:pythagoras'." } } }, "codeBlock": { "type": "object", "required": ["type", "code"], "additionalProperties": false, "properties": { "type": { "const": "code" }, "language": { "type": "string", "pattern": "^[a-z0-9+#-]{1,30}$", "default": "text" }, "code": { "type": "string", "maxLength": 20000 }, "caption": { "type": "string", "maxLength": 300 }, "highlight_lines": { "type": "array", "items": { "type": "integer", "minimum": 1 }, "maxItems": 200 } } }, "image": { "type": "object", "required": ["type", "alt"], "additionalProperties": false, "oneOf": [ { "required": ["asset"] }, { "required": ["src"] } ], "properties": { "type": { "const": "image" }, "asset": { "type": "string", "format": "uuid", "description": "MediaAsset id resolved by the platform; preferred over external src." }, "src": { "type": "string", "pattern": "^(https://|/media/)", "maxLength": 2000, "description": "External or platform-relative URL. Only https or platform media paths." }, "alt": { "type": "string", "maxLength": 1000, "description": "Required alternative text. Empty string only for purely decorative images." }, "caption": { "$ref": "#/$defs/inlineArray" }, "width": { "type": "integer", "minimum": 16, "maximum": 4096 } } }, "list": { "type": "object", "required": ["type", "items"], "additionalProperties": false, "properties": { "type": { "const": "list" }, "ordered": { "type": "boolean", "default": false }, "items": { "type": "array", "minItems": 1, "maxItems": 200, "items": { "type": "object", "required": ["blocks"], "additionalProperties": false, "properties": { "blocks": { "$ref": "#/$defs/blockArray" } } } } } }, "table": { "type": "object", "required": ["type", "rows"], "additionalProperties": false, "properties": { "type": { "const": "table" }, "caption": { "type": "string", "maxLength": 300 }, "header": { "$ref": "#/$defs/tableRow" }, "rows": { "type": "array", "minItems": 1, "maxItems": 200, "items": { "$ref": "#/$defs/tableRow" } } } }, "tableRow": { "type": "array", "minItems": 1, "maxItems": 30, "items": { "$ref": "#/$defs/inlineArray" } }, "callout": { "type": "object", "required": ["type", "variant", "blocks"], "additionalProperties": false, "properties": { "type": { "const": "callout" }, "variant": { "enum": ["note", "tip", "warning", "definition", "theorem", "lemma", "proof", "example"] }, "title": { "type": "string", "maxLength": 200 }, "blocks": { "$ref": "#/$defs/blockArray" } } }, "quote": { "type": "object", "required": ["type", "blocks"], "additionalProperties": false, "properties": { "type": { "const": "quote" }, "blocks": { "$ref": "#/$defs/blockArray" }, "attribution": { "$ref": "#/$defs/inlineArray" } } }, "widgetEmbed": { "type": "object", "required": ["type", "widget", "fallback"], "additionalProperties": false, "description": "Embeds a sandboxed interactive widget. Widgets are untrusted: they run in a sandboxed iframe and never have network access. A static fallback is mandatory for accessibility and low-bandwidth rendering.", "properties": { "type": { "const": "widget" }, "widget": { "$ref": "#/$defs/slug" }, "widget_version": { "type": "string", "pattern": "^\\d+(\\.\\d+){0,2}$", "description": "Pinned or major-only widget version, e.g. '1' or '1.4.2'." }, "params": { "type": "object", "description": "Parameters validated against the widget manifest's params_schema." }, "height": { "type": "integer", "minimum": 64, "maximum": 2048 }, "fallback": { "$ref": "#/$defs/blockArray", "description": "Static content rendered when the widget cannot run." } } }, "divider": { "type": "object", "required": ["type"], "additionalProperties": false, "properties": { "type": { "const": "divider" } } }, "inline": { "oneOf": [ { "$ref": "#/$defs/textInline" }, { "$ref": "#/$defs/mathInline" }, { "$ref": "#/$defs/linkInline" }, { "$ref": "#/$defs/referenceInline" } ] }, "textInline": { "type": "object", "required": ["type", "text"], "additionalProperties": false, "properties": { "type": { "const": "text" }, "text": { "type": "string", "maxLength": 10000 }, "marks": { "type": "array", "uniqueItems": true, "items": { "enum": ["bold", "italic", "code", "underline", "strikethrough", "subscript", "superscript"] } } } }, "mathInline": { "type": "object", "required": ["type", "latex"], "additionalProperties": false, "properties": { "type": { "const": "math" }, "latex": { "type": "string", "minLength": 1, "maxLength": 2000 } } }, "linkInline": { "type": "object", "required": ["type", "href", "children"], "additionalProperties": false, "properties": { "type": { "const": "link" }, "href": { "type": "string", "pattern": "^(https?://|/|#)", "maxLength": 2000 }, "children": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "oneOf": [ { "$ref": "#/$defs/textInline" }, { "$ref": "#/$defs/mathInline" } ] } } } }, "referenceInline": { "type": "object", "required": ["type", "kind", "id"], "additionalProperties": false, "description": "Internal cross-reference to another platform entity; resolved to a link at render time and preserved across OER export/import.", "properties": { "type": { "const": "ref" }, "kind": { "enum": ["problem", "course", "lesson", "glossary", "user"] }, "id": { "type": "string", "maxLength": 64 }, "label": { "type": "string", "maxLength": 300 } } }, "slug": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$" } } }