1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-31 10:51:44 +02:00

improve Editor.marks and Editor.nodes abstraction

This commit is contained in:
Ian Storm Taylor
2019-11-29 23:15:08 -05:00
parent 0da1dd128c
commit 68569f286e
51 changed files with 381 additions and 237 deletions

View File

@@ -18,7 +18,8 @@
"@types/esrever": "^0.2.0",
"esrever": "^0.2.0",
"immer": "^5.0.0",
"is-plain-object": "^3.0.0"
"is-plain-object": "^3.0.0",
"tiny-warning": "^1.0.3"
},
"keywords": [
"canvas",

View File

@@ -1,3 +1,4 @@
import warning from 'tiny-warning'
import { reverse as reverseText } from 'esrever'
import {
@@ -20,6 +21,7 @@ import {
Text,
TextEntry,
} from '../../..'
import { MarkMatch } from '../../mark'
export const LocationQueries = {
/**
@@ -44,69 +46,19 @@ export const LocationQueries = {
hanging?: boolean
} = {}
): Mark[] {
const { union = false, hanging = false } = options
let { at = editor.selection } = options
warning(
false,
'The `Editor.activeMarks` helper is deprecated, use `Editor.marks` instead.'
)
if (!at) {
return []
}
at = Editor.range(editor, at)
if (!hanging) {
at = Editor.unhangRange(editor, at)
}
// If the range is collapsed at the start of a text node, it should carry
// over the marks from the previous text node in the same block.
if (Range.isCollapsed(at) && at.anchor.offset === 0) {
const { anchor } = at
const prev = Editor.previous(editor, anchor, 'text')
if (prev && Path.isSibling(anchor.path, prev[1])) {
const [prevNode, prevPath] = prev
if (Text.isText(prevNode)) {
at = Editor.range(editor, prevPath)
}
}
}
const marks: Mark[] = []
let first = true
for (const [node] of Editor.texts(editor, { at })) {
if (first) {
marks.push(...node.marks)
first = false
continue
}
if (union) {
for (const mark of node.marks) {
if (!Mark.exists(mark, marks)) {
marks.push(mark)
}
}
} else {
// PERF: If we're doing an intersection and the result hits zero it can
// never increase again, so we can exit early.
if (marks.length === 0) {
break
}
// Iterate backwards so that removing marks doesn't impact indexing.
for (let i = marks.length - 1; i >= 0; i--) {
const existing = marks[i]
if (!Mark.exists(existing, node.marks)) {
marks.splice(i, 1)
}
}
}
}
return marks
return Array.from(
Editor.marks(editor, {
at: options.at,
mode: options.union ? 'distinct' : 'universal',
continuing: true,
}),
([m]) => m
)
},
/**
@@ -368,23 +320,117 @@ export const LocationQueries = {
editor: Editor,
options: {
at?: Location | Span
match?: MarkMatch
mode?: 'all' | 'first' | 'distinct' | 'universal'
reverse?: boolean
continuing?: boolean
} = {}
): Iterable<MarkEntry> {
const { at = editor.selection } = options
const { match, mode = 'all', reverse = false, continuing = false } = options
let { at = editor.selection } = options
if (!at) {
return
}
const [from, to] = toSpan(editor, at, options)
// If the range is collapsed at the start of a text node, it should continue
// the marks from the previous text node in the same block.
if (
continuing &&
Range.isRange(at) &&
Range.isCollapsed(at) &&
at.anchor.offset === 0
) {
const { anchor } = at
const prev = Editor.previous(editor, anchor, 'text')
yield* Node.marks(editor, {
...options,
if (prev && Path.isSibling(anchor.path, prev[1])) {
const [, prevPath] = prev
at = Editor.range(editor, prevPath)
}
}
const [from, to] = toSpan(editor, at, options)
const iterable = Node.texts(editor, {
reverse,
from,
to,
pass: ([n]) => Element.isElement(n) && editor.isVoid(n),
})
let universalMarks: Mark[] = []
let distinctMarks: Mark[] = []
let universalEntries: MarkEntry[] = []
let first = true
for (const entry of iterable) {
const [node, path] = entry
const isMatch = (m: MarkMatch, entry: MarkEntry) => {
if (typeof m === 'function') {
return m(entry)
} else {
return Mark.matches(entry[0], m)
}
}
debugger
if (mode === 'universal') {
debugger
if (first) {
universalMarks.push(...node.marks)
universalEntries = node.marks.map((m, i) => [m, i, node, path])
first = false
continue
}
// PERF: If we're in universal mode and the eligible marks hits zero
// it can never increase again, so we can exit early.
if (universalMarks.length === 0) {
debugger
return
}
debugger
for (let i = universalMarks.length - 1; i >= 0; i--) {
const existing = universalMarks[i]
if (!Mark.exists(existing, node.marks)) {
universalMarks.splice(i, 1)
}
}
} else {
for (let index = 0; index < node.marks.length; index++) {
const mark = node.marks[index]
const markEntry: MarkEntry = [mark, index, node, path]
if (match != null && !isMatch(match, markEntry)) {
continue
}
if (mode === 'distinct') {
if (Mark.exists(mark, distinctMarks)) {
continue
} else {
distinctMarks.push(mark)
}
}
yield markEntry
// After matching a mark, if we're in first mode skip to the next text.
if (mode === 'first') {
break
}
}
}
}
// In universal mode, the marks are collected while iterating and we can
// only be certain of which are universal when we've finished.
if (mode === 'universal') {
debugger
yield* universalEntries
}
},
/**
@@ -413,6 +459,11 @@ export const LocationQueries = {
reverse?: boolean
}
): Iterable<NodeEntry> {
warning(
false,
'The `Editor.matches` helper is deprecated, use `Editor.nodes` instead.'
)
const { at = editor.selection, reverse = false } = options
let { match } = options
@@ -453,7 +504,11 @@ export const LocationQueries = {
const span: Span = [from, to]
let i = 0
for (const entry of Editor.matches(editor, { at: span, match })) {
for (const entry of Editor.nodes(editor, {
at: span,
match,
mode: 'highest',
})) {
if (i === 1) {
return entry
}
@@ -487,10 +542,17 @@ export const LocationQueries = {
editor: Editor,
options: {
at?: Location | Span
match?: NodeMatch
mode?: 'all' | 'highest'
reverse?: boolean
} = {}
): Iterable<NodeEntry> {
const { at = editor.selection } = options
const {
at = editor.selection,
match,
mode = 'all',
reverse = false,
} = options
if (!at) {
return
@@ -498,13 +560,32 @@ export const LocationQueries = {
const [from, to] = toSpan(editor, at, options)
const iterable = Node.nodes(editor, {
...options,
reverse,
from,
to,
pass: ([n]) => Element.isElement(n) && editor.isVoid(n),
})
let prev: NodeEntry | undefined
for (const entry of iterable) {
if (match != null) {
if (mode === 'highest' && prev) {
const [, prevPath] = prev
const [, path] = entry
if (Path.compare(path, prevPath) === 0) {
continue
}
}
if (!Editor.isMatch(editor, entry, match)) {
continue
}
prev = entry
}
yield entry
}
},
@@ -751,10 +832,11 @@ export const LocationQueries = {
const span: Span = [from, to]
let i = 0
for (const entry of Editor.matches(editor, {
for (const entry of Editor.nodes(editor, {
match,
at: span,
reverse: true,
mode: 'highest',
})) {
if (i === 1) {
return entry

View File

@@ -144,7 +144,7 @@ export const NodeTransforms = {
return
}
const matches = Editor.matches(editor, { at, match })
const matches = Editor.nodes(editor, { at, match, mode: 'highest' })
const pathRefs = Array.from(matches, ([, p]) => Editor.pathRef(editor, p))
for (const pathRef of pathRefs) {
@@ -352,7 +352,7 @@ export const NodeTransforms = {
}
const toRef = Editor.pathRef(editor, to)
const targets = Editor.matches(editor, { at, match })
const targets = Editor.nodes(editor, { at, match, mode: 'highest' })
const pathRefs = Array.from(targets, ([, p]) => Editor.pathRef(editor, p))
for (const pathRef of pathRefs) {
@@ -401,7 +401,7 @@ export const NodeTransforms = {
at = Editor.unhangRange(editor, at)
}
const depths = Editor.matches(editor, { at, match })
const depths = Editor.nodes(editor, { at, match, mode: 'highest' })
const pathRefs = Array.from(depths, ([, p]) => Editor.pathRef(editor, p))
for (const pathRef of pathRefs) {
@@ -446,7 +446,11 @@ export const NodeTransforms = {
at = Editor.unhangRange(editor, at)
}
for (const [node, path] of Editor.matches(editor, { at, match })) {
for (const [node, path] of Editor.nodes(editor, {
at,
match,
mode: 'highest',
})) {
const properties: Partial<Node> = {}
const newProperties: Partial<Node> = {}
@@ -631,7 +635,7 @@ export const NodeTransforms = {
return
}
const matches = Editor.matches(editor, { at, match })
const matches = Editor.nodes(editor, { at, match, mode: 'highest' })
const pathRefs = Array.from(matches, ([, p]) => Editor.pathRef(editor, p))
for (const pathRef of pathRefs) {
@@ -699,7 +703,14 @@ export const NodeTransforms = {
}
const roots: NodeEntry[] = editor.isInline(element)
? Array.from(Editor.matches(editor, { ...options, at, match: 'block' }))
? Array.from(
Editor.nodes(editor, {
...options,
at,
match: 'block',
mode: 'highest',
})
)
: [[editor, []]]
for (const [, rootPath] of roots) {
@@ -712,7 +723,7 @@ export const NodeTransforms = {
}
const matches = Array.from(
Editor.matches(editor, { ...options, at: a, match })
Editor.nodes(editor, { ...options, at: a, match, mode: 'highest' })
)
if (matches.length > 0) {

View File

@@ -92,8 +92,9 @@ export const TextTransforms = {
// Get the highest nodes that are completely inside the range, as well as
// the start and end nodes.
const matches = Editor.matches(editor, {
const matches = Editor.nodes(editor, {
at,
mode: 'highest',
match: ([n, p]) =>
(Element.isElement(n) && editor.isVoid(n)) ||
(!Path.isCommon(p, start.path) && !Path.isCommon(p, end.path)),

View File

@@ -1,24 +0,0 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
export const input = (
<editor>
<block>
<mark key="a">
<anchor />o
</mark>
n
<mark key="b">
e<focus />
</mark>
</block>
</editor>
)
export const run = editor => {
return Array.from(Editor.activeMarks(editor, { union: true }))
}
export const output = [{ key: 'a' }, { key: 'b' }]

View File

@@ -1,21 +1,23 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
<block>
<mark key="a">one</mark>
<cursor />
two
<text>
<cursor />
two
</text>
</block>
<block />
</editor>
)
export const run = editor => {
return Array.from(Editor.activeMarks(editor))
return Array.from(Editor.marks(editor, { continuing: true }), ([m]) => m)
}
export const output = [{ key: 'a' }]

View File

@@ -0,0 +1,28 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../../..'
export const input = (
<editor>
<block>
<mark key="a">
<mark key="b">
<anchor />o
</mark>
</mark>
n
<mark key="a">
<mark key="b">
e<focus />
</mark>
</mark>
</block>
</editor>
)
export const run = editor => {
return Array.from(Editor.marks(editor), ([m]) => m)
}
export const output = [{ key: 'b' }, { key: 'a' }, { key: 'b' }, { key: 'a' }]

View File

@@ -0,0 +1,28 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../../..'
export const input = (
<editor>
<block>
<mark key="a">
<mark key="b">
<anchor />o
</mark>
</mark>
n
<mark key="a">
<mark key="b">
e<focus />
</mark>
</mark>
</block>
</editor>
)
export const run = editor => {
return Array.from(Editor.marks(editor, { mode: 'distinct' }), ([m]) => m)
}
export const output = [{ key: 'b' }, { key: 'a' }]

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@@ -16,8 +16,8 @@ export const input = (
<block>
<mark key="a">
<mark key="b">
o<focus />
ne
t<focus />
wo
</mark>
</mark>
</block>
@@ -25,7 +25,7 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.activeMarks(editor))
return Array.from(Editor.marks(editor, { mode: 'universal' }), ([m]) => m)
}
export const output = [{ key: 'b' }, { key: 'a' }]

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@@ -16,7 +16,7 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.activeMarks(editor))
return Array.from(Editor.marks(editor, { mode: 'universal' }), ([m]) => m)
}
export const output = []

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@@ -17,7 +17,7 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.activeMarks(editor))
return Array.from(Editor.marks(editor, { mode: 'universal' }), ([m]) => m)
}
export const output = []

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@@ -18,7 +18,7 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.activeMarks(editor))
return Array.from(Editor.marks(editor, { mode: 'universal' }), ([m]) => m)
}
export const output = []

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@@ -17,7 +17,7 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.activeMarks(editor))
return Array.from(Editor.marks(editor, { mode: 'universal' }), ([m]) => m)
}
export const output = [{ key: 'b' }, { key: 'a' }]

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@@ -13,7 +13,7 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.activeMarks(editor))
return Array.from(Editor.marks(editor, { mode: 'universal' }), ([m]) => m)
}
export const output = []

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@@ -15,7 +15,7 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.activeMarks(editor))
return Array.from(Editor.marks(editor, { mode: 'universal' }), ([m]) => m)
}
export const output = [{ key: 'a' }]

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@@ -12,7 +12,7 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.activeMarks(editor))
return Array.from(Editor.marks(editor, { mode: 'universal' }), ([m]) => m)
}
export const output = []

View File

@@ -13,12 +13,12 @@ export const input = (
export const run = editor => {
return Array.from(
Editor.matches(editor, { at: [], match: 'block', reverse: true })
Editor.nodes(editor, { at: [], match: 'block', mode: 'highest' })
)
}
export const output = [
[<block>three</block>, [2]],
[<block>two</block>, [1]],
[<block>one</block>, [0]],
[<block>two</block>, [1]],
[<block>three</block>, [2]],
]

View File

@@ -15,7 +15,9 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.matches(editor, { at: [], match: 'block' }))
return Array.from(
Editor.nodes(editor, { at: [], match: 'block', mode: 'highest' })
)
}
export const output = [

View File

@@ -12,11 +12,18 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.matches(editor, { at: [], match: 'block' }))
return Array.from(
Editor.nodes(editor, {
at: [],
match: 'block',
mode: 'highest',
reverse: true,
})
)
}
export const output = [
[<block>one</block>, [0]],
[<block>two</block>, [1]],
[<block>three</block>, [2]],
[<block>two</block>, [1]],
[<block>one</block>, [0]],
]

View File

@@ -12,7 +12,9 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.matches(editor, { at: [], match: 'block' }))
return Array.from(
Editor.nodes(editor, { at: [], match: 'block', mode: 'highest' })
)
}
export const output = []

View File

@@ -10,7 +10,9 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.matches(editor, { at: [], match: 'block' }))
return Array.from(
Editor.nodes(editor, { at: [], match: 'block', mode: 'highest' })
)
}
export const output = [[<block>one</block>, [0]]]

View File

@@ -12,7 +12,9 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.matches(editor, { at: [], match: 'block' }))
return Array.from(
Editor.nodes(editor, { at: [], match: 'block', mode: 'highest' })
)
}
export const output = [

View File

@@ -13,7 +13,11 @@ export const input = (
export const run = editor => {
return Array.from(
Editor.matches(editor, { at: [], match: ([, p]) => p.length === 1 })
Editor.nodes(editor, {
at: [],
match: ([, p]) => p.length === 1,
mode: 'highest',
})
)
}

View File

@@ -10,7 +10,9 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.matches(editor, { at: [], match: 'inline' }))
return Array.from(
Editor.nodes(editor, { at: [], match: 'inline', mode: 'highest' })
)
}
export const output = [[<text>one</text>, [0, 0]]]

View File

@@ -13,14 +13,14 @@ export const input = (
export const run = editor => {
return Array.from(
Editor.matches(editor, { at: [], match: 'inline', reverse: true })
Editor.nodes(editor, { at: [], match: 'inline', mode: 'highest' })
)
}
export const output = [
[<text>five</text>, [0, 4]],
[<inline>four</inline>, [0, 3]],
[<text>three</text>, [0, 2]],
[<inline>two</inline>, [0, 1]],
[<text>one</text>, [0, 0]],
[<inline>two</inline>, [0, 1]],
[<text>three</text>, [0, 2]],
[<inline>four</inline>, [0, 3]],
[<text>five</text>, [0, 4]],
]

View File

@@ -16,7 +16,9 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.matches(editor, { at: [], match: 'inline' }))
return Array.from(
Editor.nodes(editor, { at: [], match: 'inline', mode: 'highest' })
)
}
export const output = [

View File

@@ -12,13 +12,20 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.matches(editor, { at: [], match: 'inline' }))
return Array.from(
Editor.nodes(editor, {
at: [],
match: 'inline',
mode: 'highest',
reverse: true,
})
)
}
export const output = [
[<text>one</text>, [0, 0]],
[<inline>two</inline>, [0, 1]],
[<text>three</text>, [0, 2]],
[<inline>four</inline>, [0, 3]],
[<text>five</text>, [0, 4]],
[<inline>four</inline>, [0, 3]],
[<text>three</text>, [0, 2]],
[<inline>two</inline>, [0, 1]],
[<text>one</text>, [0, 0]],
]

View File

@@ -12,7 +12,9 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.matches(editor, { at: [], match: 'inline' }))
return Array.from(
Editor.nodes(editor, { at: [], match: 'inline', mode: 'highest' })
)
}
export const output = [

View File

@@ -12,7 +12,9 @@ export const input = (
)
export const run = editor => {
return Array.from(Editor.matches(editor, { at: [], match: 'inline' }))
return Array.from(
Editor.nodes(editor, { at: [], match: 'inline', mode: 'highest' })
)
}
export const output = [

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>

View File

@@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>