Skip to main content

Architecture Overview

Barocss Editor uses a model-first, DSL-first architecture. All operations work on a model, and everything (templates, marks, decorators, operations) is defined using DSL.

Core Structure

DSL-First Philosophy

In Barocss, everything is defined using DSL:

  • Templates: define('paragraph', element('p', {}, [slot('content')]))
  • Marks: defineMark('bold', element('strong', {}, [data('text')])) or defineMark('bold', external(BoldComponent))
  • Decorators: defineDecorator('highlight', element('span', {}, []))
  • Operations: defineOperationDSL('insertText', (p) => insertText({ text: p.text }))

This unified approach provides consistency, type safety, and composability across all definitions.

Dual Rendering Pipeline

The same DSL templates power two rendering targets:

Both paths share the same template definitions — you define once and render to either target.

Role of Each Layer

1. DSL Layer (packages/dsl)

Role: Functional template definition

  • element() - HTML element template
  • data() - Data binding
  • when() - Conditional rendering
  • component() - Component template
  • slot() - Slot template
  • portal() - Portal template
  • external() - Wrap React components or DOM mount/unmount objects

All builders are pure functions.

Input: Builder parameters Output: Template

2a. VNodeBuilder (packages/renderer-dom)

Role: Convert DSL templates to VNode

  • Template lookup from Registry
  • Data binding (data(), className, style)
  • Component resolution
  • Conditional rendering (build time)

Input: Template × Model data Output: VNode tree

2b. buildToReact (packages/renderer-react)

Role: Convert DSL templates directly to ReactNode

  • Same template lookup from Registry
  • Same data binding and component resolution
  • Supports external(ReactComponent) for marks and blocks
  • No VNode intermediate — outputs ReactNode directly

Input: Template × Model data Output: ReactNode

3. DOMReconcile (packages/renderer-dom)

Role: Convert VNode differences to DOM changes

  • WIP tree creation and management
  • Change detection (tag, attrs, children)
  • Priority-based processing
  • Minimal DOM changes applied
  • Children reconcile (React-style)

Input: prevVNode, nextVNode Output: DOM updates

4 Steps of Reconcile

reconcile(prevVNode, nextVNode, container, context) {
// 1. Create WIP Tree
const wipTree = createWorkInProgressTree(nextVNode, prevVNode);

// 2. Detect changes and assign priority
detectChangesAndAssignPriority(prevVNode, nextVNode);

// 3. Process by priority
processByPriority(context, processWorkInProgress);

// 4. Execute DOM updates
executeDOMUpdates(container, finalizeDOMUpdate);
}

Children Reconcile Core

// Reconciliation that directly manipulates DOM
reconcileChildren(wip, prevChildren, nextChildren) {
while (prevIndex < prevChildren.length || nextIndex < nextChildren.length) {
if (!prevChild) {
// Add new child
const newNode = createNewDOMNode(nextChild);
domNode.insertBefore(newNode, referenceNode);

// Important: Set child WIP's domNode
wip.children[nextIndex].domNode = newNode;
} else if (isSameNode(prevChild, nextChild)) {
// Same node - skip
} else {
// Replace node
const newNode = createNewDOMNode(nextChild);
domNode.replaceChild(newNode, oldNode);
wip.children[nextIndex].domNode = newNode;
}
}
}

Core Rule: DOM nodes created/replaced in reconcile must be set in child WIP's domNode to prevent duplicate append

Key Classes

Class/FunctionLayerRole
element, data, when, componentDSLTemplate builder (pure functions)
VNodeBuilderVNodeDSL template → VNode conversion
DOMReconcileRendererVNode → DOM reconcile orchestration
WorkInProgressManagerRendererWIP tree creation and management
ChangeDetectionRendererChange detection
DOMProcessorRendererDOM manipulation (insert/update/remove)
ComponentManagerRendererComponent lifecycle
PortalManagerRendererPortal rendering
DOMOperationsRendererDOM creation/modification utilities

Core Rules

  1. DSL builders define templates as pure functions
  2. VNodeBuilder only converts templates and data to VNode
  3. DOMReconcile converts VNode differences to DOM changes
  4. WIP pattern batches actual changes
  5. children reconcile must set created DOM nodes in child WIP's domNode
  6. finalizeDOMUpdate prevents duplicate append (isAlreadyInDOM check)

Package Map

packages/
├─ schema/ # Document structure and validation
├─ datastore/ # Transactional node store (overlay/COW)
├─ model/ # Operations, transactions, undo/redo
├─ dsl/ # Declarative template builders ⭐
├─ renderer-dom/ # VNode reconciliation → DOM
├─ renderer-react/ # Direct → ReactNode (no VNode)
├─ editor-core/ # Commands, selection, extensions, history
├─ editor-view-dom/ # DOM input handling, selection sync
├─ editor-view-react/ # React view layer with hooks
├─ extensions/ # 30+ built-in extensions
├─ collaboration/ # CRDT/OT base adapter
├─ collaboration-yjs/ # Yjs adapter
├─ collaboration-liveblocks/ # Liveblocks adapter
├─ converter/ # HTML/Markdown/LaTeX/PDF conversion
├─ shared/ # Platform, keybinding, i18n utilities
├─ text-analyzer/ # Smart text diff (LCP/LCS)
├─ dom-observer/ # DOM mutation observer
└─ devtool/ # Dev tools (tree view, event log)

Usage Examples

For detailed examples, see Practical Examples

Basic Render

// Define DSL template
define('paragraph', element('p', {}, [data('text')]));

// Render
const renderer = new DOMRenderer();
const model = { stype: 'paragraph', text: 'Hello' };
renderer.render(container, model);
// Result: <p>Hello</p>

Update (Automatic Reconcile)

model.text = 'New';
renderer.render(container, model);
// Result: <p>New</p> (only text changed without full regeneration)

Complex Template

define('article', element('article',
{ className: data('className') },
[
when(
(d) => d('published') === true,
element('span', {}, ['Published'])
),
element('h1', {}, [data('title')]),
component('author', { name: data('author') })
]
));