@reiwuzen/blocky 1.0.0

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/README.md ADDED
@@ -0,0 +1,250 @@
1
+ # @reiwuzen/blocky
2
+
3
+ Pure TypeScript block editor engine. No UI, no framework, no opinions on rendering.
4
+
5
+ Handles everything an editor needs at the data layer — content mutation, formatting, markdown shortcuts, serialization, and history. Bring your own renderer.
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @reiwuzen/blocky
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Concepts
18
+
19
+ ### Block
20
+
21
+ ```ts
22
+ type Block<T extends BlockType> = {
23
+ id: string;
24
+ type: T;
25
+ meta: BlockMeta<T>;
26
+ content: BlockContent<T>;
27
+ }
28
+ ```
29
+
30
+ ### Block Types
31
+
32
+ | Type | Content | Meta |
33
+ |------|---------|------|
34
+ | `paragraph` | `TextNode[]` | — |
35
+ | `heading1` / `heading2` / `heading3` | `TextNode[]` | — |
36
+ | `bullet` | `TextNode[]` | `{ depth: number }` |
37
+ | `number` | `TextNode[]` | `{ depth: number }` |
38
+ | `todo` | `TextNode[]` | `{ depth: number, checked?: true }` |
39
+ | `code` | `[CodeNode]` | `{ language?: string }` |
40
+ | `equation` | `[EquationNode]` | — |
41
+
42
+ ### Node
43
+
44
+ ```ts
45
+ type Node =
46
+ | {
47
+ type: "text";
48
+ text: string;
49
+ bold?: true;
50
+ italic?: true;
51
+ underline?: true;
52
+ strikethrough?: true;
53
+ highlighted?: "yellow" | "green";
54
+ color?: "red" | "blue" | "green";
55
+ link?: string;
56
+ }
57
+ | { type: "code"; text: string }
58
+ | { type: "equation"; latex: string }
59
+ ```
60
+
61
+ Rich blocks hold `TextNode[]` and can contain inline `code` and `equation` nodes.
62
+ Leaf blocks (`code`, `equation`) always hold a single-element tuple `[Node]`.
63
+
64
+ ---
65
+
66
+ ## Result
67
+
68
+ All engine functions return `Result<T>` from [`@reiwuzen/result`](https://npmjs.com/package/@reiwuzen/result) — no silent failures, no thrown exceptions.
69
+
70
+ ```ts
71
+ result.match(
72
+ (value) => { /* success */ },
73
+ (error) => { /* failure, error is a string */ }
74
+ )
75
+
76
+ // chaining
77
+ deleteRange(block, 0, 0, 0, 5)
78
+ .andThen((content) => insertAt({ ...block, content }, 0, 0, incoming))
79
+ .match(...)
80
+ ```
81
+
82
+ ---
83
+
84
+ ## API
85
+
86
+ ### Content — `engine/content`
87
+
88
+ ```ts
89
+ // Insert a node at any position — end, start, or mid-node
90
+ insertAt(block, nodeIndex, offset, incoming): Result<BlockContent<T>>
91
+
92
+ // Delete the last character from the last node
93
+ deleteLastChar(block): Result<BlockContent>
94
+
95
+ // Delete a selected range
96
+ deleteRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset): Result<BlockContent<T>>
97
+
98
+ // Replace a selected range with a node — atomic deleteRange + insertAt
99
+ replaceRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset, incoming): Result<BlockContent<T>>
100
+
101
+ // Split a block at cursor position → [original, newParagraph]
102
+ splitBlock(block, nodeIndex, offset): Result<[AnyBlock, AnyBlock]>
103
+
104
+ // Merge blockB content into blockA
105
+ mergeBlocks(blockA, blockB): Result<AnyBlock>
106
+ ```
107
+
108
+ ### Format — `engine/format`
109
+
110
+ ```ts
111
+ type NodeSelection = {
112
+ startIndex: number;
113
+ startOffset: number;
114
+ endIndex: number;
115
+ endOffset: number; // exclusive
116
+ }
117
+
118
+ toggleBold(nodes, selection): Result<Node[]>
119
+ toggleItalic(nodes, selection): Result<Node[]>
120
+ toggleUnderline(nodes, selection): Result<Node[]>
121
+ toggleStrikethrough(nodes, selection): Result<Node[]>
122
+ toggleHighlight(nodes, selection, "yellow" | "green"): Result<Node[]>
123
+ toggleColor(nodes, selection, "red" | "blue" | "green"): Result<Node[]>
124
+ setLink(nodes, selection, href): Result<Node[]>
125
+ removeLink(nodes, selection): Result<Node[]>
126
+ ```
127
+
128
+ Auto-detects toggle — if all selected nodes already have the format, it removes it.
129
+
130
+ ### Transform — `engine/transform`
131
+
132
+ ```ts
133
+ // Call on every space keypress — converts paragraph to another type
134
+ // if content starts with a markdown shortcut at position 0
135
+ applyMarkdownTransform(block, cursorOffset): Result<{ block, converted: boolean }>
136
+
137
+ // Convert a block to a new type, preserving content where possible
138
+ changeBlockType(block, targetType): Result<Block<T>>
139
+
140
+ // Toggle checked state on a todo block
141
+ toggleTodo(block): Result<Block<"todo">>
142
+
143
+ // Increase / decrease depth for bullet, number, todo (max depth: 6)
144
+ indentBlock(block): Result<IndentableBlock>
145
+ outdentBlock(block): Result<IndentableBlock>
146
+ ```
147
+
148
+ **Markdown shortcuts:**
149
+
150
+ | Typed | Result |
151
+ |-------|--------|
152
+ | `- ` | `bullet` |
153
+ | `1. ` | `number` |
154
+ | `[] ` | `todo` |
155
+ | `# ` | `heading1` |
156
+ | `## ` | `heading2` |
157
+ | `### ` | `heading3` |
158
+
159
+ ### Serializer — `engine/serializer`
160
+
161
+ ```ts
162
+ // Blocks ↔ JSON
163
+ serialize(blocks): Result<string>
164
+ deserialize(json): Result<AnyBlock[]>
165
+
166
+ // Nodes ↔ JSON (clipboard)
167
+ serializeNodes(nodes): Result<string>
168
+ deserializeNodes(json): Result<Node[]>
169
+
170
+ // Plain text extraction
171
+ toPlainText(nodes): string
172
+
173
+ // Blocks → markdown string
174
+ toMarkdown(blocks): string
175
+ ```
176
+
177
+ ### History — `engine/history`
178
+
179
+ Pure functions — no classes, no mutation.
180
+
181
+ ```ts
182
+ createHistory(initialBlocks): History
183
+ push(history, blocks, maxSize?): History // default maxSize: 100
184
+ undo(history): Result<History>
185
+ redo(history): Result<History>
186
+ canUndo(history): boolean
187
+ canRedo(history): boolean
188
+ currentBlocks(history): AnyBlock[]
189
+ ```
190
+
191
+ ```ts
192
+ // Typical usage
193
+ let h = createHistory(initialBlocks);
194
+
195
+ // after every engine operation
196
+ h = push(h, newBlocks);
197
+
198
+ // undo / redo
199
+ undo(h).match(
200
+ (h2) => { h = h2; render(currentBlocks(h)); },
201
+ (e) => console.error(e)
202
+ );
203
+ ```
204
+
205
+ ### Utils — `utils/block`
206
+
207
+ ```ts
208
+ generateId(fn?): string
209
+ createBlock(type, idFn?): Result<Block<T>>
210
+ insertBlockAfter(blocks, afterId, type, idFn?): Result<{ blocks, newId }>
211
+ deleteBlock(blocks, id): { blocks, prevId }
212
+ duplicateBlock(block, newId): AnyBlock
213
+ moveBlock(blocks, id, "up" | "down"): Result<AnyBlock[]>
214
+ ```
215
+
216
+ ---
217
+
218
+ ## Package Structure
219
+
220
+ ```
221
+ src/
222
+ ├── index.ts
223
+ ├── types/
224
+ │ └── block.ts
225
+ └── engine/
226
+ ├── content.ts ← insertAt, deleteLastChar, deleteRange, replaceRange, splitBlock, mergeBlocks
227
+ ├── format.ts ← toggleBold, toggleItalic, toggleColor, setLink, ...
228
+ ├── transform.ts ← applyMarkdownTransform, changeBlockType, toggleTodo, indent/outdent
229
+ ├── serializer.ts ← serialize, deserialize, toMarkdown, toPlainText, ...
230
+ └── history.ts ← createHistory, push, undo, redo
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Local Development
236
+
237
+ ```bash
238
+ # in /@reiwuzen/blocky
239
+ npm install
240
+ npm run dev
241
+
242
+ # in your project
243
+ "dependencies": { "@reiwuzen/blocky": "../@reiwuzen/blocky" }
244
+ ```
245
+
246
+ ---
247
+
248
+ ## License
249
+
250
+ MIT
@@ -0,0 +1,46 @@
1
+ import type { Node, AnyBlock, BlockContent, BlockType } from '../types/block';
2
+ import { Result } from '@reiwuzen/result';
3
+ /**
4
+ * Insert a Node at a specific position within block content.
5
+ *
6
+ * - nodeIndex: which node to insert into
7
+ * - offset: char position within that node
8
+ *
9
+ * End-of-node (offset === length) naturally becomes an append —
10
+ * sliceNode produces no right half, incoming merges with left or gets pushed.
11
+ * No separate append function needed.
12
+ */
13
+ export declare function insertAt<T extends BlockType>(block: AnyBlock, nodeIndex: number, offset: number, incoming: Node): Result<BlockContent<T>>;
14
+ export declare function deleteLastChar(block: AnyBlock): Result<BlockContent<BlockType>>;
15
+ /**
16
+ * Delete content across a selection.
17
+ * After deletion, left and right boundaries are merged if formats match.
18
+ *
19
+ * For code/equation blocks: deletes within the single tuple node's text/latex.
20
+ */
21
+ export declare function deleteRange<T extends BlockType>(block: AnyBlock, startNodeIndex: number, startOffset: number, endNodeIndex: number, endOffset: number): Result<BlockContent<T>>;
22
+ /**
23
+ * Split a block at a given position into two blocks.
24
+ * The original block keeps content before the cursor.
25
+ * A new block gets content after the cursor — always of type "paragraph".
26
+ *
27
+ * Not supported for code/equation blocks — returns Err.
28
+ */
29
+ export declare function splitBlock(block: AnyBlock, nodeIndex: number, offset: number): Result<[AnyBlock, AnyBlock]>;
30
+ /**
31
+ * Merge blockB into blockA — blockB's content is appended to blockA's content.
32
+ * blockA's type and meta are preserved.
33
+ *
34
+ * Not supported if either block is code or equation — returns Err.
35
+ */
36
+ export declare function mergeBlocks(blockA: AnyBlock, blockB: AnyBlock): Result<AnyBlock>;
37
+ /**
38
+ * Replace a selected range with an incoming Node — atomic deleteRange + insertAt.
39
+ * This is what fires when the user has a selection and types a character.
40
+ *
41
+ * Internally chains:
42
+ * 1. deleteRange — remove selected content
43
+ * 2. insertAt — insert incoming at the start of the deleted range
44
+ */
45
+ export declare function replaceRange<T extends BlockType>(block: AnyBlock, startNodeIndex: number, startOffset: number, endNodeIndex: number, endOffset: number, incoming: Node): Result<BlockContent<T>>;
46
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/engine/content.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE9E,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAmD1C;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,SAAS,EAC1C,KAAK,EAAE,QAAQ,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,IAAI,GACb,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CA4EzB;AAID,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAgC/E;AAKD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,SAAS,EAC7C,KAAK,EAAE,QAAQ,EACf,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CA8DzB;AAID;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,QAAQ,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,MAAM,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAwC9B;AAID;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,QAAQ,EAChB,MAAM,EAAE,QAAQ,GACf,MAAM,CAAC,QAAQ,CAAC,CAsBlB;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,SAAS,EAC9C,KAAK,EAAE,QAAQ,EACf,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,IAAI,GACb,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAUzB"}
@@ -0,0 +1,317 @@
1
+ import { generateId } from '../utils/block';
2
+ import { Result } from '@reiwuzen/result';
3
+ import { formatsMatch } from './format';
4
+ // ─── Helpers ───────────────────────────────────────────────────────────────────
5
+ function isClean(node) {
6
+ return (node.bold === undefined &&
7
+ node.italic === undefined &&
8
+ node.underline === undefined &&
9
+ node.strikethrough === undefined &&
10
+ node.highlighted === undefined &&
11
+ node.color === undefined &&
12
+ node.link === undefined);
13
+ }
14
+ function getTextLength(node) {
15
+ if (node.type === "text" || node.type === "code")
16
+ return node.text.length;
17
+ if (node.type === "equation")
18
+ return node.latex.length;
19
+ return 0;
20
+ }
21
+ function tryMerge(a, b) {
22
+ if (a.type !== b.type)
23
+ return null;
24
+ if (a.type === "code" && b.type === "code")
25
+ return { ...a, text: a.text + b.text };
26
+ if (a.type === "equation" && b.type === "equation")
27
+ return { ...a, latex: a.latex + b.latex };
28
+ if (a.type === "text" && b.type === "text") {
29
+ if ((isClean(a) && isClean(b)) || formatsMatch(a, b))
30
+ return { ...a, text: a.text + b.text };
31
+ }
32
+ return null;
33
+ }
34
+ function sliceNode(node, start, end) {
35
+ if (start >= end)
36
+ return null;
37
+ if (node.type === "text") {
38
+ const text = node.text.slice(start, end);
39
+ return text.length ? { ...node, text } : null;
40
+ }
41
+ if (node.type === "code") {
42
+ const text = node.text.slice(start, end);
43
+ return text.length ? { ...node, text } : null;
44
+ }
45
+ if (node.type === "equation") {
46
+ const latex = node.latex.slice(start, end);
47
+ return latex.length ? { ...node, latex } : null;
48
+ }
49
+ return null;
50
+ }
51
+ // ─── insertAt (sole public API) ────────────────────────────────────────────────
52
+ /**
53
+ * Insert a Node at a specific position within block content.
54
+ *
55
+ * - nodeIndex: which node to insert into
56
+ * - offset: char position within that node
57
+ *
58
+ * End-of-node (offset === length) naturally becomes an append —
59
+ * sliceNode produces no right half, incoming merges with left or gets pushed.
60
+ * No separate append function needed.
61
+ */
62
+ export function insertAt(block, nodeIndex, offset, incoming) {
63
+ // ── code block ────────────────────────────────────────────────────────────
64
+ if (block.type === "code") {
65
+ if (incoming.type !== "code")
66
+ return Result.Err(`code block only accepts a code node, got "${incoming.type}"`);
67
+ const node = block.content[0];
68
+ if (offset < 0 || offset > node.text.length)
69
+ return Result.Err(`offset (${offset}) out of bounds for code node`);
70
+ const text = node.text.slice(0, offset) + incoming.text + node.text.slice(offset);
71
+ return Result.Ok([{ ...node, text }]);
72
+ }
73
+ // ── equation block ────────────────────────────────────────────────────────
74
+ if (block.type === "equation") {
75
+ if (incoming.type !== "equation")
76
+ return Result.Err(`equation block only accepts an equation node, got "${incoming.type}"`);
77
+ const node = block.content[0];
78
+ if (offset < 0 || offset > node.latex.length)
79
+ return Result.Err(`offset (${offset}) out of bounds for equation node`);
80
+ const latex = node.latex.slice(0, offset) + incoming.latex + node.latex.slice(offset);
81
+ return Result.Ok([{ ...node, latex }]);
82
+ }
83
+ // ── rich blocks ───────────────────────────────────────────────────────────
84
+ const content = block.content;
85
+ // Empty content — just push
86
+ if (content.length === 0) {
87
+ return Result.Ok([incoming]);
88
+ }
89
+ if (nodeIndex < 0 || nodeIndex >= content.length)
90
+ return Result.Err(`nodeIndex (${nodeIndex}) out of bounds (length=${content.length})`);
91
+ const target = content[nodeIndex];
92
+ const targetLen = getTextLength(target);
93
+ if (offset < 0 || offset > targetLen)
94
+ return Result.Err(`offset (${offset}) out of bounds for node at index ${nodeIndex}`);
95
+ const before = content.slice(0, nodeIndex);
96
+ const after = content.slice(nodeIndex + 1);
97
+ const middle = [];
98
+ // Left half of split — empty when offset=0
99
+ const left = sliceNode(target, 0, offset);
100
+ if (left)
101
+ middle.push(left);
102
+ // Incoming — try merge with left half
103
+ if (middle.length > 0) {
104
+ const merged = tryMerge(middle[middle.length - 1], incoming);
105
+ if (merged)
106
+ middle[middle.length - 1] = merged;
107
+ else
108
+ middle.push(incoming);
109
+ }
110
+ else {
111
+ middle.push(incoming);
112
+ }
113
+ // Right half — empty when offset=targetLen (end of node → natural append)
114
+ const right = sliceNode(target, offset, targetLen);
115
+ if (right) {
116
+ const merged = tryMerge(middle[middle.length - 1], right);
117
+ if (merged)
118
+ middle[middle.length - 1] = merged;
119
+ else
120
+ middle.push(right);
121
+ }
122
+ // Merge across all boundaries
123
+ const result = [];
124
+ for (const node of [...before, ...middle, ...after]) {
125
+ const prev = result[result.length - 1];
126
+ const merged = prev ? tryMerge(prev, node) : null;
127
+ if (merged)
128
+ result[result.length - 1] = merged;
129
+ else
130
+ result.push(node);
131
+ }
132
+ return Result.Ok(result);
133
+ }
134
+ // ─── deleteLastChar ────────────────────────────────────────────────────────────
135
+ export function deleteLastChar(block) {
136
+ if (block.type === "code") {
137
+ const node = block.content[0];
138
+ if (!node.text.length)
139
+ return Result.Err("Nothing to delete");
140
+ return Result.Ok([{ ...node, text: node.text.slice(0, -1) }]);
141
+ }
142
+ if (block.type === "equation") {
143
+ const node = block.content[0];
144
+ if (!node.latex.length)
145
+ return Result.Err("Nothing to delete");
146
+ return Result.Ok([{ ...node, latex: node.latex.slice(0, -1) }]);
147
+ }
148
+ const next = [...block.content];
149
+ for (let i = next.length - 1; i >= 0; i--) {
150
+ const node = next[i];
151
+ if (node.type === "text" || node.type === "code") {
152
+ const trimmed = node.text.slice(0, -1);
153
+ if (!trimmed.length)
154
+ next.splice(i, 1);
155
+ else
156
+ next[i] = { ...node, text: trimmed };
157
+ return Result.Ok(next);
158
+ }
159
+ if (node.type === "equation") {
160
+ const trimmed = node.latex.slice(0, -1);
161
+ if (!trimmed.length)
162
+ next.splice(i, 1);
163
+ else
164
+ next[i] = { ...node, latex: trimmed };
165
+ return Result.Ok(next);
166
+ }
167
+ }
168
+ return Result.Err("Nothing to delete");
169
+ }
170
+ // ─── deleteRange ───────────────────────────────────────────────────────────────
171
+ /**
172
+ * Delete content across a selection.
173
+ * After deletion, left and right boundaries are merged if formats match.
174
+ *
175
+ * For code/equation blocks: deletes within the single tuple node's text/latex.
176
+ */
177
+ export function deleteRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset) {
178
+ // ── code block ────────────────────────────────────────────────────────────
179
+ if (block.type === "code") {
180
+ const node = block.content[0];
181
+ if (startOffset < 0 || endOffset > node.text.length || startOffset > endOffset)
182
+ return Result.Err(`invalid range [${startOffset}, ${endOffset}] for code node`);
183
+ const text = node.text.slice(0, startOffset) + node.text.slice(endOffset);
184
+ return Result.Ok([{ ...node, text }]);
185
+ }
186
+ // ── equation block ────────────────────────────────────────────────────────
187
+ if (block.type === "equation") {
188
+ const node = block.content[0];
189
+ if (startOffset < 0 || endOffset > node.latex.length || startOffset > endOffset)
190
+ return Result.Err(`invalid range [${startOffset}, ${endOffset}] for equation node`);
191
+ const latex = node.latex.slice(0, startOffset) + node.latex.slice(endOffset);
192
+ return Result.Ok([{ ...node, latex }]);
193
+ }
194
+ // ── rich blocks ───────────────────────────────────────────────────────────
195
+ const nodes = block.content;
196
+ if (startNodeIndex < 0 || endNodeIndex >= nodes.length)
197
+ return Result.Err(`node indices [${startNodeIndex}, ${endNodeIndex}] out of bounds`);
198
+ if (startNodeIndex > endNodeIndex)
199
+ return Result.Err(`startNodeIndex (${startNodeIndex}) > endNodeIndex (${endNodeIndex})`);
200
+ const startNode = nodes[startNodeIndex];
201
+ const endNode = nodes[endNodeIndex];
202
+ if (startOffset < 0 || startOffset > getTextLength(startNode))
203
+ return Result.Err(`startOffset (${startOffset}) out of bounds`);
204
+ if (endOffset < 0 || endOffset > getTextLength(endNode))
205
+ return Result.Err(`endOffset (${endOffset}) out of bounds`);
206
+ const before = nodes.slice(0, startNodeIndex);
207
+ const after = nodes.slice(endNodeIndex + 1);
208
+ const middle = [];
209
+ // Keep left part of start node
210
+ const left = sliceNode(startNode, 0, startOffset);
211
+ if (left)
212
+ middle.push(left);
213
+ // Keep right part of end node
214
+ const right = sliceNode(endNode, endOffset, getTextLength(endNode));
215
+ if (right) {
216
+ const merged = middle.length > 0 ? tryMerge(middle[middle.length - 1], right) : null;
217
+ if (merged)
218
+ middle[middle.length - 1] = merged;
219
+ else
220
+ middle.push(right);
221
+ }
222
+ // Merge all boundaries
223
+ const result = [];
224
+ for (const node of [...before, ...middle, ...after]) {
225
+ const prev = result[result.length - 1];
226
+ const merged = prev ? tryMerge(prev, node) : null;
227
+ if (merged)
228
+ result[result.length - 1] = merged;
229
+ else
230
+ result.push(node);
231
+ }
232
+ return Result.Ok(result);
233
+ }
234
+ // ─── splitBlock ────────────────────────────────────────────────────────────────
235
+ /**
236
+ * Split a block at a given position into two blocks.
237
+ * The original block keeps content before the cursor.
238
+ * A new block gets content after the cursor — always of type "paragraph".
239
+ *
240
+ * Not supported for code/equation blocks — returns Err.
241
+ */
242
+ export function splitBlock(block, nodeIndex, offset) {
243
+ var _a;
244
+ if (block.type === "code" || block.type === "equation")
245
+ return Result.Err(`splitBlock is not supported for "${block.type}" blocks`);
246
+ const nodes = block.content;
247
+ if (nodes.length > 0 && (nodeIndex < 0 || nodeIndex >= nodes.length))
248
+ return Result.Err(`nodeIndex (${nodeIndex}) out of bounds`);
249
+ const target = (_a = nodes[nodeIndex]) !== null && _a !== void 0 ? _a : null;
250
+ const targetLen = target ? getTextLength(target) : 0;
251
+ if (offset < 0 || offset > targetLen)
252
+ return Result.Err(`offset (${offset}) out of bounds`);
253
+ // Content before cursor stays in original block
254
+ const beforeNodes = [
255
+ ...nodes.slice(0, nodeIndex),
256
+ ...(target && offset > 0 ? [sliceNode(target, 0, offset)].filter(Boolean) : []),
257
+ ];
258
+ // Content after cursor goes to new block
259
+ const afterNodes = [
260
+ ...(target && offset < targetLen ? [sliceNode(target, offset, targetLen)].filter(Boolean) : []),
261
+ ...nodes.slice(nodeIndex + 1),
262
+ ];
263
+ const original = {
264
+ ...block,
265
+ content: beforeNodes,
266
+ };
267
+ const newBlock = {
268
+ id: generateId(),
269
+ type: "paragraph",
270
+ content: afterNodes,
271
+ meta: {},
272
+ };
273
+ return Result.Ok([original, newBlock]);
274
+ }
275
+ // ─── mergeBlocks ───────────────────────────────────────────────────────────────
276
+ /**
277
+ * Merge blockB into blockA — blockB's content is appended to blockA's content.
278
+ * blockA's type and meta are preserved.
279
+ *
280
+ * Not supported if either block is code or equation — returns Err.
281
+ */
282
+ export function mergeBlocks(blockA, blockB) {
283
+ if (blockA.type === "code" || blockA.type === "equation")
284
+ return Result.Err(`mergeBlocks: blockA cannot be of type "${blockA.type}"`);
285
+ if (blockB.type === "code" || blockB.type === "equation")
286
+ return Result.Err(`mergeBlocks: blockB cannot be of type "${blockB.type}"`);
287
+ const nodesA = blockA.content;
288
+ const nodesB = blockB.content;
289
+ // Merge boundaries between the two arrays
290
+ const result = [...nodesA];
291
+ for (const node of nodesB) {
292
+ const prev = result[result.length - 1];
293
+ const merged = prev ? tryMerge(prev, node) : null;
294
+ if (merged)
295
+ result[result.length - 1] = merged;
296
+ else
297
+ result.push(node);
298
+ }
299
+ return Result.Ok({
300
+ ...blockA,
301
+ content: result,
302
+ });
303
+ }
304
+ // ─── replaceRange ──────────────────────────────────────────────────────────────
305
+ /**
306
+ * Replace a selected range with an incoming Node — atomic deleteRange + insertAt.
307
+ * This is what fires when the user has a selection and types a character.
308
+ *
309
+ * Internally chains:
310
+ * 1. deleteRange — remove selected content
311
+ * 2. insertAt — insert incoming at the start of the deleted range
312
+ */
313
+ export function replaceRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset, incoming) {
314
+ return deleteRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset)
315
+ .andThen((content) => insertAt({ ...block, content }, startNodeIndex, startOffset, incoming));
316
+ }
317
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/engine/content.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { Node } from '../types/block';
2
+ import { Result } from '@reiwuzen/result';
3
+ type TextNode = Extract<Node, {
4
+ type: "text";
5
+ }>;
6
+ type TextFormat = Pick<TextNode, "bold" | "italic" | "underline" | "strikethrough" | "highlighted" | "color" | "link">;
7
+ export type NodeSelection = {
8
+ startIndex: number;
9
+ startOffset: number;
10
+ endIndex: number;
11
+ endOffset: number;
12
+ };
13
+ export declare function isTextNode(node: Node): node is TextNode;
14
+ export declare function formatNodes(nodes: Node[], sel: NodeSelection, format: keyof TextFormat, value?: unknown): Result<Node[]>;
15
+ export declare function mergeAdjacentNodes(nodes: Node[]): Node[];
16
+ export declare function formatsMatch(a: TextNode, b: TextNode): boolean;
17
+ export declare const toggleBold: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
18
+ export declare const toggleItalic: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
19
+ export declare const toggleUnderline: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
20
+ export declare const toggleStrikethrough: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
21
+ export declare const toggleHighlight: (nodes: Node[], sel: NodeSelection, color?: "yellow" | "green") => Result<Node[], string>;
22
+ export declare const toggleColor: (nodes: Node[], sel: NodeSelection, color: "red" | "blue" | "green") => Result<Node[], string>;
23
+ export declare const setLink: (nodes: Node[], sel: NodeSelection, href: string) => Result<Node[], string>;
24
+ export declare const removeLink: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
25
+ export {};
26
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/engine/format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAI1C,KAAK,QAAQ,GAAG,OAAO,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEhD,KAAK,UAAU,GAAG,IAAI,CACpB,QAAQ,EACR,MAAM,GAAG,QAAQ,GAAG,WAAW,GAAG,eAAe,GAAG,aAAa,GAAG,OAAO,GAAG,MAAM,CACrF,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AA4CF,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,QAAQ,CAEvD;AAiDD,wBAAgB,WAAW,CACzB,KAAK,EAAE,IAAI,EAAE,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,UAAU,EACxB,KAAK,GAAE,OAAc,GACpB,MAAM,CAAC,IAAI,EAAE,CAAC,CAqChB;AAID,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,CAWxD;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAK9D;AAID,eAAO,MAAM,UAAU,GAAI,OAAO,IAAI,EAAE,EAAE,KAAK,aAAa,2BAC3B,CAAC;AAElC,eAAO,MAAM,YAAY,GAAI,OAAO,IAAI,EAAE,EAAE,KAAK,aAAa,2BAC3B,CAAC;AAEpC,eAAO,MAAM,eAAe,GAAI,OAAO,IAAI,EAAE,EAAE,KAAK,aAAa,2BAC3B,CAAC;AAEvC,eAAO,MAAM,mBAAmB,GAAI,OAAO,IAAI,EAAE,EAAE,KAAK,aAAa,2BAC3B,CAAC;AAE3C,eAAO,MAAM,eAAe,GAC1B,OAAO,IAAI,EAAE,EACb,KAAK,aAAa,EAClB,QAAO,QAAQ,GAAG,OAAkB,2BACY,CAAC;AAEnD,eAAO,MAAM,WAAW,GACtB,OAAO,IAAI,EAAE,EACb,KAAK,aAAa,EAClB,OAAO,KAAK,GAAG,MAAM,GAAG,OAAO,2BACW,CAAC;AAE7C,eAAO,MAAM,OAAO,GAAI,OAAO,IAAI,EAAE,EAAE,KAAK,aAAa,EAAE,MAAM,MAAM,2BAChC,CAAC;AAExC,eAAO,MAAM,UAAU,GAAI,OAAO,IAAI,EAAE,EAAE,KAAK,aAAa,2BAChB,CAAC"}