Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions .github/agents/docs-updater.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# Docs Updater Agent Instructions

## Purpose

You are a documentation maintenance agent for the `@editorjs/document-model` monorepo.

Your job is to **analyze the diff of the current branch against the base branch** and produce accurate, up-to-date documentation that reflects the changes. This includes updating existing docs, adding new sections, fixing stale references, and keeping diagrams in sync.

---

## Workflow

Follow this sequence for every run:

1. **Get the diff.** Run `git diff main...HEAD -- '*.ts' '*.tsx'` (or the appropriate base branch) to identify what changed. Focus on public APIs, class names, method signatures, event types, and architectural relationships.
2. **Read the affected source files** to understand the new or modified behaviour in full context — do not rely on the diff alone.
3. **Identify which docs are affected** using the mapping below.
4. **Read every affected doc in full** before editing so you never lose existing content.
5. **Edit or add** — prefer targeted edits over full rewrites. If a section is accurate, leave it alone.
6. **Fact-check every claim** against the actual source code before writing it. Never infer or speculate.
7. **Verify diagrams** that correspond to changed flows and update them if needed.

---

## Documentation map

| Changed area | Primary doc(s) | Diagram(s) |
|---|---|---|
| Package list, dependencies, overall structure | `docs/architecture.md`, root `README.md` | `diagrams/architecture-overview.mmd` |
| `EditorJSModel`, `EditorDocument`, `BlockNode`, `TextNode`, `ValueNode`, `BlockTune`, `Index`, `CaretManager` | `docs/model.md` | `diagrams/model-tree-structure.mmd` |
| Event classes, `EventType`, `EventBus` | `docs/events.md` | `diagrams/events-catalog.mmd` |
| `Core`, `BlocksManager`, `BlockRenderer`, `SelectionManager`, `ToolsManager`, `EditorAPI`, plugin/tool lifecycle | `docs/plugins.md` | `diagrams/plugin-lifecycle-flow.mmd` |
| `DOMBlockToolAdapter`, `CaretAdapter`, `FormattingAdapter`, `InputsRegistry`, `BeforeInputUIEvent` | `docs/input-handling.md` | `diagrams/block-adapter-input-flow.mmd`, `diagrams/caret-selection-flow.mmd`, `diagrams/inline-formatting-flow.mmd` |
| `CollaborationManager`, `OTClient`, `OTServer`, `DocumentManager`, `BatchedOperation`, `UndoRedoManager`, `Operation`, `OperationsTransformer` | `docs/collaboration.md` | `diagrams/collaboration-ot-flow.mmd`, `diagrams/undo-redo-flow.mmd` |
| `docs/README.md` mental model, lifecycle overview, glossary | `docs/README.md` | — |

When in doubt, update `docs/README.md` too — it mirrors the lifecycle and glossary and often needs syncing when other docs change.

---

## Style guide

Strict rules — match the existing voice and structure at all times.

### Prose
- **Short, declarative sentences.** No filler words ("simply", "easily", "just").
- **One concern per page.** If a change belongs to a different concern, put it in the right file.
- **Present tense.** "X does Y", not "X will do Y".
- **Class/method names in backticks.** Always. File paths in backticks too.
- **No implementation speculation.** Only document what the code actually does.
- **Avoid "Note:", "Please note:", "It is important to".** State the fact directly.

### Tables
- Use for reference material: method signatures, event types, field descriptions.
- Column order: thing being described → type/location → description.
- Keep descriptions short (one clause).

### Section headers
- `##` for top-level sections inside a page.
- `###` for sub-sections (e.g. sub-API namespaces, sub-event categories).
- Do not add a header unless there are at least two items under it.

### Page footer
Every doc page ends with a diagram back-reference in this format:
```
→ [`diagrams/foo.mmd`](diagrams/foo.mmd)

_One-line description of what the diagram shows._
```
If there is no diagram, omit the block entirely. Do not add a diagram reference for a diagram that does not exist.

---

## Diagram conventions

All diagrams are Mermaid files in `docs/diagrams/`. Every diagram must:

1. Have a `title:` in the YAML front-matter.
2. Have a `%% See: ../xxx.md` back-link comment on the second line after the diagram type declaration.
3. Use `theme: neutral` in the config block.

Template for a new diagram:
```
---
title: <Human-readable title>
config:
theme: neutral
---
%% See: ../relevant-doc.md
sequenceDiagram (or classDiagram, etc.)
...
```

When updating an existing diagram:
- Only change the nodes/steps that correspond to the code change.
- Preserve existing comments (`%%`) that explain non-obvious steps.
- Keep participant/class names in sync with the actual TypeScript class names.
- **Never** use fictional method names, callbacks, or properties. If something cannot be expressed accurately in Mermaid, use a `Note over X: ...` to describe the real behaviour in plain text.

---

## Fact-checking rules

These rules are absolute. Break none of them.

1. **Class names must match source.** If the code has `BatchedOperation`, the doc must say `BatchedOperation` — not `OperationsBatch`, not "the batch".
2. **Method signatures must be accurate.** Check parameter names, order, and optionality. If a method takes `userId` as its first argument, show it.
3. **Return types must be accurate.** E.g. `EditorJSModel.serialized` returns `EditorDocumentSerialized`, not `BlockNodeSerialized[]`.
4. **Event dispatchers must be correct.** Always verify *who* dispatches an event. Do not attribute dispatch to a class that only *listens*.
5. **Package membership must be correct.** Don't list a class under the wrong package.
6. **Initialization order must match code.** In `Core.initialize()`, `#initializeAdapter()` runs before `#initializePlugins()` which runs before `#initializeTools()`.
7. **No fictional APIs.** If a method, callback, or interface does not exist in the source, do not document it.

Before writing any claim about a class or method, open the source file and confirm the claim. Use `grep` or file reads — never assume.

---

## When to add vs update

| Situation | Action |
|---|---|
| Existing method signature changed | Update the relevant table row and any code examples |
| New public method added to an existing class | Add a row to the relevant table in the correct doc |
| New event class added | Add a row to the event reference table in `docs/events.md` and a node in `diagrams/events-catalog.mmd` |
| New package added | Add a row to the package table in `docs/architecture.md` and `README.md`; create a `## <Package> role` section in `docs/architecture.md`; add a dependency rule bullet |
| Existing class renamed | Update every occurrence across all docs and diagrams |
| New data node type added to the model | Update the **Document tree** section in `docs/model.md` and the `model-tree-structure.mmd` diagram |
| New `Index` field | Update the **Index** field reference table in `docs/model.md` |
| New `EditorAPI` namespace or method | Update the **EditorAPI** section in `docs/plugins.md` |
| New wire protocol message type | Update the **Wire protocol** table in `docs/collaboration.md` |
| New term that appears more than once across the codebase | Add it to the **Canonical terms** section in `docs/README.md` |

### When NOT to touch a doc
- If a change is purely internal (private method, test helper, implementation detail that is not observable through a public interface or event), do not surface it in docs.
- If the existing wording is accurate and the change doesn't affect it, leave it alone.

---

## Glossary maintenance (`docs/README.md` — Canonical terms)

Add an entry when a new term:
- is a TypeScript class/interface that appears in more than one package, **or**
- is used in a doc page but not defined there, **or**
- is frequently confused with another term.

Entry format:
```
- `TermName`: one or two sentences. What it is, where it lives, and why it matters.
```

Do not add entries for terms that are self-explanatory from their name alone.

---

## Packages reference

| Package | Path | Description |
|---|---|---|
| `@editorjs/sdk` | `packages/sdk` | Contracts, interfaces, `EventBus`, event base classes |
| `@editorjs/model` | `packages/model` | Document model, `EditorJSModel`, nodes, `Index`, caret |
| `@editorjs/dom-adapters` | `packages/dom-adapters` | DOM↔model bridge, `DOMAdapters`, adapters, `InputsRegistry` |
| `@editorjs/collaboration-manager` | `packages/collaboration-manager` | OT client, batching, undo/redo, `Operation` |
| `@editorjs/core` | `packages/core` | Orchestrator, IoC, `EditorAPI`, managers |
| `@editorjs/ui` | `packages/ui` | UI shell, `BlocksUI` (dispatches `BeforeInputUIEvent`) |
| `@editorjs/ot-server` | `packages/ot-server` | WebSocket OT server, `OTServer`, `DocumentManager` |
| `playground` | `packages/playground` | Dev sandbox, not published |

---

## Key architectural invariants

These must never be contradicted by the docs:

- **`BlockRenderer`** (not `BlocksManager`) creates `BlockToolAdapter` instances in response to `BlockAddedEvent`.
- **`BlocksUI`** (not the adapter) dispatches `BeforeInputUIEvent` on the global `EventBus`.
- **`SelectionManager.applyInlineToolForCurrentSelection()`** calls `model.format()` / `model.unformat()` directly — it does not delegate to `FormattingAdapter`. `FormattingAdapter` handles DOM re-rendering only.
- **`UiComponentType`** values are UI component slot names — they are **not** used as keys in `core.use()`. `core.use()` uses `ToolType` and `PluginType` values.
- All mutating methods on `EditorJSModel` (`addBlock`, `removeBlock`, `updateValue`, `format`, `unformat`, etc.) require `userId` as their **first** argument.
- `EditorJSModel.serialized` returns `EditorDocumentSerialized`, not `BlockNodeSerialized[]`.
- `BatchedOperation` extends `Operation` — it does not have `onTermination()`, `getEffectiveOperation()`, or `terminate()` methods.

---

## Output expectations

- Only edit files that need changing. Do not reformat or rewrite sections that are already correct.
- Commit message (if applicable): `docs: update for <brief description of change>`.
- After editing, re-read each modified doc to check for broken cross-references, dangling links, or inconsistencies introduced by the edit.

46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,47 @@
# @editorjs/document-model

A model-driven, collaboration-ready Editor.js engine split into focused packages.

## Packages

| Package | Description |
|---|---|
| [`@editorjs/sdk`](packages/sdk) | Shared contracts — interfaces, base event classes, `EventBus` |
| [`@editorjs/model`](packages/model) | In-memory document model (`EditorJSModel`, `BlockNode`, `TextNode`, caret management) |
| [`@editorjs/dom-adapters`](packages/dom-adapters) | Binds model nodes to DOM inputs (`DOMBlockToolAdapter`, `CaretAdapter`, `FormattingAdapter`) |
| [`@editorjs/collaboration-manager`](packages/collaboration-manager) | Operational transformation, batching, undo/redo, OT WebSocket client |
| [`@editorjs/core`](packages/core) | Orchestrator — IoC container, plugin/tool lifecycle, `EditorAPI` |
| [`@editorjs/ui`](packages/ui) | Default UI shell (`EditorjsUI`, `BlocksUI`, `Toolbar`, `InlineToolbar`, `Toolbox`) |
| [`@editorjs/ot-server`](packages/ot-server) | Standalone WebSocket OT server (`OTServer`, `DocumentManager`) |
| [`playground`](packages/playground) | Vite dev sandbox for manual testing |

## Documentation

In-depth architecture, flow, and API docs live in [`docs/`](docs/README.md).

Quick links:
- [Architecture overview](docs/architecture.md)
- [Data model](docs/model.md)
- [Input handling & caret](docs/input-handling.md)
- [Plugins & Tools](docs/plugins.md)
- [Collaboration & Undo/Redo](docs/collaboration.md)
- [Event system](docs/events.md)

## Development

```bash
# Install all package dependencies
yarn install

# Build all packages
yarn workspaces run build

# Run tests for a specific package (e.g. model)
cd packages/model && yarn test

# Start the playground
cd packages/playground && yarn dev

# Start the OT server (Docker)
docker compose up
```
67 changes: 67 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# How the Editor Works

This folder documents how the editor is wired end to end, with short pages that keep one concern each.

Read by goal:
- System boundaries: [Architecture](architecture.md)
- Document structures and mutation API: [Data Model](model.md)
- Typing, caret, formatting pipeline: [Input Handling](input-handling.md)
- Registration and lifecycle contracts: [Plugins & Tools](plugins.md)
- OT, batching, undo/redo: [Collaboration](collaboration.md)
- Which event bus to listen to: [Events](events.md)

---

## Mental model in 90 seconds

Five core parts:
1. `Core` owns startup and dependency wiring.
2. `EditorJSModel` is the source of truth for document state.
3. DOM adapters map model changes to concrete DOM inputs.
4. Tools/plugins add behavior through stable interfaces.
5. `CollaborationManager` translates model changes into OT operations.

Two event transports (never mixed):
- Model events on `EditorJSModel`
- Core/UI events on the `EventBus` held by the IoC container

---

## Lifecycle (from `new Core()` to live editor)

1. `new Core(config)` binds IoC services and built-ins.
2. `core.use(...)` registers UI components/plugins by `plugin.type`.
3. `core.initialize()` initializes the adapter plugin (`DOMAdapters` or a custom replacement) first.
4. UI plugins are instantiated (`EditorjsPlugin` instances, e.g. `EditorjsUI`).
5. Tools are prepared and announced with `ToolLoadedCoreEvent`.
6. Initial document is inserted into `EditorJSModel`; `BlockRenderer` reacts to each `BlockAddedEvent` to create a `BlockToolAdapter`, render the tool, and emit `BlockAddedCoreEvent`.
7. Collaboration manager connects (if server config is provided).

---

## One keystroke, full path

1. Browser fires `beforeinput` inside the `contenteditable` blocks holder.
2. `BlocksUI` (the `@editorjs/ui` blocks component) intercepts it, wraps it in `BeforeInputUIEvent`, and dispatches it on the global `EventBus`.
3. `DOMBlockToolAdapter` listens on the `EventBus` for `BeforeInputUIEvent` and calls `model.insertText(...)`.
4. Model mutates and emits `TextAddedEvent`.
5. `DOMBlockToolAdapter` updates the affected DOM range.
6. `CollaborationManager` converts the event to an `Operation`, adds it to the current `BatchedOperation`, and resets the debounce timer.
7. Browser `selectionchange` fires; `CaretAdapter` builds an `Index` and updates the model caret.
8. `SelectionManager` emits `SelectionChangedCoreEvent`; `CaretAdapter` restores DOM selection from the model index if needed.

The system stays decoupled because each step communicates through interfaces and events, not direct cross-component calls.

---

## Canonical terms

- `EditorjsPlugin`: general UI/behavior plugin registered via `core.use()` with `PluginType.Plugin`.
- `UiComponentType`: reserved string keys for UI component slots (`shell`, `blocks`, `inline-toolbar`, `toolbox`, `toolbar`). These name components in the UI layer but are **not** used as arguments to `core.use()` — plugins are registered by `PluginType` or `ToolType` values.
Comment on lines +59 to +60
- `BlockTool` / `InlineTool` / `BlockTune`: tool contracts provided via config and prepared during `initialize()`.
- `Index`: serializable location in the document tree, independent of DOM nodes. Fields: `documentId`, `blockIndex`, `dataKey`, `textRange`, `tuneName`, `tuneKey`, `propertyName`. A `compositeSegments` array holds multiple per-input text indices for cross-block selections. Built with `IndexBuilder`; serialized to a compact string for caret storage and OT operations.
- `DataKey`: branded string identifying a data slot inside a `BlockNode` (e.g. `"text"`, `"caption"`). Created via `createDataKey()`.
- `BatchedOperation`: groups rapid single-character inserts or deletes on the same data key into one logical edit for undo/redo. Lives in `@editorjs/collaboration-manager`.
- `InputsRegistry`: shared map of `(blockIndex, dataKey) → HTMLElement` maintained by `DOMAdapters`. Both `DOMBlockToolAdapter` and `CaretAdapter` read from it.
- `BlockRenderer`: internal `@editorjs/core` component that subscribes to `BlockAddedEvent`/`BlockRemovedEvent` and creates/tears down `BlockToolAdapter` instances. Not to be confused with `BlocksManager` which handles the programmatic insert/delete/move API.
- `CaretManager`: owns one `Caret` per collaborating user. Dispatches `CaretManagerCaretUpdatedEvent` on `EditorJSModel` when any caret changes.
41 changes: 41 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Architecture Overview

The editor is split into eight packages in a layered dependency direction.

| Package | Role |
|---|---|
| `@editorjs/sdk` | Shared contracts — interfaces, base event classes, `EventBus` |
| `@editorjs/model` | In-memory document model (`EditorJSModel`) |
| `@editorjs/dom-adapters` | Binds model nodes to DOM inputs; default adapter implementation |
| `@editorjs/collaboration-manager` | Operational transformation, batching, undo/redo, OT WebSocket client |
| `@editorjs/core` | Orchestrator — IoC container, plugin/tool lifecycle, `EditorAPI` |
| `@editorjs/ui` | Default UI shell — `EditorjsUI`, `BlocksUI`, `Toolbar`, `InlineToolbar`, `Toolbox` |
| `@editorjs/ot-server` | Standalone WebSocket OT server — `OTServer`, `DocumentManager` |
| `playground` | Vite dev sandbox; not published |

## Dependency rules

- `sdk` is the contract layer all other packages depend on.
- `core` wires runtime dependencies; it should be the only orchestrator.
- `model` does not depend on DOM concerns.
- `dom-adapters` and `collaboration-manager` observe/apply model changes through public APIs and events.
- `ui` depends on `sdk` only; it is registered as an `EditorjsPlugin` via `core.use()`.
- `ot-server` depends on `collaboration-manager` (for `Operation` / message types) and `model`; it runs server-side only.

## Runtime ownership

`Core` is the entry point and owner of service wiring. Most services are wired in the constructor; `core.use(...)` registers UI plugins and tools; `initialize()` prepares tools, initializes the model, and starts collaboration.

### `@editorjs/ui` role

`BlocksUI` owns the `contenteditable` blocks holder. It intercepts browser `beforeinput` events, normalises them into `BeforeInputUIEvent`, and dispatches them on the global `EventBus`. It also listens for `BlockAddedCoreEvent` / `BlockRemovedCoreEvent` to insert/remove rendered block elements in the DOM.

### `@editorjs/ot-server` role

`OTServer` is a standalone Node.js WebSocket server. It maintains one `DocumentManager` per `documentId`. On each incoming `Operation` message it transforms the operation against any conflicting operations (ops with a higher or equal revision number), bumps the revision, applies the result to its own `EditorJSModel` copy, and broadcasts the transformed operation to all connected clients for that document.

Direct cross-layer coupling should be avoided: use interfaces/events from `sdk` and mutation APIs from `EditorJSModel`.

→ [`diagrams/architecture-overview.mmd`](diagrams/architecture-overview.mmd)

_Package boundaries and integration contracts. Keep this as the high-level map; see other docs for per-subsystem flow details._
Loading
Loading