docblocks-core 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -0
- package/LICENSE +21 -0
- package/README.md +168 -2
- package/dist/index.d.mts +1492 -0
- package/dist/index.mjs +6541 -0
- package/package.json +33 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# docblocks-core
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- The README's `createDoc`'s contract stands the actual initial state (only
|
|
8
|
+
the root block — no auto first paragraph).
|
|
9
|
+
|
|
10
|
+
## 0.1.0
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- The initial release of the headless CRDT document engine
|
|
15
|
+
(dependencies: `yjs`, `y-protocols`; no UI dependencies — the server,
|
|
16
|
+
the blob resolution, and the persistence are consumer-side concerns).
|
|
17
|
+
- The document data format (JSON `v: 1`): the document meta (`id`,
|
|
18
|
+
`title`, the optional `avatar`, `createdAt`) + the flat block map
|
|
19
|
+
(`id → block`) with the explicit order via `children: [blockId]`; the
|
|
20
|
+
JSON ↔ Yjs layout is isomorphic (every JSON field has exactly one Yjs
|
|
21
|
+
image — the root's `children` live in the registered `rootOrder` array,
|
|
22
|
+
and each non-root block carries a Yjs-layout-only `parent` field that
|
|
23
|
+
never reaches the JSON).
|
|
24
|
+
- The block and mark registry as the single source of truth, with the
|
|
25
|
+
generated discriminated-union types (an invalid attribute is a type
|
|
26
|
+
error at the call site) and the typed command errors (the
|
|
27
|
+
`UnknownBlockError`, the `InvalidRangeError`, the `DetachedBlockError`, ...).
|
|
28
|
+
- The v1 block types: `root`, `paragraph`, `quote`, `heading` (levels
|
|
29
|
+
1–6), `list-item` (`bullet` / `number` / `task`, the nesting over the
|
|
30
|
+
`indent` attribute), `callout` (the `variant` + the additive
|
|
31
|
+
`color?` / `icon?` overrides), `code`, `divider`, `image` (the opaque
|
|
32
|
+
`src` key), `embed`, the `table` / `table-row` / `table-cell` triple
|
|
33
|
+
(the cells are the mini-docs; the column set is the shared
|
|
34
|
+
`columnOrder` id array + the cell's `columnId` binding), the
|
|
35
|
+
`database` block (the inline `columns` + `rows` form and the external
|
|
36
|
+
`tableId` form), and the five canvas types (`surface`, `shape`,
|
|
37
|
+
`connector`, `frame`, `canvas-text`) with the optional `geometry`.
|
|
38
|
+
- The text model: a single `Y.Text` per text block with the
|
|
39
|
+
character-attribute marks (`bold`, `italic`, `strike`, `underline`,
|
|
40
|
+
`code`, `link`, `color`, `highlight`, `mention`) and the JSON run
|
|
41
|
+
array (`content: [{ text, marks }]`); the formatting is a pure
|
|
42
|
+
attribute op, so the character identity survives the concurrent
|
|
43
|
+
formatting and deletion.
|
|
44
|
+
- The semantic command API (the only path for the normal editing): the
|
|
45
|
+
block commands (`insertBlock` / `updateBlock` / `moveBlock` /
|
|
46
|
+
`removeBlock` / `changeBlockType`), the text commands (`insertText` /
|
|
47
|
+
`deleteText` / `setMark` / `unsetMark`), the document meta
|
|
48
|
+
(`setTitle` / `setAvatar`), the geometry and the z-order
|
|
49
|
+
(`setGeometry`, `setZOrder`), the database row commands, and the table
|
|
50
|
+
row / column commands; `transact` batches the commands into one undo
|
|
51
|
+
step and one wire update.
|
|
52
|
+
- The selection view (points and ranges, the rebase rules over the
|
|
53
|
+
concurrent edits) and the per-document undo / redo history; the
|
|
54
|
+
selection is the client / view state, never document data (the other
|
|
55
|
+
clients' selections travel only in the ephemeral awareness).
|
|
56
|
+
- The CRDT-level power-user API: `encode` / `encodeUpdate` /
|
|
57
|
+
`applyUpdate` / `toJSON` / `fromJSON`, and the explicit `doc.raw:
|
|
58
|
+
Y.Doc` escape hatch (not guaranteed across versions).
|
|
59
|
+
- The transport layer: the `SyncProvider` interface + the
|
|
60
|
+
`WsSyncProvider` (the standard y-protocols over WebSocket,
|
|
61
|
+
wire-compatible with `y-websocket` / `pycrdt`) and the `attach(doc,
|
|
62
|
+
provider)` glue (the origin guard + the full-state bootstrap; the
|
|
63
|
+
send buffers over the not-open socket); the presence over the
|
|
64
|
+
y-protocols awareness (the selection + the `updatedAt` per peer, the
|
|
65
|
+
~30 s staleness TTL).
|
|
66
|
+
- The frame as the viewport window into its own surface (no children,
|
|
67
|
+
no content, no `zIndex`) + the page frame block (the `frameId?` weak
|
|
68
|
+
reference); the canvas z-order stands the derived dense 1..N mirror
|
|
69
|
+
(`derivedZIndex`, re-derived on load).
|
|
70
|
+
- The complementary reader / validator postures: the strict validator
|
|
71
|
+
(only the known types and marks) and the lenient reader (the unknown
|
|
72
|
+
type → the placeholder fallback, the unknown mark → ignored, the
|
|
73
|
+
unknown attribute → ignored) — the forward-compatibility across the
|
|
74
|
+
minor versions of the major data format.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wereintouch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,169 @@
|
|
|
1
|
-
#
|
|
1
|
+
# docblocks-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The headless CRDT document engine for docblocks — the data half: the
|
|
4
|
+
document model, the editing commands, the selection, and the sync
|
|
5
|
+
transport. There is no UI in this package; rendering and editing live in
|
|
6
|
+
[`docblocks-react-ui`](https://www.npmjs.com/package/docblocks-react-ui).
|
|
7
|
+
|
|
8
|
+
The engine is built on [Yjs](https://yjs.dev) (a CRDT): documents converge
|
|
9
|
+
across peers even when updates arrive out of order, duplicated, or with
|
|
10
|
+
losses. The only runtime dependencies are `yjs`, `y-protocols`, and `lib0`.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm i docblocks-core
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The package is ESM-only (`import`, not `require`); it stands Node
|
|
19
|
+
`^22.22.2 || ^24.15.0 || >=26.0.0` (the `engines` field).
|
|
20
|
+
|
|
21
|
+
A document is created, edited with commands, and read back as plain JSON —
|
|
22
|
+
every read returns a **snapshot** (a plain object), never a live Yjs
|
|
23
|
+
reference, so mutating a returned block does nothing.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createDoc, fromJSON } from "docblocks-core";
|
|
27
|
+
|
|
28
|
+
const doc = createDoc({ id: "my-doc" });
|
|
29
|
+
|
|
30
|
+
const para = doc.insertBlock("paragraph");
|
|
31
|
+
doc.insertText(para, 0, "hello world");
|
|
32
|
+
doc.setMark(para, 0, 5, { type: "bold" });
|
|
33
|
+
|
|
34
|
+
const json = doc.toJSON(); // { v, meta, blocks, children }
|
|
35
|
+
// …persist or send `json` …
|
|
36
|
+
const restored = fromJSON(json); // a fresh Doc over the same data
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Commands are `Doc` methods. Each command runs inside a transaction — one
|
|
40
|
+
undo step, one wire update. Batch several into a single step with
|
|
41
|
+
`transact`:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
doc.transact(() => {
|
|
45
|
+
const h = doc.insertBlock("heading", { level: 1 }, { after: para });
|
|
46
|
+
doc.insertText(h, 0, "Title");
|
|
47
|
+
}); // one undo step, one wire update
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The block and mark registries drive the command types: `doc.insertBlock`
|
|
51
|
+
accepts only a known block type, and its `attrs` are required exactly when
|
|
52
|
+
the type demands them — `doc.insertBlock("heading", { level: 7 })` is a
|
|
53
|
+
compile-time error. Unknown input throws a typed error
|
|
54
|
+
(`UnknownBlockError`, `InvalidRangeError`, …); commands never no-op
|
|
55
|
+
silently.
|
|
56
|
+
|
|
57
|
+
## The Doc
|
|
58
|
+
|
|
59
|
+
`createDoc(options?)` builds a document with only the root block (an empty
|
|
60
|
+
page — no auto first paragraph; the react-ui editor guarantees the trailing
|
|
61
|
+
editable paragraph). The other entry points:
|
|
62
|
+
|
|
63
|
+
- `fromJSON(json)` — build a `Doc` from a JSON snapshot.
|
|
64
|
+
- `loadDoc(bytes)` — build a `Doc` from an encoded full state (a base
|
|
65
|
+
snapshot; apply diffs with `applyUpdate`).
|
|
66
|
+
- `doc.toJSON()` — the JSON snapshot (`{ v, meta, blocks, children }`).
|
|
67
|
+
- `doc.encode()` / `doc.encodeUpdate(stateVector?)` — the encoded full
|
|
68
|
+
state / the incremental update.
|
|
69
|
+
- `doc.applyUpdate(bytes)` — apply an encoded update (the remote-apply path).
|
|
70
|
+
|
|
71
|
+
`doc.raw` exposes the underlying `Y.Doc`; it is an explicit escape hatch
|
|
72
|
+
and its shape is not guaranteed.
|
|
73
|
+
|
|
74
|
+
## Commands
|
|
75
|
+
|
|
76
|
+
Editing goes through the semantic API (the only path for normal editing).
|
|
77
|
+
The commands translate into `Y.Doc` mutations inside a transaction:
|
|
78
|
+
|
|
79
|
+
- **Blocks** — `insertBlock`, `updateBlock`, `moveBlock`, `removeBlock`,
|
|
80
|
+
`changeBlockType`.
|
|
81
|
+
- **Text** — `insertText`, `deleteText`, `setMark`, `unsetMark`, and the
|
|
82
|
+
range forms (`deleteTextRange`, `setMarkRange`).
|
|
83
|
+
- **Document meta** — `setTitle`, `setAvatar`, `setGeometry`, `setZOrder`.
|
|
84
|
+
- **Database rows** — the row commands (`insertRow`, `setCell`,
|
|
85
|
+
`removeRow`, `moveRow`, …).
|
|
86
|
+
- **Table rows / columns** — `insertTableRow`, `removeTableRow`,
|
|
87
|
+
`insertTableColumn`, `removeTableColumn`, the move / duplicate / color /
|
|
88
|
+
width commands, and `setTableRowColor`.
|
|
89
|
+
|
|
90
|
+
Reads: `getBlock(id)`, `getChildren(id)`, `getRuns(id)`, `getText(id)`,
|
|
91
|
+
`getTextLength(id)`. History: `undo()` / `redo()` (with `canUndo()` /
|
|
92
|
+
`canRedo()`) — one `transact` is one undo step, and a command batched
|
|
93
|
+
into a `transact` is one step across the batch.
|
|
94
|
+
|
|
95
|
+
## Sync
|
|
96
|
+
|
|
97
|
+
`attach(doc, provider)` is the glue between a `Doc` and a
|
|
98
|
+
`SyncProvider`: it bootstraps the session (sending the full local state
|
|
99
|
+
first), then routes local transactions out and remote updates in. `detach()`
|
|
100
|
+
on the returned session tears the session down.
|
|
101
|
+
|
|
102
|
+
`WsSyncProvider` speaks the standard y-protocols wire over a WebSocket
|
|
103
|
+
(wire-compatible with a standard y-websocket / pycrdt server):
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { WsSyncProvider, attach } from "docblocks-core";
|
|
107
|
+
|
|
108
|
+
const provider = new WsSyncProvider("wss://my-server/ws/room-1", {
|
|
109
|
+
token: "…",
|
|
110
|
+
});
|
|
111
|
+
await provider.connect();
|
|
112
|
+
const session = attach(doc, provider);
|
|
113
|
+
// … on teardown:
|
|
114
|
+
session.detach();
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Reactivity: `doc.onChange(handler)` fires for every transaction;
|
|
118
|
+
`doc.onRemoteChange(handler)` fires for non-local-origin transactions only
|
|
119
|
+
(the feed a sync consumer's write-back guards hook into).
|
|
120
|
+
|
|
121
|
+
## Selection
|
|
122
|
+
|
|
123
|
+
Selection is client / view state, not document data — it is not in the JSON
|
|
124
|
+
and not in document updates. `createSelection(doc)` builds a `SelectionView`
|
|
125
|
+
over the doc; other clients' selections travel only in the ephemeral
|
|
126
|
+
awareness channel (the presence payload). `presenceSelectionOf(selection)`
|
|
127
|
+
projects a selection into the presence payload shape.
|
|
128
|
+
|
|
129
|
+
## Data format
|
|
130
|
+
|
|
131
|
+
The JSON format is `v: 1`: `meta` (the doc `id`, `title`, the optional
|
|
132
|
+
`avatar`) plus a **flat block map** (`id → block`) with explicit order via
|
|
133
|
+
`children: [blockId]`; nesting (lists, table cells) is `children`. The text
|
|
134
|
+
of a text block is `content: [{ text, marks }]` runs. JSON ↔ Yjs is
|
|
135
|
+
isomorphic — every JSON field has exactly one Yjs image.
|
|
136
|
+
|
|
137
|
+
The v1 registry covers the page types (`paragraph`, `quote`, `heading`,
|
|
138
|
+
`list-item`, `callout`, `code`, `divider`, `image`, `embed`, `table` /
|
|
139
|
+
`table-row` / `table-cell`, `database`), the five canvas types (`surface`,
|
|
140
|
+
`shape`, `connector`, `frame`, `canvas-text`), and `root`; the v1 marks
|
|
141
|
+
are `bold`, `italic`, `strike`, `underline`, `code` (exclusive), `link`,
|
|
142
|
+
`color`, `highlight`, and `mention`. The type lists stand exported
|
|
143
|
+
(`PAGE_BLOCK_TYPES`, `LEAF_BLOCK_TYPES`, `BLOCK_REGISTRY`,
|
|
144
|
+
`MARK_REGISTRY`, `CALLOUT_ICONS`).
|
|
145
|
+
|
|
146
|
+
The block and mark registries are the extension point. Adding a block type
|
|
147
|
+
or mark is additive (a minor): the registry entry plus a renderer; the
|
|
148
|
+
validator and the commands stay registry-driven. The reader tolerates the
|
|
149
|
+
unknown (an unknown block type renders a placeholder, an unknown mark is
|
|
150
|
+
ignored and its text stays); the validator is strict (`validateDoc` reports
|
|
151
|
+
only known types and marks — it protects the data you own). A breaking
|
|
152
|
+
change is a major: the reader rejects the unknown major, and a migration
|
|
153
|
+
helper converts the data. `flattenDoc(json)` normalizes the legacy nested
|
|
154
|
+
shape into the flat page model at load.
|
|
155
|
+
|
|
156
|
+
## Validation
|
|
157
|
+
|
|
158
|
+
`validateDoc(json, lenient?)` returns the list of validation errors (empty
|
|
159
|
+
is valid). Strict (the default) is the writer's contract — it checks the
|
|
160
|
+
known types and mark values. Lenient is the reader boundary — unknown types
|
|
161
|
+
and marks pass through (the reader preserves them), so a reader of `v1.0`
|
|
162
|
+
can safely load `v1.5` data.
|
|
163
|
+
|
|
164
|
+
## Non-goals
|
|
165
|
+
|
|
166
|
+
This package is deliberately headless. It does not ship a UI (that is
|
|
167
|
+
`docblocks-react-ui`), a server (any Yjs-compatible server is the receiving
|
|
168
|
+
side), or blob resolution (the consumer provides a `BlobStore`). It holds no
|
|
169
|
+
persistence — snapshot and update storage is a consumer-side concern.
|