@reiwuzen/blocky 1.1.1 → 1.2.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 +954 -0
  2. package/dist/index.d.cts +306 -0
  3. package/dist/index.d.ts +306 -11
  4. package/dist/index.js +881 -15
  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/package.json CHANGED
@@ -1,30 +1,27 @@
1
1
  {
2
2
  "name": "@reiwuzen/blocky",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Pure TypeScript block editor engine. Headless, framework-agnostic — content mutation, formatting, transforms, serialization, and history.",
5
+ "author": "Rei WuZen",
6
+ "license": "ISC",
5
7
  "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
+ "sideEffects": false,
9
+ "main": "./dist/index.cjs",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "require": "./dist/index.cjs"
17
+ }
18
+ },
8
19
  "files": [
9
20
  "dist"
10
21
  ],
11
- "scripts": {
12
- "build": "tsc",
13
- "dev": "tsc --watch",
14
- "clean": "rimraf dist",
15
- "test": "tsx tests/test.ts"
22
+ "engines": {
23
+ "node": ">=18"
16
24
  },
17
- "keywords": [
18
- "block-editor",
19
- "headless-editor",
20
- "block-based",
21
- "rich-text",
22
- "editor-engine",
23
- "editor-core",
24
- "typescript"
25
- ],
26
- "author": "Rei WuZen",
27
- "license": "ISC",
28
25
  "dependencies": {
29
26
  "uuid": "^13.0.0"
30
27
  },
@@ -33,13 +30,23 @@
33
30
  },
34
31
  "devDependencies": {
35
32
  "rimraf": "^6.0.1",
33
+ "tsup": "^8.5.1",
36
34
  "tsx": "^4.19.3",
37
35
  "typescript": "^5.9.3"
38
36
  },
39
- "exports": {
40
- ".": {
41
- "import": "./dist/index.js",
42
- "types": "./dist/index.d.ts"
43
- }
37
+ "keywords": [
38
+ "block-editor",
39
+ "headless-editor",
40
+ "block-based",
41
+ "rich-text",
42
+ "editor-engine",
43
+ "editor-core",
44
+ "typescript"
45
+ ],
46
+ "scripts": {
47
+ "clean": "rimraf dist",
48
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
49
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
50
+ "test": "tsx tests/test.ts"
44
51
  }
45
- }
52
+ }
@@ -1,13 +0,0 @@
1
- import type { AnyBlock, Node } from "../types/block";
2
- import { Result } from "@reiwuzen/result";
3
- import { splitBlock, mergeBlocks } from "./content";
4
- /**─── Wrappers ──────────────────────────────────────────────────────────────────
5
- * Each function runs the content engine operation and returns Result<AnyBlock>
6
- instead of Result<BlockContent<T>> — caller gets the full updated block back.
7
- */
8
- export declare function blockInsertAt(block: AnyBlock, nodeIndex: number, offset: number, incoming: Node): Result<AnyBlock>;
9
- export declare function blockDeleteLastChar(block: AnyBlock): Result<AnyBlock>;
10
- export declare function blockDeleteRange(block: AnyBlock, startNodeIndex: number, startOffset: number, endNodeIndex: number, endOffset: number): Result<AnyBlock>;
11
- export declare function blockReplaceRange(block: AnyBlock, startNodeIndex: number, startOffset: number, endNodeIndex: number, endOffset: number, incoming: Node): Result<AnyBlock>;
12
- export { splitBlock, mergeBlocks };
13
- //# sourceMappingURL=block.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"block.d.ts","sourceRoot":"","sources":["../../src/engine/block.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAoB,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAKL,UAAU,EACV,WAAW,EACZ,MAAM,WAAW,CAAC;AAEnB;;;EAGE;AAEF,wBAAgB,aAAa,CAC3B,KAAK,EAAE,QAAQ,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,IAAI,GACb,MAAM,CAAC,QAAQ,CAAC,CAQlB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,CAQrE;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,QAAQ,EACf,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,QAAQ,CAAC,CAQlB;AAED,wBAAgB,iBAAiB,CAC/B,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,QAAQ,CAAC,CASlB;AAGD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC"}
@@ -1,26 +0,0 @@
1
- import { insertAt, deleteLastChar, deleteRange, replaceRange, splitBlock, mergeBlocks, } from "./content";
2
- /**─── Wrappers ──────────────────────────────────────────────────────────────────
3
- * Each function runs the content engine operation and returns Result<AnyBlock>
4
- instead of Result<BlockContent<T>> — caller gets the full updated block back.
5
- */
6
- export function blockInsertAt(block, nodeIndex, offset, incoming) {
7
- return insertAt(block, nodeIndex, offset, incoming).map((content) => ({
8
- ...block,
9
- content,
10
- }));
11
- }
12
- export function blockDeleteLastChar(block) {
13
- return deleteLastChar(block).map((content) => ({
14
- ...block,
15
- content,
16
- }));
17
- }
18
- export function blockDeleteRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset) {
19
- return deleteRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset).map((content) => ({ ...block, content }));
20
- }
21
- export function blockReplaceRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset, incoming) {
22
- return replaceRange(block, startNodeIndex, startOffset, endNodeIndex, endOffset, incoming).map((content) => ({ ...block, content }));
23
- }
24
- // splitBlock already returns Result<[AnyBlock, AnyBlock]> — re-exported as-is
25
- export { splitBlock, mergeBlocks };
26
- //# sourceMappingURL=block.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"block.js","sourceRoot":"","sources":["../../src/engine/block.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,QAAQ,EACR,cAAc,EACd,WAAW,EACX,YAAY,EACZ,UAAU,EACV,WAAW,GACZ,MAAM,WAAW,CAAC;AAEnB;;;EAGE;AAEF,MAAM,UAAU,aAAa,CAC3B,KAAe,EACf,SAAiB,EACjB,MAAc,EACd,QAAc;IAEd,OAAO,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,GAAG,CACrD,CAAC,OAAO,EAAE,EAAE,CACV,CAAC;QACC,GAAG,KAAK;QACR,OAAO;KACR,CAAa,CACjB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAe;IACjD,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC,GAAG,CAC9B,CAAC,OAAO,EAAE,EAAE,CACV,CAAC;QACC,GAAG,KAAK;QACR,OAAO;KACR,CAAa,CACjB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,KAAe,EACf,cAAsB,EACtB,WAAmB,EACnB,YAAoB,EACpB,SAAiB;IAEjB,OAAO,WAAW,CAChB,KAAK,EACL,cAAc,EACd,WAAW,EACX,YAAY,EACZ,SAAS,CACV,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,CAAa,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,KAAe,EACf,cAAsB,EACtB,WAAmB,EACnB,YAAoB,EACpB,SAAiB,EACjB,QAAc;IAEd,OAAO,YAAY,CACjB,KAAK,EACL,cAAc,EACd,WAAW,EACX,YAAY,EACZ,SAAS,EACT,QAAQ,CACT,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,CAAa,CAAC,CAAC;AAC1D,CAAC;AAED,8EAA8E;AAC9E,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC"}
@@ -1,46 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1,317 +0,0 @@
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
@@ -1 +0,0 @@
1
- {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/engine/content.ts"],"names":[],"mappings":"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"}
@@ -1,38 +0,0 @@
1
- import type { AnyBlock } from '../types/block';
2
- import type { NodeSelection } from './format';
3
- import { Result } from '@reiwuzen/result';
4
- export type CursorPosition = {
5
- nodeIndex: number;
6
- offset: number;
7
- };
8
- /**
9
- * Convert a flat UI cursor offset to { nodeIndex, offset }.
10
- *
11
- * The browser gives a single number representing position in the
12
- * concatenated text of the block. This function walks the node array
13
- * and finds which node that position falls in and where within it.
14
- *
15
- * e.g. nodes = ["Hello"(5), " World"(6)]
16
- * flatOffset=0 → { nodeIndex: 0, offset: 0 }
17
- * flatOffset=5 → { nodeIndex: 0, offset: 5 } (end of first node)
18
- * flatOffset=6 → { nodeIndex: 1, offset: 1 }
19
- * flatOffset=11 → { nodeIndex: 1, offset: 6 } (end of last node)
20
- */
21
- export declare function flatToPosition(block: AnyBlock, flatOffset: number): Result<CursorPosition, string>;
22
- /**
23
- * Convert a flat UI selection { start, end } to NodeSelection.
24
- *
25
- * The browser gives two flat offsets from window.getSelection().
26
- * This calls flatToPosition twice and returns a NodeSelection
27
- * ready to pass to toggleBold, formatNodes, deleteRange, etc.
28
- *
29
- * e.g. nodes = ["Hello"(bold), " World"]
30
- * { start: 3, end: 8 } → { startIndex:0, startOffset:3, endIndex:1, endOffset:3 }
31
- */
32
- export declare function flatToSelection(block: AnyBlock, start: number, end: number): Result<NodeSelection, string>;
33
- /**
34
- * Inverse — convert { nodeIndex, offset } back to a flat offset.
35
- * Useful after engine operations to restore cursor position in the DOM.
36
- */
37
- export declare function positionToFlat(block: AnyBlock, nodeIndex: number, offset: number): Result<number, string>;
38
- //# sourceMappingURL=cursor.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../src/engine/cursor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAI1C,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAYF;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,QAAQ,EACf,UAAU,EAAE,MAAM,GACjB,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,CAqChC;AAID;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,QAAQ,EACf,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAY/B;AAID;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,QAAQ,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAexB"}
@@ -1,90 +0,0 @@
1
- import { Result } from '@reiwuzen/result';
2
- // ─── Helpers ───────────────────────────────────────────────────────────────────
3
- function getTextLength(node) {
4
- if (node.type === "text" || node.type === "code")
5
- return node.text.length;
6
- if (node.type === "equation")
7
- return node.latex.length;
8
- return 0;
9
- }
10
- // ─── flatToPosition ────────────────────────────────────────────────────────────
11
- /**
12
- * Convert a flat UI cursor offset to { nodeIndex, offset }.
13
- *
14
- * The browser gives a single number representing position in the
15
- * concatenated text of the block. This function walks the node array
16
- * and finds which node that position falls in and where within it.
17
- *
18
- * e.g. nodes = ["Hello"(5), " World"(6)]
19
- * flatOffset=0 → { nodeIndex: 0, offset: 0 }
20
- * flatOffset=5 → { nodeIndex: 0, offset: 5 } (end of first node)
21
- * flatOffset=6 → { nodeIndex: 1, offset: 1 }
22
- * flatOffset=11 → { nodeIndex: 1, offset: 6 } (end of last node)
23
- */
24
- export function flatToPosition(block, flatOffset) {
25
- // code/equation blocks are always a single node — offset maps directly
26
- if (block.type === "code" || block.type === "equation") {
27
- const node = block.content[0];
28
- const len = getTextLength(node);
29
- if (flatOffset < 0 || flatOffset > len)
30
- return Result.Err(`flatOffset (${flatOffset}) out of bounds (length=${len})`);
31
- return Result.Ok({ nodeIndex: 0, offset: flatOffset });
32
- }
33
- const nodes = block.content;
34
- if (flatOffset < 0)
35
- return Result.Err(`flatOffset (${flatOffset}) cannot be negative`);
36
- let accumulated = 0;
37
- for (let i = 0; i < nodes.length; i++) {
38
- const len = getTextLength(nodes[i]);
39
- // offset lands within this node OR at its end (but not the last node)
40
- if (flatOffset <= accumulated + len) {
41
- return Result.Ok({ nodeIndex: i, offset: flatOffset - accumulated });
42
- }
43
- accumulated += len;
44
- }
45
- // flatOffset is exactly at the end of the last node
46
- if (flatOffset === accumulated) {
47
- const last = nodes.length - 1;
48
- return Result.Ok({ nodeIndex: Math.max(0, last), offset: getTextLength(nodes[last]) });
49
- }
50
- return Result.Err(`flatOffset (${flatOffset}) out of bounds (total length=${accumulated})`);
51
- }
52
- // ─── flatToSelection ───────────────────────────────────────────────────────────
53
- /**
54
- * Convert a flat UI selection { start, end } to NodeSelection.
55
- *
56
- * The browser gives two flat offsets from window.getSelection().
57
- * This calls flatToPosition twice and returns a NodeSelection
58
- * ready to pass to toggleBold, formatNodes, deleteRange, etc.
59
- *
60
- * e.g. nodes = ["Hello"(bold), " World"]
61
- * { start: 3, end: 8 } → { startIndex:0, startOffset:3, endIndex:1, endOffset:3 }
62
- */
63
- export function flatToSelection(block, start, end) {
64
- if (start > end)
65
- return Result.Err(`start (${start}) cannot be greater than end (${end})`);
66
- return flatToPosition(block, start).andThen((startPos) => flatToPosition(block, end).map((endPos) => ({
67
- startIndex: startPos.nodeIndex,
68
- startOffset: startPos.offset,
69
- endIndex: endPos.nodeIndex,
70
- endOffset: endPos.offset,
71
- })));
72
- }
73
- // ─── positionToFlat ────────────────────────────────────────────────────────────
74
- /**
75
- * Inverse — convert { nodeIndex, offset } back to a flat offset.
76
- * Useful after engine operations to restore cursor position in the DOM.
77
- */
78
- export function positionToFlat(block, nodeIndex, offset) {
79
- if (block.type === "code" || block.type === "equation")
80
- return Result.Ok(offset);
81
- const nodes = block.content;
82
- if (nodeIndex < 0 || nodeIndex >= nodes.length)
83
- return Result.Err(`nodeIndex (${nodeIndex}) out of bounds`);
84
- let flat = 0;
85
- for (let i = 0; i < nodeIndex; i++) {
86
- flat += getTextLength(nodes[i]);
87
- }
88
- return Result.Ok(flat + offset);
89
- }
90
- //# sourceMappingURL=cursor.js.map