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