Architecture Overview
notectl follows a strict layered architecture. Each layer has clear responsibilities and dependencies only flow downward.
Layer Diagram
Section titled “Layer Diagram”┌─────────────────────────────────────────┐│ 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└─────────────────────┴──────────────────┘Layers
Section titled “Layers”Model (model/)
Section titled “Model (model/)”The foundation. Immutable data types with no DOM dependency:
Document— Root container withchildren: BlockNode[]BlockNode— Block-level nodes (paragraph, heading, list_item, etc.)TextNode— Inline text segments with marksInlineNode— Atomic inline elements (hard break, emoji, mention, etc.)Mark— Inline annotations (bold, italic, link, etc.)Selection— Anchor + head positionsSchema— Describes valid node and mark typesNodeSpec/MarkSpec— Type declarations with DOM rendering
All types are deeply readonly. Mutations create new instances.
State (state/)
Section titled “State (state/)”Manages editor state transitions:
EditorState— Immutable state container (doc + selection + schema + storedMarks)Transaction— A sequence of steps that transform stateTransactionBuilder— Fluent API for constructing transactionsStepApplication— Pure functions that apply steps to produce new stateHistory— Undo/redo via transaction inversion with grouping
View (view/)
Section titled “View (view/)”DOM management and user interaction:
EditorView— Orchestrates dispatch, reconciliation, and input handlingReconciler— Block-level DOM diffing (compares old/new state, patches DOM)SelectionSync— Syncs DOM selection with model selectionNodeView— Custom rendering interface for complex blocks (e.g., tables)
Input (input/)
Section titled “Input (input/)”Processes user input:
InputManager— Orchestrates all input handler lifecycle and delegationInputHandler—beforeinputevent → commandsKeyboardHandler— Key events → keymap dispatchPasteHandler— Clipboard paste handlingInputRule— Pattern-based text transforms (e.g.,#→ heading)
Commands (commands/)
Section titled “Commands (commands/)”High-level editing operations, split by category:
- Text editing —
insertText,splitBlock,mergeBlockBackward,mergeBlockForward,selectAll - Mark operations —
toggleMark,toggleBold,toggleItalic,toggleUnderline,isMarkActive - Deletion —
deleteBackward,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 - Utilities —
sharesParent,isInsideIsolating,isVoidBlock,canCrossBlockBoundary
Decorations (decorations/)
Section titled “Decorations (decorations/)”Lightweight visual annotations that do not alter the document model:
Decoration— Inline, node, or widget decorations rendered by the view layerDecorationSet— Container for managing decoration collectionsPositionMapping— Maps decoration positions across state transitions
Serialization (serialization/)
Section titled “Serialization (serialization/)”HTML import/export:
DocumentSerializer— Converts Document to HTMLDocumentParser— Parses HTML into DocumentCSSClassCollector— Collects CSS classes for class-based export
Style (style/)
Section titled “Style (style/)”Runtime style management:
StyleRuntime— Dynamic stylesheet injection and management
I18n (i18n/)
Section titled “I18n (i18n/)”Internationalization support:
LocaleService— Locale resolution and managementLocale— Supported locale constants (9 languages)
Platform (platform/)
Section titled “Platform (platform/)”Environment detection:
- Browser and OS detection utilities for platform-specific behavior
Plugins (plugins/)
Section titled “Plugins (plugins/)”Feature modules that compose the editor. Each plugin:
- Registers schema extensions (NodeSpec, MarkSpec)
- Registers commands and keymaps
- Registers input rules
- Registers toolbar items
- Reacts to state changes
Editor (editor/)
Section titled “Editor (editor/)”The public API layer:
NotectlEditor— Web Component (<notectl-editor>)createEditor()— Factory function
Internally decomposed into focused controllers:
EditorLifecycleCoordinator— Init/destroy state machine and readiness trackingEditorConfigController— Runtime config and observed attribute managementEditorEventEmitter— Typed event emitter (stateChange,focus,blur, etc.)EditorStyleCoordinator— Shadow DOM stylesheet managementEditorThemeController— Theme application and CSS variable generationPaperLayoutController— Paper size and layout managementPluginBootstrapper— Auto-registration of essential plugins and toolbar processingContentSerializer— Module of pure functions for content serialization (JSON, HTML, plain text)
Key Design Principles
Section titled “Key Design Principles”DOM Isolation
Section titled “DOM Isolation”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.).
Immutability
Section titled “Immutability”All data in model/ and state/ is immutable. Changes produce new objects:
const newState = oldState.apply(transaction);// oldState is unchangedStep-Based Changes
Section titled “Step-Based Changes”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
Plugin Encapsulation
Section titled “Plugin Encapsulation”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
Stylesheet Delivery
Section titled “Stylesheet Delivery”Static editor and plugin styles live in .css files and are imported with Vite’s
?inline query. The build processes and minifies them into JavaScript strings;
consumers do not need a separate CSS import. Both ESM and UMD use the same CSS
build options, targeting Chrome 67, Firefox 63, Safari 12.1, and Edge 79 as listed
in the installation requirements. JavaScript build targets are configured separately.
Logical properties such as border-inline-start and the inset shorthand are
excluded from that lowering and ship as authored, as they did before the pipeline
change. For those targets Lightning CSS could only emulate them with :lang()
selectors, so direction-dependent styling would follow the page language instead of
dir. Browsers that predate these properties ignore them, as in earlier releases.
pnpm size:check fails if such an emulation, for example from a :dir() selector,
reaches the build output.
At runtime, the editor adopts its base and theme stylesheets in the Shadow DOM.
Plugins continue to register their styles through PluginContext.registerStyleSheet().
The existing runtime style and nonce handling is unchanged. Styles generated from
shared constants, such as paper dimensions and syntax token rules, remain TypeScript.
Tests process CSS imports through Vite too, so theme and accessibility checks
exercise real styles instead of empty CSS mocks.
Block-Level Reconciliation
Section titled “Block-Level Reconciliation”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.