Skip to content

Architecture Overview

notectl follows a strict layered architecture. Each layer has clear responsibilities and dependencies only flow downward.

┌─────────────────────────────────────────┐
│ editor/ │ Web Component (public API)
├─────────────────────────────────────────┤
│ plugins/ │ Feature modules
├──────────┬──────────┬───────────────────┤
│ commands/│ input/ │ view/ │ High-level ops, input, DOM
├──────────┴──────────┴───────────────────┤
│ state/ │ decorations/ │ State + visual annotations
├─────────────────────┴───────────────────┤
│ serialization/ │ style/ │ i18n/ │ Cross-cutting concerns
├──────────────────┴──────────┴──────────┤
│ model/ │ platform/ │ Immutable data types + env
└─────────────────────┴──────────────────┘

The foundation. Immutable data types with no DOM dependency:

  • Document — Root container with children: BlockNode[]
  • BlockNode — Block-level nodes (paragraph, heading, list_item, etc.)
  • TextNode — Inline text segments with marks
  • InlineNode — Atomic inline elements (hard break, emoji, mention, etc.)
  • Mark — Inline annotations (bold, italic, link, etc.)
  • Selection — Anchor + head positions
  • Schema — Describes valid node and mark types
  • NodeSpec / MarkSpec — Type declarations with DOM rendering

All types are deeply readonly. Mutations create new instances.

Manages editor state transitions:

  • EditorState — Immutable state container (doc + selection + schema + storedMarks)
  • Transaction — A sequence of steps that transform state
  • TransactionBuilder — Fluent API for constructing transactions
  • StepApplication — Pure functions that apply steps to produce new state
  • History — Undo/redo via transaction inversion with grouping

DOM management and user interaction:

  • EditorView — Orchestrates dispatch, reconciliation, and input handling
  • Reconciler — Block-level DOM diffing (compares old/new state, patches DOM)
  • SelectionSync — Syncs DOM selection with model selection
  • NodeView — Custom rendering interface for complex blocks (e.g., tables)

Processes user input:

  • InputManager — Orchestrates all input handler lifecycle and delegation
  • InputHandlerbeforeinput event → commands
  • KeyboardHandler — Key events → keymap dispatch
  • PasteHandler — Clipboard paste handling
  • InputRule — Pattern-based text transforms (e.g., # → heading)

High-level editing operations, split by category:

  • Text editinginsertText, splitBlock, mergeBlockBackward, mergeBlockForward, selectAll
  • Mark operationstoggleMark, toggleBold, toggleItalic, toggleUnderline, isMarkActive
  • DeletiondeleteBackward, deleteForward, deleteWordBackward, deleteWordForward, deleteSoftLineBackward, deleteSoftLineForward
  • Movement (model)moveCharacterForward, moveCharacterBackward, moveToBlockStart, moveToBlockEnd, moveToDocumentStart, moveToDocumentEnd, extendCharacterForward, extendCharacterBackward, extendToDocumentStart, extendToDocumentEnd
  • Movement (view)moveWordForward, moveWordBackward, moveToLineStart, moveToLineEnd, moveLineUp, moveLineDown, extendWordForward, extendWordBackward, extendToLineStart, extendToLineEnd, extendLineUp, extendLineDown
  • UtilitiessharesParent, isInsideIsolating, isVoidBlock, canCrossBlockBoundary

Lightweight visual annotations that do not alter the document model:

  • Decoration — Inline, node, or widget decorations rendered by the view layer
  • DecorationSet — Container for managing decoration collections
  • PositionMapping — Maps decoration positions across state transitions

HTML import/export:

  • DocumentSerializer — Converts Document to HTML
  • DocumentParser — Parses HTML into Document
  • CSSClassCollector — Collects CSS classes for class-based export

Runtime style management:

  • StyleRuntime — Dynamic stylesheet injection and management

Internationalization support:

  • LocaleService — Locale resolution and management
  • Locale — Supported locale constants (9 languages)

Environment detection:

  • Browser and OS detection utilities for platform-specific behavior

Feature modules that compose the editor. Each plugin:

  1. Registers schema extensions (NodeSpec, MarkSpec)
  2. Registers commands and keymaps
  3. Registers input rules
  4. Registers toolbar items
  5. Reacts to state changes

The public API layer:

  • NotectlEditor — Web Component (<notectl-editor>)
  • createEditor() — Factory function

Internally decomposed into focused controllers:

  • EditorLifecycleCoordinator — Init/destroy state machine and readiness tracking
  • EditorConfigController — Runtime config and observed attribute management
  • EditorEventEmitter — Typed event emitter (stateChange, focus, blur, etc.)
  • EditorStyleCoordinator — Shadow DOM stylesheet management
  • EditorThemeController — Theme application and CSS variable generation
  • PaperLayoutController — Paper size and layout management
  • PluginBootstrapper — Auto-registration of essential plugins and toolbar processing
  • ContentSerializer — Module of pure functions for content serialization (JSON, HTML, plain text)

Only view/ touches the DOM. The model and state layers are purely data-driven and can run without a browser (useful for testing, server-side processing, etc.).

All data in model/ and state/ is immutable. Changes produce new objects:

const newState = oldState.apply(transaction);
// oldState is unchanged

Every change is expressed as a sequence of atomic Steps. Each step stores enough data to be inverted. This enables:

  • Undo/redo without state snapshots
  • Transaction composition
  • Change inspection/auditing

Plugins never access editor internals directly. Everything goes through PluginContext. This makes plugins:

  • Testable in isolation
  • Safe from breaking changes in internal APIs
  • Composable without conflicts

The Reconciler diffs at block granularity — if a block hasn’t changed, its DOM is untouched. Within a changed block, inline content is fully rebuilt. This is a good balance between performance and simplicity.