Skip to content

Word, Notion and presentation schemas ​

These profiles test larger documents against the same document engine. Each profile supplies an external schema and a complete sample model. They use the existing editing API, collaboration rules and editor converters.

Use them as a starting point for application design. A profile describes stored content and editing constraints. It does not provide DOCX/PPTX import, page layout, a database engine or a slide canvas.

Profile coverage ​

ProfileDocument structureDeclared properties and constraints
Word-style documentSections, headings, paragraphs, lists, quotes, tables, figures with captions, page breaks, headers and footersPage size and nested margin fields; one required header and footer per section; paragraph alignment, spacing and indentation
Notion-style pageRecursive blocks, tasks, toggles, callouts, columns, tables, a database-shaped grid, embeds and equationsTask state, collapse state, callout tone, column width, two to four columns, independent database view fields and cell properties
PresentationOrdered slides, titles, text boxes, images, shapes, tables, charts, nested object groups and speaker notesRequired slide title and notes; canvas properties; independent position, size, rotation and stacking fields on framed objects

Shared content includes bold, italic, underline, strike, code, value formatting, links, mentions, inline images and nested cell content. The samples include Unicode text, rich inline content, lists, a 2×2 table and product-specific structures.

The source files are fixtures/product-schemas.ts and fixtures/product-documents.ts. They are repository fixtures, not additional library packages or public package exports. The default demo continues to use DEMO_SCHEMA.

Load and edit a sample ​

Run this example from the repository root with Node 22.18 or later. The fixture imports below are local repository files.

ts
import {Doc} from "@barocss/doc-sync";
import {WORD_SCHEMA} from "./fixtures/product-schemas.ts";
import {PRODUCT_MODELS} from "./fixtures/product-documents.ts";

const doc = new Doc("alice", WORD_SCHEMA);
doc.edit([
  {op: "delete", at: [0], count: doc.toJSON().children!.length},
  {op: "insert", at: [0], content: PRODUCT_MODELS.word.children!},
]);

// Update one margin field. Other margin fields retain their values.
doc.edit([{op: "update", at: [1], attrs: {page: {margin: {top: 40}}}}]);

// Move the appendix section before the first section.
doc.edit([{op: "move", from: [2], at: [1]}]);

const restored = Doc.fromSnapshot("bob", doc.snapshot(), WORD_SCHEMA);
console.log(restored.toJSON());

Use NOTION_SCHEMA with PRODUCT_MODELS.notion, or PRESENTATION_SCHEMA with PRODUCT_MODELS.presentation, to load the other profiles. Paths address the current model. Resolve a path again after a structural change. See model operations for command semantics and document rules for field and required-child behavior.

Native editor demo ​

Run npm run dev:products from the repository root after installing the comparison harness dependencies in apps/demo/products/README.md. The default address is http://127.0.0.1:5413/. Choose ?profile=word, ?profile=notion or ?profile=presentation. Add size=large to open a fresh document with 100,000 generated UTF-16 text units, in addition to the rich sample.

Each page has three editable native views: Slate, Lexical and ProseMirror. They share a document through HTTP/SSE and use participant-specific undo/redo. The application supplies schema-aware elements and safe labels for media and atoms. Each profile has a separate backend with temporary storage. Existing documents are retained when reloaded.

Chrome checks cover 90 common editing cases and 39 schema-context/structure cases. They include Unicode input/deletion, forward/backward selection through remote insertion, native formatting, undo/redo that preserves foreign edits, nested split/join, headers/footers/captions, columns/database cells/tasks, notes/object groups/titles, and merged hidden content. The full native, client and server models are checked. Nine more large-document checks verify that an unchanged distant DOM paragraph remains attached after remote input in every profile/editor combination. Twelve UI and request-guard checks bring the native demo total to 150 assertions. This extends the headless unit coverage below.

A repeated before/after comparison contains 2,160 measured keyboard inputs after 360 excluded warmups, across sample and 100k documents, all three profiles and all three input editors. With unchanged native root blocks retained, large-document input-to-peer rendering-opportunity p95 changes from 305.6–610.1 ms to 70.1–176.5 ms. Sample results remain about 47–49 ms. This is a localhost development run with three editors in one Chrome page; the endpoint is DOM match plus a second animation frame, not physical screen timing. Read the remote editor performance report for per-source figures, repeated runs, raw records, remaining full reads and limits. The earlier native baseline remains available.

The views expose editable structure. Presentation objects use an outline, toggles remain expanded for editing, and media use labeled placeholders. Product layout, collapse/query UI and file fidelity remain application work.

Complete schemas and samples ​

The code below is included directly from the tested files at build time.

Complete schema definitions
ts
import type {AttrSpec, NodeSpec, Schema} from "@barocss/doc-sync";

const textStyle = (): AttrSpec => ({default: {align: "left", spacing: 1, indent: 0}, fields: {
    align: {default: "left", values: ["left", "center", "right", "justify"]},
    spacing: {default: 1, min: 0.5, max: 4}, indent: {default: 0, min: 0, max: 12},
}});
const frame = (): AttrSpec => ({default: {x: 0, y: 0, width: 320, height: 180, rotation: 0, z: 0}, fields: {
    x: {default: 0, min: -100000, max: 100000}, y: {default: 0, min: -100000, max: 100000},
    width: {default: 320, min: 1, max: 100000}, height: {default: 180, min: 1, max: 100000},
    rotation: {default: 0, min: -360, max: 360}, z: {default: 0, min: -10000, max: 10000},
}});
const base = (): Record<string, NodeSpec> => ({
    paragraph: {shape: "text", children: ["link", "mention", "inline-image"], attrs: {style: textStyle()}},
    heading1: {shape: "text", next: "paragraph"}, heading2: {shape: "text", next: "paragraph"},
    quote: {shape: "text"}, "code-block": {shape: "text", attrs: {language: {default: "text"}}},
    link: {shape: "span", mark: "link", attr: "url"},
    mention: {shape: "inline", attrs: {kind: {default: "user", values: ["user", "page", "date"]}, id: {default: ""}, label: {default: ""}}},
    "inline-image": {shape: "inline", attrs: {src: {default: ""}, alt: {default: ""}, width: {default: 160, min: 1, max: 100000}, height: {default: 90, min: 1, max: 100000}}},
    list: {shape: "group", children: ["list-item"], by: ["listType"]},
    "list-item": {shape: "text", attrs: {listType: {default: "bullet", values: ["bullet", "numbered"], inherit: true}, level: {default: 0, min: 0, max: 12, inherit: true}}},
    figure: {shape: "flow", children: ["image-block", "caption"], default: "image-block", childRules: {"image-block": {min: 1, max: 1}}, attrs: {alt: {default: ""}}},
    "image-block": {shape: "text", text: false, children: ["inline-image"]}, caption: {shape: "text"},
    table: {shape: "grid", children: ["row"], attrs: {header: {default: false, values: [false, true]}}},
    row: {shape: "row", children: ["cell"]},
    cell: {shape: "cell", children: ["cell-body"], attrs: {
        colspan: {default: 1, min: 1, computed: true}, rowspan: {default: 1, min: 1, computed: true}, spanned: {default: false, computed: true},
        backgroundColor: {default: "", pattern: "^(#[0-9a-fA-F]{6})?$"},
        properties: {default: {label: "", complete: false}, fields: {label: {default: ""}, complete: {default: false, values: [false, true]}}},
    }},
    "cell-body": {shape: "flow", children: ["paragraph", "list", "figure"], default: "paragraph"},
    divider: {shape: "atom"},
});
const marks = ["bold", "italic", "underline", "strike", "code"];
const values = () => ({link: {default: "", pattern: "^(https?://\\S+)?$"}, color: {default: "", inherit: true}, fontSize: {default: 12, min: 8, max: 128, inherit: true}});

/** Representative editing profiles, not import/export schemas for proprietary formats. */
export const WORD_SCHEMA: Schema = {
    root: "word-document", marks, markValues: values(), nodes: {
        ...base(),
        "word-document": {shape: "flow", children: ["paragraph", "section"], default: "paragraph"},
        section: {shape: "flow", children: ["paragraph", "heading1", "heading2", "quote", "code-block", "list", "table", "figure", "page-break", "header", "footer"], default: "paragraph", childRules: {header: {min: 1, max: 1}, footer: {min: 1, max: 1}}, attrs: {
            page: {default: {width: 794, height: 1123, margin: {top: 72, right: 72, bottom: 72, left: 72}}, fields: {
                width: {default: 794, min: 100, max: 10000}, height: {default: 1123, min: 100, max: 10000},
                margin: {default: {top: 72, right: 72, bottom: 72, left: 72}, fields: {top: {default: 72, min: 0}, right: {default: 72, min: 0}, bottom: {default: 72, min: 0}, left: {default: 72, min: 0}}},
            }},
        }},
        header: {shape: "flow", children: ["paragraph"], default: "paragraph"},
        footer: {shape: "flow", children: ["paragraph"], default: "paragraph"}, "page-break": {shape: "atom"},
    },
};

const notionBlocks = ["paragraph", "heading1", "heading2", "quote", "code-block", "task", "bullet-block", "toggle", "callout", "columns", "table", "database", "figure", "embed", "equation", "divider"];
export const NOTION_SCHEMA: Schema = {
    root: "notion-page", marks, markValues: values(), nodes: {
        ...base(),
        "notion-page": {shape: "flow", children: notionBlocks, default: "paragraph"},
        task: {shape: "text", attrs: {checked: {default: false, values: [false, true]}}},
        "bullet-block": {shape: "flow", children: ["paragraph", "bullet-block", "task"], default: "paragraph", attrs: {ordered: {default: false, values: [false, true]}}},
        toggle: {shape: "flow", children: notionBlocks, default: "paragraph", attrs: {collapsed: {default: false, values: [false, true]}}},
        callout: {shape: "flow", children: notionBlocks, default: "paragraph", attrs: {tone: {default: "info", values: ["info", "warning", "success"]}, icon: {default: "info"}}},
        columns: {shape: "flow", children: ["paragraph", "column"], default: "paragraph", childRules: {column: {min: 2, max: 4}}},
        column: {shape: "flow", children: notionBlocks, default: "paragraph", attrs: {width: {default: 1, min: 0.1, max: 12}}},
        database: {shape: "grid", children: ["row"], attrs: {header: {default: false, values: [false, true]}, view: {default: {kind: "table", groupBy: ""}, fields: {kind: {default: "table", values: ["table", "board"]}, groupBy: {default: ""}}}}},
        embed: {shape: "atom", attrs: {src: {default: ""}, kind: {default: "bookmark", values: ["bookmark", "file", "video", "audio"]}}},
        equation: {shape: "atom", attrs: {latex: {default: ""}}},
    },
};

const objects = ["paragraph", "text-box", "image", "shape", "table", "chart", "object-group"];
export const PRESENTATION_SCHEMA: Schema = {
    root: "presentation", marks, markValues: values(), nodes: {
        ...base(),
        presentation: {shape: "flow", children: ["paragraph", "slide"], default: "paragraph"},
        slide: {shape: "flow", children: ["title", ...objects, "notes"], default: "title", childRules: {title: {min: 1, max: 1}, notes: {min: 1, max: 1}}, attrs: {
            layout: {default: "blank", values: ["blank", "title", "two-column"]},
            canvas: {default: {width: 1280, height: 720, background: "#ffffff"}, fields: {width: {default: 1280, min: 100, max: 10000}, height: {default: 720, min: 100, max: 10000}, background: {default: "#ffffff"}}},
        }},
        title: {shape: "text", next: "paragraph"},
        "text-box": {shape: "flow", children: ["paragraph", "list"], default: "paragraph", attrs: {frame: frame()}},
        image: {shape: "atom", attrs: {src: {default: ""}, alt: {default: ""}, frame: frame()}},
        shape: {shape: "atom", attrs: {kind: {default: "rectangle", values: ["rectangle", "ellipse", "line"]}, frame: frame(), fill: {default: "#ffffff"}}},
        chart: {shape: "atom", attrs: {kind: {default: "bar", values: ["bar", "line", "pie"]}, data: {default: []}, frame: frame()}},
        "object-group": {shape: "flow", children: objects, default: "paragraph", attrs: {frame: frame()}},
        notes: {shape: "flow", children: ["paragraph", "list"], default: "paragraph"},
    },
};
export const PRODUCT_SCHEMAS = {word: WORD_SCHEMA, notion: NOTION_SCHEMA, presentation: PRESENTATION_SCHEMA};
Complete sample models
ts
import {canonical, type NodeJSON} from "@barocss/doc-sync";
import {PRODUCT_SCHEMAS} from "./product-schemas.ts";

const p = (text: string, type = "paragraph"): NodeJSON => ({type, runs: [{text, marks: {}}]});
const cell = (text: string): NodeJSON => ({type: "cell", children: [{type: "cell-body", children: [p(text)]}]});
const table = (type = "table"): NodeJSON => ({type, children: [
    {type: "row", children: [cell("Name"), cell("Status")]},
    {type: "row", children: [cell("Draft"), cell("Review")]},
]});
const list = (): NodeJSON => ({type: "list", attrs: {listType: "numbered"}, children: [
    {...p("Research", "list-item"), attrs: {listType: "numbered", level: 0}},
    {...p("Implementation", "list-item"), attrs: {listType: "numbered", level: 1}},
]});
const rich = (): NodeJSON => ({type: "paragraph", runs: [
    {text: "Shared 😀 ", marks: {bold: true, color: "#123456", fontSize: 18}},
    {type: "link", attrs: {url: "https://example.test/design"}, runs: [{text: "design", marks: {italic: true}}]},
    {type: "mention", attrs: {kind: "user", id: "reviewer", label: "Reviewer"}},
]});
const figure = (): NodeJSON => ({type: "figure", attrs: {alt: "Design preview"}, children: [
    {type: "image-block", runs: [{type: "inline-image", attrs: {src: "https://example.test/preview.png", alt: "Preview", width: 640, height: 360}}]},
    p("Figure 1. Shared design", "caption"),
]});
const word: NodeJSON = {type: "word-document", children: [p("Project brief"), {type: "section", children: [
    p("Opening text"), p("Scope", "heading1"), rich(), p("Notes", "heading2"), p("A quoted requirement", "quote"),
    list(), table(), figure(), {type: "page-break"}, p("After the break"),
    {type: "header", children: [p("Project header")]}, {type: "footer", children: [p("Page footer")]},
]}, {type: "section", attrs: {page: {width: 1123, height: 794, margin: {top: 36, right: 36, bottom: 36, left: 36}}}, children: [
    p("Landscape appendix"), {type: "header", children: [p("Appendix header")]}, {type: "footer", children: [p("Appendix footer")]},
]}]};
const notion: NodeJSON = {type: "notion-page", children: [p("Team workspace"), p("Plan", "heading1"), rich(),
    {...p("Review this draft", "task"), attrs: {checked: false}},
    {type: "bullet-block", children: [p("Parent item"), {type: "bullet-block", children: [p("Nested item")]}]},
    {type: "toggle", attrs: {collapsed: true}, children: [p("Details"), {type: "callout", attrs: {tone: "warning", icon: "warning"}, children: [p("Check dependencies"), p("Implementation note", "quote")]}]},
    {type: "columns", children: [p("Work streams"), {type: "column", children: [p("Design")]}, {type: "column", children: [p("Engineering")]}]},
    table(), table("database"), {...p("const ready = true;", "code-block"), attrs: {language: "typescript"}},
    figure(), {type: "embed", attrs: {src: "https://example.test/spec", kind: "bookmark"}}, {type: "equation", attrs: {latex: "E=mc^2"}}, {type: "divider"}, p("End of page"),
]};
const presentation: NodeJSON = {type: "presentation", children: [p("Quarterly plan"), {type: "slide", attrs: {layout: "title"}, children: [
    p("Overview", "title"), {type: "text-box", attrs: {frame: {x: 80, y: 120, width: 600, height: 180, rotation: 0, z: 1}}, children: [rich(), list()]},
    {type: "image", attrs: {src: "https://example.test/roadmap.png", alt: "Roadmap"}},
    {type: "shape", attrs: {kind: "ellipse", fill: "#123456"}},
    {type: "object-group", children: [p("Group label"), {type: "shape"}, {type: "text-box", children: [p("Grouped text")]}]},
    {type: "notes", children: [p("Presenter only: introduce the roadmap.")]},
]}, {type: "slide", attrs: {layout: "two-column"}, children: [
    p("Results", "title"), {type: "text-box", children: [p("Result summary")]}, table(),
    {type: "chart", attrs: {kind: "bar", data: [{label: "Q1", value: 4}, {label: "Q2", value: 7}]}},
    {type: "notes", children: [p("Discuss the two metrics.")]},
]}]};

export const PRODUCT_MODELS = {
    word: canonical(PRODUCT_SCHEMAS.word, word),
    notion: canonical(PRODUCT_SCHEMAS.notion, notion),
    presentation: canonical(PRODUCT_SCHEMAS.presentation, presentation),
};

What the tests establish ​

The profile suite has 33 cases, with the same 11 cases applied to each profile. Run it from the repository root:

sh
node --test packages/doc-sync/test/product-schemas.test.ts
CaseChecked result
Schema, canonical model and snapshotThe schema is valid. Loading preserves the complete canonical sample. Restoration preserves the model and does not change the schema.
Three editor conversionsProseMirror, Slate and Lexical headless conversion each preserve the sample data.
Concurrent delete, format and insert, in two receipt ordersThree clients, server state and event mirrors converge. Undoing a deletion restores the concurrently formatted character; undoing that formatting removes the participant's formatting.
Independent nested fieldsConcurrent margin, database-view or object-frame fields both survive. Undo and redo affect the issuing participant's field.
Merged cells and concurrent hidden inputHidden text and cell fields survive merging, conversion, undo and redo.
Structured movement and concurrent descendant inputMoving a section, toggle or slide preserves another participant's input inside it through undo, redo and snapshot restoration.
Required childrenDeletion preserves the minimum visible children. The replacement child accepts input and survives snapshot restoration.
Invalid storage referencesSnapshots reject malformed required identities, character anchors and anchors from another flow.

The network cases use three clients through the in-memory transport. They check each replica, the server, model-event application, pending queues and snapshot restoration. They require zero event-application fallbacks. The test source is product-schemas.test.ts.

These profiles exposed two storage validation errors and a cell normalization error: derived required-flow body slots were rejected during restoration; trailing paragraphs after required blocks were checked against a physical rather than owning flow; and nested cell-field defaults differed between canonical models and document output. The regression cases cover these fixes. Concurrent editing policies are unchanged.

Application work still required ​

AreaRemaining application responsibility
Word-style documentsPagination, section layout, header/footer placement and repetition, page numbers, tracked changes and DOCX fidelity
Notion-style pagesCollapse UI, database queries, relations, formulas, permissions and embed rendering; the database profile stores grid content and view properties only
PresentationsCanvas rendering, object drag/resize, coordinate transforms, connectors, themes, animations, presenter mode and PPTX fidelity; chart data is an atomic value, not an independently editable series model
All threeProduct-specific node views and keyboard behavior, paste/import policy, accessibility, production-scale latency and actual OS IME validation

The presentation tree preserves slide and object order. A frame's z field is stored metadata; this fixture does not implement a renderer that sorts or composes it. A flow starts with its default text block, so object groups and slide contents can include an empty leading paragraph or title. Applications must account for that rule when rendering a canvas.

The unit suite includes headless model conversions. The separate native demo covers the bounded editing cases described above. These checks do not establish full product UI, layout, real OS IME or release performance acceptance. Read editor design lessons before choosing a binding, and status and limits before planning a release.

Independent packages. Schema-defined document behavior.