@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.
- package/dist/index.cjs +954 -0
- package/dist/index.d.cts +306 -0
- package/dist/index.d.ts +306 -11
- package/dist/index.js +881 -15
- package/package.json +32 -25
- package/dist/engine/block.d.ts +0 -13
- package/dist/engine/block.d.ts.map +0 -1
- package/dist/engine/block.js +0 -26
- package/dist/engine/block.js.map +0 -1
- package/dist/engine/content.d.ts +0 -46
- package/dist/engine/content.d.ts.map +0 -1
- package/dist/engine/content.js +0 -317
- package/dist/engine/content.js.map +0 -1
- package/dist/engine/cursor.d.ts +0 -38
- package/dist/engine/cursor.d.ts.map +0 -1
- package/dist/engine/cursor.js +0 -90
- package/dist/engine/cursor.js.map +0 -1
- package/dist/engine/format.d.ts +0 -26
- package/dist/engine/format.d.ts.map +0 -1
- package/dist/engine/format.js +0 -116
- package/dist/engine/format.js.map +0 -1
- package/dist/engine/history.d.ts +0 -35
- package/dist/engine/history.d.ts.map +0 -1
- package/dist/engine/history.js +0 -62
- package/dist/engine/history.js.map +0 -1
- package/dist/engine/serializer.d.ts +0 -46
- package/dist/engine/serializer.d.ts.map +0 -1
- package/dist/engine/serializer.js +0 -205
- package/dist/engine/serializer.js.map +0 -1
- package/dist/engine/transform.d.ts +0 -47
- package/dist/engine/transform.d.ts.map +0 -1
- package/dist/engine/transform.js +0 -195
- package/dist/engine/transform.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types/block.d.ts +0 -44
- package/dist/types/block.d.ts.map +0 -1
- package/dist/types/block.js +0 -2
- package/dist/types/block.js.map +0 -1
- package/dist/types/editor.d.ts +0 -1
- package/dist/types/editor.d.ts.map +0 -1
- package/dist/types/editor.js +0 -2
- package/dist/types/editor.js.map +0 -1
- package/dist/utils/block.d.ts +0 -32
- package/dist/utils/block.d.ts.map +0 -1
- package/dist/utils/block.js +0 -97
- 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.
|
|
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
|
-
"
|
|
7
|
-
"
|
|
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
|
-
"
|
|
12
|
-
"
|
|
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
|
-
"
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
}
|
package/dist/engine/block.d.ts
DELETED
|
@@ -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"}
|
package/dist/engine/block.js
DELETED
|
@@ -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
|
package/dist/engine/block.js.map
DELETED
|
@@ -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"}
|
package/dist/engine/content.d.ts
DELETED
|
@@ -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"}
|
package/dist/engine/content.js
DELETED
|
@@ -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":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAc,YAAY,EAAE,MAAM,UAAU,CAAC;AASpD,kFAAkF;AAElF,SAAS,OAAO,CAAC,IAAsC;IACrD,OAAO,CACL,IAAI,CAAC,IAAI,KAAc,SAAS;QAChC,IAAI,CAAC,MAAM,KAAY,SAAS;QAChC,IAAI,CAAC,SAAS,KAAS,SAAS;QAChC,IAAI,CAAC,aAAa,KAAK,SAAS;QAChC,IAAI,CAAC,WAAW,KAAO,SAAS;QAChC,IAAI,CAAC,KAAK,KAAa,SAAS;QAChC,IAAI,CAAC,IAAI,KAAc,SAAS,CACjC,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,IAAU;IAC/B,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;IAC1E,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IACvD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,QAAQ,CAAC,CAAO,EAAE,CAAO;IAChC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,IAAQ,CAAC,CAAC,IAAI,KAAK,MAAM;QAAM,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3F,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC;IAC9F,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,IAAQ,CAAC,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC/C,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC;YAClD,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,SAAS,CAAC,IAAU,EAAE,KAAa,EAAE,GAAW;IACvD,IAAI,KAAK,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAM,CAAC;QAAC,MAAM,IAAI,GAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC,MAAM,CAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,CAAE,CAAC,CAAC,IAAI,CAAC;IAAC,CAAC;IAC9H,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAM,CAAC;QAAC,MAAM,IAAI,GAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC,MAAM,CAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,CAAE,CAAC,CAAC,IAAI,CAAC;IAAC,CAAC;IAC9H,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAAC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAAC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAAC,CAAC;IAC9H,OAAO,IAAI,CAAC;AACd,CAAC;AAED,kFAAkF;AAElF;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CACtB,KAAe,EACf,SAAiB,EACjB,MAAc,EACd,QAAc;IAGd,6EAA6E;IAC7E,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1B,IAAI,QAAQ,CAAC,IAAI,KAAK,MAAM;YAC1B,OAAO,MAAM,CAAC,GAAG,CAAC,6CAA6C,QAAQ,CAAC,IAAI,GAAG,CAAC,CAAC;QACnF,MAAM,IAAI,GAAI,KAAK,CAAC,OAAsB,CAAC,CAAC,CAAC,CAAC;QAC9C,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM;YACzC,OAAO,MAAM,CAAC,GAAG,CAAC,WAAW,MAAM,+BAA+B,CAAC,CAAC;QACtE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAClF,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,CAA+B,CAAC,CAAC;IACtE,CAAC;IAED,6EAA6E;IAC7E,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC9B,IAAI,QAAQ,CAAC,IAAI,KAAK,UAAU;YAC9B,OAAO,MAAM,CAAC,GAAG,CAAC,sDAAsD,QAAQ,CAAC,IAAI,GAAG,CAAC,CAAC;QAC5F,MAAM,IAAI,GAAI,KAAK,CAAC,OAA0B,CAAC,CAAC,CAAC,CAAC;QAClD,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM;YAC1C,OAAO,MAAM,CAAC,GAAG,CAAC,WAAW,MAAM,mCAAmC,CAAC,CAAC;QAC1E,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACtF,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,CAA+B,CAAC,CAAC;IACvE,CAAC;IAED,6EAA6E;IAC7E,MAAM,OAAO,GAAG,KAAK,CAAC,OAAiB,CAAC;IAExC,4BAA4B;IAC5B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,QAAQ,CAA+B,CAAC,CAAC;IAC7D,CAAC;IAED,IAAI,SAAS,GAAG,CAAC,IAAI,SAAS,IAAI,OAAO,CAAC,MAAM;QAC9C,OAAO,MAAM,CAAC,GAAG,CAAC,cAAc,SAAS,2BAA2B,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;IAEzF,MAAM,MAAM,GAAM,OAAO,CAAC,SAAS,CAAC,CAAC;IACrC,MAAM,SAAS,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAExC,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,SAAS;QAClC,OAAO,MAAM,CAAC,GAAG,CAAC,WAAW,MAAM,qCAAqC,SAAS,EAAE,CAAC,CAAC;IAEvF,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAI,OAAO,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAW,EAAE,CAAC;IAE1B,2CAA2C;IAC3C,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;IAC1C,IAAI,IAAI;QAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAE5B,sCAAsC;IACtC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC7D,IAAI,MAAM;YAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;;YAC1C,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxB,CAAC;IAED,0EAA0E;IAC1E,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;IACnD,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAC1D,IAAI,MAAM;YAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;;YAC1C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAED,8BAA8B;IAC9B,MAAM,MAAM,GAAW,EAAE,CAAC;IAC1B,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,GAAK,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAClD,IAAI,MAAM;YAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;;YAC1C,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,MAAM,CAAC,EAAE,CAAC,MAAoC,CAAC,CAAC;AACzD,CAAC;AAED,kFAAkF;AAElF,MAAM,UAAU,cAAc,CAAC,KAAe;IAC5C,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAI,KAAK,CAAC,OAAsB,CAAC,CAAC,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QAC9D,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAuC,CAAC,CAAC;IACtG,CAAC;IAED,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAI,KAAK,CAAC,OAA0B,CAAC,CAAC,CAAC,CAAC;QAClD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM;YAAE,OAAO,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QAC/D,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAuC,CAAC,CAAC;IACxG,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,GAAI,KAAK,CAAC,OAAkB,CAAC,CAAC;IAE5C,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACrB,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACjD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACvC,IAAI,CAAC,OAAO,CAAC,MAAM;gBAAE,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;;gBAClC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;YAC1C,OAAO,MAAM,CAAC,EAAE,CAAC,IAA0C,CAAC,CAAC;QAC/D,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACxC,IAAI,CAAC,OAAO,CAAC,MAAM;gBAAE,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;;gBAClC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;YAC3C,OAAO,MAAM,CAAC,EAAE,CAAC,IAA0C,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;AACzC,CAAC;AAGD,kFAAkF;AAElF;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,KAAe,EACf,cAAsB,EACtB,WAAmB,EACnB,YAAoB,EACpB,SAAiB;IAGjB,6EAA6E;IAC7E,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAI,KAAK,CAAC,OAAsB,CAAC,CAAC,CAAC,CAAC;QAC9C,IAAI,WAAW,GAAG,CAAC,IAAI,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,WAAW,GAAG,SAAS;YAC5E,OAAO,MAAM,CAAC,GAAG,CAAC,kBAAkB,WAAW,KAAK,SAAS,iBAAiB,CAAC,CAAC;QAClF,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC1E,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,CAAqB,CAAC,CAAC;IAC5D,CAAC;IAED,6EAA6E;IAC7E,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAI,KAAK,CAAC,OAA0B,CAAC,CAAC,CAAC,CAAC;QAClD,IAAI,WAAW,GAAG,CAAC,IAAI,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,WAAW,GAAG,SAAS;YAC7E,OAAO,MAAM,CAAC,GAAG,CAAC,kBAAkB,WAAW,KAAK,SAAS,qBAAqB,CAAC,CAAC;QACtF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC7E,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,CAAoB,CAAC,CAAC;IAC5D,CAAC;IAED,6EAA6E;IAC7E,MAAM,KAAK,GAAG,KAAK,CAAC,OAAiB,CAAC;IAEtC,IAAI,cAAc,GAAG,CAAC,IAAI,YAAY,IAAI,KAAK,CAAC,MAAM;QACpD,OAAO,MAAM,CAAC,GAAG,CAAC,iBAAiB,cAAc,KAAK,YAAY,iBAAiB,CAAC,CAAC;IACvF,IAAI,cAAc,GAAG,YAAY;QAC/B,OAAO,MAAM,CAAC,GAAG,CAAC,mBAAmB,cAAc,qBAAqB,YAAY,GAAG,CAAC,CAAC;IAE3F,MAAM,SAAS,GAAG,KAAK,CAAC,cAAc,CAAC,CAAC;IACxC,MAAM,OAAO,GAAK,KAAK,CAAC,YAAY,CAAC,CAAC;IAEtC,IAAI,WAAW,GAAG,CAAC,IAAI,WAAW,GAAG,aAAa,CAAC,SAAS,CAAC;QAC3D,OAAO,MAAM,CAAC,GAAG,CAAC,gBAAgB,WAAW,iBAAiB,CAAC,CAAC;IAClE,IAAI,SAAS,GAAG,CAAC,IAAI,SAAS,GAAG,aAAa,CAAC,OAAO,CAAC;QACrD,OAAO,MAAM,CAAC,GAAG,CAAC,cAAc,SAAS,iBAAiB,CAAC,CAAC;IAE9D,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAI,KAAK,CAAC,KAAK,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAW,EAAE,CAAC;IAE1B,+BAA+B;IAC/B,MAAM,IAAI,GAAG,SAAS,CAAC,SAAS,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC;IAClD,IAAI,IAAI;QAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAE5B,8BAA8B;IAC9B,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,EAAE,SAAS,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IACpE,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACrF,IAAI,MAAM;YAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;;YAC1C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAED,uBAAuB;IACvB,MAAM,MAAM,GAAW,EAAE,CAAC;IAC1B,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,GAAK,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAClD,IAAI,MAAM;YAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;;YAC1C,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,MAAM,CAAC,EAAE,CAAC,MAA0B,CAAC,CAAC;AAC/C,CAAC;AAED,kFAAkF;AAElF;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACxB,KAAe,EACf,SAAiB,EACjB,MAAc;;IAEd,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU;QACpD,OAAO,MAAM,CAAC,GAAG,CAAC,oCAAoC,KAAK,CAAC,IAAI,UAAU,CAAC,CAAC;IAE9E,MAAM,KAAK,GAAG,KAAK,CAAC,OAAiB,CAAC;IAEtC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,IAAI,SAAS,IAAI,KAAK,CAAC,MAAM,CAAC;QAClE,OAAO,MAAM,CAAC,GAAG,CAAC,cAAc,SAAS,iBAAiB,CAAC,CAAC;IAE9D,MAAM,MAAM,GAAM,MAAA,KAAK,CAAC,SAAS,CAAC,mCAAI,IAAI,CAAC;IAC3C,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAErD,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,SAAS;QAClC,OAAO,MAAM,CAAC,GAAG,CAAC,WAAW,MAAM,iBAAiB,CAAC,CAAC;IAExD,gDAAgD;IAChD,MAAM,WAAW,GAAW;QAC1B,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC;QAC5B,GAAG,CAAE,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,CAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAE;KACnF,CAAC;IAEF,yCAAyC;IACzC,MAAM,UAAU,GAAW;QACzB,GAAG,CAAE,MAAM,IAAI,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAE;QAClG,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;KAC9B,CAAC;IAEF,MAAM,QAAQ,GAAa;QACzB,GAAG,KAAK;QACR,OAAO,EAAE,WAAW;KACT,CAAC;IAEd,MAAM,QAAQ,GAAa;QACzB,EAAE,EAAE,UAAU,EAAE;QAChB,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,UAAU;QACnB,IAAI,EAAE,EAAE;KACG,CAAC;IAEd,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,kFAAkF;AAElF;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,MAAgB,EAChB,MAAgB;IAEhB,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU;QACtD,OAAO,MAAM,CAAC,GAAG,CAAC,0CAA0C,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC;IAC9E,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU;QACtD,OAAO,MAAM,CAAC,GAAG,CAAC,0CAA0C,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC;IAE9E,MAAM,MAAM,GAAG,MAAM,CAAC,OAAiB,CAAC;IACxC,MAAM,MAAM,GAAG,MAAM,CAAC,OAAiB,CAAC;IAExC,0CAA0C;IAC1C,MAAM,MAAM,GAAW,CAAC,GAAG,MAAM,CAAC,CAAC;IACnC,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAK,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAClD,IAAI,MAAM;YAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;;YAC1C,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,MAAM,CAAC,EAAE,CAAC;QACf,GAAG,MAAM;QACT,OAAO,EAAE,MAAM;KACJ,CAAC,CAAC;AACjB,CAAC;AAGD,kFAAkF;AAElF;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,KAAe,EACf,cAAsB,EACtB,WAAmB,EACnB,YAAoB,EACpB,SAAiB,EACjB,QAAc;IAEd,OAAO,WAAW,CAAI,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,CAAC;SAC/E,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CACnB,QAAQ,CACN,EAAE,GAAG,KAAK,EAAE,OAAO,EAAc,EACjC,cAAc,EACd,WAAW,EACX,QAAQ,CACT,CACF,CAAC;AACN,CAAC"}
|
package/dist/engine/cursor.d.ts
DELETED
|
@@ -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"}
|
package/dist/engine/cursor.js
DELETED
|
@@ -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
|