@reiwuzen/blocky 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/index.cjs +954 -0
  2. package/dist/index.d.cts +306 -0
  3. package/dist/index.d.ts +306 -11
  4. package/dist/index.js +881 -15
  5. package/package.json +32 -25
  6. package/dist/engine/block.d.ts +0 -13
  7. package/dist/engine/block.d.ts.map +0 -1
  8. package/dist/engine/block.js +0 -26
  9. package/dist/engine/block.js.map +0 -1
  10. package/dist/engine/content.d.ts +0 -46
  11. package/dist/engine/content.d.ts.map +0 -1
  12. package/dist/engine/content.js +0 -317
  13. package/dist/engine/content.js.map +0 -1
  14. package/dist/engine/cursor.d.ts +0 -38
  15. package/dist/engine/cursor.d.ts.map +0 -1
  16. package/dist/engine/cursor.js +0 -90
  17. package/dist/engine/cursor.js.map +0 -1
  18. package/dist/engine/format.d.ts +0 -26
  19. package/dist/engine/format.d.ts.map +0 -1
  20. package/dist/engine/format.js +0 -116
  21. package/dist/engine/format.js.map +0 -1
  22. package/dist/engine/history.d.ts +0 -35
  23. package/dist/engine/history.d.ts.map +0 -1
  24. package/dist/engine/history.js +0 -62
  25. package/dist/engine/history.js.map +0 -1
  26. package/dist/engine/serializer.d.ts +0 -46
  27. package/dist/engine/serializer.d.ts.map +0 -1
  28. package/dist/engine/serializer.js +0 -205
  29. package/dist/engine/serializer.js.map +0 -1
  30. package/dist/engine/transform.d.ts +0 -47
  31. package/dist/engine/transform.d.ts.map +0 -1
  32. package/dist/engine/transform.js +0 -195
  33. package/dist/engine/transform.js.map +0 -1
  34. package/dist/index.d.ts.map +0 -1
  35. package/dist/index.js.map +0 -1
  36. package/dist/types/block.d.ts +0 -44
  37. package/dist/types/block.d.ts.map +0 -1
  38. package/dist/types/block.js +0 -2
  39. package/dist/types/block.js.map +0 -1
  40. package/dist/types/editor.d.ts +0 -1
  41. package/dist/types/editor.d.ts.map +0 -1
  42. package/dist/types/editor.js +0 -2
  43. package/dist/types/editor.js.map +0 -1
  44. package/dist/utils/block.d.ts +0 -32
  45. package/dist/utils/block.d.ts.map +0 -1
  46. package/dist/utils/block.js +0 -97
  47. package/dist/utils/block.js.map +0 -1
@@ -0,0 +1,306 @@
1
+ import { Result } from '@reiwuzen/result';
2
+
3
+ type BlockType = "paragraph" | "heading1" | "heading2" | "heading3" | "bullet" | "number" | "todo" | "equation" | "code";
4
+ type Node = {
5
+ type: "text";
6
+ text: string;
7
+ highlighted?: "yellow" | "green";
8
+ color?: "red" | 'blue' | 'green';
9
+ bold?: true;
10
+ italic?: true;
11
+ underline?: true;
12
+ strikethrough?: true;
13
+ link?: string;
14
+ } | {
15
+ type: "code";
16
+ text: string;
17
+ } | {
18
+ type: "equation";
19
+ latex: string;
20
+ };
21
+ type BlockMeta<T extends BlockType> = T extends "todo" ? {
22
+ checked?: true;
23
+ depth: number;
24
+ } : T extends "bullet" | "number" ? {
25
+ depth: number;
26
+ } : T extends "code" ? {
27
+ language?: string;
28
+ } : Record<string, never>;
29
+ type BlockContent<T extends BlockType> = T extends "code" ? [Extract<Node, {
30
+ type: "code";
31
+ }>] : T extends "equation" ? [Extract<Node, {
32
+ type: "equation";
33
+ }>] : Node[];
34
+ type Block<T extends BlockType = BlockType> = {
35
+ id: string;
36
+ type: T;
37
+ meta: BlockMeta<T>;
38
+ content: BlockContent<T>;
39
+ };
40
+ /**
41
+ * Discriminated Union of Block
42
+ */
43
+ type AnyBlock = {
44
+ [K in BlockType]: Block<K>;
45
+ }[BlockType];
46
+
47
+ /**
48
+ * Insert a Node at a specific position within block content.
49
+ *
50
+ * - nodeIndex: which node to insert into
51
+ * - offset: char position within that node
52
+ *
53
+ * End-of-node (offset === length) naturally becomes an append —
54
+ * sliceNode produces no right half, incoming merges with left or gets pushed.
55
+ * No separate append function needed.
56
+ */
57
+ declare function insertAt<T extends BlockType>(block: AnyBlock, nodeIndex: number, offset: number, incoming: Node): Result<BlockContent<T>>;
58
+ declare function deleteLastChar(block: AnyBlock): Result<BlockContent<BlockType>>;
59
+ /**
60
+ * Delete content across a selection.
61
+ * After deletion, left and right boundaries are merged if formats match.
62
+ *
63
+ * For code/equation blocks: deletes within the single tuple node's text/latex.
64
+ */
65
+ declare function deleteRange<T extends BlockType>(block: AnyBlock, startNodeIndex: number, startOffset: number, endNodeIndex: number, endOffset: number): Result<BlockContent<T>>;
66
+ /**
67
+ * Split a block at a given position into two blocks.
68
+ * The original block keeps content before the cursor.
69
+ * A new block gets content after the cursor — always of type "paragraph".
70
+ *
71
+ * Not supported for code/equation blocks — returns Err.
72
+ */
73
+ declare function splitBlock(block: AnyBlock, nodeIndex: number, offset: number): Result<[AnyBlock, AnyBlock]>;
74
+ /**
75
+ * Merge blockB into blockA — blockB's content is appended to blockA's content.
76
+ * blockA's type and meta are preserved.
77
+ *
78
+ * Not supported if either block is code or equation — returns Err.
79
+ */
80
+ declare function mergeBlocks(blockA: AnyBlock, blockB: AnyBlock): Result<AnyBlock>;
81
+ /**
82
+ * Replace a selected range with an incoming Node — atomic deleteRange + insertAt.
83
+ * This is what fires when the user has a selection and types a character.
84
+ *
85
+ * Internally chains:
86
+ * 1. deleteRange — remove selected content
87
+ * 2. insertAt — insert incoming at the start of the deleted range
88
+ */
89
+ declare function replaceRange<T extends BlockType>(block: AnyBlock, startNodeIndex: number, startOffset: number, endNodeIndex: number, endOffset: number, incoming: Node): Result<BlockContent<T>>;
90
+
91
+ type TextNode = Extract<Node, {
92
+ type: "text";
93
+ }>;
94
+ type TextFormat = Pick<TextNode, "bold" | "italic" | "underline" | "strikethrough" | "highlighted" | "color" | "link">;
95
+ type NodeSelection = {
96
+ startIndex: number;
97
+ startOffset: number;
98
+ endIndex: number;
99
+ endOffset: number;
100
+ };
101
+ declare function formatNodes(nodes: Node[], sel: NodeSelection, format: keyof TextFormat, value?: unknown): Result<Node[]>;
102
+ declare function mergeAdjacentNodes(nodes: Node[]): Node[];
103
+ declare const toggleBold: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
104
+ declare const toggleItalic: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
105
+ declare const toggleUnderline: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
106
+ declare const toggleStrikethrough: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
107
+ declare const toggleHighlight: (nodes: Node[], sel: NodeSelection, color?: "yellow" | "green") => Result<Node[], string>;
108
+ declare const toggleColor: (nodes: Node[], sel: NodeSelection, color: "red" | "blue" | "green") => Result<Node[], string>;
109
+ declare const setLink: (nodes: Node[], sel: NodeSelection, href: string) => Result<Node[], string>;
110
+ declare const removeLink: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
111
+
112
+ type TransformResult = {
113
+ block: AnyBlock;
114
+ converted: boolean;
115
+ };
116
+ /**
117
+ * Call this when a space is typed.
118
+ *
119
+ * Guards (returns converted=false if any fail):
120
+ * 1. block.type must be "paragraph"
121
+ * 2. cursorOffset must be ≤ trigger.length + 1
122
+ * — ensures the space was typed right after the trigger at position 0
123
+ * — blocks mid-text spaces like "hello - " from converting
124
+ * 3. text must start with a known trigger prefix
125
+ */
126
+ declare function applyMarkdownTransform(block: AnyBlock, cursorOffset: number): Result<TransformResult>;
127
+ /**
128
+ * Convert a block to a new type while preserving content as much as possible.
129
+ *
130
+ * rich → rich keep content as-is, update type + meta
131
+ * rich → code strip all formatting, concat all text → [CodeNode]
132
+ * rich → equation strip all formatting, concat all text → [EquationNode]
133
+ * code → rich single TextNode with code.text
134
+ * equation → rich single TextNode with equation.latex
135
+ * code → equation [EquationNode] with code.text as latex
136
+ * equation → code [CodeNode] with equation.latex as text
137
+ */
138
+ declare function changeBlockType<T extends BlockType>(block: AnyBlock, targetType: T): Result<Block<T>>;
139
+ /**
140
+ * Toggle the checked state of a todo block.
141
+ * checked: undefined → checked: true → checked: undefined (cycle)
142
+ */
143
+ declare function toggleTodo(block: AnyBlock): Result<Block<"todo">>;
144
+ type IndentableBlock = Block<"bullet"> | Block<"number"> | Block<"todo">;
145
+ /**
146
+ * Increase the depth of a bullet, number, or todo block by 1.
147
+ * Max depth is 6. Returns Err if block type is not indentable.
148
+ */
149
+ declare function indentBlock(block: AnyBlock): Result<IndentableBlock>;
150
+ /**
151
+ * Decrease the depth of a bullet, number, or todo block by 1.
152
+ * Min depth is 0. Returns Err if block type is not indentable or already at 0.
153
+ */
154
+ declare function outdentBlock(block: AnyBlock): Result<IndentableBlock>;
155
+
156
+ /**
157
+ * Serialize an array of blocks to a JSON string.
158
+ * Wraps JSON.stringify in Result.try so any circular ref or
159
+ * other stringify error surfaces as Err rather than throwing.
160
+ */
161
+ declare function serialize(blocks: AnyBlock[]): Result<string, unknown>;
162
+ /**
163
+ * Deserialize a JSON string back into AnyBlock[].
164
+ * Validates:
165
+ * - valid JSON
166
+ * - top level is an array
167
+ * - each item has id (string), type (known BlockType), meta (object), content (array)
168
+ */
169
+ declare function deserialize(json: string): Result<AnyBlock[], string>;
170
+ /**
171
+ * Serialize a Node[] to a JSON string — for clipboard copy.
172
+ * Preserves all formatting.
173
+ */
174
+ declare function serializeNodes(nodes: Node[]): Result<string, unknown>;
175
+ /**
176
+ * Deserialize a JSON string back into Node[] — for clipboard paste.
177
+ * Validates each node shape before returning.
178
+ */
179
+ declare function deserializeNodes(json: string): Result<Node[], string>;
180
+ /**
181
+ * Extract plain text from a Node[] — strips all formatting.
182
+ * text → text
183
+ * code → text
184
+ * equation → latex
185
+ */
186
+ declare function toPlainText(nodes: Node[]): string;
187
+ /**
188
+ * Convert an array of blocks to a markdown string.
189
+ *
190
+ * paragraph → plain inline
191
+ * heading1-3 → # / ## / ###
192
+ * bullet → - (indented by depth)
193
+ * number → 1. (indented by depth)
194
+ * todo → - [ ] / - [x] (indented by depth)
195
+ * code → ```language\n...\n```
196
+ * equation → $$...$$ (block equation)
197
+ */
198
+ declare function toMarkdown(blocks: AnyBlock[]): string;
199
+
200
+ type CursorPosition = {
201
+ nodeIndex: number;
202
+ offset: number;
203
+ };
204
+ /**
205
+ * Convert a flat UI cursor offset to { nodeIndex, offset }.
206
+ *
207
+ * The browser gives a single number representing position in the
208
+ * concatenated text of the block. This function walks the node array
209
+ * and finds which node that position falls in and where within it.
210
+ *
211
+ * e.g. nodes = ["Hello"(5), " World"(6)]
212
+ * flatOffset=0 → { nodeIndex: 0, offset: 0 }
213
+ * flatOffset=5 → { nodeIndex: 0, offset: 5 } (end of first node)
214
+ * flatOffset=6 → { nodeIndex: 1, offset: 1 }
215
+ * flatOffset=11 → { nodeIndex: 1, offset: 6 } (end of last node)
216
+ */
217
+ declare function flatToPosition(block: AnyBlock, flatOffset: number): Result<CursorPosition, string>;
218
+ /**
219
+ * Convert a flat UI selection { start, end } to NodeSelection.
220
+ *
221
+ * The browser gives two flat offsets from window.getSelection().
222
+ * This calls flatToPosition twice and returns a NodeSelection
223
+ * ready to pass to toggleBold, formatNodes, deleteRange, etc.
224
+ *
225
+ * e.g. nodes = ["Hello"(bold), " World"]
226
+ * { start: 3, end: 8 } → { startIndex:0, startOffset:3, endIndex:1, endOffset:3 }
227
+ */
228
+ declare function flatToSelection(block: AnyBlock, start: number, end: number): Result<NodeSelection, string>;
229
+ /**
230
+ * Inverse — convert { nodeIndex, offset } back to a flat offset.
231
+ * Useful after engine operations to restore cursor position in the DOM.
232
+ */
233
+ declare function positionToFlat(block: AnyBlock, nodeIndex: number, offset: number): Result<number, string>;
234
+
235
+ type HistoryEntry = {
236
+ blocks: AnyBlock[];
237
+ timestamp: number;
238
+ };
239
+ type History = {
240
+ past: HistoryEntry[];
241
+ present: HistoryEntry;
242
+ future: HistoryEntry[];
243
+ };
244
+ /**
245
+ * Create a new history instance with the given initial blocks.
246
+ */
247
+ declare function createHistory(initialBlocks: AnyBlock[]): History;
248
+ /**
249
+ * Push a new state onto the history stack.
250
+ * Clears the future — same as any editor after a new action post-undo.
251
+ * Optionally cap the max history size to avoid unbounded memory growth.
252
+ */
253
+ declare function push(history: History, blocks: AnyBlock[], maxSize?: number): History;
254
+ /**
255
+ * Move back one step in history.
256
+ * Returns Err if there is nothing to undo.
257
+ */
258
+ declare function undo(history: History): Result<History, string>;
259
+ /**
260
+ * Move forward one step in history.
261
+ * Returns Err if there is nothing to redo.
262
+ */
263
+ declare function redo(history: History): Result<History, string>;
264
+ declare const canUndo: (history: History) => boolean;
265
+ declare const canRedo: (history: History) => boolean;
266
+ declare const currentBlocks: (history: History) => AnyBlock[];
267
+
268
+ /**
269
+ * Generate a unique block id.
270
+ * Pass a custom fn to override the default (uuid v7).
271
+ */
272
+ declare function generateId(fn?: () => string): string;
273
+ /**
274
+ * Create a new block of the given type with default content and meta.
275
+ * Optionally pass a custom id generator.
276
+ *
277
+ * @param type - the block type to create
278
+ * @param idFn - optional custom id generator (defaults to uuid v7)
279
+ */
280
+ declare function createBlock<T extends BlockType>(type: T, idFn?: () => string): Result<Block<T>, unknown>;
281
+ declare function insertBlockAfter<T extends BlockType>(blocks: AnyBlock[], afterId: string, type: T, idFn?: () => string): Result<{
282
+ blocks: AnyBlock[];
283
+ newId: string;
284
+ }, unknown>;
285
+ declare function deleteBlock(blocks: AnyBlock[], id: string): {
286
+ blocks: AnyBlock[];
287
+ prevId: string;
288
+ };
289
+ declare function duplicateBlock(incoming: AnyBlock, newId: string): AnyBlock;
290
+ /**
291
+ * Move a block up or down by one position in the array.
292
+ * Returns the same array unchanged if block is already at the boundary.
293
+ * Returns Err if id not found.
294
+ */
295
+ declare function moveBlock(blocks: AnyBlock[], id: string, direction: "up" | "down"): Result<AnyBlock[], string>;
296
+
297
+ /**─── Wrappers ──────────────────────────────────────────────────────────────────
298
+ * Each function runs the content engine operation and returns Result<AnyBlock>
299
+ instead of Result<BlockContent<T>> — caller gets the full updated block back.
300
+ */
301
+ declare function blockInsertAt(block: AnyBlock, nodeIndex: number, offset: number, incoming: Node): Result<AnyBlock>;
302
+ declare function blockDeleteLastChar(block: AnyBlock): Result<AnyBlock>;
303
+ declare function blockDeleteRange(block: AnyBlock, startNodeIndex: number, startOffset: number, endNodeIndex: number, endOffset: number): Result<AnyBlock>;
304
+ declare function blockReplaceRange(block: AnyBlock, startNodeIndex: number, startOffset: number, endNodeIndex: number, endOffset: number, incoming: Node): Result<AnyBlock>;
305
+
306
+ export { type AnyBlock, type Block, type BlockContent, type BlockMeta, type BlockType, type History, type HistoryEntry, type Node, type NodeSelection, applyMarkdownTransform, blockDeleteLastChar, blockDeleteRange, blockInsertAt, blockReplaceRange, canRedo, canUndo, changeBlockType, createBlock, createHistory, currentBlocks, deleteBlock, deleteLastChar, deleteRange, deserialize, deserializeNodes, duplicateBlock, flatToPosition, flatToSelection, formatNodes, generateId, indentBlock, insertAt, insertBlockAfter, mergeAdjacentNodes, mergeBlocks, moveBlock, outdentBlock, positionToFlat, push, redo, removeLink, replaceRange, serialize, serializeNodes, setLink, splitBlock, toMarkdown, toPlainText, toggleBold, toggleColor, toggleHighlight, toggleItalic, toggleStrikethrough, toggleTodo, toggleUnderline, undo };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,306 @@
1
- export { insertAt, deleteLastChar, deleteRange, replaceRange, splitBlock, mergeBlocks } from "./engine/content";
2
- export { formatNodes, toggleBold, toggleItalic, toggleUnderline, toggleStrikethrough, toggleHighlight, toggleColor, setLink, removeLink, mergeAdjacentNodes } from "./engine/format";
3
- export { applyMarkdownTransform, changeBlockType, toggleTodo, indentBlock, outdentBlock } from "./engine/transform";
4
- export { serialize, deserialize, serializeNodes, deserializeNodes, toPlainText, toMarkdown } from "./engine/serializer";
5
- export { flatToPosition, flatToSelection, positionToFlat, } from './engine/cursor';
6
- export { createHistory, push, undo, redo, canUndo, canRedo, currentBlocks } from "./engine/history";
7
- export type { History, HistoryEntry } from "./engine/history";
8
- export { generateId, createBlock, insertBlockAfter, deleteBlock, duplicateBlock, moveBlock } from "./utils/block";
9
- export type { Node, Block, BlockContent, BlockMeta, BlockType, AnyBlock } from "./types/block";
10
- export type { NodeSelection } from './engine/format';
11
- //# sourceMappingURL=index.d.ts.map
1
+ import { Result } from '@reiwuzen/result';
2
+
3
+ type BlockType = "paragraph" | "heading1" | "heading2" | "heading3" | "bullet" | "number" | "todo" | "equation" | "code";
4
+ type Node = {
5
+ type: "text";
6
+ text: string;
7
+ highlighted?: "yellow" | "green";
8
+ color?: "red" | 'blue' | 'green';
9
+ bold?: true;
10
+ italic?: true;
11
+ underline?: true;
12
+ strikethrough?: true;
13
+ link?: string;
14
+ } | {
15
+ type: "code";
16
+ text: string;
17
+ } | {
18
+ type: "equation";
19
+ latex: string;
20
+ };
21
+ type BlockMeta<T extends BlockType> = T extends "todo" ? {
22
+ checked?: true;
23
+ depth: number;
24
+ } : T extends "bullet" | "number" ? {
25
+ depth: number;
26
+ } : T extends "code" ? {
27
+ language?: string;
28
+ } : Record<string, never>;
29
+ type BlockContent<T extends BlockType> = T extends "code" ? [Extract<Node, {
30
+ type: "code";
31
+ }>] : T extends "equation" ? [Extract<Node, {
32
+ type: "equation";
33
+ }>] : Node[];
34
+ type Block<T extends BlockType = BlockType> = {
35
+ id: string;
36
+ type: T;
37
+ meta: BlockMeta<T>;
38
+ content: BlockContent<T>;
39
+ };
40
+ /**
41
+ * Discriminated Union of Block
42
+ */
43
+ type AnyBlock = {
44
+ [K in BlockType]: Block<K>;
45
+ }[BlockType];
46
+
47
+ /**
48
+ * Insert a Node at a specific position within block content.
49
+ *
50
+ * - nodeIndex: which node to insert into
51
+ * - offset: char position within that node
52
+ *
53
+ * End-of-node (offset === length) naturally becomes an append —
54
+ * sliceNode produces no right half, incoming merges with left or gets pushed.
55
+ * No separate append function needed.
56
+ */
57
+ declare function insertAt<T extends BlockType>(block: AnyBlock, nodeIndex: number, offset: number, incoming: Node): Result<BlockContent<T>>;
58
+ declare function deleteLastChar(block: AnyBlock): Result<BlockContent<BlockType>>;
59
+ /**
60
+ * Delete content across a selection.
61
+ * After deletion, left and right boundaries are merged if formats match.
62
+ *
63
+ * For code/equation blocks: deletes within the single tuple node's text/latex.
64
+ */
65
+ declare function deleteRange<T extends BlockType>(block: AnyBlock, startNodeIndex: number, startOffset: number, endNodeIndex: number, endOffset: number): Result<BlockContent<T>>;
66
+ /**
67
+ * Split a block at a given position into two blocks.
68
+ * The original block keeps content before the cursor.
69
+ * A new block gets content after the cursor — always of type "paragraph".
70
+ *
71
+ * Not supported for code/equation blocks — returns Err.
72
+ */
73
+ declare function splitBlock(block: AnyBlock, nodeIndex: number, offset: number): Result<[AnyBlock, AnyBlock]>;
74
+ /**
75
+ * Merge blockB into blockA — blockB's content is appended to blockA's content.
76
+ * blockA's type and meta are preserved.
77
+ *
78
+ * Not supported if either block is code or equation — returns Err.
79
+ */
80
+ declare function mergeBlocks(blockA: AnyBlock, blockB: AnyBlock): Result<AnyBlock>;
81
+ /**
82
+ * Replace a selected range with an incoming Node — atomic deleteRange + insertAt.
83
+ * This is what fires when the user has a selection and types a character.
84
+ *
85
+ * Internally chains:
86
+ * 1. deleteRange — remove selected content
87
+ * 2. insertAt — insert incoming at the start of the deleted range
88
+ */
89
+ declare function replaceRange<T extends BlockType>(block: AnyBlock, startNodeIndex: number, startOffset: number, endNodeIndex: number, endOffset: number, incoming: Node): Result<BlockContent<T>>;
90
+
91
+ type TextNode = Extract<Node, {
92
+ type: "text";
93
+ }>;
94
+ type TextFormat = Pick<TextNode, "bold" | "italic" | "underline" | "strikethrough" | "highlighted" | "color" | "link">;
95
+ type NodeSelection = {
96
+ startIndex: number;
97
+ startOffset: number;
98
+ endIndex: number;
99
+ endOffset: number;
100
+ };
101
+ declare function formatNodes(nodes: Node[], sel: NodeSelection, format: keyof TextFormat, value?: unknown): Result<Node[]>;
102
+ declare function mergeAdjacentNodes(nodes: Node[]): Node[];
103
+ declare const toggleBold: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
104
+ declare const toggleItalic: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
105
+ declare const toggleUnderline: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
106
+ declare const toggleStrikethrough: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
107
+ declare const toggleHighlight: (nodes: Node[], sel: NodeSelection, color?: "yellow" | "green") => Result<Node[], string>;
108
+ declare const toggleColor: (nodes: Node[], sel: NodeSelection, color: "red" | "blue" | "green") => Result<Node[], string>;
109
+ declare const setLink: (nodes: Node[], sel: NodeSelection, href: string) => Result<Node[], string>;
110
+ declare const removeLink: (nodes: Node[], sel: NodeSelection) => Result<Node[], string>;
111
+
112
+ type TransformResult = {
113
+ block: AnyBlock;
114
+ converted: boolean;
115
+ };
116
+ /**
117
+ * Call this when a space is typed.
118
+ *
119
+ * Guards (returns converted=false if any fail):
120
+ * 1. block.type must be "paragraph"
121
+ * 2. cursorOffset must be ≤ trigger.length + 1
122
+ * — ensures the space was typed right after the trigger at position 0
123
+ * — blocks mid-text spaces like "hello - " from converting
124
+ * 3. text must start with a known trigger prefix
125
+ */
126
+ declare function applyMarkdownTransform(block: AnyBlock, cursorOffset: number): Result<TransformResult>;
127
+ /**
128
+ * Convert a block to a new type while preserving content as much as possible.
129
+ *
130
+ * rich → rich keep content as-is, update type + meta
131
+ * rich → code strip all formatting, concat all text → [CodeNode]
132
+ * rich → equation strip all formatting, concat all text → [EquationNode]
133
+ * code → rich single TextNode with code.text
134
+ * equation → rich single TextNode with equation.latex
135
+ * code → equation [EquationNode] with code.text as latex
136
+ * equation → code [CodeNode] with equation.latex as text
137
+ */
138
+ declare function changeBlockType<T extends BlockType>(block: AnyBlock, targetType: T): Result<Block<T>>;
139
+ /**
140
+ * Toggle the checked state of a todo block.
141
+ * checked: undefined → checked: true → checked: undefined (cycle)
142
+ */
143
+ declare function toggleTodo(block: AnyBlock): Result<Block<"todo">>;
144
+ type IndentableBlock = Block<"bullet"> | Block<"number"> | Block<"todo">;
145
+ /**
146
+ * Increase the depth of a bullet, number, or todo block by 1.
147
+ * Max depth is 6. Returns Err if block type is not indentable.
148
+ */
149
+ declare function indentBlock(block: AnyBlock): Result<IndentableBlock>;
150
+ /**
151
+ * Decrease the depth of a bullet, number, or todo block by 1.
152
+ * Min depth is 0. Returns Err if block type is not indentable or already at 0.
153
+ */
154
+ declare function outdentBlock(block: AnyBlock): Result<IndentableBlock>;
155
+
156
+ /**
157
+ * Serialize an array of blocks to a JSON string.
158
+ * Wraps JSON.stringify in Result.try so any circular ref or
159
+ * other stringify error surfaces as Err rather than throwing.
160
+ */
161
+ declare function serialize(blocks: AnyBlock[]): Result<string, unknown>;
162
+ /**
163
+ * Deserialize a JSON string back into AnyBlock[].
164
+ * Validates:
165
+ * - valid JSON
166
+ * - top level is an array
167
+ * - each item has id (string), type (known BlockType), meta (object), content (array)
168
+ */
169
+ declare function deserialize(json: string): Result<AnyBlock[], string>;
170
+ /**
171
+ * Serialize a Node[] to a JSON string — for clipboard copy.
172
+ * Preserves all formatting.
173
+ */
174
+ declare function serializeNodes(nodes: Node[]): Result<string, unknown>;
175
+ /**
176
+ * Deserialize a JSON string back into Node[] — for clipboard paste.
177
+ * Validates each node shape before returning.
178
+ */
179
+ declare function deserializeNodes(json: string): Result<Node[], string>;
180
+ /**
181
+ * Extract plain text from a Node[] — strips all formatting.
182
+ * text → text
183
+ * code → text
184
+ * equation → latex
185
+ */
186
+ declare function toPlainText(nodes: Node[]): string;
187
+ /**
188
+ * Convert an array of blocks to a markdown string.
189
+ *
190
+ * paragraph → plain inline
191
+ * heading1-3 → # / ## / ###
192
+ * bullet → - (indented by depth)
193
+ * number → 1. (indented by depth)
194
+ * todo → - [ ] / - [x] (indented by depth)
195
+ * code → ```language\n...\n```
196
+ * equation → $$...$$ (block equation)
197
+ */
198
+ declare function toMarkdown(blocks: AnyBlock[]): string;
199
+
200
+ type CursorPosition = {
201
+ nodeIndex: number;
202
+ offset: number;
203
+ };
204
+ /**
205
+ * Convert a flat UI cursor offset to { nodeIndex, offset }.
206
+ *
207
+ * The browser gives a single number representing position in the
208
+ * concatenated text of the block. This function walks the node array
209
+ * and finds which node that position falls in and where within it.
210
+ *
211
+ * e.g. nodes = ["Hello"(5), " World"(6)]
212
+ * flatOffset=0 → { nodeIndex: 0, offset: 0 }
213
+ * flatOffset=5 → { nodeIndex: 0, offset: 5 } (end of first node)
214
+ * flatOffset=6 → { nodeIndex: 1, offset: 1 }
215
+ * flatOffset=11 → { nodeIndex: 1, offset: 6 } (end of last node)
216
+ */
217
+ declare function flatToPosition(block: AnyBlock, flatOffset: number): Result<CursorPosition, string>;
218
+ /**
219
+ * Convert a flat UI selection { start, end } to NodeSelection.
220
+ *
221
+ * The browser gives two flat offsets from window.getSelection().
222
+ * This calls flatToPosition twice and returns a NodeSelection
223
+ * ready to pass to toggleBold, formatNodes, deleteRange, etc.
224
+ *
225
+ * e.g. nodes = ["Hello"(bold), " World"]
226
+ * { start: 3, end: 8 } → { startIndex:0, startOffset:3, endIndex:1, endOffset:3 }
227
+ */
228
+ declare function flatToSelection(block: AnyBlock, start: number, end: number): Result<NodeSelection, string>;
229
+ /**
230
+ * Inverse — convert { nodeIndex, offset } back to a flat offset.
231
+ * Useful after engine operations to restore cursor position in the DOM.
232
+ */
233
+ declare function positionToFlat(block: AnyBlock, nodeIndex: number, offset: number): Result<number, string>;
234
+
235
+ type HistoryEntry = {
236
+ blocks: AnyBlock[];
237
+ timestamp: number;
238
+ };
239
+ type History = {
240
+ past: HistoryEntry[];
241
+ present: HistoryEntry;
242
+ future: HistoryEntry[];
243
+ };
244
+ /**
245
+ * Create a new history instance with the given initial blocks.
246
+ */
247
+ declare function createHistory(initialBlocks: AnyBlock[]): History;
248
+ /**
249
+ * Push a new state onto the history stack.
250
+ * Clears the future — same as any editor after a new action post-undo.
251
+ * Optionally cap the max history size to avoid unbounded memory growth.
252
+ */
253
+ declare function push(history: History, blocks: AnyBlock[], maxSize?: number): History;
254
+ /**
255
+ * Move back one step in history.
256
+ * Returns Err if there is nothing to undo.
257
+ */
258
+ declare function undo(history: History): Result<History, string>;
259
+ /**
260
+ * Move forward one step in history.
261
+ * Returns Err if there is nothing to redo.
262
+ */
263
+ declare function redo(history: History): Result<History, string>;
264
+ declare const canUndo: (history: History) => boolean;
265
+ declare const canRedo: (history: History) => boolean;
266
+ declare const currentBlocks: (history: History) => AnyBlock[];
267
+
268
+ /**
269
+ * Generate a unique block id.
270
+ * Pass a custom fn to override the default (uuid v7).
271
+ */
272
+ declare function generateId(fn?: () => string): string;
273
+ /**
274
+ * Create a new block of the given type with default content and meta.
275
+ * Optionally pass a custom id generator.
276
+ *
277
+ * @param type - the block type to create
278
+ * @param idFn - optional custom id generator (defaults to uuid v7)
279
+ */
280
+ declare function createBlock<T extends BlockType>(type: T, idFn?: () => string): Result<Block<T>, unknown>;
281
+ declare function insertBlockAfter<T extends BlockType>(blocks: AnyBlock[], afterId: string, type: T, idFn?: () => string): Result<{
282
+ blocks: AnyBlock[];
283
+ newId: string;
284
+ }, unknown>;
285
+ declare function deleteBlock(blocks: AnyBlock[], id: string): {
286
+ blocks: AnyBlock[];
287
+ prevId: string;
288
+ };
289
+ declare function duplicateBlock(incoming: AnyBlock, newId: string): AnyBlock;
290
+ /**
291
+ * Move a block up or down by one position in the array.
292
+ * Returns the same array unchanged if block is already at the boundary.
293
+ * Returns Err if id not found.
294
+ */
295
+ declare function moveBlock(blocks: AnyBlock[], id: string, direction: "up" | "down"): Result<AnyBlock[], string>;
296
+
297
+ /**─── Wrappers ──────────────────────────────────────────────────────────────────
298
+ * Each function runs the content engine operation and returns Result<AnyBlock>
299
+ instead of Result<BlockContent<T>> — caller gets the full updated block back.
300
+ */
301
+ declare function blockInsertAt(block: AnyBlock, nodeIndex: number, offset: number, incoming: Node): Result<AnyBlock>;
302
+ declare function blockDeleteLastChar(block: AnyBlock): Result<AnyBlock>;
303
+ declare function blockDeleteRange(block: AnyBlock, startNodeIndex: number, startOffset: number, endNodeIndex: number, endOffset: number): Result<AnyBlock>;
304
+ declare function blockReplaceRange(block: AnyBlock, startNodeIndex: number, startOffset: number, endNodeIndex: number, endOffset: number, incoming: Node): Result<AnyBlock>;
305
+
306
+ export { type AnyBlock, type Block, type BlockContent, type BlockMeta, type BlockType, type History, type HistoryEntry, type Node, type NodeSelection, applyMarkdownTransform, blockDeleteLastChar, blockDeleteRange, blockInsertAt, blockReplaceRange, canRedo, canUndo, changeBlockType, createBlock, createHistory, currentBlocks, deleteBlock, deleteLastChar, deleteRange, deserialize, deserializeNodes, duplicateBlock, flatToPosition, flatToSelection, formatNodes, generateId, indentBlock, insertAt, insertBlockAfter, mergeAdjacentNodes, mergeBlocks, moveBlock, outdentBlock, positionToFlat, push, redo, removeLink, replaceRange, serialize, serializeNodes, setLink, splitBlock, toMarkdown, toPlainText, toggleBold, toggleColor, toggleHighlight, toggleItalic, toggleStrikethrough, toggleTodo, toggleUnderline, undo };