@reiwuzen/blocky 1.3.4 → 1.4.1

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 CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  applyMarkdownTransform: () => applyMarkdownTransform,
24
+ areBlocksSame: () => areBlocksSame,
24
25
  canRedo: () => canRedo,
25
26
  canUndo: () => canUndo,
26
27
  changeBlockType: () => changeBlockType,
@@ -619,18 +620,15 @@ function buildMetaForTarget(targetType) {
619
620
  case "number":
620
621
  case "todo":
621
622
  return { depth: 0 };
622
- case "heading1":
623
- case "heading2":
624
- case "heading3":
625
- case "paragraph":
626
- case "code":
627
- case "equation":
623
+ default:
628
624
  return {};
629
625
  }
630
626
  }
627
+ function isList(type) {
628
+ return type === "bullet" || type === "todo" || type === "number";
629
+ }
631
630
  function deriveMeta(block, targetType) {
632
- if (isList(block.type) && isList(targetType))
633
- return block.meta;
631
+ if (isList(block.type) && isList(targetType)) return block.meta;
634
632
  return buildMetaForTarget(targetType);
635
633
  }
636
634
  function toggleTodo(block) {
@@ -645,9 +643,6 @@ var MAX_DEPTH = 6;
645
643
  function isIndentable(block) {
646
644
  return block.type === "bullet" || block.type === "number" || block.type === "todo";
647
645
  }
648
- function isList(type) {
649
- return type === "bullet" || type === "todo" || type === "number";
650
- }
651
646
  function indentBlock(block) {
652
647
  if (!isIndentable(block))
653
648
  return import_result4.Result.Err(
@@ -671,6 +666,31 @@ function outdentBlock(block) {
671
666
  meta: { ...block.meta, depth: block.meta.depth - 1 }
672
667
  });
673
668
  }
669
+ function areBlocksSame(blocks, withBlocks) {
670
+ if (blocks.length !== withBlocks.length) return false;
671
+ return blocks.every((a, i) => {
672
+ const b = withBlocks[i];
673
+ if (a.type !== b.type) return false;
674
+ if (JSON.stringify(a.meta) !== JSON.stringify(b.meta)) return false;
675
+ if (a.content.length !== b.content.length) return false;
676
+ return a.content.every((an, j) => {
677
+ const bn = b.content[j];
678
+ if (an.type !== bn.type) return false;
679
+ switch (an.type) {
680
+ case "code":
681
+ return an.text === bn.text;
682
+ case "equation":
683
+ return an.latex === bn.latex;
684
+ case "text": {
685
+ const bt = bn;
686
+ return an.text === bt.text && an.bold === bt.bold && an.italic === bt.italic && an.underline === bt.underline && an.strikethrough === bt.strikethrough && an.highlighted === bt.highlighted && an.color === bt.color && an.link === bt.link;
687
+ }
688
+ default:
689
+ return false;
690
+ }
691
+ });
692
+ });
693
+ }
674
694
 
675
695
  // src/engine/serializer.ts
676
696
  var import_result5 = require("@reiwuzen/result");
@@ -769,6 +789,7 @@ function nodesToMarkdown(nodes) {
769
789
  return nodes.map((n) => {
770
790
  if (n.type === "code") return `\`${n.text}\``;
771
791
  if (n.type === "equation") return `$${n.latex}$`;
792
+ if (n.type === "ref") return `[${n.label}](${n.href})`;
772
793
  let text = n.text;
773
794
  if (n.bold) text = `**${text}**`;
774
795
  if (n.italic) text = `*${text}*`;
@@ -857,6 +878,7 @@ var currentBlocks = (history) => history.present.blocks;
857
878
  // Annotate the CommonJS export names for ESM import in node:
858
879
  0 && (module.exports = {
859
880
  applyMarkdownTransform,
881
+ areBlocksSame,
860
882
  canRedo,
861
883
  canUndo,
862
884
  changeBlockType,
package/dist/index.d.cts CHANGED
@@ -5,12 +5,16 @@ type Node = {
5
5
  type: "text";
6
6
  text: string;
7
7
  highlighted?: "yellow" | "green";
8
- color?: "red" | 'blue' | 'green';
8
+ color?: "red" | "blue" | "green";
9
9
  bold?: true;
10
10
  italic?: true;
11
11
  underline?: true;
12
12
  strikethrough?: true;
13
13
  link?: string;
14
+ } | {
15
+ type: "ref";
16
+ href: string;
17
+ label: string;
14
18
  } | {
15
19
  type: "code";
16
20
  text: string;
@@ -111,47 +115,98 @@ declare const removeLink: (nodes: Node[], sel: NodeSelection) => Result<Node[],
111
115
 
112
116
  type TransformResult = {
113
117
  block: AnyBlock;
118
+ /** true if the block type was actually changed, false if no trigger matched */
114
119
  converted: boolean;
115
120
  };
116
121
  /**
117
- * Call this when a space is typed.
122
+ * Attempt to convert a paragraph block into another type based on a
123
+ * Markdown-style prefix typed by the user.
124
+ *
125
+ * Call this when a **space** is typed inside a paragraph block.
126
+ *
127
+ * Supported triggers (must appear at position 0, immediately before the space):
128
+ *
129
+ * | Typed | Converts to |
130
+ * |----------|-------------|
131
+ * | `-` | bullet |
132
+ * | `1.` | number |
133
+ * | `[]` | todo |
134
+ * | `#` | heading1 |
135
+ * | `##` | heading2 |
136
+ * | `###` | heading3 |
118
137
  *
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
138
+ * Guards returns `converted: false` without mutating if any fail:
139
+ * 1. `block.type` must be `"paragraph"`
140
+ * 2. The text must start with a known trigger prefix
141
+ * 3. `cursorOffset` must be `trigger.length + 1` ensures the space
142
+ * was typed right after the trigger (blocks mid-text triggers like
143
+ * `"hello - "` from accidentally converting)
144
+ *
145
+ * On success the returned block has the trigger prefix stripped from its
146
+ * content and a freshly built `meta` for the target type.
125
147
  */
126
148
  declare function applyMarkdownTransform(block: AnyBlock, cursorOffset: number): Result<TransformResult>;
127
149
  /**
128
- * Convert a block to a new type while preserving content as much as possible.
150
+ * Convert a block to a new type, preserving content and meta where possible.
151
+ *
152
+ * Content conversion rules:
129
153
  *
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
154
+ * | From | To | Content result |
155
+ * |-------------|-------------|----------------------------------------|
156
+ * | rich | rich | kept as-is |
157
+ * | rich | code | all text concatenated `[CodeNode]` |
158
+ * | rich | equation | all text concatenated `[EquationNode]`|
159
+ * | code | rich | `code.text` single `TextNode` |
160
+ * | equation | rich | `equation.latex` single `TextNode` |
161
+ * | code | equation | `code.text` as `latex` |
162
+ * | equation | code | `equation.latex` as `text` |
163
+ *
164
+ * Meta conversion rules:
165
+ * - list → list : `meta` (including `depth`) is preserved
166
+ * - anything else: fresh default `meta` is built for the target type
137
167
  */
138
168
  declare function changeBlockType<T extends BlockType>(block: AnyBlock, targetType: T): Result<Block<T>>;
139
169
  /**
140
170
  * Toggle the checked state of a todo block.
141
- * checked: undefined → checked: true → checked: undefined (cycle)
171
+ *
172
+ * Cycle: `undefined → true → undefined`
173
+ *
174
+ * Returns `Err` if the block is not of type `"todo"`.
142
175
  */
143
176
  declare function toggleTodo(block: AnyBlock): Result<Block<"todo">>;
144
177
  type IndentableBlock = Block<"bullet"> | Block<"number"> | Block<"todo">;
145
178
  /**
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.
179
+ * Increase the indentation depth of a list block (`bullet`, `number`, `todo`) by 1.
180
+ *
181
+ * - Maximum depth is `6`.
182
+ * - Returns `Err` if the block type is not indentable or is already at max depth.
148
183
  */
149
184
  declare function indentBlock(block: AnyBlock): Result<IndentableBlock>;
150
185
  /**
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.
186
+ * Decrease the indentation depth of a list block (`bullet`, `number`, `todo`) by 1.
187
+ *
188
+ * - Minimum depth is `0`.
189
+ * - Returns `Err` if the block type is not indentable or is already at depth 0.
153
190
  */
154
191
  declare function outdentBlock(block: AnyBlock): Result<IndentableBlock>;
192
+ /**
193
+ * Deep-equality check between two block arrays.
194
+ *
195
+ * Two arrays are considered the same when:
196
+ * - They have the same length
197
+ * - Every block at the same index has the same `id`, `type`, `meta`, and `content`
198
+ *
199
+ * Content nodes are compared field-by-field. Unknown extra fields on nodes are
200
+ * ignored so the check stays stable across schema additions.
201
+ *
202
+ * Useful for preventing redundant saves or snapshot writes:
203
+ * ```ts
204
+ * if (!areBlocksSame(current, saved)) {
205
+ * await createSnapshot(current);
206
+ * }
207
+ * ```
208
+ */
209
+ declare function areBlocksSame(blocks: AnyBlock[], withBlocks: AnyBlock[]): boolean;
155
210
 
156
211
  /**
157
212
  * Serialize an array of blocks to a JSON string.
@@ -267,4 +322,4 @@ declare function duplicateBlockAfter(blocks: AnyBlock[], id: string, newId?: str
267
322
  */
268
323
  declare function moveBlock(blocks: AnyBlock[], id: string, direction: "up" | "down"): Result<AnyBlock[], string>;
269
324
 
270
- export { type AnyBlock, type Block, type BlockContent, type BlockMeta, type BlockType, type History, type HistoryEntry, type Node, type NodeSelection, applyMarkdownTransform, canRedo, canUndo, changeBlockType, createBlock, createBlockAfter, createHistory, currentBlocks, deleteBlock, deleteLastChar, deleteRange, deserialize, deserializeNodes, duplicateBlock, duplicateBlockAfter, formatNodes, generateId, indentBlock, insertAt, insertBlockAfter, mergeAdjacentNodes, mergeBlocks, moveBlock, outdentBlock, push, redo, removeLink, replaceRange, serialize, serializeNodes, setLink, splitBlock, toMarkdown, toPlainText, toggleBold, toggleColor, toggleHighlight, toggleItalic, toggleStrikethrough, toggleTodo, toggleUnderline, undo };
325
+ export { type AnyBlock, type Block, type BlockContent, type BlockMeta, type BlockType, type History, type HistoryEntry, type Node, type NodeSelection, applyMarkdownTransform, areBlocksSame, canRedo, canUndo, changeBlockType, createBlock, createBlockAfter, createHistory, currentBlocks, deleteBlock, deleteLastChar, deleteRange, deserialize, deserializeNodes, duplicateBlock, duplicateBlockAfter, formatNodes, generateId, indentBlock, insertAt, insertBlockAfter, mergeAdjacentNodes, mergeBlocks, moveBlock, outdentBlock, push, redo, removeLink, replaceRange, serialize, serializeNodes, setLink, splitBlock, toMarkdown, toPlainText, toggleBold, toggleColor, toggleHighlight, toggleItalic, toggleStrikethrough, toggleTodo, toggleUnderline, undo };
package/dist/index.d.ts CHANGED
@@ -5,12 +5,16 @@ type Node = {
5
5
  type: "text";
6
6
  text: string;
7
7
  highlighted?: "yellow" | "green";
8
- color?: "red" | 'blue' | 'green';
8
+ color?: "red" | "blue" | "green";
9
9
  bold?: true;
10
10
  italic?: true;
11
11
  underline?: true;
12
12
  strikethrough?: true;
13
13
  link?: string;
14
+ } | {
15
+ type: "ref";
16
+ href: string;
17
+ label: string;
14
18
  } | {
15
19
  type: "code";
16
20
  text: string;
@@ -111,47 +115,98 @@ declare const removeLink: (nodes: Node[], sel: NodeSelection) => Result<Node[],
111
115
 
112
116
  type TransformResult = {
113
117
  block: AnyBlock;
118
+ /** true if the block type was actually changed, false if no trigger matched */
114
119
  converted: boolean;
115
120
  };
116
121
  /**
117
- * Call this when a space is typed.
122
+ * Attempt to convert a paragraph block into another type based on a
123
+ * Markdown-style prefix typed by the user.
124
+ *
125
+ * Call this when a **space** is typed inside a paragraph block.
126
+ *
127
+ * Supported triggers (must appear at position 0, immediately before the space):
128
+ *
129
+ * | Typed | Converts to |
130
+ * |----------|-------------|
131
+ * | `-` | bullet |
132
+ * | `1.` | number |
133
+ * | `[]` | todo |
134
+ * | `#` | heading1 |
135
+ * | `##` | heading2 |
136
+ * | `###` | heading3 |
118
137
  *
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
138
+ * Guards returns `converted: false` without mutating if any fail:
139
+ * 1. `block.type` must be `"paragraph"`
140
+ * 2. The text must start with a known trigger prefix
141
+ * 3. `cursorOffset` must be `trigger.length + 1` ensures the space
142
+ * was typed right after the trigger (blocks mid-text triggers like
143
+ * `"hello - "` from accidentally converting)
144
+ *
145
+ * On success the returned block has the trigger prefix stripped from its
146
+ * content and a freshly built `meta` for the target type.
125
147
  */
126
148
  declare function applyMarkdownTransform(block: AnyBlock, cursorOffset: number): Result<TransformResult>;
127
149
  /**
128
- * Convert a block to a new type while preserving content as much as possible.
150
+ * Convert a block to a new type, preserving content and meta where possible.
151
+ *
152
+ * Content conversion rules:
129
153
  *
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
154
+ * | From | To | Content result |
155
+ * |-------------|-------------|----------------------------------------|
156
+ * | rich | rich | kept as-is |
157
+ * | rich | code | all text concatenated `[CodeNode]` |
158
+ * | rich | equation | all text concatenated `[EquationNode]`|
159
+ * | code | rich | `code.text` single `TextNode` |
160
+ * | equation | rich | `equation.latex` single `TextNode` |
161
+ * | code | equation | `code.text` as `latex` |
162
+ * | equation | code | `equation.latex` as `text` |
163
+ *
164
+ * Meta conversion rules:
165
+ * - list → list : `meta` (including `depth`) is preserved
166
+ * - anything else: fresh default `meta` is built for the target type
137
167
  */
138
168
  declare function changeBlockType<T extends BlockType>(block: AnyBlock, targetType: T): Result<Block<T>>;
139
169
  /**
140
170
  * Toggle the checked state of a todo block.
141
- * checked: undefined → checked: true → checked: undefined (cycle)
171
+ *
172
+ * Cycle: `undefined → true → undefined`
173
+ *
174
+ * Returns `Err` if the block is not of type `"todo"`.
142
175
  */
143
176
  declare function toggleTodo(block: AnyBlock): Result<Block<"todo">>;
144
177
  type IndentableBlock = Block<"bullet"> | Block<"number"> | Block<"todo">;
145
178
  /**
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.
179
+ * Increase the indentation depth of a list block (`bullet`, `number`, `todo`) by 1.
180
+ *
181
+ * - Maximum depth is `6`.
182
+ * - Returns `Err` if the block type is not indentable or is already at max depth.
148
183
  */
149
184
  declare function indentBlock(block: AnyBlock): Result<IndentableBlock>;
150
185
  /**
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.
186
+ * Decrease the indentation depth of a list block (`bullet`, `number`, `todo`) by 1.
187
+ *
188
+ * - Minimum depth is `0`.
189
+ * - Returns `Err` if the block type is not indentable or is already at depth 0.
153
190
  */
154
191
  declare function outdentBlock(block: AnyBlock): Result<IndentableBlock>;
192
+ /**
193
+ * Deep-equality check between two block arrays.
194
+ *
195
+ * Two arrays are considered the same when:
196
+ * - They have the same length
197
+ * - Every block at the same index has the same `id`, `type`, `meta`, and `content`
198
+ *
199
+ * Content nodes are compared field-by-field. Unknown extra fields on nodes are
200
+ * ignored so the check stays stable across schema additions.
201
+ *
202
+ * Useful for preventing redundant saves or snapshot writes:
203
+ * ```ts
204
+ * if (!areBlocksSame(current, saved)) {
205
+ * await createSnapshot(current);
206
+ * }
207
+ * ```
208
+ */
209
+ declare function areBlocksSame(blocks: AnyBlock[], withBlocks: AnyBlock[]): boolean;
155
210
 
156
211
  /**
157
212
  * Serialize an array of blocks to a JSON string.
@@ -267,4 +322,4 @@ declare function duplicateBlockAfter(blocks: AnyBlock[], id: string, newId?: str
267
322
  */
268
323
  declare function moveBlock(blocks: AnyBlock[], id: string, direction: "up" | "down"): Result<AnyBlock[], string>;
269
324
 
270
- export { type AnyBlock, type Block, type BlockContent, type BlockMeta, type BlockType, type History, type HistoryEntry, type Node, type NodeSelection, applyMarkdownTransform, canRedo, canUndo, changeBlockType, createBlock, createBlockAfter, createHistory, currentBlocks, deleteBlock, deleteLastChar, deleteRange, deserialize, deserializeNodes, duplicateBlock, duplicateBlockAfter, formatNodes, generateId, indentBlock, insertAt, insertBlockAfter, mergeAdjacentNodes, mergeBlocks, moveBlock, outdentBlock, push, redo, removeLink, replaceRange, serialize, serializeNodes, setLink, splitBlock, toMarkdown, toPlainText, toggleBold, toggleColor, toggleHighlight, toggleItalic, toggleStrikethrough, toggleTodo, toggleUnderline, undo };
325
+ export { type AnyBlock, type Block, type BlockContent, type BlockMeta, type BlockType, type History, type HistoryEntry, type Node, type NodeSelection, applyMarkdownTransform, areBlocksSame, canRedo, canUndo, changeBlockType, createBlock, createBlockAfter, createHistory, currentBlocks, deleteBlock, deleteLastChar, deleteRange, deserialize, deserializeNodes, duplicateBlock, duplicateBlockAfter, formatNodes, generateId, indentBlock, insertAt, insertBlockAfter, mergeAdjacentNodes, mergeBlocks, moveBlock, outdentBlock, push, redo, removeLink, replaceRange, serialize, serializeNodes, setLink, splitBlock, toMarkdown, toPlainText, toggleBold, toggleColor, toggleHighlight, toggleItalic, toggleStrikethrough, toggleTodo, toggleUnderline, undo };
package/dist/index.js CHANGED
@@ -552,18 +552,15 @@ function buildMetaForTarget(targetType) {
552
552
  case "number":
553
553
  case "todo":
554
554
  return { depth: 0 };
555
- case "heading1":
556
- case "heading2":
557
- case "heading3":
558
- case "paragraph":
559
- case "code":
560
- case "equation":
555
+ default:
561
556
  return {};
562
557
  }
563
558
  }
559
+ function isList(type) {
560
+ return type === "bullet" || type === "todo" || type === "number";
561
+ }
564
562
  function deriveMeta(block, targetType) {
565
- if (isList(block.type) && isList(targetType))
566
- return block.meta;
563
+ if (isList(block.type) && isList(targetType)) return block.meta;
567
564
  return buildMetaForTarget(targetType);
568
565
  }
569
566
  function toggleTodo(block) {
@@ -578,9 +575,6 @@ var MAX_DEPTH = 6;
578
575
  function isIndentable(block) {
579
576
  return block.type === "bullet" || block.type === "number" || block.type === "todo";
580
577
  }
581
- function isList(type) {
582
- return type === "bullet" || type === "todo" || type === "number";
583
- }
584
578
  function indentBlock(block) {
585
579
  if (!isIndentable(block))
586
580
  return Result4.Err(
@@ -604,6 +598,31 @@ function outdentBlock(block) {
604
598
  meta: { ...block.meta, depth: block.meta.depth - 1 }
605
599
  });
606
600
  }
601
+ function areBlocksSame(blocks, withBlocks) {
602
+ if (blocks.length !== withBlocks.length) return false;
603
+ return blocks.every((a, i) => {
604
+ const b = withBlocks[i];
605
+ if (a.type !== b.type) return false;
606
+ if (JSON.stringify(a.meta) !== JSON.stringify(b.meta)) return false;
607
+ if (a.content.length !== b.content.length) return false;
608
+ return a.content.every((an, j) => {
609
+ const bn = b.content[j];
610
+ if (an.type !== bn.type) return false;
611
+ switch (an.type) {
612
+ case "code":
613
+ return an.text === bn.text;
614
+ case "equation":
615
+ return an.latex === bn.latex;
616
+ case "text": {
617
+ const bt = bn;
618
+ return an.text === bt.text && an.bold === bt.bold && an.italic === bt.italic && an.underline === bt.underline && an.strikethrough === bt.strikethrough && an.highlighted === bt.highlighted && an.color === bt.color && an.link === bt.link;
619
+ }
620
+ default:
621
+ return false;
622
+ }
623
+ });
624
+ });
625
+ }
607
626
 
608
627
  // src/engine/serializer.ts
609
628
  import { Result as Result5 } from "@reiwuzen/result";
@@ -702,6 +721,7 @@ function nodesToMarkdown(nodes) {
702
721
  return nodes.map((n) => {
703
722
  if (n.type === "code") return `\`${n.text}\``;
704
723
  if (n.type === "equation") return `$${n.latex}$`;
724
+ if (n.type === "ref") return `[${n.label}](${n.href})`;
705
725
  let text = n.text;
706
726
  if (n.bold) text = `**${text}**`;
707
727
  if (n.italic) text = `*${text}*`;
@@ -789,6 +809,7 @@ var canRedo = (history) => history.future.length > 0;
789
809
  var currentBlocks = (history) => history.present.blocks;
790
810
  export {
791
811
  applyMarkdownTransform,
812
+ areBlocksSame,
792
813
  canRedo,
793
814
  canUndo,
794
815
  changeBlockType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reiwuzen/blocky",
3
- "version": "1.3.4",
3
+ "version": "1.4.1",
4
4
  "description": "Pure TypeScript block editor engine. Headless, framework-agnostic — content mutation, formatting, transforms, serialization, and history.",
5
5
  "author": "Rei WuZen",
6
6
  "license": "ISC",