Skip to content

Commit 893cac3

Browse files
authored
Optimize Editor#normalizeNode (ianstormtaylor#5970)
* optimized normalize-node * added changeset * lint fix
1 parent 0e6c691 commit 893cac3

File tree

2 files changed

+109
-94
lines changed

2 files changed

+109
-94
lines changed

.changeset/ten-fans-fly.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'slate': minor
3+
---
4+
5+
Optimized normalizeNode implementation, should work the same, but may behave slightly differently if you give it something really malformed

packages/slate/src/core/normalize-node.ts

Lines changed: 104 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,134 @@
11
import { WithEditorFirstArg } from '../utils/types'
2-
import { Text } from '../interfaces/text'
3-
import { Element } from '../interfaces/element'
4-
import { Transforms } from '../interfaces/transforms'
5-
import { Descendant, Node } from '../interfaces/node'
6-
import { Editor } from '../interfaces/editor'
2+
import {
3+
Editor,
4+
Element,
5+
Descendant,
6+
Text,
7+
Transforms,
8+
Node,
9+
Path,
10+
Ancestor,
11+
} from '../interfaces'
712

813
export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
914
editor,
1015
entry,
1116
options
1217
) => {
13-
const [node, path] = entry
18+
const [node, path] = entry as [{}, Path] // node is not yet normalized, treat as hostile
1419

1520
// There are no core normalizations for text nodes.
16-
if (Node.isText(node)) {
21+
if (Node.isText(node as Node)) {
1722
return
1823
}
1924

20-
// Ensure that block and inline nodes have at least one text child.
21-
if (Node.isElement(node) && node.children.length === 0) {
25+
if (!('children' in node)) {
26+
// If the node is not a text node, and doesn't have a `children` field,
27+
// then we have an invalid node that will upset slate.
28+
//
29+
// eg: `{ type: 'some_node' }`.
30+
//
31+
// To prevent slate from breaking, we can add the `children` field,
32+
// and now that it is valid, we can to many more operations easily,
33+
// such as extend normalizers to fix erronous structure.
34+
;(node as Element).children = []
35+
}
36+
let element = node as Ancestor // we will have to refetch the element any time we modify its children since it clones to a new immutable reference when we do
37+
38+
// Ensure that elements have at least one child.
39+
if (element !== editor && element.children.length === 0) {
2240
const child = { text: '' }
23-
Transforms.insertNodes(editor, child, {
24-
at: path.concat(0),
25-
voids: true,
26-
})
27-
return
41+
Transforms.insertNodes(editor, child, { at: path.concat(0), voids: true })
42+
element = Node.get(editor, path) as Element
2843
}
2944

30-
// Determine whether the node should have block or inline children.
45+
// Determine whether the node should have only block or only inline children.
46+
// - The editor should have only block children.
47+
// - Inline elements should have only inline children.
48+
// - Elements that begin with a text child or an inline element child should have only inline children.
49+
// - All other elements should have only block children.
3150
const shouldHaveInlines =
32-
node === editor
33-
? false
34-
: Node.isElement(node) &&
35-
(editor.isInline(node) ||
36-
node.children.length === 0 ||
37-
Node.isText(node.children[0]) ||
38-
editor.isInline(node.children[0]))
39-
40-
// Since we'll be applying operations while iterating, keep track of an
41-
// index that accounts for any added/removed nodes.
42-
let n = 0
51+
!(element === editor) &&
52+
(editor.isInline(element) ||
53+
Node.isText(element.children[0]) ||
54+
editor.isInline(element.children[0]))
4355

44-
for (let i = 0; i < node.children.length; i++, n++) {
45-
const currentNode = Node.get(editor, path)
46-
if (Node.isText(currentNode)) continue
47-
const child = currentNode.children[n] as Descendant
48-
const prev = currentNode.children[n - 1] as Descendant
49-
const isLast = i === node.children.length - 1
50-
const isInlineOrText =
51-
Node.isText(child) || (Node.isElement(child) && editor.isInline(child))
56+
if (shouldHaveInlines) {
57+
// Since we'll be applying operations while iterating, we also modify `n` when adding/removing nodes.
58+
for (let n = 0; n < element.children.length; n++) {
59+
const child = element.children[n]
60+
const prev = element.children[n - 1] as Descendant | undefined
5261

53-
// Only allow block nodes in the top-level children and parent blocks
54-
// that only contain block nodes. Similarly, only allow inline nodes in
55-
// other inline nodes, or parent blocks that only contain inlines and
56-
// text.
57-
if (isInlineOrText !== shouldHaveInlines) {
58-
if (isInlineOrText) {
59-
if (options?.fallbackElement) {
60-
Transforms.wrapNodes(editor, options.fallbackElement(), {
61-
at: path.concat(n),
62-
voids: true,
63-
})
64-
} else {
65-
Transforms.removeNodes(editor, { at: path.concat(n), voids: true })
62+
if (Node.isText(child)) {
63+
if (prev != null && Node.isText(prev)) {
64+
// Merge adjacent text nodes that are empty or match.
65+
if (child.text === '') {
66+
Transforms.removeNodes(editor, {
67+
at: path.concat(n),
68+
voids: true,
69+
})
70+
element = Node.get(editor, path) as Element
71+
n--
72+
} else if (prev.text === '') {
73+
Transforms.removeNodes(editor, {
74+
at: path.concat(n - 1),
75+
voids: true,
76+
})
77+
element = Node.get(editor, path) as Element
78+
n--
79+
} else if (Text.equals(child, prev, { loose: true })) {
80+
Transforms.mergeNodes(editor, { at: path.concat(n), voids: true })
81+
element = Node.get(editor, path) as Element
82+
n--
83+
}
6684
}
6785
} else {
68-
Transforms.unwrapNodes(editor, { at: path.concat(n), voids: true })
69-
}
70-
n--
71-
} else if (Node.isElement(child)) {
72-
// Ensure that inline nodes are surrounded by text nodes.
73-
if (editor.isInline(child)) {
74-
if (prev == null || !Node.isText(prev)) {
75-
const newChild = { text: '' }
76-
Transforms.insertNodes(editor, newChild, {
77-
at: path.concat(n),
78-
voids: true,
79-
})
80-
n++
81-
} else if (isLast) {
82-
const newChild = { text: '' }
83-
Transforms.insertNodes(editor, newChild, {
84-
at: path.concat(n + 1),
85-
voids: true,
86-
})
87-
n++
86+
if (editor.isInline(child)) {
87+
// Ensure that inline nodes are surrounded by text nodes.
88+
if (prev == null || !Node.isText(prev)) {
89+
const newChild = { text: '' }
90+
Transforms.insertNodes(editor, newChild, {
91+
at: path.concat(n),
92+
voids: true,
93+
})
94+
element = Node.get(editor, path) as Element
95+
n++
96+
}
97+
if (n === element.children.length - 1) {
98+
const newChild = { text: '' }
99+
Transforms.insertNodes(editor, newChild, {
100+
at: path.concat(n + 1),
101+
voids: true,
102+
})
103+
element = Node.get(editor, path) as Element
104+
n++
105+
}
106+
} else {
107+
// Allow only inline nodes to be in other inline nodes, or in parent blocks that only
108+
// contain inlines and text.
109+
Transforms.unwrapNodes(editor, { at: path.concat(n), voids: true })
110+
element = Node.get(editor, path) as Element
111+
n--
88112
}
89113
}
90-
} else {
91-
// If the child is not a text node, and doesn't have a `children` field,
92-
// then we have an invalid node that will upset slate.
93-
//
94-
// eg: `{ type: 'some_node' }`.
95-
//
96-
// To prevent slate from breaking, we can add the `children` field,
97-
// and now that it is valid, we can to many more operations easily,
98-
// such as extend normalizers to fix erronous structure.
99-
if (!Node.isText(child) && !('children' in child)) {
100-
const elementChild = child as Element
101-
elementChild.children = []
102-
}
114+
}
115+
} else {
116+
// Since we'll be applying operations while iterating, we also modify `n` when adding/removing nodes
117+
for (let n = 0; n < element.children.length; n++) {
118+
const child = element.children[n]
103119

104-
// Merge adjacent text nodes that are empty or match.
105-
if (prev != null && Node.isText(prev)) {
106-
if (Text.equals(child, prev, { loose: true })) {
107-
Transforms.mergeNodes(editor, { at: path.concat(n), voids: true })
108-
n--
109-
} else if (prev.text === '') {
110-
Transforms.removeNodes(editor, {
111-
at: path.concat(n - 1),
112-
voids: true,
113-
})
114-
n--
115-
} else if (child.text === '') {
116-
Transforms.removeNodes(editor, {
120+
// Allow only block nodes in the top-level children and parent blocks that only contain block nodes
121+
if (Node.isText(child) || editor.isInline(child)) {
122+
if (options?.fallbackElement) {
123+
Transforms.wrapNodes(editor, options.fallbackElement(), {
117124
at: path.concat(n),
118125
voids: true,
119126
})
120-
n--
127+
} else {
128+
Transforms.removeNodes(editor, { at: path.concat(n), voids: true })
121129
}
130+
element = Node.get(editor, path) as Ancestor
131+
n--
122132
}
123133
}
124134
}

0 commit comments

Comments
 (0)