Skip to content

Commit db37e5b

Browse files
authored
Merge pull request #96 from inokawa/object-void
Serialize void node data on parse phase
2 parents 59c42a6 + 00ae97c commit db37e5b

File tree

9 files changed

+120
-79
lines changed

9 files changed

+120
-79
lines changed

e2e/edix.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,16 @@ export const getText = async (
3636
(element, [NON_EDITABLE_PLACEHOLDER, { blockTag }]) => {
3737
const document = element.ownerDocument;
3838
return window.edix
39-
.takeDomSnapshot(document, element, {
40-
_isBlock: blockTag
41-
? (n) => n.tagName === blockTag.toUpperCase()
42-
: undefined,
43-
})
39+
.takeDomSnapshot(
40+
document,
41+
element,
42+
{
43+
_isBlock: blockTag
44+
? (n) => n.tagName === blockTag.toUpperCase()
45+
: undefined,
46+
},
47+
() => ({})
48+
)
4449
.map((r) => {
4550
return r.reduce<string>((acc, n) => {
4651
return acc + (n.type === 1 ? n.text : NON_EDITABLE_PLACEHOLDER);
@@ -61,11 +66,16 @@ export const getSeletedText = (
6166
const selection = document.getSelection()!;
6267
const range = selection.getRangeAt(0)!.cloneContents();
6368
return window.edix
64-
.takeDomSnapshot(document, range, {
65-
_isBlock: blockTag
66-
? (n) => n.tagName === blockTag.toUpperCase()
67-
: undefined,
68-
})
69+
.takeDomSnapshot(
70+
document,
71+
range,
72+
{
73+
_isBlock: blockTag
74+
? (n) => n.tagName === blockTag.toUpperCase()
75+
: undefined,
76+
},
77+
() => ({})
78+
)
6979
.map((r) => {
7080
return r.reduce<string>((acc, n) => {
7181
return acc + (n.type === 1 ? n.text : NON_EDITABLE_PLACEHOLDER);

src/core/commands/edit.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@ import { compareLine, comparePosition } from "../position";
22
import {
33
DocFragment,
44
NODE_TEXT,
5-
NodeRef,
5+
NodeData,
66
Position,
77
SelectionSnapshot,
88
Writeable,
99
} from "../types";
1010

11-
const isTextNode = (node: NodeRef) => node.type === NODE_TEXT;
12-
const getNodeSize = (node: NodeRef): number =>
11+
const isTextNode = (node: NodeData) => node.type === NODE_TEXT;
12+
const getNodeSize = (node: NodeData): number =>
1313
isTextNode(node) ? node.text.length : 1;
1414

15-
const insertNodeAfter = (line: NodeRef[], index: number, node: NodeRef) => {
15+
const insertNodeAfter = (line: NodeData[], index: number, node: NodeData) => {
1616
const target = line[index]!;
1717
if (isTextNode(node) && isTextNode(target)) {
1818
line[index] = { type: NODE_TEXT, text: target.text + node.text };
@@ -21,8 +21,8 @@ const insertNodeAfter = (line: NodeRef[], index: number, node: NodeRef) => {
2121
}
2222
};
2323

24-
const join = (...lines: (readonly NodeRef[])[]): readonly NodeRef[] => {
25-
const line: NodeRef[] = [];
24+
const join = (...lines: (readonly NodeData[])[]): readonly NodeData[] => {
25+
const line: NodeData[] = [];
2626
for (let i = 0; i < lines.length; i++) {
2727
const current = lines[i]!;
2828
if (!line.length) {
@@ -37,9 +37,9 @@ const join = (...lines: (readonly NodeRef[])[]): readonly NodeRef[] => {
3737
};
3838

3939
const split = (
40-
line: readonly NodeRef[],
40+
line: readonly NodeData[],
4141
offset: number
42-
): [readonly NodeRef[], readonly NodeRef[]] => {
42+
): [readonly NodeData[], readonly NodeData[]] => {
4343
for (let i = 0; i < line.length; i++) {
4444
const node = line[i]!;
4545
const length = getNodeSize(node);
@@ -106,7 +106,7 @@ const replaceRange = (
106106
const before = splitByStart[0];
107107
const after = end ? split(doc[end[0]]!, end[1])[1] : splitByStart[1];
108108

109-
const lines: (readonly NodeRef[])[] = [...fragment];
109+
const lines: (readonly NodeData[])[] = [...fragment];
110110
if (lines.length) {
111111
lines[0] = join(before, lines[0]!);
112112
lines[lines.length - 1] = join(lines[lines.length - 1]!, after);

src/core/dom/index.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { comparePosition } from "../position";
1414
import {
1515
DocFragment,
1616
Position,
17-
NodeRef,
17+
NodeData,
1818
SelectionSnapshot,
1919
NODE_TEXT,
2020
NODE_VOID,
@@ -304,15 +304,16 @@ export const takeSelectionSnapshot = (
304304
export const takeDomSnapshot = (
305305
document: Document,
306306
root: Node,
307-
config: ParserConfig
307+
config: ParserConfig,
308+
serializeVoid: (node: Element) => Record<string, unknown> | void
308309
): DocFragment => {
309310
return parse(
310311
(readNext) => {
311312
let type: NodeType | void;
312-
let row: NodeRef[] | null = null;
313+
let row: NodeData[] | null = null;
313314
let text = "";
314315

315-
const rows: NodeRef[][] = [];
316+
const rows: NodeData[][] = [];
316317

317318
const completeNode = (element?: Element) => {
318319
if (!row) {
@@ -323,7 +324,10 @@ export const takeDomSnapshot = (
323324
text = "";
324325
}
325326
if (element) {
326-
row.push({ type: NODE_VOID, node: element });
327+
const data = serializeVoid(element);
328+
if (data) {
329+
row.push({ type: NODE_VOID, data });
330+
}
327331
}
328332
};
329333
const completeRow = () => {

src/core/editable.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ export const editable = <T>(
114114
{
115115
schema: {
116116
single: isSingleline,
117-
data: serialize,
117+
js: docToJS,
118+
void: serializeVoid,
118119
copy,
119120
paste: getPastableData,
120121
},
@@ -165,9 +166,9 @@ export const editable = <T>(
165166
const document = getCurrentDocument(element);
166167

167168
const history = createHistory<
168-
readonly [value: T, selection: SelectionSnapshot]
169+
readonly [value: DocFragment, selection: SelectionSnapshot]
169170
>([
170-
serialize(takeDomSnapshot(document, element, parserConfig)),
171+
takeDomSnapshot(document, element, parserConfig, serializeVoid),
171172
currentSelection,
172173
]);
173174

@@ -218,20 +219,19 @@ export const editable = <T>(
218219
};
219220

220221
const updateState = (
221-
dom: DocFragment,
222+
doc: DocFragment,
222223
selection: SelectionSnapshot,
223224
prevSelection: SelectionSnapshot
224225
) => {
225226
if (!readonly) {
226227
if (isSingleline) {
227-
[dom, selection] = flatten(dom, selection);
228+
[doc, selection] = flatten(doc, selection);
228229
}
229-
const value = serialize(dom);
230230

231231
history.set([history.get()[0], prevSelection]);
232-
history.push([value, selection]);
232+
history.push([doc, selection]);
233233
currentSelection = selection;
234-
onChange(value);
234+
onChange(docToJS(doc));
235235
}
236236

237237
restoreSelectionOnTimeout();
@@ -258,7 +258,12 @@ export const editable = <T>(
258258
isSingleline,
259259
parserConfig
260260
);
261-
const value = takeDomSnapshot(document, element, parserConfig);
261+
const value = takeDomSnapshot(
262+
document,
263+
element,
264+
parserConfig,
265+
serializeVoid
266+
);
262267

263268
// Revert DOM
264269
let m: MutationRecord | undefined;
@@ -298,7 +303,8 @@ export const editable = <T>(
298303
const dom: Writeable<DocFragment> = takeDomSnapshot(
299304
document,
300305
element,
301-
parserConfig
306+
parserConfig,
307+
serializeVoid
302308
) as Writeable<DocFragment>; // TODO improve type
303309

304310
let command: (typeof commands)[number] | undefined;
@@ -330,7 +336,7 @@ export const editable = <T>(
330336

331337
if (nextHistory) {
332338
currentSelection = nextHistory[1];
333-
onChange(nextHistory[0]);
339+
onChange(docToJS(nextHistory[0]));
334340

335341
restoreSelectionOnTimeout();
336342
}
@@ -396,7 +402,7 @@ export const editable = <T>(
396402

397403
copy(
398404
dataTransfer,
399-
takeDomSnapshot(document, selected, parserConfig),
405+
takeDomSnapshot(document, selected, parserConfig, serializeVoid),
400406
selected
401407
);
402408
};
@@ -408,7 +414,7 @@ export const editable = <T>(
408414
} else {
409415
execCommand(
410416
InsertFragment,
411-
takeDomSnapshot(document, data, parserConfig)
417+
takeDomSnapshot(document, data, parserConfig, serializeVoid)
412418
);
413419
}
414420
};

src/core/schema/plain.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { NODE_TEXT, type DocFragment } from "../types";
22
import type { EditableSchema } from "./types";
33

4-
const serializeToString = (snapshot: DocFragment): string => {
5-
return snapshot.reduce((acc, r, i) => {
4+
const toString = (doc: DocFragment): string => {
5+
return doc.reduce((acc, r, i) => {
66
if (i !== 0) {
77
acc += "\n";
88
}
@@ -22,9 +22,10 @@ export const plainSchema = ({
2222
} = {}): EditableSchema<string> => {
2323
return {
2424
single: !multiline,
25-
data: serializeToString,
25+
js: toString,
26+
void: () => {}, // not supported
2627
copy: (dataTransfer, data) => {
27-
dataTransfer.setData("text/plain", serializeToString(data));
28+
dataTransfer.setData("text/plain", toString(data));
2829
},
2930
paste: (dataTransfer) => {
3031
return dataTransfer.getData("text/plain");

src/core/schema/structured.ts

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import { isCommentNode } from "../dom/parser";
2-
import { NODE_TEXT, type NodeRef } from "../types";
2+
import { NODE_TEXT, TextNode, type NodeData } from "../types";
33
import type { EditableSchema } from "./types";
44

55
export interface EditableVoidSerializer<T> {
66
is: (node: HTMLElement) => boolean;
77
data: (node: HTMLElement) => T;
8-
plain: (node: HTMLElement) => string;
8+
plain: (data: T) => string;
99
}
1010

11-
const toString = (node: Node): string => node.textContent!;
11+
const emptyString = (): string => "";
1212

1313
export const voidNode = <const D>({
1414
is,
1515
data,
16-
plain = toString,
16+
plain = emptyString,
1717
}: {
1818
is: (node: HTMLElement) => boolean;
1919
data: (node: HTMLElement) => D;
20-
plain?: (node: HTMLElement) => string;
20+
plain?: (data: D) => string;
2121
}): EditableVoidSerializer<D> => {
2222
return {
2323
is,
@@ -30,11 +30,12 @@ type Prettify<T> = {
3030
[K in keyof T]: T[K];
3131
} & {};
3232

33+
type ExtractVoidData<T> = T extends EditableVoidSerializer<infer D> ? D : never;
3334
type ExtractVoidNode<T> = Prettify<
3435
{
3536
[K in keyof T]: {
3637
type: K;
37-
data: T[K] extends EditableVoidSerializer<infer D> ? D : never;
38+
data: ExtractVoidData<T[K]>;
3839
};
3940
}[keyof T]
4041
>;
@@ -56,41 +57,57 @@ export const schema = <
5657
? (ExtractVoidNode<V> | { type: "text"; text: string })[][]
5758
: (ExtractVoidNode<V> | { type: "text"; text: string })[]
5859
> => {
60+
type VoidNodeData = ExtractVoidData<V[keyof V]>;
61+
type TextNodeType = { type: "text"; text: string };
5962
type VoidNodeType = ExtractVoidNode<V>;
60-
type RowType = (VoidNodeType | { type: "text"; text: string })[];
63+
type RowType = (TextNodeType | VoidNodeType)[];
6164

6265
const voidSerializers = Object.entries(voids);
6366

64-
const serializeRow = (r: readonly NodeRef[]): RowType => {
67+
const textCache = new WeakMap<TextNode, TextNodeType>();
68+
// TODO replace VoidNodeData with VoidNode
69+
const voidCache = new WeakMap<VoidNodeData, VoidNodeType>();
70+
71+
const serializeRow = (r: readonly NodeData[]): RowType => {
6572
return r.reduce((acc, t) => {
6673
if (t.type === NODE_TEXT) {
67-
acc.push({ type: "text", text: t.text });
68-
} else {
69-
for (const [type, s] of voidSerializers) {
70-
if (s.is(t.node as HTMLElement)) {
71-
acc.push({
72-
type,
73-
data: s.data(t.node as HTMLElement),
74-
} as VoidNodeType);
75-
break;
76-
}
74+
let text = textCache.get(t);
75+
if (!text) {
76+
textCache.set(t, (text = { type: "text", text: t.text }));
7777
}
78+
acc.push(text);
79+
} else {
80+
acc.push(voidCache.get(t.data as VoidNodeData)!);
7881
}
7982
return acc;
8083
}, [] as RowType);
8184
};
8285

8386
return {
8487
single: !multiline,
85-
data: multiline
86-
? (snap) => {
87-
return snap.map(serializeRow);
88+
js: multiline
89+
? (doc) => {
90+
return doc.map(serializeRow);
8891
}
89-
: (snap) => {
90-
return serializeRow(snap[0]!) satisfies RowType as any; // TODO improve type
92+
: (doc) => {
93+
return serializeRow(doc[0]!) satisfies RowType as any; // TODO improve type
9194
},
92-
copy: (dataTransfer, snap, dom) => {
93-
const str = snap.reduce((acc, r, i) => {
95+
void: (element) => {
96+
for (const [type, s] of voidSerializers) {
97+
if (s.is(element as HTMLElement)) {
98+
const data = s.data(element as HTMLElement) as VoidNodeData;
99+
// TODO improve
100+
voidCache.set(data, {
101+
type,
102+
data: { ...data },
103+
} as VoidNodeType);
104+
return data;
105+
}
106+
}
107+
return;
108+
},
109+
copy: (dataTransfer, doc, dom) => {
110+
const str = doc.reduce((acc, r, i) => {
94111
if (i !== 0) {
95112
acc += "\n";
96113
}
@@ -99,14 +116,9 @@ export const schema = <
99116
r.reduce((acc, t) => {
100117
if (t.type === NODE_TEXT) {
101118
return acc + t.text;
102-
} else {
103-
for (const [, s] of voidSerializers) {
104-
if (s.is(t.node as HTMLElement)) {
105-
return acc + s.plain(t.node as HTMLElement);
106-
}
107-
}
108119
}
109-
return acc;
120+
const voidNode = voidCache.get(t.data as VoidNodeData)!;
121+
return acc + voids[voidNode.type]!.plain(t.data);
110122
}, "")
111123
);
112124
}, "");

0 commit comments

Comments
 (0)