@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.
- package/dist/cake/core/mapping/cursor-source-map.d.ts +11 -0
- package/dist/cake/core/mapping/cursor-source-map.d.ts.map +1 -1
- package/dist/cake/core/mapping/cursor-source-map.js +159 -21
- package/dist/cake/core/runtime.d.ts +4 -0
- package/dist/cake/core/runtime.d.ts.map +1 -1
- package/dist/cake/core/runtime.js +332 -215
- package/dist/cake/dom/render.d.ts +32 -2
- package/dist/cake/dom/render.d.ts.map +1 -1
- package/dist/cake/dom/render.js +401 -118
- package/dist/cake/editor/cake-editor.d.ts +8 -1
- package/dist/cake/editor/cake-editor.d.ts.map +1 -1
- package/dist/cake/editor/cake-editor.js +172 -100
- package/dist/cake/editor/internal/editor-text-model.d.ts +49 -0
- package/dist/cake/editor/internal/editor-text-model.d.ts.map +1 -0
- package/dist/cake/editor/internal/editor-text-model.js +284 -0
- package/dist/cake/editor/selection/selection-geometry-dom.d.ts +5 -1
- package/dist/cake/editor/selection/selection-geometry-dom.d.ts.map +1 -1
- package/dist/cake/editor/selection/selection-geometry-dom.js +4 -5
- package/dist/cake/editor/selection/selection-layout-dom.d.ts.map +1 -1
- package/dist/cake/editor/selection/selection-layout-dom.js +2 -5
- package/dist/cake/editor/selection/selection-layout.d.ts +2 -15
- package/dist/cake/editor/selection/selection-layout.d.ts.map +1 -1
- package/dist/cake/editor/selection/selection-layout.js +1 -99
- package/dist/cake/editor/selection/selection-navigation.d.ts +4 -0
- package/dist/cake/editor/selection/selection-navigation.d.ts.map +1 -1
- package/dist/cake/editor/selection/selection-navigation.js +1 -2
- package/dist/cake/extensions/link/link.d.ts.map +1 -1
- package/dist/cake/extensions/link/link.js +1 -7
- package/dist/cake/extensions/shared/structural-reparse-policy.js +2 -2
- package/package.json +5 -2
- package/dist/cake/editor/selection/visible-text.d.ts +0 -5
- package/dist/cake/editor/selection/visible-text.d.ts.map +0 -1
- package/dist/cake/editor/selection/visible-text.js +0 -66
- package/dist/cake/engine/cake-engine.d.ts +0 -230
- package/dist/cake/engine/cake-engine.d.ts.map +0 -1
- package/dist/cake/engine/cake-engine.js +0 -3589
- package/dist/cake/engine/selection/selection-geometry-dom.d.ts +0 -24
- package/dist/cake/engine/selection/selection-geometry-dom.d.ts.map +0 -1
- package/dist/cake/engine/selection/selection-geometry-dom.js +0 -302
- package/dist/cake/engine/selection/selection-geometry.d.ts +0 -22
- package/dist/cake/engine/selection/selection-geometry.d.ts.map +0 -1
- package/dist/cake/engine/selection/selection-geometry.js +0 -158
- package/dist/cake/engine/selection/selection-layout-dom.d.ts +0 -50
- package/dist/cake/engine/selection/selection-layout-dom.d.ts.map +0 -1
- package/dist/cake/engine/selection/selection-layout-dom.js +0 -781
- package/dist/cake/engine/selection/selection-layout.d.ts +0 -55
- package/dist/cake/engine/selection/selection-layout.d.ts.map +0 -1
- package/dist/cake/engine/selection/selection-layout.js +0 -128
- package/dist/cake/engine/selection/selection-navigation.d.ts +0 -22
- package/dist/cake/engine/selection/selection-navigation.d.ts.map +0 -1
- package/dist/cake/engine/selection/selection-navigation.js +0 -229
- package/dist/cake/engine/selection/visible-text.d.ts +0 -5
- package/dist/cake/engine/selection/visible-text.d.ts.map +0 -1
- package/dist/cake/engine/selection/visible-text.js +0 -66
- package/dist/cake/react/CakeEditor.d.ts +0 -58
- package/dist/cake/react/CakeEditor.d.ts.map +0 -1
- 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;
|
|
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"}
|
package/dist/cake/dom/render.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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 = [];
|