@blankdotpage/cake 0.1.68 → 0.1.69

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 (57) hide show
  1. package/dist/cake/core/mapping/cursor-source-map.d.ts +11 -0
  2. package/dist/cake/core/mapping/cursor-source-map.d.ts.map +1 -1
  3. package/dist/cake/core/mapping/cursor-source-map.js +159 -21
  4. package/dist/cake/core/runtime.d.ts +4 -0
  5. package/dist/cake/core/runtime.d.ts.map +1 -1
  6. package/dist/cake/core/runtime.js +332 -215
  7. package/dist/cake/dom/render.d.ts +32 -2
  8. package/dist/cake/dom/render.d.ts.map +1 -1
  9. package/dist/cake/dom/render.js +401 -118
  10. package/dist/cake/editor/cake-editor.d.ts +8 -1
  11. package/dist/cake/editor/cake-editor.d.ts.map +1 -1
  12. package/dist/cake/editor/cake-editor.js +172 -100
  13. package/dist/cake/editor/internal/editor-text-model.d.ts +49 -0
  14. package/dist/cake/editor/internal/editor-text-model.d.ts.map +1 -0
  15. package/dist/cake/editor/internal/editor-text-model.js +284 -0
  16. package/dist/cake/editor/selection/selection-geometry-dom.d.ts +5 -1
  17. package/dist/cake/editor/selection/selection-geometry-dom.d.ts.map +1 -1
  18. package/dist/cake/editor/selection/selection-geometry-dom.js +4 -5
  19. package/dist/cake/editor/selection/selection-layout-dom.d.ts.map +1 -1
  20. package/dist/cake/editor/selection/selection-layout-dom.js +2 -5
  21. package/dist/cake/editor/selection/selection-layout.d.ts +2 -15
  22. package/dist/cake/editor/selection/selection-layout.d.ts.map +1 -1
  23. package/dist/cake/editor/selection/selection-layout.js +1 -99
  24. package/dist/cake/editor/selection/selection-navigation.d.ts +4 -0
  25. package/dist/cake/editor/selection/selection-navigation.d.ts.map +1 -1
  26. package/dist/cake/editor/selection/selection-navigation.js +1 -2
  27. package/dist/cake/extensions/link/link.d.ts.map +1 -1
  28. package/dist/cake/extensions/link/link.js +1 -7
  29. package/dist/cake/extensions/shared/structural-reparse-policy.js +2 -2
  30. package/package.json +5 -2
  31. package/dist/cake/editor/selection/visible-text.d.ts +0 -5
  32. package/dist/cake/editor/selection/visible-text.d.ts.map +0 -1
  33. package/dist/cake/editor/selection/visible-text.js +0 -66
  34. package/dist/cake/engine/cake-engine.d.ts +0 -230
  35. package/dist/cake/engine/cake-engine.d.ts.map +0 -1
  36. package/dist/cake/engine/cake-engine.js +0 -3589
  37. package/dist/cake/engine/selection/selection-geometry-dom.d.ts +0 -24
  38. package/dist/cake/engine/selection/selection-geometry-dom.d.ts.map +0 -1
  39. package/dist/cake/engine/selection/selection-geometry-dom.js +0 -302
  40. package/dist/cake/engine/selection/selection-geometry.d.ts +0 -22
  41. package/dist/cake/engine/selection/selection-geometry.d.ts.map +0 -1
  42. package/dist/cake/engine/selection/selection-geometry.js +0 -158
  43. package/dist/cake/engine/selection/selection-layout-dom.d.ts +0 -50
  44. package/dist/cake/engine/selection/selection-layout-dom.d.ts.map +0 -1
  45. package/dist/cake/engine/selection/selection-layout-dom.js +0 -781
  46. package/dist/cake/engine/selection/selection-layout.d.ts +0 -55
  47. package/dist/cake/engine/selection/selection-layout.d.ts.map +0 -1
  48. package/dist/cake/engine/selection/selection-layout.js +0 -128
  49. package/dist/cake/engine/selection/selection-navigation.d.ts +0 -22
  50. package/dist/cake/engine/selection/selection-navigation.d.ts.map +0 -1
  51. package/dist/cake/engine/selection/selection-navigation.js +0 -229
  52. package/dist/cake/engine/selection/visible-text.d.ts +0 -5
  53. package/dist/cake/engine/selection/visible-text.d.ts.map +0 -1
  54. package/dist/cake/engine/selection/visible-text.js +0 -66
  55. package/dist/cake/react/CakeEditor.d.ts +0 -58
  56. package/dist/cake/react/CakeEditor.d.ts.map +0 -1
  57. package/dist/cake/react/CakeEditor.js +0 -225
@@ -1,6 +1,31 @@
1
1
  import type { Doc, Inline } from "../core/types";
2
2
  import type { Runtime } from "../core/runtime";
3
- import { createDomMap } from "./dom-map";
3
+ import { createDomMap, type TextRun } from "./dom-map";
4
+ export type RenderSnapshotBlock = {
5
+ nodeStart: number;
6
+ nodeEnd: number;
7
+ runStart: number;
8
+ runEnd: number;
9
+ cursorStart: number;
10
+ cursorEnd: number;
11
+ lineStart: number;
12
+ lineEnd: number;
13
+ };
14
+ export type RenderSnapshot = {
15
+ blocks: RenderSnapshotBlock[];
16
+ nodes: Node[];
17
+ runs: TextRun[];
18
+ };
19
+ export type DirtyCursorRange = {
20
+ previous: {
21
+ start: number;
22
+ end: number;
23
+ };
24
+ next: {
25
+ start: number;
26
+ end: number;
27
+ };
28
+ };
4
29
  export type RenderResult = {
5
30
  root: HTMLElement;
6
31
  map: ReturnType<typeof createDomMap>;
@@ -8,8 +33,13 @@ export type RenderResult = {
8
33
  export type RenderContentResult = {
9
34
  content: Node[];
10
35
  map: ReturnType<typeof createDomMap>;
36
+ snapshot: RenderSnapshot;
37
+ };
38
+ export type RenderDocContentOptions = {
39
+ previousSnapshot?: RenderSnapshot | null;
40
+ dirtyCursorRange?: DirtyCursorRange | null;
11
41
  };
12
- export declare function renderDocContent(doc: Doc, dom: Runtime["dom"], root?: HTMLElement): RenderContentResult;
42
+ export declare function renderDocContent(doc: Doc, dom: Runtime["dom"], root?: HTMLElement, options?: RenderDocContentOptions): RenderContentResult;
13
43
  export declare function renderDoc(doc: Doc, dom: Runtime["dom"]): RenderResult;
14
44
  export declare function mergeInlineForRender(inlines: Inline[]): Inline[];
15
45
  //# sourceMappingURL=render.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../src/cake/dom/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,GAAG,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE/C,OAAO,EACL,YAAY,EAGb,MAAM,WAAW,CAAC;AAEnB,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,IAAI,EAAE,CAAC;IAChB,GAAG,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;CACtC,CAAC;AASF,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,EACnB,IAAI,CAAC,EAAE,WAAW,GACjB,mBAAmB,CA4WrB;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,YAAY,CA0IrE;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAiChE"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../src/cake/dom/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,GAAG,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAG/C,OAAO,EACL,YAAY,EAEZ,KAAK,OAAO,EACb,MAAM,WAAW,CAAC;AAEnB,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE;QACR,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,IAAI,EAAE,CAAC;IAChB,GAAG,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;IACrC,QAAQ,EAAE,cAAc,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,gBAAgB,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC,gBAAgB,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC5C,CAAC;AA2KF,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,EACnB,IAAI,CAAC,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE,uBAAuB,GAChC,mBAAmB,CAqsBrB;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,YAAY,CASrE;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAiChE"}
@@ -1,3 +1,4 @@
1
+ import { graphemeCount } from "../shared/segmenter";
1
2
  import { createDomMap, createTextRun as createTextRunBase, } from "./dom-map";
2
3
  function normalizeNodes(result) {
3
4
  if (!result) {
@@ -5,8 +6,209 @@ function normalizeNodes(result) {
5
6
  }
6
7
  return Array.isArray(result) ? result : [result];
7
8
  }
8
- export function renderDocContent(doc, dom, root) {
9
+ function isManagedRootNode(node) {
10
+ if (!(node instanceof Element)) {
11
+ return false;
12
+ }
13
+ return (node.hasAttribute("data-line-index") ||
14
+ node.hasAttribute("data-block-wrapper") ||
15
+ node.hasAttribute("data-block-atom") ||
16
+ node.classList.contains("cake-line"));
17
+ }
18
+ function countInlineCursorUnits(inline) {
19
+ if (inline.type === "text") {
20
+ return graphemeCount(inline.text);
21
+ }
22
+ if (inline.type === "inline-wrapper") {
23
+ let total = 0;
24
+ for (const child of inline.children) {
25
+ total += countInlineCursorUnits(child);
26
+ }
27
+ return total;
28
+ }
29
+ if (inline.type === "inline-atom") {
30
+ return 1;
31
+ }
32
+ return 0;
33
+ }
34
+ function measureBlock(block) {
35
+ if (block.type === "paragraph") {
36
+ let cursorLength = 0;
37
+ for (const inline of block.content) {
38
+ cursorLength += countInlineCursorUnits(inline);
39
+ }
40
+ return { cursorLength, lineCount: 1 };
41
+ }
42
+ if (block.type === "block-atom") {
43
+ return { cursorLength: 1, lineCount: 1 };
44
+ }
45
+ if (block.type === "block-wrapper") {
46
+ let cursorLength = 0;
47
+ let lineCount = 0;
48
+ for (let i = 0; i < block.blocks.length; i += 1) {
49
+ const child = block.blocks[i];
50
+ const measured = measureBlock(child);
51
+ cursorLength += measured.cursorLength;
52
+ lineCount += measured.lineCount;
53
+ if (i < block.blocks.length - 1) {
54
+ cursorLength += 1;
55
+ }
56
+ }
57
+ return { cursorLength, lineCount };
58
+ }
59
+ return { cursorLength: 0, lineCount: 0 };
60
+ }
61
+ function measureTopLevelBlocks(doc) {
62
+ const measuredBlocks = [];
63
+ let cursorOffset = 0;
64
+ let lineIndex = 0;
65
+ doc.blocks.forEach((block, index) => {
66
+ const measured = measureBlock(block);
67
+ measuredBlocks.push({
68
+ cursorStart: cursorOffset,
69
+ cursorEnd: cursorOffset + measured.cursorLength,
70
+ lineStart: lineIndex,
71
+ lineEnd: lineIndex + measured.lineCount,
72
+ });
73
+ cursorOffset += measured.cursorLength;
74
+ lineIndex += measured.lineCount;
75
+ if (index < doc.blocks.length - 1) {
76
+ cursorOffset += 1;
77
+ }
78
+ });
79
+ return measuredBlocks;
80
+ }
81
+ function blockRangeForCursorRange(blocks, start, end) {
82
+ if (blocks.length === 0) {
83
+ return { start: 0, end: 0 };
84
+ }
85
+ const rangeStart = Math.min(start, end);
86
+ const rangeEnd = Math.max(start, end);
87
+ let first = -1;
88
+ let last = -1;
89
+ for (let i = 0; i < blocks.length; i += 1) {
90
+ const block = blocks[i];
91
+ const spanStart = block.cursorStart;
92
+ const spanEnd = i < blocks.length - 1 ? block.cursorEnd + 1 : block.cursorEnd;
93
+ if (spanEnd < rangeStart || spanStart > rangeEnd) {
94
+ continue;
95
+ }
96
+ if (first === -1) {
97
+ first = i;
98
+ }
99
+ last = i;
100
+ }
101
+ if (first === -1) {
102
+ if (rangeEnd <= blocks[0].cursorStart) {
103
+ return { start: 0, end: 0 };
104
+ }
105
+ return { start: blocks.length, end: blocks.length };
106
+ }
107
+ return { start: first, end: last + 1 };
108
+ }
109
+ function shiftRun(run, delta) {
110
+ if (delta === 0) {
111
+ return run;
112
+ }
113
+ return {
114
+ node: run.node,
115
+ cursorStart: run.cursorStart + delta,
116
+ cursorEnd: run.cursorEnd + delta,
117
+ boundaryOffsets: run.boundaryOffsets,
118
+ };
119
+ }
120
+ function snapshotMatchesRoot(root, snapshot) {
121
+ const managedChildren = Array.from(root.childNodes).filter(isManagedRootNode);
122
+ if (managedChildren.length !== snapshot.nodes.length) {
123
+ return false;
124
+ }
125
+ for (let i = 0; i < managedChildren.length; i += 1) {
126
+ if (managedChildren[i] !== snapshot.nodes[i]) {
127
+ return false;
128
+ }
129
+ }
130
+ return true;
131
+ }
132
+ export function renderDocContent(doc, dom, root, options) {
133
+ const measuredBlocks = measureTopLevelBlocks(doc);
134
+ const previousSnapshot = options?.previousSnapshot ?? null;
135
+ const dirtyCursorRange = options?.dirtyCursorRange ?? null;
136
+ const canReuseSnapshot = Boolean(root && previousSnapshot && snapshotMatchesRoot(root, previousSnapshot));
137
+ let oldDirtyStart = 0;
138
+ let oldDirtyEnd = previousSnapshot?.blocks.length ?? 0;
139
+ let newDirtyStart = 0;
140
+ let newDirtyEnd = doc.blocks.length;
141
+ if (canReuseSnapshot) {
142
+ if (dirtyCursorRange) {
143
+ const previousRange = blockRangeForCursorRange(previousSnapshot.blocks, dirtyCursorRange.previous.start, dirtyCursorRange.previous.end);
144
+ const nextRange = blockRangeForCursorRange(measuredBlocks, dirtyCursorRange.next.start, dirtyCursorRange.next.end);
145
+ oldDirtyStart = previousRange.start;
146
+ oldDirtyEnd = previousRange.end;
147
+ newDirtyStart = nextRange.start;
148
+ newDirtyEnd = nextRange.end;
149
+ }
150
+ else {
151
+ oldDirtyStart = 0;
152
+ oldDirtyEnd = 0;
153
+ newDirtyStart = 0;
154
+ newDirtyEnd = 0;
155
+ }
156
+ const oldDirtyLineCount = previousSnapshot.blocks
157
+ .slice(oldDirtyStart, oldDirtyEnd)
158
+ .reduce((sum, block) => sum + (block.lineEnd - block.lineStart), 0);
159
+ const newDirtyLineCount = measuredBlocks
160
+ .slice(newDirtyStart, newDirtyEnd)
161
+ .reduce((sum, block) => sum + (block.lineEnd - block.lineStart), 0);
162
+ // If dirty blocks changed total line count, line indices shift for all trailing
163
+ // blocks. Re-render tail to keep data-line-index attributes correct.
164
+ if (oldDirtyLineCount !== newDirtyLineCount) {
165
+ oldDirtyEnd = previousSnapshot.blocks.length;
166
+ newDirtyEnd = measuredBlocks.length;
167
+ }
168
+ let canReusePrefixSuffix = true;
169
+ for (let i = 0; i < newDirtyStart; i += 1) {
170
+ const oldBlock = previousSnapshot.blocks[i];
171
+ const nextMeasured = measuredBlocks[i];
172
+ if (!oldBlock || !nextMeasured) {
173
+ canReusePrefixSuffix = false;
174
+ break;
175
+ }
176
+ if (oldBlock.cursorEnd - oldBlock.cursorStart !==
177
+ nextMeasured.cursorEnd - nextMeasured.cursorStart ||
178
+ oldBlock.lineEnd - oldBlock.lineStart !==
179
+ nextMeasured.lineEnd - nextMeasured.lineStart) {
180
+ canReusePrefixSuffix = false;
181
+ break;
182
+ }
183
+ }
184
+ if (canReusePrefixSuffix) {
185
+ for (let i = newDirtyEnd; i < measuredBlocks.length; i += 1) {
186
+ const oldIndex = oldDirtyEnd + (i - newDirtyEnd);
187
+ const oldBlock = previousSnapshot.blocks[oldIndex];
188
+ const nextMeasured = measuredBlocks[i];
189
+ if (!oldBlock || !nextMeasured) {
190
+ canReusePrefixSuffix = false;
191
+ break;
192
+ }
193
+ if (oldBlock.cursorEnd - oldBlock.cursorStart !==
194
+ nextMeasured.cursorEnd - nextMeasured.cursorStart ||
195
+ oldBlock.lineEnd - oldBlock.lineStart !==
196
+ nextMeasured.lineEnd - nextMeasured.lineStart) {
197
+ canReusePrefixSuffix = false;
198
+ break;
199
+ }
200
+ }
201
+ }
202
+ if (!canReusePrefixSuffix) {
203
+ oldDirtyStart = 0;
204
+ oldDirtyEnd = previousSnapshot.blocks.length;
205
+ newDirtyStart = 0;
206
+ newDirtyEnd = measuredBlocks.length;
207
+ }
208
+ }
9
209
  const runs = [];
210
+ const contentNodes = [];
211
+ const snapshotBlocks = [];
10
212
  let cursorOffset = 0;
11
213
  let lineIndex = 0;
12
214
  function createTextRun(node) {
@@ -52,6 +254,70 @@ export function renderDocContent(doc, dom, root) {
52
254
  }
53
255
  return "unknown";
54
256
  }
257
+ function isManagedBlockElement(element) {
258
+ return (element.hasAttribute("data-line-index") ||
259
+ element.hasAttribute("data-block-wrapper") ||
260
+ element.hasAttribute("data-block-atom") ||
261
+ element.classList.contains("cake-line"));
262
+ }
263
+ function canReuseRenderedBlockElement(existing, rendered) {
264
+ if (!isManagedBlockElement(existing) || !isManagedBlockElement(rendered)) {
265
+ return false;
266
+ }
267
+ if (existing.tagName !== rendered.tagName) {
268
+ return false;
269
+ }
270
+ return (existing.getAttribute("data-block-wrapper") ===
271
+ rendered.getAttribute("data-block-wrapper") &&
272
+ existing.getAttribute("data-block-atom") ===
273
+ rendered.getAttribute("data-block-atom"));
274
+ }
275
+ function syncManagedBlockAttributes(existing, rendered) {
276
+ if (existing.className !== rendered.className) {
277
+ existing.className = rendered.className;
278
+ }
279
+ const existingStyle = existing.getAttribute("style");
280
+ const renderedStyle = rendered.getAttribute("style");
281
+ if (existingStyle !== renderedStyle) {
282
+ if (renderedStyle === null) {
283
+ existing.removeAttribute("style");
284
+ }
285
+ else {
286
+ existing.setAttribute("style", renderedStyle);
287
+ }
288
+ }
289
+ const managedAttributes = [
290
+ "data-line-index",
291
+ "data-block-wrapper",
292
+ "data-block-atom",
293
+ "aria-placeholder",
294
+ ];
295
+ for (const name of managedAttributes) {
296
+ const next = rendered.getAttribute(name);
297
+ if (next === null) {
298
+ existing.removeAttribute(name);
299
+ }
300
+ else if (existing.getAttribute(name) !== next) {
301
+ existing.setAttribute(name, next);
302
+ }
303
+ }
304
+ }
305
+ function reconcileRenderedBlockElement(existing, rendered) {
306
+ syncManagedBlockAttributes(existing, rendered);
307
+ const existingChildren = Array.from(existing.childNodes);
308
+ const renderedChildren = Array.from(rendered.childNodes);
309
+ const nextChildren = renderedChildren.map((child, index) => {
310
+ const existingChild = existingChildren[index];
311
+ if (child instanceof Element &&
312
+ existingChild instanceof Element &&
313
+ canReuseRenderedBlockElement(existingChild, child)) {
314
+ return reconcileRenderedBlockElement(existingChild, child);
315
+ }
316
+ return child;
317
+ });
318
+ existing.replaceChildren(...nextChildren);
319
+ return existing;
320
+ }
55
321
  function getInlineKey(inline) {
56
322
  if (inline.type === "text") {
57
323
  return "text";
@@ -171,7 +437,16 @@ export function renderDocContent(doc, dom, root) {
171
437
  for (const renderBlock of dom.blockRenderers) {
172
438
  const result = renderBlock(block, context);
173
439
  if (result) {
174
- return normalizeNodes(result);
440
+ const renderedNodes = normalizeNodes(result);
441
+ const renderedElement = renderedNodes.length === 1 && renderedNodes[0] instanceof Element
442
+ ? renderedNodes[0]
443
+ : null;
444
+ if (existing instanceof Element &&
445
+ renderedElement &&
446
+ canReuseRenderedBlockElement(existing, renderedElement)) {
447
+ return [reconcileRenderedBlockElement(existing, renderedElement)];
448
+ }
449
+ return renderedNodes;
175
450
  }
176
451
  }
177
452
  if (block.type === "paragraph") {
@@ -265,8 +540,7 @@ export function renderDocContent(doc, dom, root) {
265
540
  const newChildren = [];
266
541
  blocks.forEach((block, index) => {
267
542
  const existingChild = existingChildren[index] ?? null;
268
- const canReuse = existingChild && getElementKey(existingChild) === getBlockKey(block);
269
- const nodes = reconcileBlock(block, canReuse ? existingChild : null);
543
+ const nodes = reconcileBlock(block, existingChild);
270
544
  newChildren.push(...nodes);
271
545
  if (index < blocks.length - 1) {
272
546
  cursorOffset += 1;
@@ -291,139 +565,148 @@ export function renderDocContent(doc, dom, root) {
291
565
  });
292
566
  return nodes;
293
567
  }
294
- const existingChildren = root ? Array.from(root.children) : [];
295
- const contentNodes = [];
296
- doc.blocks.forEach((block, index) => {
297
- const existingChild = existingChildren[index] ?? null;
298
- const canReuse = existingChild && getElementKey(existingChild) === getBlockKey(block);
299
- const nodes = reconcileBlock(block, canReuse ? existingChild : null);
568
+ function appendBlockSnapshot(nodes, measured, runsStart, runsEnd, cursorStart, lineStart) {
569
+ const nodeStart = contentNodes.length;
300
570
  contentNodes.push(...nodes);
301
- if (index < doc.blocks.length - 1) {
302
- cursorOffset += 1;
303
- }
304
- });
305
- return { content: contentNodes, map: createDomMap(runs) };
306
- }
307
- export function renderDoc(doc, dom) {
308
- const root = document.createElement("div");
309
- root.className = "cake-content";
310
- root.setAttribute("contenteditable", "true");
311
- const runs = [];
312
- let cursorOffset = 0;
313
- let lineIndex = 0;
314
- function createTextRun(node) {
315
- const run = createTextRunBase(node, cursorOffset);
316
- cursorOffset = run.cursorEnd;
317
- runs.push(run);
318
- return run;
571
+ const nodeEnd = contentNodes.length;
572
+ const cursorEnd = cursorStart + (measured.cursorEnd - measured.cursorStart);
573
+ const lineEnd = lineStart + (measured.lineEnd - measured.lineStart);
574
+ snapshotBlocks.push({
575
+ nodeStart,
576
+ nodeEnd,
577
+ runStart: runsStart,
578
+ runEnd: runsEnd,
579
+ cursorStart,
580
+ cursorEnd,
581
+ lineStart,
582
+ lineEnd,
583
+ });
584
+ cursorOffset = cursorEnd;
585
+ lineIndex = lineEnd;
319
586
  }
320
- const context = {
321
- renderInline,
322
- renderBlock,
323
- renderBlocks,
324
- createTextRun,
325
- getLineIndex: () => lineIndex,
326
- incrementLineIndex: () => {
327
- lineIndex += 1;
328
- },
329
- };
330
- function renderInline(inline) {
331
- for (const renderInline of dom.inlineRenderers) {
332
- const result = renderInline(inline, context);
333
- if (result) {
334
- return normalizeNodes(result);
335
- }
336
- }
337
- if (inline.type === "text") {
338
- const element = document.createElement("span");
339
- element.className = "cake-text";
340
- const node = document.createTextNode(inline.text);
341
- createTextRun(node);
342
- element.append(node);
343
- return [element];
344
- }
345
- if (inline.type === "inline-wrapper") {
346
- const element = document.createElement("span");
347
- element.classList.add("cake-inline", `cake-inline--${inline.kind}`);
348
- for (const child of inline.children) {
349
- for (const node of renderInline(child)) {
350
- element.append(node);
587
+ function appendReusedBlock(oldIndex, newIndex) {
588
+ if (!previousSnapshot) {
589
+ return false;
590
+ }
591
+ const oldBlock = previousSnapshot.blocks[oldIndex];
592
+ const measured = measuredBlocks[newIndex];
593
+ if (!oldBlock || !measured) {
594
+ return false;
595
+ }
596
+ const nodes = previousSnapshot.nodes.slice(oldBlock.nodeStart, oldBlock.nodeEnd);
597
+ if (nodes.length !== oldBlock.nodeEnd - oldBlock.nodeStart) {
598
+ return false;
599
+ }
600
+ if (root) {
601
+ for (const node of nodes) {
602
+ if (node.parentNode !== root) {
603
+ return false;
351
604
  }
352
605
  }
353
- return [element];
354
606
  }
355
- if (inline.type === "inline-atom") {
356
- const element = document.createElement("span");
357
- element.classList.add("cake-inline-atom", `cake-inline-atom--${inline.kind}`);
358
- const node = document.createTextNode(" ");
359
- createTextRun(node);
360
- element.append(node);
361
- return [element];
607
+ const blockRuns = previousSnapshot.runs.slice(oldBlock.runStart, oldBlock.runEnd);
608
+ const runShift = cursorOffset - oldBlock.cursorStart;
609
+ const runsStart = runs.length;
610
+ for (const run of blockRuns) {
611
+ runs.push(shiftRun(run, runShift));
612
+ }
613
+ const runsEnd = runs.length;
614
+ const cursorStart = cursorOffset;
615
+ const lineStart = lineIndex;
616
+ appendBlockSnapshot(nodes, measured, runsStart, runsEnd, cursorStart, lineStart);
617
+ if (newIndex < doc.blocks.length - 1) {
618
+ cursorOffset += 1;
362
619
  }
363
- return [];
620
+ return true;
364
621
  }
365
- function renderBlock(block) {
366
- for (const renderBlock of dom.blockRenderers) {
367
- const result = renderBlock(block, context);
368
- if (result) {
369
- return normalizeNodes(result);
370
- }
622
+ const managedChildren = root
623
+ ? Array.from(root.childNodes).filter(isManagedRootNode)
624
+ : [];
625
+ let fallbackToFullRender = false;
626
+ for (let i = 0; i < newDirtyStart; i += 1) {
627
+ if (!appendReusedBlock(i, i)) {
628
+ fallbackToFullRender = true;
629
+ break;
371
630
  }
372
- if (block.type === "paragraph") {
373
- const element = document.createElement("div");
374
- element.setAttribute("data-line-index", String(context.getLineIndex()));
375
- element.classList.add("cake-line");
376
- context.incrementLineIndex();
377
- if (block.content.length === 0) {
378
- // Use <br> to maintain line height for empty lines (like v1)
379
- // Also create an empty text node for cursor positioning
380
- const textNode = document.createTextNode("");
381
- createTextRun(textNode);
382
- element.append(textNode);
383
- element.append(document.createElement("br"));
631
+ }
632
+ if (!fallbackToFullRender) {
633
+ for (let i = newDirtyStart; i < newDirtyEnd; i += 1) {
634
+ const block = doc.blocks[i];
635
+ if (!block) {
636
+ continue;
384
637
  }
385
- else {
386
- const mergedContent = mergeInlineForRender(block.content);
387
- for (const inline of mergedContent) {
388
- for (const node of renderInline(inline)) {
389
- element.append(node);
390
- }
638
+ const oldIndex = oldDirtyStart + (i - newDirtyStart);
639
+ const oldBlock = previousSnapshot?.blocks[oldIndex] ?? null;
640
+ let existing = null;
641
+ if (oldBlock && previousSnapshot) {
642
+ const oldNodes = previousSnapshot.nodes.slice(oldBlock.nodeStart, oldBlock.nodeEnd);
643
+ if (oldNodes.length === 1 && oldNodes[0] instanceof Element) {
644
+ existing = oldNodes[0];
391
645
  }
392
646
  }
393
- return [element];
394
- }
395
- if (block.type === "block-wrapper") {
396
- const element = document.createElement("div");
397
- element.setAttribute("data-block-wrapper", block.kind);
398
- for (const node of renderBlocks(block.blocks)) {
399
- element.append(node);
647
+ else {
648
+ const existingNode = managedChildren[oldIndex] ?? null;
649
+ existing = existingNode instanceof Element ? existingNode : null;
650
+ }
651
+ const runsStart = runs.length;
652
+ const cursorStart = cursorOffset;
653
+ const lineStart = lineIndex;
654
+ const nodes = reconcileBlock(block, existing);
655
+ const runsEnd = runs.length;
656
+ const measured = measuredBlocks[i];
657
+ appendBlockSnapshot(nodes, measured, runsStart, runsEnd, cursorStart, lineStart);
658
+ if (i < doc.blocks.length - 1) {
659
+ cursorOffset += 1;
400
660
  }
401
- return [element];
402
661
  }
403
- if (block.type === "block-atom") {
404
- const element = document.createElement("div");
405
- element.setAttribute("data-block-atom", block.kind);
406
- element.setAttribute("data-line-index", String(context.getLineIndex()));
407
- element.classList.add("cake-line");
408
- context.incrementLineIndex();
409
- return [element];
662
+ }
663
+ if (!fallbackToFullRender) {
664
+ for (let i = newDirtyEnd; i < doc.blocks.length; i += 1) {
665
+ const oldIndex = oldDirtyEnd + (i - newDirtyEnd);
666
+ if (!appendReusedBlock(oldIndex, i)) {
667
+ fallbackToFullRender = true;
668
+ break;
669
+ }
410
670
  }
411
- return [];
412
671
  }
413
- function renderBlocks(blocks) {
414
- const nodes = [];
415
- blocks.forEach((block, index) => {
416
- nodes.push(...renderBlock(block));
417
- if (index < blocks.length - 1) {
672
+ if (fallbackToFullRender) {
673
+ runs.length = 0;
674
+ contentNodes.length = 0;
675
+ snapshotBlocks.length = 0;
676
+ cursorOffset = 0;
677
+ lineIndex = 0;
678
+ doc.blocks.forEach((block, index) => {
679
+ const existingNode = managedChildren[index] ?? null;
680
+ const existing = existingNode instanceof Element ? existingNode : null;
681
+ const runsStart = runs.length;
682
+ const cursorStart = cursorOffset;
683
+ const lineStart = lineIndex;
684
+ const nodes = reconcileBlock(block, existing);
685
+ const runsEnd = runs.length;
686
+ const measured = measuredBlocks[index];
687
+ appendBlockSnapshot(nodes, measured, runsStart, runsEnd, cursorStart, lineStart);
688
+ if (index < doc.blocks.length - 1) {
418
689
  cursorOffset += 1;
419
690
  }
420
691
  });
421
- return nodes;
422
692
  }
423
- for (const node of renderBlocks(doc.blocks)) {
424
- root.append(node);
425
- }
426
- return { root, map: createDomMap(runs) };
693
+ return {
694
+ content: contentNodes,
695
+ map: createDomMap(runs),
696
+ snapshot: {
697
+ blocks: snapshotBlocks,
698
+ nodes: contentNodes,
699
+ runs,
700
+ },
701
+ };
702
+ }
703
+ export function renderDoc(doc, dom) {
704
+ const root = document.createElement("div");
705
+ root.className = "cake-content";
706
+ root.setAttribute("contenteditable", "true");
707
+ const { content, map } = renderDocContent(doc, dom);
708
+ root.append(...content);
709
+ return { root, map };
427
710
  }
428
711
  export function mergeInlineForRender(inlines) {
429
712
  const merged = [];