@reiwuzen/blocky 1.1.2 → 1.3.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.
Files changed (47) hide show
  1. package/dist/index.cjs +985 -0
  2. package/dist/index.d.cts +314 -0
  3. package/dist/index.d.ts +314 -12
  4. package/dist/index.js +910 -16
  5. package/package.json +32 -25
  6. package/dist/engine/block.d.ts +0 -13
  7. package/dist/engine/block.d.ts.map +0 -1
  8. package/dist/engine/block.js +0 -26
  9. package/dist/engine/block.js.map +0 -1
  10. package/dist/engine/content.d.ts +0 -46
  11. package/dist/engine/content.d.ts.map +0 -1
  12. package/dist/engine/content.js +0 -317
  13. package/dist/engine/content.js.map +0 -1
  14. package/dist/engine/cursor.d.ts +0 -38
  15. package/dist/engine/cursor.d.ts.map +0 -1
  16. package/dist/engine/cursor.js +0 -90
  17. package/dist/engine/cursor.js.map +0 -1
  18. package/dist/engine/format.d.ts +0 -26
  19. package/dist/engine/format.d.ts.map +0 -1
  20. package/dist/engine/format.js +0 -116
  21. package/dist/engine/format.js.map +0 -1
  22. package/dist/engine/history.d.ts +0 -35
  23. package/dist/engine/history.d.ts.map +0 -1
  24. package/dist/engine/history.js +0 -62
  25. package/dist/engine/history.js.map +0 -1
  26. package/dist/engine/serializer.d.ts +0 -46
  27. package/dist/engine/serializer.d.ts.map +0 -1
  28. package/dist/engine/serializer.js +0 -205
  29. package/dist/engine/serializer.js.map +0 -1
  30. package/dist/engine/transform.d.ts +0 -47
  31. package/dist/engine/transform.d.ts.map +0 -1
  32. package/dist/engine/transform.js +0 -195
  33. package/dist/engine/transform.js.map +0 -1
  34. package/dist/index.d.ts.map +0 -1
  35. package/dist/index.js.map +0 -1
  36. package/dist/types/block.d.ts +0 -44
  37. package/dist/types/block.d.ts.map +0 -1
  38. package/dist/types/block.js +0 -2
  39. package/dist/types/block.js.map +0 -1
  40. package/dist/types/editor.d.ts +0 -1
  41. package/dist/types/editor.d.ts.map +0 -1
  42. package/dist/types/editor.js +0 -2
  43. package/dist/types/editor.js.map +0 -1
  44. package/dist/utils/block.d.ts +0 -32
  45. package/dist/utils/block.d.ts.map +0 -1
  46. package/dist/utils/block.js +0 -97
  47. package/dist/utils/block.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,16 +1,910 @@
1
- // ─── Content ───────────────────────────────────────────────────────────────────
2
- export { insertAt, deleteLastChar, deleteRange, replaceRange, splitBlock, mergeBlocks } from "./engine/content";
3
- // ─── Format ────────────────────────────────────────────────────────────────────
4
- export { formatNodes, toggleBold, toggleItalic, toggleUnderline, toggleStrikethrough, toggleHighlight, toggleColor, setLink, removeLink, mergeAdjacentNodes } from "./engine/format";
5
- // ─── Transform ─────────────────────────────────────────────────────────────────
6
- export { applyMarkdownTransform, changeBlockType, toggleTodo, indentBlock, outdentBlock } from "./engine/transform";
7
- // ─── Serializer ────────────────────────────────────────────────────────────────
8
- export { serialize, deserialize, serializeNodes, deserializeNodes, toPlainText, toMarkdown } from "./engine/serializer";
9
- // ─── Cursor ───────────────────────────────────────────────────────────────────
10
- export { flatToPosition, flatToSelection, positionToFlat, } from './engine/cursor';
11
- // ─── History ───────────────────────────────────────────────────────────────────
12
- export { createHistory, push, undo, redo, canUndo, canRedo, currentBlocks } from "./engine/history";
13
- // ─── Utils ─────────────────────────────────────────────────────────────────────
14
- export { generateId, createBlock, insertBlockAfter, deleteBlock, duplicateBlock, moveBlock } from "./utils/block";
15
- export { blockDeleteLastChar, blockDeleteRange, blockInsertAt, blockReplaceRange } from './engine/block';
16
- //# sourceMappingURL=index.js.map
1
+ // src/utils/block.ts
2
+ import { Result } from "@reiwuzen/result";
3
+ import { v7 } from "uuid";
4
+ function generateId(fn) {
5
+ return fn ? fn() : v7();
6
+ }
7
+ var defaultContent = {
8
+ paragraph: [],
9
+ heading1: [],
10
+ heading2: [],
11
+ heading3: [],
12
+ bullet: [],
13
+ number: [],
14
+ todo: [],
15
+ code: [{ type: "code", text: "" }],
16
+ equation: [{ type: "equation", latex: "" }]
17
+ };
18
+ var defaultMeta = {
19
+ paragraph: {},
20
+ heading1: {},
21
+ heading2: {},
22
+ heading3: {},
23
+ bullet: { depth: 0 },
24
+ number: { depth: 0 },
25
+ todo: { depth: 0 },
26
+ code: {},
27
+ equation: {}
28
+ };
29
+ function createBlock(type, idFn) {
30
+ return Result.try(() => {
31
+ const block = {
32
+ id: generateId(idFn),
33
+ type,
34
+ meta: defaultMeta[type],
35
+ content: defaultContent[type]
36
+ };
37
+ return block;
38
+ });
39
+ }
40
+ function createBlockAfter(blocks, afterId, type, idFn) {
41
+ return createBlock(type, idFn).andThen((newBlock) => {
42
+ const index = blocks.findIndex((b) => b.id === afterId);
43
+ if (index === -1) return Result.Err(`[BlockNotFound]: ${afterId}`);
44
+ const next = [...blocks];
45
+ next.splice(index + 1, 0, newBlock);
46
+ return Result.Ok({ blocks: next, newId: newBlock.id });
47
+ });
48
+ }
49
+ function insertBlockAfter(blocks, afterId, insertBlock) {
50
+ const targetBlockIndex = blocks.findIndex((b) => b.id === afterId);
51
+ if (targetBlockIndex === -1) {
52
+ return Result.Err(`No block found with id: ${afterId}`);
53
+ }
54
+ const newBlocks = [
55
+ ...blocks.slice(0, targetBlockIndex + 1),
56
+ insertBlock,
57
+ ...blocks.slice(targetBlockIndex + 1)
58
+ ];
59
+ return Result.Ok({
60
+ blocks: newBlocks,
61
+ newFocusId: insertBlock.id
62
+ });
63
+ }
64
+ function deleteBlock(blocks, id) {
65
+ const index = blocks.findIndex((b) => b.id === id);
66
+ const prevId = blocks[index - 1]?.id ?? blocks[index + 1]?.id ?? "";
67
+ return { blocks: blocks.filter((b) => b.id !== id), prevId };
68
+ }
69
+ function duplicateBlock(incoming, newId = crypto.randomUUID()) {
70
+ return {
71
+ ...incoming,
72
+ id: newId,
73
+ meta: { ...incoming.meta },
74
+ content: incoming.content.map((node) => ({ ...node }))
75
+ };
76
+ }
77
+ function duplicateBlockAfter(blocks, id, newId) {
78
+ const targetBlock = blocks.find((b) => b.id === id);
79
+ if (targetBlock == null) return Result.Err(`[BlockNotFound]: ${id}`);
80
+ const dup = duplicateBlock(targetBlock, newId);
81
+ return insertBlockAfter(blocks, id, dup);
82
+ }
83
+ function moveBlock(blocks, id, direction) {
84
+ const index = blocks.findIndex((b) => b.id === id);
85
+ if (index === -1)
86
+ return Result.Err(`Block with id "${id}" not found`);
87
+ if (direction === "up" && index === 0)
88
+ return Result.Ok([...blocks]);
89
+ if (direction === "down" && index === blocks.length - 1)
90
+ return Result.Ok([...blocks]);
91
+ const next = [...blocks];
92
+ const swapIndex = direction === "up" ? index - 1 : index + 1;
93
+ [next[index], next[swapIndex]] = [next[swapIndex], next[index]];
94
+ return Result.Ok(next);
95
+ }
96
+
97
+ // src/engine/content.ts
98
+ import { Result as Result3 } from "@reiwuzen/result";
99
+
100
+ // src/engine/format.ts
101
+ import { Result as Result2 } from "@reiwuzen/result";
102
+ function validateSelection(nodes, sel) {
103
+ const { startIndex, startOffset, endIndex, endOffset } = sel;
104
+ if (startIndex < 0 || endIndex >= nodes.length)
105
+ return Result2.Err(
106
+ `indices [${startIndex}, ${endIndex}] out of bounds (length=${nodes.length})`
107
+ );
108
+ if (startIndex > endIndex)
109
+ return Result2.Err(`startIndex (${startIndex}) > endIndex (${endIndex})`);
110
+ const startNode = nodes[startIndex];
111
+ const endNode = nodes[endIndex];
112
+ if (!isTextNode(startNode) || !isTextNode(endNode))
113
+ return Result2.Err(`Selection must start and end on a text node`);
114
+ if (startOffset < 0 || startOffset > startNode.text.length)
115
+ return Result2.Err(
116
+ `startOffset (${startOffset}) out of bounds for "${startNode.text}"`
117
+ );
118
+ if (endOffset < 0 || endOffset > endNode.text.length)
119
+ return Result2.Err(
120
+ `endOffset (${endOffset}) out of bounds for "${endNode.text}"`
121
+ );
122
+ if (startIndex === endIndex && startOffset >= endOffset)
123
+ return Result2.Err(
124
+ `startOffset (${startOffset}) must be < endOffset (${endOffset}) within same node`
125
+ );
126
+ if (!nodes.slice(startIndex, endIndex + 1).every(isTextNode))
127
+ return Result2.Err(`Selection contains non-text nodes (code/equation)`);
128
+ return Result2.Ok(void 0);
129
+ }
130
+ function isTextNode(node) {
131
+ return node.type === "text";
132
+ }
133
+ function isFormatActive(nodes, sel, format, value = true) {
134
+ return nodes.slice(sel.startIndex, sel.endIndex + 1).every((n) => n[format] === value);
135
+ }
136
+ function applyFormat(node, format, value, remove) {
137
+ const next = { ...node };
138
+ if (remove) {
139
+ delete next[format];
140
+ } else {
141
+ next[format] = value;
142
+ }
143
+ return next;
144
+ }
145
+ function splitNode(node, start, end, format, value, remove) {
146
+ const parts = [];
147
+ if (start > 0)
148
+ parts.push({ ...node, text: node.text.slice(0, start) });
149
+ parts.push(
150
+ applyFormat({ ...node, text: node.text.slice(start, end) }, format, value, remove)
151
+ );
152
+ if (end < node.text.length)
153
+ parts.push({ ...node, text: node.text.slice(end) });
154
+ return parts;
155
+ }
156
+ function formatNodes(nodes, sel, format, value = true) {
157
+ return validateSelection(nodes, sel).map(() => {
158
+ const { startIndex, startOffset, endIndex, endOffset } = sel;
159
+ const textNodes = nodes;
160
+ const remove = isFormatActive(textNodes, sel, format, value);
161
+ const result = [];
162
+ for (let i = 0; i < nodes.length; i++) {
163
+ const node = nodes[i];
164
+ if (i < startIndex || i > endIndex) {
165
+ result.push(node);
166
+ continue;
167
+ }
168
+ if (!isTextNode(node)) {
169
+ result.push(node);
170
+ continue;
171
+ }
172
+ const isSingle = startIndex === endIndex;
173
+ const isFirst = i === startIndex;
174
+ const isLast = i === endIndex;
175
+ if (isSingle) {
176
+ result.push(...splitNode(node, startOffset, endOffset, format, value, remove));
177
+ } else if (isFirst) {
178
+ result.push(...splitNode(node, startOffset, node.text.length, format, value, remove));
179
+ } else if (isLast) {
180
+ result.push(...splitNode(node, 0, endOffset, format, value, remove));
181
+ } else {
182
+ result.push(applyFormat(node, format, value, remove));
183
+ }
184
+ }
185
+ return mergeAdjacentNodes(result);
186
+ });
187
+ }
188
+ function mergeAdjacentNodes(nodes) {
189
+ const result = [];
190
+ for (const node of nodes) {
191
+ const prev = result[result.length - 1];
192
+ if (prev && isTextNode(prev) && isTextNode(node) && formatsMatch(prev, node)) {
193
+ result[result.length - 1] = { ...prev, text: prev.text + node.text };
194
+ } else {
195
+ result.push(node);
196
+ }
197
+ }
198
+ return result;
199
+ }
200
+ function formatsMatch(a, b) {
201
+ const keys = [
202
+ "bold",
203
+ "italic",
204
+ "underline",
205
+ "strikethrough",
206
+ "highlighted",
207
+ "color",
208
+ "link"
209
+ ];
210
+ return keys.every((k) => a[k] === b[k]);
211
+ }
212
+ var toggleBold = (nodes, sel) => formatNodes(nodes, sel, "bold");
213
+ var toggleItalic = (nodes, sel) => formatNodes(nodes, sel, "italic");
214
+ var toggleUnderline = (nodes, sel) => formatNodes(nodes, sel, "underline");
215
+ var toggleStrikethrough = (nodes, sel) => formatNodes(nodes, sel, "strikethrough");
216
+ var toggleHighlight = (nodes, sel, color = "yellow") => formatNodes(nodes, sel, "highlighted", color);
217
+ var toggleColor = (nodes, sel, color) => formatNodes(nodes, sel, "color", color);
218
+ var setLink = (nodes, sel, href) => formatNodes(nodes, sel, "link", href);
219
+ var removeLink = (nodes, sel) => formatNodes(nodes, sel, "link", void 0);
220
+
221
+ // src/engine/content.ts
222
+ function isClean(node) {
223
+ return node.bold === void 0 && node.italic === void 0 && node.underline === void 0 && node.strikethrough === void 0 && node.highlighted === void 0 && node.color === void 0 && node.link === void 0;
224
+ }
225
+ function getTextLength(node) {
226
+ if (node.type === "text" || node.type === "code") return node.text.length;
227
+ if (node.type === "equation") return node.latex.length;
228
+ return 0;
229
+ }
230
+ function tryMerge(a, b) {
231
+ if (a.type !== b.type) return null;
232
+ if (a.type === "code" && b.type === "code") return { ...a, text: a.text + b.text };
233
+ if (a.type === "equation" && b.type === "equation") return { ...a, latex: a.latex + b.latex };
234
+ if (a.type === "text" && b.type === "text") {
235
+ if (isClean(a) && isClean(b) || formatsMatch(a, b))
236
+ return { ...a, text: a.text + b.text };
237
+ }
238
+ return null;
239
+ }
240
+ function sliceNode(node, start, end) {
241
+ if (start >= end) return null;
242
+ if (node.type === "text") {
243
+ const text = node.text.slice(start, end);
244
+ return text.length ? { ...node, text } : null;
245
+ }
246
+ if (node.type === "code") {
247
+ const text = node.text.slice(start, end);
248
+ return text.length ? { ...node, text } : null;
249
+ }
250
+ if (node.type === "equation") {
251
+ const latex = node.latex.slice(start, end);
252
+ return latex.length ? { ...node, latex } : null;
253
+ }
254
+ return null;
255
+ }
256
+ function insertAt(block, nodeIndex, offset, incoming) {
257
+ if (block.type === "code") {
258
+ if (incoming.type !== "code")
259
+ return Result3.Err(`code block only accepts a code node, got "${incoming.type}"`);
260
+ const node = block.content[0];
261
+ if (offset < 0 || offset > node.text.length)
262
+ return Result3.Err(`offset (${offset}) out of bounds for code node`);
263
+ const text = node.text.slice(0, offset) + incoming.text + node.text.slice(offset);
264
+ return Result3.Ok([{ ...node, text }]);
265
+ }
266
+ if (block.type === "equation") {
267
+ if (incoming.type !== "equation")
268
+ return Result3.Err(`equation block only accepts an equation node, got "${incoming.type}"`);
269
+ const node = block.content[0];
270
+ if (offset < 0 || offset > node.latex.length)
271
+ return Result3.Err(`offset (${offset}) out of bounds for equation node`);
272
+ const latex = node.latex.slice(0, offset) + incoming.latex + node.latex.slice(offset);
273
+ return Result3.Ok([{ ...node, latex }]);
274
+ }
275
+ const content = block.content;
276
+ if (content.length === 0) {
277
+ return Result3.Ok([incoming]);
278
+ }
279
+ if (nodeIndex < 0 || nodeIndex >= content.length)
280
+ return Result3.Err(`nodeIndex (${nodeIndex}) out of bounds (length=${content.length})`);
281
+ const target = content[nodeIndex];
282
+ const targetLen = getTextLength(target);
283
+ if (offset < 0 || offset > targetLen)
284
+ return Result3.Err(`offset (${offset}) out of bounds for node at index ${nodeIndex}`);
285
+ const before = content.slice(0, nodeIndex);
286
+ const after = content.slice(nodeIndex + 1);
287
+ const middle = [];
288
+ const left = sliceNode(target, 0, offset);
289
+ if (left) middle.push(left);
290
+ if (middle.length > 0) {
291
+ const merged = tryMerge(middle[middle.length - 1], incoming);
292
+ if (merged) middle[middle.length - 1] = merged;
293
+ else middle.push(incoming);
294
+ } else {
295
+ middle.push(incoming);
296
+ }
297
+ const right = sliceNode(target, offset, targetLen);
298
+ if (right) {
299
+ const merged = tryMerge(middle[middle.length - 1], right);
300
+ if (merged) middle[middle.length - 1] = merged;
301
+ else middle.push(right);
302
+ }
303
+ const result = [];
304
+ for (const node of [...before, ...middle, ...after]) {
305
+ const prev = result[result.length - 1];
306
+ const merged = prev ? tryMerge(prev, node) : null;
307
+ if (merged) result[result.length - 1] = merged;
308
+ else result.push(node);
309
+ }
310
+ return Result3.Ok(result);
311
+ }
312
+ function deleteLastChar(block) {
313
+ if (block.type === "code") {
314
+ const node = block.content[0];
315
+ if (!node.text.length) return Result3.Err("Nothing to delete");
316
+ return Result3.Ok([{ ...node, text: node.text.slice(0, -1) }]);
317
+ }
318
+ if (block.type === "equation") {
319
+ const node = block.content[0];
320
+ if (!node.latex.length) return Result3.Err("Nothing to delete");
321
+ return Result3.Ok([{ ...node, latex: node.latex.slice(0, -1) }]);
322
+ }
323
+ const next = [...block.content];
324
+ for (let i = next.length - 1; i >= 0; i--) {
325
+ const node = next[i];
326
+ if (node.type === "text" || node.type === "code") {
327
+ const trimmed = node.text.slice(0, -1);
328
+ if (!trimmed.length) next.splice(i, 1);
329
+ else next[i] = { ...node, text: trimmed };
330
+ return Result3.Ok(next);
331
+ }
332
+ if (node.type === "equation") {
333
+ const trimmed = node.latex.slice(0, -1);
334
+ if (!trimmed.length) next.splice(i, 1);
335
+ else next[i] = { ...node, latex: trimmed };
336
+ return Result3.Ok(next);
337
+ }
338
+ }
339
+ return Result3.Err("Nothing to delete");
340
+ }
341
+ function deleteRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset) {
342
+ if (block.type === "code") {
343
+ const node = block.content[0];
344
+ if (startOffset < 0 || endOffset > node.text.length || startOffset > endOffset)
345
+ return Result3.Err(`invalid range [${startOffset}, ${endOffset}] for code node`);
346
+ const text = node.text.slice(0, startOffset) + node.text.slice(endOffset);
347
+ return Result3.Ok([{ ...node, text }]);
348
+ }
349
+ if (block.type === "equation") {
350
+ const node = block.content[0];
351
+ if (startOffset < 0 || endOffset > node.latex.length || startOffset > endOffset)
352
+ return Result3.Err(`invalid range [${startOffset}, ${endOffset}] for equation node`);
353
+ const latex = node.latex.slice(0, startOffset) + node.latex.slice(endOffset);
354
+ return Result3.Ok([{ ...node, latex }]);
355
+ }
356
+ const nodes = block.content;
357
+ if (startNodeIndex < 0 || endNodeIndex >= nodes.length)
358
+ return Result3.Err(`node indices [${startNodeIndex}, ${endNodeIndex}] out of bounds`);
359
+ if (startNodeIndex > endNodeIndex)
360
+ return Result3.Err(`startNodeIndex (${startNodeIndex}) > endNodeIndex (${endNodeIndex})`);
361
+ const startNode = nodes[startNodeIndex];
362
+ const endNode = nodes[endNodeIndex];
363
+ if (startOffset < 0 || startOffset > getTextLength(startNode))
364
+ return Result3.Err(`startOffset (${startOffset}) out of bounds`);
365
+ if (endOffset < 0 || endOffset > getTextLength(endNode))
366
+ return Result3.Err(`endOffset (${endOffset}) out of bounds`);
367
+ const before = nodes.slice(0, startNodeIndex);
368
+ const after = nodes.slice(endNodeIndex + 1);
369
+ const middle = [];
370
+ const left = sliceNode(startNode, 0, startOffset);
371
+ if (left) middle.push(left);
372
+ const right = sliceNode(endNode, endOffset, getTextLength(endNode));
373
+ if (right) {
374
+ const merged = middle.length > 0 ? tryMerge(middle[middle.length - 1], right) : null;
375
+ if (merged) middle[middle.length - 1] = merged;
376
+ else middle.push(right);
377
+ }
378
+ const result = [];
379
+ for (const node of [...before, ...middle, ...after]) {
380
+ const prev = result[result.length - 1];
381
+ const merged = prev ? tryMerge(prev, node) : null;
382
+ if (merged) result[result.length - 1] = merged;
383
+ else result.push(node);
384
+ }
385
+ return Result3.Ok(result);
386
+ }
387
+ function splitBlock(block, nodeIndex, offset) {
388
+ if (block.type === "code" || block.type === "equation")
389
+ return Result3.Err(`splitBlock is not supported for "${block.type}" blocks`);
390
+ const nodes = block.content;
391
+ if (nodes.length > 0 && (nodeIndex < 0 || nodeIndex >= nodes.length))
392
+ return Result3.Err(`nodeIndex (${nodeIndex}) out of bounds`);
393
+ const target = nodes[nodeIndex] ?? null;
394
+ const targetLen = target ? getTextLength(target) : 0;
395
+ if (offset < 0 || offset > targetLen)
396
+ return Result3.Err(`offset (${offset}) out of bounds`);
397
+ const beforeNodes = [
398
+ ...nodes.slice(0, nodeIndex),
399
+ ...target && offset > 0 ? [sliceNode(target, 0, offset)].filter(Boolean) : []
400
+ ];
401
+ const afterNodes = [
402
+ ...target && offset < targetLen ? [sliceNode(target, offset, targetLen)].filter(Boolean) : [],
403
+ ...nodes.slice(nodeIndex + 1)
404
+ ];
405
+ const original = {
406
+ ...block,
407
+ content: beforeNodes
408
+ };
409
+ const newBlock = {
410
+ id: generateId(),
411
+ type: "paragraph",
412
+ content: afterNodes,
413
+ meta: {}
414
+ };
415
+ return Result3.Ok([original, newBlock]);
416
+ }
417
+ function mergeBlocks(blockA, blockB) {
418
+ if (blockA.type === "code" || blockA.type === "equation")
419
+ return Result3.Err(`mergeBlocks: blockA cannot be of type "${blockA.type}"`);
420
+ if (blockB.type === "code" || blockB.type === "equation")
421
+ return Result3.Err(`mergeBlocks: blockB cannot be of type "${blockB.type}"`);
422
+ const nodesA = blockA.content;
423
+ const nodesB = blockB.content;
424
+ const result = [...nodesA];
425
+ for (const node of nodesB) {
426
+ const prev = result[result.length - 1];
427
+ const merged = prev ? tryMerge(prev, node) : null;
428
+ if (merged) result[result.length - 1] = merged;
429
+ else result.push(node);
430
+ }
431
+ return Result3.Ok({
432
+ ...blockA,
433
+ content: result
434
+ });
435
+ }
436
+ function replaceRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset, incoming) {
437
+ return deleteRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset).andThen(
438
+ (content) => insertAt(
439
+ { ...block, content },
440
+ startNodeIndex,
441
+ startOffset,
442
+ incoming
443
+ )
444
+ );
445
+ }
446
+
447
+ // src/engine/transform.ts
448
+ import { Result as Result4 } from "@reiwuzen/result";
449
+ var TRIGGERS = {
450
+ "-": "bullet",
451
+ "1.": "number",
452
+ "[]": "todo",
453
+ "#": "heading1",
454
+ "##": "heading2",
455
+ "###": "heading3"
456
+ };
457
+ function getRawText(block) {
458
+ const first = block.content[0];
459
+ if (!first || first.type !== "text") return "";
460
+ return first.text;
461
+ }
462
+ function stripPrefix(block, prefix) {
463
+ const first = block.content[0];
464
+ if (!first || first.type !== "text") return block.content;
465
+ const stripped = first.text.slice(prefix.length + 1);
466
+ if (stripped.length === 0) return [];
467
+ return [{ ...first, text: stripped }, ...block.content.slice(1)];
468
+ }
469
+ function buildMeta(type) {
470
+ switch (type) {
471
+ case "bullet":
472
+ case "number":
473
+ case "todo":
474
+ return { depth: 0 };
475
+ case "heading1":
476
+ case "heading2":
477
+ case "heading3":
478
+ return {};
479
+ }
480
+ }
481
+ function applyMarkdownTransform(block, cursorOffset) {
482
+ if (block.type !== "paragraph")
483
+ return Result4.Ok({ block, converted: false });
484
+ const text = getRawText(block);
485
+ const match = Object.keys(TRIGGERS).sort((a, b) => b.length - a.length).find((trigger) => text === trigger || text.startsWith(trigger + " "));
486
+ if (!match)
487
+ return Result4.Ok({ block, converted: false });
488
+ const triggerEnd = match.length + 1;
489
+ if (cursorOffset < triggerEnd)
490
+ return Result4.Ok({ block, converted: false });
491
+ const targetType = TRIGGERS[match];
492
+ const strippedContent = stripPrefix(block, match);
493
+ const converted = {
494
+ id: block.id,
495
+ type: targetType,
496
+ content: strippedContent,
497
+ meta: buildMeta(targetType)
498
+ };
499
+ return Result4.Ok({ block: converted, converted: true });
500
+ }
501
+ function changeBlockType(block, targetType) {
502
+ if (block.type === targetType)
503
+ return Result4.Ok(block);
504
+ const content = deriveContent(block, targetType);
505
+ const meta = buildMetaForTarget(targetType);
506
+ return Result4.Ok({
507
+ id: block.id,
508
+ type: targetType,
509
+ content,
510
+ meta
511
+ });
512
+ }
513
+ function extractText(block) {
514
+ if (block.type === "code") return block.content[0].text;
515
+ if (block.type === "equation") return block.content[0].latex;
516
+ return block.content.map((n) => {
517
+ if (n.type === "text") return n.text;
518
+ if (n.type === "code") return n.text;
519
+ if (n.type === "equation") return n.latex;
520
+ return "";
521
+ }).join("");
522
+ }
523
+ function deriveContent(block, targetType) {
524
+ const text = extractText(block);
525
+ if (targetType === "code")
526
+ return [{ type: "code", text }];
527
+ if (targetType === "equation")
528
+ return [{ type: "equation", latex: text }];
529
+ if (block.type === "code" || block.type === "equation") {
530
+ return text.length ? [{ type: "text", text }] : [];
531
+ }
532
+ return block.content;
533
+ }
534
+ function buildMetaForTarget(targetType) {
535
+ switch (targetType) {
536
+ case "bullet":
537
+ case "number":
538
+ case "todo":
539
+ return { depth: 0 };
540
+ case "heading1":
541
+ case "heading2":
542
+ case "heading3":
543
+ case "paragraph":
544
+ case "code":
545
+ case "equation":
546
+ return {};
547
+ }
548
+ }
549
+ function toggleTodo(block) {
550
+ if (block.type !== "todo")
551
+ return Result4.Err(`toggleTodo expects a "todo" block, got "${block.type}"`);
552
+ const todo = block;
553
+ const checked = todo.meta.checked ? void 0 : true;
554
+ const meta = checked ? { ...todo.meta, checked } : (({ checked: _, ...rest }) => rest)(todo.meta);
555
+ return Result4.Ok({ ...todo, meta });
556
+ }
557
+ var MAX_DEPTH = 6;
558
+ function isIndentable(block) {
559
+ return block.type === "bullet" || block.type === "number" || block.type === "todo";
560
+ }
561
+ function indentBlock(block) {
562
+ if (!isIndentable(block))
563
+ return Result4.Err(`indentBlock only supports bullet, number, todo \u2014 got "${block.type}"`);
564
+ if (block.meta.depth >= MAX_DEPTH)
565
+ return Result4.Err(`already at max depth (${MAX_DEPTH})`);
566
+ return Result4.Ok({
567
+ ...block,
568
+ meta: { ...block.meta, depth: block.meta.depth + 1 }
569
+ });
570
+ }
571
+ function outdentBlock(block) {
572
+ if (!isIndentable(block))
573
+ return Result4.Err(`outdentBlock only supports bullet, number, todo \u2014 got "${block.type}"`);
574
+ if (block.meta.depth <= 0)
575
+ return Result4.Err(`already at min depth (0)`);
576
+ return Result4.Ok({
577
+ ...block,
578
+ meta: { ...block.meta, depth: block.meta.depth - 1 }
579
+ });
580
+ }
581
+
582
+ // src/engine/serializer.ts
583
+ import { Result as Result5 } from "@reiwuzen/result";
584
+ function serialize(blocks) {
585
+ return Result5.try(() => JSON.stringify(blocks));
586
+ }
587
+ function deserialize(json) {
588
+ return Result5.try(() => JSON.parse(json)).mapErr(() => "Invalid JSON string").andThen((parsed) => {
589
+ if (!Array.isArray(parsed))
590
+ return Result5.Err("Expected an array at top level");
591
+ const validTypes = /* @__PURE__ */ new Set([
592
+ "paragraph",
593
+ "heading1",
594
+ "heading2",
595
+ "heading3",
596
+ "bullet",
597
+ "number",
598
+ "todo",
599
+ "code",
600
+ "equation"
601
+ ]);
602
+ for (let i = 0; i < parsed.length; i++) {
603
+ const block = parsed[i];
604
+ if (typeof block !== "object" || block === null)
605
+ return Result5.Err(`Block at index ${i} is not an object`);
606
+ if (typeof block.id !== "string" || !block.id.length)
607
+ return Result5.Err(`Block at index ${i} has invalid id`);
608
+ if (!validTypes.has(block.type))
609
+ return Result5.Err(`Block at index ${i} has unknown type "${block.type}"`);
610
+ if (typeof block.meta !== "object" || block.meta === null)
611
+ return Result5.Err(`Block at index ${i} has invalid meta`);
612
+ if (!Array.isArray(block.content))
613
+ return Result5.Err(`Block at index ${i} has invalid content`);
614
+ const contentErr = validateContent(block.content, block.type, i);
615
+ if (contentErr) return Result5.Err(contentErr);
616
+ }
617
+ return Result5.Ok(parsed);
618
+ });
619
+ }
620
+ function validateContent(content, type, blockIndex) {
621
+ const isLeaf = type === "code" || type === "equation";
622
+ if (isLeaf && content.length !== 1)
623
+ return `Block at index ${blockIndex} (${type}) must have exactly 1 content node`;
624
+ for (let i = 0; i < content.length; i++) {
625
+ const node = content[i];
626
+ const err = validateNode(node, blockIndex, i);
627
+ if (err) return err;
628
+ }
629
+ return null;
630
+ }
631
+ function validateNode(node, blockIndex, nodeIndex) {
632
+ const prefix = `Block[${blockIndex}].content[${nodeIndex}]`;
633
+ if (typeof node !== "object" || node === null)
634
+ return `${prefix} is not an object`;
635
+ const n = node;
636
+ if (n.type === "text") {
637
+ if (typeof n.text !== "string")
638
+ return `${prefix} text node missing "text" string`;
639
+ return null;
640
+ }
641
+ if (n.type === "code") {
642
+ if (typeof n.text !== "string")
643
+ return `${prefix} code node missing "text" string`;
644
+ return null;
645
+ }
646
+ if (n.type === "equation") {
647
+ if (typeof n.latex !== "string")
648
+ return `${prefix} equation node missing "latex" string`;
649
+ return null;
650
+ }
651
+ return `${prefix} has unknown node type "${n.type}"`;
652
+ }
653
+ function serializeNodes(nodes) {
654
+ return Result5.try(() => JSON.stringify(nodes));
655
+ }
656
+ function deserializeNodes(json) {
657
+ return Result5.try(() => JSON.parse(json)).mapErr(() => "Invalid JSON string").andThen((parsed) => {
658
+ if (!Array.isArray(parsed))
659
+ return Result5.Err("Expected an array of nodes");
660
+ for (let i = 0; i < parsed.length; i++) {
661
+ const err = validateNode(parsed[i], 0, i);
662
+ if (err) return Result5.Err(err);
663
+ }
664
+ return Result5.Ok(parsed);
665
+ });
666
+ }
667
+ function toPlainText(nodes) {
668
+ return nodes.map((n) => {
669
+ if (n.type === "text") return n.text;
670
+ if (n.type === "code") return n.text;
671
+ if (n.type === "equation") return n.latex;
672
+ return "";
673
+ }).join("");
674
+ }
675
+ function nodesToMarkdown(nodes) {
676
+ return nodes.map((n) => {
677
+ if (n.type === "code") return `\`${n.text}\``;
678
+ if (n.type === "equation") return `$${n.latex}$`;
679
+ let text = n.text;
680
+ if (n.bold) text = `**${text}**`;
681
+ if (n.italic) text = `*${text}*`;
682
+ if (n.underline) text = `<u>${text}</u>`;
683
+ if (n.strikethrough) text = `~~${text}~~`;
684
+ if (n.link) text = `[${text}](${n.link})`;
685
+ return text;
686
+ }).join("");
687
+ }
688
+ function toMarkdown(blocks) {
689
+ return blocks.map((block) => {
690
+ switch (block.type) {
691
+ case "paragraph":
692
+ return nodesToMarkdown(block.content);
693
+ case "heading1":
694
+ return `# ${nodesToMarkdown(block.content)}`;
695
+ case "heading2":
696
+ return `## ${nodesToMarkdown(block.content)}`;
697
+ case "heading3":
698
+ return `### ${nodesToMarkdown(block.content)}`;
699
+ case "bullet": {
700
+ const indent = " ".repeat(block.meta.depth);
701
+ return `${indent}- ${nodesToMarkdown(block.content)}`;
702
+ }
703
+ case "number": {
704
+ const indent = " ".repeat(block.meta.depth);
705
+ return `${indent}1. ${nodesToMarkdown(block.content)}`;
706
+ }
707
+ case "todo": {
708
+ const indent = " ".repeat(block.meta.depth);
709
+ const checkbox = block.meta.checked ? "[x]" : "[ ]";
710
+ return `${indent}- ${checkbox} ${nodesToMarkdown(block.content)}`;
711
+ }
712
+ case "code": {
713
+ const lang = block.meta.language ?? "";
714
+ return `\`\`\`${lang}
715
+ ${block.content[0].text}
716
+ \`\`\``;
717
+ }
718
+ case "equation":
719
+ return `$$${block.content[0].latex}$$`;
720
+ }
721
+ }).join("\n\n");
722
+ }
723
+
724
+ // src/engine/cursor.ts
725
+ import { Result as Result6 } from "@reiwuzen/result";
726
+ function getTextLength2(node) {
727
+ if (node.type === "text" || node.type === "code") return node.text.length;
728
+ if (node.type === "equation") return node.latex.length;
729
+ return 0;
730
+ }
731
+ function flatToPosition(block, flatOffset) {
732
+ if (block.type === "code" || block.type === "equation") {
733
+ const node = block.content[0];
734
+ const len = getTextLength2(node);
735
+ if (flatOffset < 0 || flatOffset > len)
736
+ return Result6.Err(`flatOffset (${flatOffset}) out of bounds (length=${len})`);
737
+ return Result6.Ok({ nodeIndex: 0, offset: flatOffset });
738
+ }
739
+ const nodes = block.content;
740
+ if (flatOffset < 0)
741
+ return Result6.Err(`flatOffset (${flatOffset}) cannot be negative`);
742
+ let accumulated = 0;
743
+ for (let i = 0; i < nodes.length; i++) {
744
+ const len = getTextLength2(nodes[i]);
745
+ if (flatOffset <= accumulated + len) {
746
+ return Result6.Ok({ nodeIndex: i, offset: flatOffset - accumulated });
747
+ }
748
+ accumulated += len;
749
+ }
750
+ if (flatOffset === accumulated) {
751
+ const last = nodes.length - 1;
752
+ return Result6.Ok({ nodeIndex: Math.max(0, last), offset: getTextLength2(nodes[last]) });
753
+ }
754
+ return Result6.Err(
755
+ `flatOffset (${flatOffset}) out of bounds (total length=${accumulated})`
756
+ );
757
+ }
758
+ function flatToSelection(block, start, end) {
759
+ if (start > end)
760
+ return Result6.Err(`start (${start}) cannot be greater than end (${end})`);
761
+ return flatToPosition(block, start).andThen(
762
+ (startPos) => flatToPosition(block, end).map((endPos) => ({
763
+ startIndex: startPos.nodeIndex,
764
+ startOffset: startPos.offset,
765
+ endIndex: endPos.nodeIndex,
766
+ endOffset: endPos.offset
767
+ }))
768
+ );
769
+ }
770
+ function positionToFlat(block, nodeIndex, offset) {
771
+ if (block.type === "code" || block.type === "equation")
772
+ return Result6.Ok(offset);
773
+ const nodes = block.content;
774
+ if (nodeIndex < 0 || nodeIndex >= nodes.length)
775
+ return Result6.Err(`nodeIndex (${nodeIndex}) out of bounds`);
776
+ let flat = 0;
777
+ for (let i = 0; i < nodeIndex; i++) {
778
+ flat += getTextLength2(nodes[i]);
779
+ }
780
+ return Result6.Ok(flat + offset);
781
+ }
782
+
783
+ // src/engine/history.ts
784
+ import { Result as Result7 } from "@reiwuzen/result";
785
+ function createHistory(initialBlocks) {
786
+ return {
787
+ past: [],
788
+ present: { blocks: initialBlocks, timestamp: Date.now() },
789
+ future: []
790
+ };
791
+ }
792
+ function push(history, blocks, maxSize = 100) {
793
+ const past = [...history.past, history.present].slice(-maxSize);
794
+ return {
795
+ past,
796
+ present: { blocks, timestamp: Date.now() },
797
+ future: []
798
+ };
799
+ }
800
+ function undo(history) {
801
+ if (history.past.length === 0)
802
+ return Result7.Err("Nothing to undo");
803
+ const previous = history.past[history.past.length - 1];
804
+ return Result7.Ok({
805
+ past: history.past.slice(0, -1),
806
+ present: previous,
807
+ future: [history.present, ...history.future]
808
+ });
809
+ }
810
+ function redo(history) {
811
+ if (history.future.length === 0)
812
+ return Result7.Err("Nothing to redo");
813
+ const next = history.future[0];
814
+ return Result7.Ok({
815
+ past: [...history.past, history.present],
816
+ present: next,
817
+ future: history.future.slice(1)
818
+ });
819
+ }
820
+ var canUndo = (history) => history.past.length > 0;
821
+ var canRedo = (history) => history.future.length > 0;
822
+ var currentBlocks = (history) => history.present.blocks;
823
+
824
+ // src/engine/block.ts
825
+ function blockInsertAt(block, nodeIndex, offset, incoming) {
826
+ return insertAt(block, nodeIndex, offset, incoming).map(
827
+ (content) => ({
828
+ ...block,
829
+ content
830
+ })
831
+ );
832
+ }
833
+ function blockDeleteLastChar(block) {
834
+ return deleteLastChar(block).map(
835
+ (content) => ({
836
+ ...block,
837
+ content
838
+ })
839
+ );
840
+ }
841
+ function blockDeleteRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset) {
842
+ return deleteRange(
843
+ block,
844
+ startNodeIndex,
845
+ startOffset,
846
+ endNodeIndex,
847
+ endOffset
848
+ ).map((content) => ({ ...block, content }));
849
+ }
850
+ function blockReplaceRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset, incoming) {
851
+ return replaceRange(
852
+ block,
853
+ startNodeIndex,
854
+ startOffset,
855
+ endNodeIndex,
856
+ endOffset,
857
+ incoming
858
+ ).map((content) => ({ ...block, content }));
859
+ }
860
+ export {
861
+ applyMarkdownTransform,
862
+ blockDeleteLastChar,
863
+ blockDeleteRange,
864
+ blockInsertAt,
865
+ blockReplaceRange,
866
+ canRedo,
867
+ canUndo,
868
+ changeBlockType,
869
+ createBlock,
870
+ createBlockAfter,
871
+ createHistory,
872
+ currentBlocks,
873
+ deleteBlock,
874
+ deleteLastChar,
875
+ deleteRange,
876
+ deserialize,
877
+ deserializeNodes,
878
+ duplicateBlock,
879
+ duplicateBlockAfter,
880
+ flatToPosition,
881
+ flatToSelection,
882
+ formatNodes,
883
+ generateId,
884
+ indentBlock,
885
+ insertAt,
886
+ insertBlockAfter,
887
+ mergeAdjacentNodes,
888
+ mergeBlocks,
889
+ moveBlock,
890
+ outdentBlock,
891
+ positionToFlat,
892
+ push,
893
+ redo,
894
+ removeLink,
895
+ replaceRange,
896
+ serialize,
897
+ serializeNodes,
898
+ setLink,
899
+ splitBlock,
900
+ toMarkdown,
901
+ toPlainText,
902
+ toggleBold,
903
+ toggleColor,
904
+ toggleHighlight,
905
+ toggleItalic,
906
+ toggleStrikethrough,
907
+ toggleTodo,
908
+ toggleUnderline,
909
+ undo
910
+ };