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

remove marks, in favor of text properties (#3235)

* remove marks, in favor of text properties

* fix lint

* fix more examples

* update docs
This commit is contained in:
Ian Storm Taylor
2019-12-05 11:21:15 -05:00
committed by GitHub
parent 31df397930
commit 4c03b497d9
205 changed files with 792 additions and 4208 deletions

View File

@@ -1,22 +0,0 @@
/** @jsx jsx */
import { jsx } from '../..'
export const run = editor => {
editor.exec({ type: 'add_mark', mark: { key: 'a' } })
}
export const input = (
<editor>
<block>
o<anchor />
ne
</block>
<block>
tw
<focus />o
</block>
</editor>
)
export const output = input

View File

@@ -1,20 +0,0 @@
/** @jsx jsx */
import { jsx } from '../..'
export const run = editor => {
editor.exec({ type: 'add_mark', mark: { key: 'a' } })
}
export const input = (
<editor>
<block>
<mark key="b">
w<anchor />o
</mark>
r<focus />d
</block>
</editor>
)
export const output = input

View File

@@ -1,20 +0,0 @@
/** @jsx jsx */
import { jsx } from '../..'
export const run = editor => {
editor.exec({ type: 'add_mark', mark: { key: 'a' } })
}
export const input = (
<editor>
<block>
<mark key="a">
w<anchor />o
</mark>
r<focus />d
</block>
</editor>
)
export const output = input

View File

@@ -1,20 +0,0 @@
/** @jsx jsx */
import { jsx } from '../..'
export const run = editor => {
editor.exec({ type: 'add_mark', mark: { key: 'a' } })
}
export const input = (
<editor>
<block>
<anchor />
wo
<focus />
rd
</block>
</editor>
)
export const output = input

View File

@@ -1,27 +0,0 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
export const run = editor => {
editor.delete()
}
export const input = (
<editor>
<block>
<mark key="a">
on
<anchor />e
</mark>
<mark key="c">
tw
<focus />o
</mark>
</block>
</editor>
)
export const output = input
export const skip = true

View File

@@ -1,21 +0,0 @@
/** @jsx jsx */
import { jsx } from '../..'
export const run = editor => {
editor.exec({ type: 'remove_mark', mark: { key: true } })
}
export const input = (
<editor>
<block>
<mark key>
<anchor />
one
<focus />
</mark>
</block>
</editor>
)
export const output = input

View File

@@ -1,9 +1,7 @@
import {
Element,
Descendant,
Mark,
Node,
Path,
Range,
Text,
Editor,
@@ -37,7 +35,7 @@ const resolveDescendants = (children: any[]): Descendant[] => {
const prev = nodes[nodes.length - 1]
if (typeof child === 'string') {
const text = { text: child, marks: [] }
const text = { text: child }
STRINGS.add(text)
child = text
}
@@ -49,8 +47,7 @@ const resolveDescendants = (children: any[]): Descendant[] => {
Text.isText(prev) &&
STRINGS.has(prev) &&
STRINGS.has(c) &&
c.marks.every(m => Mark.exists(m, prev.marks)) &&
prev.marks.every(m => Mark.exists(m, c.marks))
Text.equals(prev, c, { loose: true })
) {
prev.text += c.text
} else {
@@ -143,43 +140,6 @@ export function createFragment(
return resolveDescendants(children)
}
/**
* Create a `Text` object with a mark applied.
*/
export function createMark(
tagName: string,
attributes: { [key: string]: any },
children: any[]
): Text {
const mark = { ...attributes }
const nodes = resolveDescendants(children)
if (nodes.length > 1) {
throw new Error(
`The <mark> hyperscript tag must only contain a single node's worth of children.`
)
}
if (nodes.length === 0) {
return { text: '', marks: [mark] }
}
const [node] = nodes
if (!Text.isText(node)) {
throw new Error(
`The <mark> hyperscript tag must only contain text content as children.`
)
}
if (!Mark.exists(mark, node.marks)) {
node.marks.push(mark)
}
return node
}
/**
* Create a `Selection` object.
*/
@@ -237,7 +197,7 @@ export function createText(
let [node] = nodes
if (node == null) {
node = { text: '', marks: [] }
node = { text: '' }
}
if (!Text.isText(node)) {
@@ -245,8 +205,8 @@ export function createText(
The <text> hyperscript tag can only contain text content as children.`)
}
// COMPAT: Re-create the node, because if they used the <text> tag we want to
// guarantee that it won't be merge with other string children.
// COMPAT: If they used the <text> tag we want to guarantee that it won't be
// merge with other string children.
STRINGS.delete(node)
Object.assign(node, attributes)

View File

@@ -1,5 +1,5 @@
import isPlainObject from 'is-plain-object'
import { Element, Mark } from 'slate'
import { Element } from 'slate'
import {
createAnchor,
createCursor,
@@ -7,7 +7,6 @@ import {
createElement,
createFocus,
createFragment,
createMark,
createSelection,
createText,
} from './creators'
@@ -23,7 +22,6 @@ const DEFAULT_CREATORS = {
element: createElement,
focus: createFocus,
fragment: createFragment,
mark: createMark,
selection: createSelection,
text: createText,
}
@@ -54,16 +52,13 @@ const createHyperscript = (
options: {
creators?: HyperscriptCreators
elements?: HyperscriptShorthands
marks?: HyperscriptShorthands
} = {}
) => {
const { elements = {}, marks = {} } = options
const { elements = {} } = options
const elementCreators = normalizeElements(elements)
const markCreators = normalizeMarks(marks)
const creators = {
...DEFAULT_CREATORS,
...elementCreators,
...markCreators,
...options.creators,
}
@@ -132,32 +127,4 @@ const normalizeElements = (elements: HyperscriptShorthands) => {
return creators
}
/**
* Normalize a dictionary of mark shorthands into creator functions.
*/
const normalizeMarks = (marks: HyperscriptShorthands) => {
const creators: HyperscriptCreators<Mark> = {}
for (const tagName in marks) {
const props = marks[tagName]
if (typeof props !== 'object') {
throw new Error(
`Properties specified for a hyperscript shorthand should be an object, but for the custom mark <${tagName}> tag you passed: ${props}`
)
}
creators[tagName] = (
tagName: string,
attributes: { [key: string]: any },
children: any[]
) => {
return createMark('mark', { ...props, ...attributes }, children)
}
}
return creators
}
export { createHyperscript, HyperscriptCreators, HyperscriptShorthands }

View File

@@ -1,4 +1,4 @@
import { Mark, Node, Path, Text } from 'slate'
import { Node, Path, Text } from 'slate'
/**
* A weak map to hold anchor tokens.
@@ -23,23 +23,17 @@ export class Token {}
*/
export class AnchorToken extends Token {
focused: boolean
marks: Mark[] | null
offset?: number
path?: Path
constructor(
props: {
focused?: boolean
marks?: Mark[] | null
offset?: number
path?: Path
} = {}
) {
super()
const { focused = true, marks = null, offset, path } = props
this.focused = focused
this.marks = marks
const { offset, path } = props
this.offset = offset
this.path = path
}
@@ -50,23 +44,17 @@ export class AnchorToken extends Token {
*/
export class FocusToken extends Token {
focused: boolean
marks: Mark[] | null
offset?: number
path?: Path
constructor(
props: {
focused?: boolean
marks?: Mark[] | null
offset?: number
path?: Path
} = {}
) {
super()
const { focused = true, marks = null, offset, path } = props
this.focused = focused
this.marks = marks
const { offset, path } = props
this.offset = offset
this.path = path
}

View File

@@ -18,7 +18,6 @@ export const output = {
children: [
{
text: 'word',
marks: [],
},
],
},

View File

@@ -23,7 +23,6 @@ export const output = {
children: [
{
text: '',
marks: [],
},
],
},
@@ -31,7 +30,6 @@ export const output = {
children: [
{
text: '',
marks: [],
},
],
},

View File

@@ -21,7 +21,6 @@ export const output = {
children: [
{
text: 'one',
marks: [],
},
],
},
@@ -29,7 +28,6 @@ export const output = {
children: [
{
text: 'two',
marks: [],
},
],
},

View File

@@ -21,7 +21,6 @@ export const output = {
children: [
{
text: 'one',
marks: [],
},
],
},
@@ -29,7 +28,6 @@ export const output = {
children: [
{
text: 'two',
marks: [],
},
],
},

View File

@@ -21,7 +21,6 @@ export const output = {
children: [
{
text: 'one',
marks: [],
},
],
},
@@ -29,7 +28,6 @@ export const output = {
children: [
{
text: 'two',
marks: [],
},
],
},

View File

@@ -16,7 +16,6 @@ export const output = {
children: [
{
text: '',
marks: [],
},
],
},

View File

@@ -17,7 +17,6 @@ export const output = {
children: [
{
text: 'one',
marks: [],
},
],
},

View File

@@ -17,7 +17,6 @@ export const output = {
children: [
{
text: 'one',
marks: [],
},
],
},

View File

@@ -21,7 +21,6 @@ export const output = {
children: [
{
text: 'word',
marks: [],
},
],
},

View File

@@ -22,7 +22,6 @@ export const output = {
children: [
{
text: 'word',
marks: [],
},
],
},

View File

@@ -21,7 +21,6 @@ export const output = {
children: [
{
text: 'word',
marks: [],
},
],
},

View File

@@ -17,7 +17,6 @@ export const output = {
children: [
{
text: 'one',
marks: [],
},
],
},

View File

@@ -1,34 +0,0 @@
/** @jsx jsx */
import { jsx } from 'slate-hyperscript'
export const input = (
<editor>
<element>
<cursor focused={false} />
</element>
</editor>
)
export const output = {
children: [
{
children: [
{
text: '',
marks: [],
},
],
},
],
selection: {
anchor: {
path: [0, 0],
offset: 0,
},
focus: {
path: [0, 0],
offset: 0,
},
},
}

View File

@@ -1,40 +0,0 @@
/** @jsx jsx */
import { jsx } from 'slate-hyperscript'
export const input = (
<editor>
<element>
<mark>one</mark>
<cursor />
two
</element>
</editor>
)
export const output = {
children: [
{
children: [
{
text: 'one',
marks: [{}],
},
{
text: 'two',
marks: [],
},
],
},
],
selection: {
anchor: {
path: [0, 0],
offset: 3,
},
focus: {
path: [0, 0],
offset: 3,
},
},
}

View File

@@ -1,42 +0,0 @@
/** @jsx jsx */
import { jsx } from 'slate-hyperscript'
export const input = (
<editor>
<element>
<mark>
one
<cursor />
</mark>
two
</element>
</editor>
)
export const output = {
children: [
{
children: [
{
text: 'one',
marks: [{}],
},
{
text: 'two',
marks: [],
},
],
},
],
selection: {
anchor: {
path: [0, 0],
offset: 3,
},
focus: {
path: [0, 0],
offset: 3,
},
},
}

View File

@@ -1,42 +0,0 @@
/** @jsx jsx */
import { jsx } from 'slate-hyperscript'
export const input = (
<editor>
<element>
<mark>
o<cursor />
ne
</mark>
two
</element>
</editor>
)
export const output = {
children: [
{
children: [
{
text: 'one',
marks: [{}],
},
{
text: 'two',
marks: [],
},
],
},
],
selection: {
anchor: {
path: [0, 0],
offset: 1,
},
focus: {
path: [0, 0],
offset: 1,
},
},
}

View File

@@ -1,42 +0,0 @@
/** @jsx jsx */
import { jsx } from 'slate-hyperscript'
export const input = (
<editor>
<element>
<mark>
<cursor />
one
</mark>
two
</element>
</editor>
)
export const output = {
children: [
{
children: [
{
text: 'one',
marks: [{}],
},
{
text: 'two',
marks: [],
},
],
},
],
selection: {
anchor: {
path: [0, 0],
offset: 0,
},
focus: {
path: [0, 0],
offset: 0,
},
},
}

View File

@@ -1,34 +0,0 @@
/** @jsx jsx */
import { jsx } from 'slate-hyperscript'
export const input = (
<editor>
<element>
<cursor marks={[]} />
</element>
</editor>
)
export const output = {
children: [
{
children: [
{
text: '',
marks: [],
},
],
},
],
selection: {
anchor: {
path: [0, 0],
offset: 0,
},
focus: {
path: [0, 0],
offset: 0,
},
},
}

View File

@@ -18,7 +18,6 @@ export const output = {
children: [
{
text: '',
marks: [],
},
],
},

View File

@@ -15,7 +15,6 @@ export const output = {
children: [
{
text: 'word',
marks: [],
},
],
}

View File

@@ -14,7 +14,6 @@ export const output = {
children: [
{
text: 'word',
marks: [],
},
],
},

View File

@@ -8,7 +8,6 @@ export const output = {
children: [
{
text: 'word',
marks: [],
},
],
}

View File

@@ -12,7 +12,6 @@ export const output = {
children: [
{
text: '',
marks: [],
},
],
}

View File

@@ -12,7 +12,6 @@ export const output = {
children: [
{
text: 'word',
marks: [],
},
],
}

View File

@@ -13,7 +13,6 @@ export const output = [
children: [
{
text: 'word',
marks: [],
},
],
},

View File

@@ -7,6 +7,5 @@ export const input = <fragment>word</fragment>
export const output = [
{
text: 'word',
marks: [],
},
]

View File

@@ -1,16 +0,0 @@
/** @jsx jsx */
import { createHyperscript } from 'slate-hyperscript'
const jsx = createHyperscript({
marks: {
b: { type: 'bold' },
},
})
export const input = <b>word</b>
export const output = {
text: 'word',
marks: [{ type: 'bold' }],
}

View File

@@ -1,14 +0,0 @@
/** @jsx jsx */
import { jsx } from 'slate-hyperscript'
export const input = (
<mark type="a">
<mark type="b">word</mark>
</mark>
)
export const output = {
text: 'word',
marks: [{ type: 'b' }, { type: 'a' }],
}

View File

@@ -1,10 +0,0 @@
/** @jsx jsx */
import { jsx } from 'slate-hyperscript'
export const input = <mark>word</mark>
export const output = {
text: 'word',
marks: [{}],
}

View File

@@ -1,14 +0,0 @@
/** @jsx jsx */
import { jsx } from 'slate-hyperscript'
export const input = (
<mark>
<text>word</text>
</mark>
)
export const output = {
text: 'word',
marks: [{}],
}

View File

@@ -18,7 +18,6 @@ export const output = {
children: [
{
text: 'word',
marks: [],
},
],
},

View File

@@ -2,9 +2,9 @@
import { jsx } from 'slate-hyperscript'
export const input = <text />
export const input = <text a />
export const output = {
text: '',
marks: [],
a: true,
}

View File

@@ -2,9 +2,9 @@
import { jsx } from 'slate-hyperscript'
export const input = <text>word</text>
export const input = <text a>word</text>
export const output = {
text: 'word',
marks: [],
a: true,
}

View File

@@ -3,12 +3,13 @@
import { jsx } from 'slate-hyperscript'
export const input = (
<text>
<text>word</text>
<text b>
<text a>word</text>
</text>
)
export const output = {
text: 'word',
marks: [],
a: true,
b: true,
}

View File

@@ -6,11 +6,7 @@ import TextComponent from './text'
import { ReactEditor } from '..'
import { useEditor } from '../hooks/use-editor'
import { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps'
import {
RenderDecorationProps,
RenderElementProps,
RenderMarkProps,
} from './editable'
import { RenderElementProps, RenderLeafProps } from './editable'
/**
* Children.
@@ -20,18 +16,16 @@ const Children = (props: {
decorate: (entry: NodeEntry) => Range[]
decorations: Range[]
node: Ancestor
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
renderElement?: (props: RenderElementProps) => JSX.Element
renderMark?: (props: RenderMarkProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
selection: Range | null
}) => {
const {
decorate,
decorations,
node,
renderDecoration,
renderElement,
renderMark,
renderLeaf,
selection,
} = props
const editor = useEditor()
@@ -65,9 +59,8 @@ const Children = (props: {
decorations={ds}
element={n}
key={key.id}
renderDecoration={renderDecoration}
renderElement={renderElement}
renderMark={renderMark}
renderLeaf={renderLeaf}
selection={sel}
/>
)
@@ -78,8 +71,7 @@ const Children = (props: {
key={key.id}
isLast={isLeafBlock && i === node.children.length}
parent={node}
renderDecoration={renderDecoration}
renderMark={renderMark}
renderLeaf={renderLeaf}
text={n}
/>
)

View File

@@ -5,7 +5,7 @@ import React, {
useMemo,
useCallback,
} from 'react'
import { Editor, Element, NodeEntry, Node, Range, Text, Mark } from 'slate'
import { Editor, Element, NodeEntry, Node, Range, Text } from 'slate'
import debounce from 'debounce'
import scrollIntoView from 'scroll-into-view-if-needed'
@@ -15,7 +15,6 @@ import { IS_FIREFOX, IS_SAFARI } from '../utils/environment'
import { ReactEditor } from '..'
import { ReadOnlyContext } from '../hooks/use-read-only'
import { useSlate } from '../hooks/use-slate'
import { Leaf } from '../utils/leaf'
import {
DOMElement,
DOMNode,
@@ -34,20 +33,6 @@ import {
PLACEHOLDER_SYMBOL,
} from '../utils/weak-maps'
/**
* `RenderDecorationProps` are passed to the `renderDecoration` handler.
*/
export interface RenderDecorationProps {
children: any
decoration: Range
leaf: Leaf
text: Text
attributes: {
'data-slate-decoration': true
}
}
/**
* `RenderElementProps` are passed to the `renderElement` handler.
*/
@@ -56,8 +41,8 @@ export interface RenderElementProps {
children: any
element: Element
attributes: {
'data-slate-inline'?: true
'data-slate-node': 'element'
'data-slate-inline'?: true
'data-slate-void'?: true
dir?: 'rtl'
ref: any
@@ -65,16 +50,15 @@ export interface RenderElementProps {
}
/**
* `RenderMarkProps` are passed to the `renderMark` handler.
* `RenderLeafProps` are passed to the `renderLeaf` handler.
*/
export interface RenderMarkProps {
export interface RenderLeafProps {
children: any
mark: Mark
leaf: Leaf
leaf: Text
text: Text
attributes: {
'data-slate-mark': true
'data-slate-leaf': true
}
}
@@ -90,21 +74,19 @@ export const Editable = (
readOnly?: boolean
role?: string
style?: React.CSSProperties
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
renderElement?: (props: RenderElementProps) => JSX.Element
renderMark?: (props: RenderMarkProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
} & React.TextareaHTMLAttributes<HTMLDivElement>
) => {
const {
autoFocus,
decorate = defaultDecorate,
onDOMBeforeInput: propsOnDOMBeforeInput,
placeholder,
readOnly = false,
renderDecoration,
renderElement,
renderMark,
autoFocus,
renderLeaf,
style = {},
onDOMBeforeInput: propsOnDOMBeforeInput,
...attributes
} = props
const editor = useSlate()
@@ -906,9 +888,8 @@ export const Editable = (
decorate={decorate}
decorations={decorations}
node={editor}
renderDecoration={renderDecoration}
renderElement={renderElement}
renderMark={renderMark}
renderLeaf={renderLeaf}
selection={editor.selection}
/>
</div>

View File

@@ -13,12 +13,7 @@ import {
NODE_TO_INDEX,
KEY_TO_ELEMENT,
} from '../utils/weak-maps'
import {
RenderDecorationProps,
RenderElementProps,
RenderMarkProps,
} from './editable'
import { isRangeListEqual } from '../utils/leaf'
import { RenderElementProps, RenderLeafProps } from './editable'
/**
* Element.
@@ -28,18 +23,16 @@ const Element = (props: {
decorate: (entry: NodeEntry) => Range[]
decorations: Range[]
element: SlateElement
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
renderElement?: (props: RenderElementProps) => JSX.Element
renderMark?: (props: RenderMarkProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
selection: Range | null
}) => {
const {
decorate,
decorations,
element,
renderDecoration,
renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,
renderMark,
renderLeaf,
selection,
} = props
const ref = useRef<HTMLElement>(null)
@@ -53,9 +46,8 @@ const Element = (props: {
decorate={decorate}
decorations={decorations}
node={element}
renderDecoration={renderDecoration}
renderElement={renderElement}
renderMark={renderMark}
renderLeaf={renderLeaf}
selection={selection}
/>
)
@@ -141,9 +133,8 @@ const MemoizedElement = React.memo(Element, (prev, next) => {
return (
prev.decorate === next.decorate &&
prev.element === next.element &&
prev.renderDecoration === next.renderDecoration &&
prev.renderElement === next.renderElement &&
prev.renderMark === next.renderMark &&
prev.renderLeaf === next.renderLeaf &&
isRangeListEqual(prev.decorations, next.decorations) &&
(prev.selection === next.selection ||
(!!prev.selection &&
@@ -167,4 +158,29 @@ export const DefaultElement = (props: RenderElementProps) => {
)
}
/**
* Check if a list of ranges is equal to another.
*
* PERF: this requires the two lists to also have the ranges inside them in the
* same order, but this is an okay constraint for us since decorations are
* kept in order, and the odd case where they aren't is okay to re-render for.
*/
const isRangeListEqual = (list: Range[], another: Range[]): boolean => {
if (list.length !== another.length) {
return false
}
for (let i = 0; i < list.length; i++) {
const range = list[i]
const other = another[i]
if (!Range.equals(range, other)) {
return false
}
}
return true
}
export default MemoizedElement

View File

@@ -2,9 +2,8 @@ import React from 'react'
import { Text, Element } from 'slate'
import String from './string'
import { Leaf as SlateLeaf } from '../utils/leaf'
import { PLACEHOLDER_SYMBOL } from '../utils/weak-maps'
import { RenderDecorationProps, RenderMarkProps } from './editable'
import { RenderLeafProps } from './editable'
/**
* Individual leaves in a text node with unique formatting.
@@ -12,10 +11,9 @@ import { RenderDecorationProps, RenderMarkProps } from './editable'
const Leaf = (props: {
isLast: boolean
leaf: SlateLeaf
leaf: Text
parent: Element
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
renderMark?: (props: RenderMarkProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
text: Text
}) => {
const {
@@ -23,117 +21,64 @@ const Leaf = (props: {
isLast,
text,
parent,
renderDecoration = (props: RenderDecorationProps) => (
<DefaultDecoration {...props} />
),
renderMark = (props: RenderMarkProps) => <DefaultMark {...props} />,
renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
} = props
let children = (
<String isLast={isLast} leaf={leaf} parent={parent} text={text} />
)
if (leaf[PLACEHOLDER_SYMBOL]) {
children = (
<React.Fragment>
<span
contentEditable={false}
style={{
pointerEvents: 'none',
display: 'inline-block',
verticalAlign: 'text-top',
width: '0',
maxWidth: '100%',
whiteSpace: 'nowrap',
opacity: '0.333',
}}
>
{leaf.placeholder}
</span>
{children}
</React.Fragment>
)
}
// COMPAT: Having the `data-` attributes on these leaf elements ensures that
// in certain misbehaving browsers they aren't weirdly cloned/destroyed by
// contenteditable behaviors. (2019/05/08)
for (const mark of leaf.marks) {
const ret = renderMark({
children,
leaf,
mark,
text,
attributes: {
'data-slate-mark': true,
},
})
if (ret) {
children = ret
}
const attributes: {
'data-slate-leaf': true
} = {
'data-slate-leaf': true,
}
for (const decoration of leaf.decorations) {
const p = {
children,
decoration,
leaf,
text,
attributes: {
'data-slate-decoration': true,
},
}
if (PLACEHOLDER_SYMBOL in decoration) {
// @ts-ignore
children = <PlaceholderDecoration {...p} />
} else {
// @ts-ignore
const ret = renderDecoration(p)
if (ret) {
children = ret
}
}
}
return <span data-slate-leaf>{children}</span>
return renderLeaf({ attributes, children, leaf, text })
}
const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
return (
next.parent === prev.parent &&
next.isLast === prev.isLast &&
next.renderDecoration === prev.renderDecoration &&
next.renderMark === prev.renderMark &&
next.renderLeaf === prev.renderLeaf &&
next.text === prev.text &&
SlateLeaf.equals(next.leaf, prev.leaf)
Text.matches(next.leaf, prev.leaf)
)
})
/**
* The default custom decoration renderer.
* The default custom leaf renderer.
*/
export const DefaultDecoration = (props: RenderDecorationProps) => {
export const DefaultLeaf = (props: RenderLeafProps) => {
const { attributes, children } = props
return <span {...attributes}>{children}</span>
}
/**
* The default custom mark renderer.
*/
export const DefaultMark = (props: RenderMarkProps) => {
const { attributes, children } = props
return <span {...attributes}>{children}</span>
}
/**
* A custom decoration for the default placeholder behavior.
*/
const PlaceholderDecoration = (props: RenderDecorationProps) => {
const { decoration, attributes, children } = props
const { placeholder } = decoration
return (
<span {...attributes}>
<span
contentEditable={false}
style={{
pointerEvents: 'none',
display: 'inline-block',
verticalAlign: 'text-top',
width: '0',
maxWidth: '100%',
whiteSpace: 'nowrap',
opacity: '0.333',
}}
>
{placeholder}
</span>
{children}
</span>
)
}
export default MemoizedLeaf

View File

@@ -2,7 +2,6 @@ import React from 'react'
import { Editor, Text, Path, Element, Node } from 'slate'
import { ReactEditor, useEditor } from '..'
import { Leaf } from '../utils/leaf'
/**
* Leaf content strings.
@@ -10,7 +9,7 @@ import { Leaf } from '../utils/leaf'
const String = (props: {
isLast: boolean
leaf: Leaf
leaf: Text
parent: Element
text: Text
}) => {

View File

@@ -2,9 +2,8 @@ import React, { useLayoutEffect, useRef } from 'react'
import { Range, Element, Text as SlateText } from 'slate'
import Leaf from './leaf'
import { Leaf as SlateLeaf } from '../utils/leaf'
import { ReactEditor, useEditor } from '..'
import { RenderDecorationProps, RenderMarkProps } from './editable'
import { RenderLeafProps } from './editable'
import {
KEY_TO_ELEMENT,
NODE_TO_ELEMENT,
@@ -19,18 +18,10 @@ const Text = (props: {
decorations: Range[]
isLast: boolean
parent: Element
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
renderMark?: (props: RenderMarkProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
text: SlateText
}) => {
const {
decorations,
isLast,
parent,
renderDecoration,
renderMark,
text,
} = props
const { decorations, isLast, parent, renderLeaf, text } = props
const editor = useEditor()
const ref = useRef<HTMLSpanElement>(null)
const leaves = getLeaves(text, decorations)
@@ -47,8 +38,7 @@ const Text = (props: {
leaf={leaf}
text={text}
parent={parent}
renderDecoration={renderDecoration}
renderMark={renderMark}
renderLeaf={renderLeaf}
/>
)
}
@@ -76,12 +66,12 @@ const Text = (props: {
* Get the leaves for a text node given decorations.
*/
const getLeaves = (node: SlateText, decorations: Range[]): SlateLeaf[] => {
const { text, marks } = node
let leaves: SlateLeaf[] = [{ text, marks, decorations: [] }]
const getLeaves = (node: SlateText, decorations: Range[]): SlateText[] => {
let leaves: SlateText[] = [{ ...node }]
const compile = (range: Range, key?: string) => {
const [start, end] = Range.edges(range)
for (const dec of decorations) {
const { anchor, focus, ...rest } = dec
const [start, end] = Range.edges(dec)
const next = []
let o = 0
@@ -92,7 +82,7 @@ const getLeaves = (node: SlateText, decorations: Range[]): SlateLeaf[] => {
// If the range encompases the entire leaf, add the range.
if (start.offset <= offset && end.offset >= offset + length) {
leaf.decorations.push(range)
Object.assign(leaf, rest)
next.push(leaf)
continue
}
@@ -115,14 +105,18 @@ const getLeaves = (node: SlateText, decorations: Range[]): SlateLeaf[] => {
let after
if (end.offset < offset + length) {
;[middle, after] = SlateLeaf.split(middle, end.offset - offset)
const off = end.offset - offset
after = { ...middle, text: middle.text.slice(off) }
middle = { ...middle, text: middle.text.slice(0, off) }
}
if (start.offset > offset) {
;[before, middle] = SlateLeaf.split(middle, start.offset - offset)
const off = start.offset - offset
before = { ...middle, text: middle.text.slice(0, off) }
middle = { ...middle, text: middle.text.slice(off) }
}
middle.decorations.push(range)
Object.assign(middle, rest)
if (before) {
next.push(before)
@@ -138,28 +132,16 @@ const getLeaves = (node: SlateText, decorations: Range[]): SlateLeaf[] => {
leaves = next
}
for (const range of decorations) {
compile(range)
}
return leaves
}
const MemoizedText = React.memo(Text, (prev, next) => {
if (
return (
next.parent === prev.parent &&
next.isLast === prev.isLast &&
next.renderDecoration === prev.renderDecoration &&
next.renderMark === prev.renderMark &&
next.renderLeaf === prev.renderLeaf &&
next.text === prev.text
) {
return SlateLeaf.equals(
{ ...next.text, decorations: next.decorations },
{ ...prev.text, decorations: prev.decorations }
)
}
return false
)
})
export default MemoizedText

View File

@@ -1,6 +1,6 @@
export * from './components/editable'
export { DefaultElement } from './components/element'
export { DefaultMark, DefaultDecoration } from './components/leaf'
export { DefaultLeaf } from './components/leaf'
export * from './hooks/use-editor'
export * from './hooks/use-focused'
export * from './hooks/use-read-only'

View File

@@ -1,113 +0,0 @@
import isPlainObject from 'is-plain-object'
import { Range, Mark } from 'slate'
/**
* The `Leaf` interface represents the individual leaves inside a text node,
* once decorations have been applied.
*/
interface Leaf {
decorations: Range[]
marks: Mark[]
text: string
}
namespace Leaf {
/**
* Check if two leaves are equal.
*/
export const equals = (leaf: Leaf, another: Leaf): boolean => {
return (
leaf.text === another.text &&
leaf.decorations.length === another.decorations.length &&
leaf.marks.length === another.marks.length &&
leaf.marks.every(m => Mark.exists(m, another.marks)) &&
another.marks.every(m => Mark.exists(m, leaf.marks)) &&
isRangeListEqual(leaf.decorations, another.decorations)
)
}
/**
* Check if a value is a `Leaf` object.
*/
export const isLeaf = (value: any): value is Leaf => {
return (
isPlainObject(value) &&
typeof value.text === 'string' &&
Mark.isMarkSet(value.marks) &&
Range.isRangeList(value.decorations)
)
}
/**
* Split a leaf into two at an offset.
*/
export const split = (leaf: Leaf, offset: number): [Leaf, Leaf] => {
return [
{
text: leaf.text.slice(0, offset),
marks: leaf.marks,
decorations: [...leaf.decorations],
},
{
text: leaf.text.slice(offset),
marks: leaf.marks,
decorations: [...leaf.decorations],
},
]
}
}
/**
* Check if a list of ranges is equal to another.
*
* PERF: this requires the two lists to also have the ranges inside them in the
* same order, but this is an okay constraint for us since decorations are
* kept in order, and the odd case where they aren't is okay to re-render for.
*/
const isRangeListEqual = (list: Range[], another: Range[]): boolean => {
if (list.length !== another.length) {
return false
}
for (let i = 0; i < list.length; i++) {
const range = list[i]
const other = another[i]
if (!Range.equals(range, other)) {
return false
}
}
return true
}
/**
* Check if a map of ranges is equal to another.
*/
const isRangeMapEqual = (
map: Record<string, Range>,
another: Record<string, Range>
): boolean => {
if (Object.keys(map).length !== Object.keys(another).length) {
return false
}
for (const key in map) {
const range = map[key]
const other = another[key]
if (!Range.equals(range, other)) {
return false
}
}
return true
}
export { Leaf, isRangeListEqual, isRangeMapEqual }

View File

@@ -1,4 +1,4 @@
import { Node, Ancestor, Editor } from 'slate'
import { Node, Ancestor, Editor, Text } from 'slate'
import { Key } from './key'
@@ -16,10 +16,11 @@ export const NODE_TO_PARENT: WeakMap<Node, Ancestor> = new WeakMap()
*/
export const EDITOR_TO_ELEMENT: WeakMap<Editor, HTMLElement> = new WeakMap()
export const NODE_TO_ELEMENT: WeakMap<Node, HTMLElement> = new WeakMap()
export const EDITOR_TO_PLACEHOLDER: WeakMap<Editor, string> = new WeakMap()
export const ELEMENT_TO_NODE: WeakMap<HTMLElement, Node> = new WeakMap()
export const NODE_TO_KEY: WeakMap<Node, Key> = new WeakMap()
export const KEY_TO_ELEMENT: WeakMap<Key, HTMLElement> = new WeakMap()
export const NODE_TO_ELEMENT: WeakMap<Node, HTMLElement> = new WeakMap()
export const NODE_TO_KEY: WeakMap<Node, Key> = new WeakMap()
/**
* Weak maps for storing editor-related state.
@@ -30,8 +31,4 @@ export const IS_FOCUSED: WeakMap<Editor, boolean> = new WeakMap()
export const IS_DRAGGING: WeakMap<Editor, boolean> = new WeakMap()
export const IS_CLICKING: WeakMap<Editor, boolean> = new WeakMap()
/**
* Symbols.
*/
export const PLACEHOLDER_SYMBOL = Symbol('placeholder')
export const PLACEHOLDER_SYMBOL = (Symbol('placeholder') as unknown) as string

View File

@@ -16,11 +16,8 @@ export const withReact = (editor: Editor): Editor => {
const matches: [Path, Key][] = []
switch (op.type) {
case 'add_mark':
case 'insert_text':
case 'remove_mark':
case 'remove_text':
case 'set_mark':
case 'set_node': {
for (const [node, path] of Editor.levels(editor, { at: op.path })) {
const key = ReactEditor.findKey(editor, node)

View File

@@ -2,47 +2,14 @@ import {
NodeEntry,
Node,
Text,
Mark,
Editor,
MarkEntry,
AncestorEntry,
Descendant,
DescendantEntry,
} from 'slate'
import { MarkError, NodeError } from './errors'
import { NodeRule, MarkRule, ChildValidation } from './rules'
/**
* Check a mark object.
*/
export const checkMark = (
editor: Editor,
entry: MarkEntry,
rule: MarkRule
): MarkError | undefined => {
const { validate: v } = rule
const [mark, index, node, path] = entry
if ('properties' in v) {
for (const k in v.properties) {
const p = v.properties[k]
const value = mark[k]
if ((typeof p === 'function' && !p(value)) || p !== value) {
return {
code: 'mark_property_invalid',
mark,
index,
node,
path,
property: k,
}
}
}
}
}
import { NodeError } from './errors'
import { NodeRule, ChildValidation } from './rules'
/**
* Check a node object.
@@ -70,15 +37,6 @@ export const checkNode = (
}
}
if ('marks' in v && v.marks != null) {
for (const entry of Node.marks(node)) {
if (!Editor.isMarkMatch(editor, entry, v.marks)) {
const [mark, index, n, p] = entry
return { code: 'mark_invalid', node: n, path: p, mark, index }
}
}
}
if ('text' in v && v.text != null) {
const text = Node.text(node)

View File

@@ -1,4 +1,4 @@
import { Ancestor, Descendant, Range, Mark, Node, Path, Text } from 'slate'
import { Ancestor, Descendant, Node, Path } from 'slate'
export interface ChildInvalidError {
code: 'child_invalid'
@@ -56,23 +56,6 @@ export interface NodeTextInvalidError {
text: string
}
export interface MarkInvalidError {
code: 'mark_invalid'
node: Text
path: Path
mark: Mark
index: number
}
export interface MarkPropertyInvalidError {
code: 'mark_property_invalid'
mark: Mark
index: number
node: Text
path: Path
property: string
}
export interface ParentInvalidError {
code: 'parent_invalid'
node: Ancestor
@@ -86,19 +69,16 @@ export interface PreviousSiblingInvalidError {
path: Path
}
export type MarkError = MarkPropertyInvalidError
export type NodeError =
| ChildInvalidError
| ChildMaxInvalidError
| ChildMinInvalidError
| FirstChildInvalidError
| LastChildInvalidError
| MarkInvalidError
| NextSiblingInvalidError
| NodePropertyInvalidError
| NodeTextInvalidError
| ParentInvalidError
| PreviousSiblingInvalidError
export type SchemaError = MarkError | NodeError
export type SchemaError = NodeError

View File

@@ -1,16 +1,5 @@
import { Editor, NodeMatch, MarkMatch } from 'slate'
import { NodeError, MarkError } from './errors'
export interface MarkValidation {
properties?: Record<string, any>
}
export interface MarkRule {
for: 'mark'
match: MarkMatch
validate: MarkValidation
normalize: (editor: Editor, error: MarkError) => void
}
import { Editor, NodeMatch } from 'slate'
import { NodeError } from './errors'
export interface ChildValidation {
match?: NodeMatch
@@ -22,7 +11,6 @@ export interface NodeValidation {
children?: ChildValidation[]
first?: NodeMatch
last?: NodeMatch
marks?: MarkMatch
next?: NodeMatch
parent?: NodeMatch
previous?: NodeMatch
@@ -37,4 +25,4 @@ export interface NodeRule {
normalize: (editor: Editor, error: NodeError) => void
}
export type SchemaRule = MarkRule | NodeRule
export type SchemaRule = NodeRule

View File

@@ -1,6 +1,6 @@
import { Editor, Text, NodeEntry } from 'slate'
import { NodeRule, SchemaRule, MarkRule } from './rules'
import { NodeRule, SchemaRule } from './rules'
import { NodeError } from './errors'
import { checkNode, checkAncestor } from './checkers'
@@ -14,23 +14,16 @@ export const withSchema = (
rules: SchemaRule[] = []
): Editor => {
const { normalizeNode } = editor
const markRules: MarkRule[] = []
const nodeRules: NodeRule[] = []
const nodeRules: NodeRule[] = rules
const parentRules: NodeRule[] = []
for (const rule of rules) {
if (rule.for === 'mark') {
markRules.push(rule)
} else {
nodeRules.push(rule)
if (
'parent' in rule.validate ||
'next' in rule.validate ||
'previous' in rule.validate
) {
parentRules.push(rule)
}
if (
'parent' in rule.validate ||
'next' in rule.validate ||
'previous' in rule.validate
) {
parentRules.push(rule)
}
}
@@ -136,12 +129,6 @@ export const withSchema = (
break
}
case 'mark_invalid': {
const { mark, path } = error
Editor.removeMarks(editor, [mark], { at: path })
break
}
case 'parent_invalid': {
const { path, index } = error
const childPath = path.concat(index)

View File

@@ -1,27 +0,0 @@
/** @jsx jsx */
import { jsx } from 'slate-hyperscript'
export const schema = [
{
for: 'node',
match: 'element',
validate: {
marks: [],
},
},
]
export const input = (
<editor>
<element>
<mark a>text</mark>
</element>
</editor>
)
export const output = (
<editor>
<element>text</element>
</editor>
)

View File

@@ -1,27 +0,0 @@
/** @jsx jsx */
import { jsx } from 'slate-hyperscript'
export const schema = [
{
for: 'node',
match: 'element',
validate: {
marks: [{ a: true }],
},
},
]
export const input = (
<editor>
<element>
<mark b>text</mark>
</element>
</editor>
)
export const output = (
<editor>
<element>text</element>
</editor>
)

View File

@@ -1,27 +0,0 @@
/** @jsx jsx */
import { jsx } from 'slate-hyperscript'
export const schema = [
{
for: 'node',
match: 'element',
validate: {
marks: [{ a: true }, { b: true }],
},
},
]
export const input = (
<editor>
<element>
<mark c>text</mark>
</element>
</editor>
)
export const output = (
<editor>
<element>text</element>
</editor>
)

View File

@@ -1,23 +0,0 @@
/** @jsx jsx */
import { jsx } from 'slate-hyperscript'
export const schema = [
{
for: 'node',
match: 'element',
validate: {
marks: [{ a: true }],
},
},
]
export const input = (
<editor>
<element>
<mark a>text</mark>
</element>
</editor>
)
export const output = input

View File

@@ -88,11 +88,6 @@ export const createEditor = (): Editor => {
if (Command.isCoreCommand(command)) {
switch (command.type) {
case 'add_mark': {
Editor.addMarks(editor, command.mark)
break
}
case 'delete_backward': {
if (selection && Range.isCollapsed(selection)) {
Editor.delete(editor, { unit: command.unit, reverse: true })
@@ -136,11 +131,6 @@ export const createEditor = (): Editor => {
Editor.insertText(editor, command.text)
break
}
case 'remove_mark': {
Editor.removeMarks(editor, [command.mark])
break
}
}
}
},
@@ -154,7 +144,7 @@ export const createEditor = (): Editor => {
// Ensure that block and inline nodes have at least one text child.
if (Element.isElement(node) && node.children.length === 0) {
const child = { text: '', marks: [] }
const child = { text: '' }
Editor.insertNodes(editor, child, { at: path.concat(0) })
return
}
@@ -194,23 +184,23 @@ export const createEditor = (): Editor => {
// Ensure that inline nodes are surrounded by text nodes.
if (editor.isInline(child)) {
if (prev == null || !Text.isText(prev)) {
const newChild = { text: '', marks: [] }
const newChild = { text: '' }
Editor.insertNodes(editor, newChild, { at: path.concat(n) })
n++
continue
}
if (isLast) {
const newChild = { text: '', marks: [] }
const newChild = { text: '' }
Editor.insertNodes(editor, newChild, { at: path.concat(n + 1) })
n++
continue
}
}
} else {
// Merge adjacent text nodes that are empty or have matching marks.
// Merge adjacent text nodes that are empty or match.
if (prev != null && Text.isText(prev)) {
if (Text.matches(child, prev)) {
if (Text.equals(child, prev, { loose: true })) {
Editor.mergeNodes(editor, { at: path.concat(n) })
n--
continue
@@ -238,11 +228,8 @@ export const createEditor = (): Editor => {
const getDirtyPaths = (op: Operation) => {
switch (op.type) {
case 'add_mark':
case 'insert_text':
case 'remove_mark':
case 'remove_text':
case 'set_mark':
case 'set_node': {
const { path } = op
return Path.levels(path)

View File

@@ -3,7 +3,6 @@ export * from './interfaces/command'
export * from './interfaces/editor'
export * from './interfaces/element'
export * from './interfaces/location'
export * from './interfaces/mark'
export * from './interfaces/node'
export * from './interfaces/operation'
export * from './interfaces/path'

View File

@@ -1,5 +1,5 @@
import isPlainObject from 'is-plain-object'
import { Mark, Node, Range } from '..'
import { Node } from '..'
/**
* `Command` objects represent an action that a user is taking on the editor.
@@ -20,32 +20,18 @@ export const Command = {
return isPlainObject(value) && typeof value.type === 'string'
},
/**
* Check if a value is an `AddMarkCommand` object.
*/
isAddMarkCommand(value: any): value is AddMarkCommand {
return (
Command.isCommand(value) &&
value.type === 'add_mark' &&
Mark.isMark(value.mark)
)
},
/**
* Check if a value is a `CoreCommand` object.
*/
isCoreCommand(value: any): value is CoreCommand {
return (
Command.isAddMarkCommand(value) ||
Command.isDeleteBackwardCommand(value) ||
Command.isDeleteForwardCommand(value) ||
Command.isDeleteFragmentCommand(value) ||
Command.isInsertTextCommand(value) ||
Command.isInsertFragmentCommand(value) ||
Command.isInsertBreakCommand(value) ||
Command.isRemoveMarkCommand(value)
Command.isInsertBreakCommand(value)
)
},
@@ -124,27 +110,6 @@ export const Command = {
typeof value.text === 'string'
)
},
/**
* Check if a value is a `RemoveMarkCommand` object.
*/
isRemoveMarkCommand(value: any): value is RemoveMarkCommand {
return (
Command.isCommand(value) &&
value.type === 'remove_mark' &&
Mark.isMark(value.mark)
)
},
}
/**
* The `AddMarkCommand` adds a mark to the current selection.
*/
export interface AddMarkCommand {
type: 'add_mark'
mark: Mark
}
/**
@@ -210,22 +175,12 @@ export interface InsertTextCommand {
text: string
}
/**
* The `RemoveMarkCommand` removes a mark in the current selection.
*/
export interface RemoveMarkCommand {
type: 'remove_mark'
mark: Mark
}
/**
* The `CoreCommand` union is a set of all of the commands that are recognized
* by Slate's "core" out of the box.
*/
export type CoreCommand =
| AddMarkCommand
| DeleteBackwardCommand
| DeleteForwardCommand
| DeleteFragmentCommand
@@ -233,4 +188,3 @@ export type CoreCommand =
| InsertFragmentCommand
| InsertNodeCommand
| InsertTextCommand
| RemoveMarkCommand

View File

@@ -4,8 +4,6 @@ import { ElementQueries } from './queries/element'
import { GeneralTransforms } from './transforms/general'
import { GeneralQueries } from './queries/general'
import { LocationQueries } from './queries/location'
import { MarkQueries } from './queries/mark'
import { MarkTransforms } from './transforms/mark'
import { NodeTransforms } from './transforms/node'
import { NodeQueries } from './queries/node'
import { RangeQueries } from './queries/range'
@@ -35,8 +33,6 @@ export const Editor = {
...GeneralQueries,
...GeneralTransforms,
...LocationQueries,
...MarkQueries,
...MarkTransforms,
...NodeQueries,
...NodeTransforms,
...RangeQueries,

View File

@@ -9,8 +9,6 @@ import {
Element,
ElementEntry,
Location,
Mark,
MarkEntry,
Node,
NodeEntry,
NodeMatch,
@@ -21,46 +19,8 @@ import {
Text,
TextEntry,
} from '../../..'
import { MarkMatch } from '../../mark'
export const LocationQueries = {
/**
* Get the marks that are "active" at a location. These are the
* marks that will be added to any text that is inserted.
*
* The `union: true` option can be passed to create a union of marks across
* the text nodes in the selection, instead of creating an intersection, which
* is the default.
*
* Note: to obey common rich text behavior, if the selection is collapsed at
* the start of a text node and there are previous text nodes in the same
* block, it will carry those marks forward from the previous text node. This
* allows for continuation of marks from previous words.
*/
activeMarks(
editor: Editor,
options: {
at?: Location
union?: boolean
hanging?: boolean
} = {}
): Mark[] {
warning(
false,
'The `Editor.activeMarks` helper is deprecated, use `Editor.marks` instead.'
)
return Array.from(
Editor.marks(editor, {
at: options.at,
mode: options.union ? 'distinct' : 'universal',
continuing: true,
}),
([m]) => m
)
},
/**
* Get the point after a location.
*/
@@ -305,116 +265,6 @@ export const LocationQueries = {
yield* levels
},
/**
* Iterate through all of the text nodes in the Editor.
*/
*marks(
editor: Editor,
options: {
at?: Location
match?: MarkMatch
mode?: 'all' | 'first' | 'distinct' | 'universal'
reverse?: boolean
continuing?: boolean
} = {}
): Iterable<MarkEntry> {
const { match, mode = 'all', reverse = false, continuing = false } = options
let { at = editor.selection } = options
if (!at) {
return
}
// 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')
if (prev && Path.isSibling(anchor.path, prev[1])) {
const [, prevPath] = prev
at = Editor.range(editor, prevPath)
}
}
const universalMarks: Mark[] = []
const distinctMarks: Mark[] = []
const universalEntries: MarkEntry[] = []
let first = true
for (const entry of Editor.texts(editor, { reverse, at })) {
const [node, path] = entry
if (mode === 'universal') {
if (first) {
for (let i = 0; i < node.marks.length; i++) {
const mark = node.marks[i]
const markEntry: MarkEntry = [mark, i, node, path]
if (match == null || Editor.isMarkMatch(editor, markEntry, match)) {
universalMarks.push(mark)
universalEntries.push(markEntry)
}
}
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) {
return
}
for (let i = universalMarks.length - 1; i >= 0; i--) {
const existing = universalMarks[i]
if (!Mark.exists(existing, node.marks)) {
universalMarks.splice(i, 1)
universalEntries.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 && !Editor.isMarkMatch(editor, markEntry, match)) {
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') {
yield* universalEntries
}
},
/**
* Get the first matching node in a single branch of the document.
*/

View File

@@ -1,17 +0,0 @@
import { Editor, Mark, MarkEntry, MarkMatch } from '../../..'
export const MarkQueries = {
/**
* Check if a mark entry is a match.
*/
isMarkMatch(editor: Editor, entry: MarkEntry, match: MarkMatch): boolean {
if (Array.isArray(match)) {
return match.some(m => Editor.isMarkMatch(editor, entry, m))
} else if (typeof match === 'function') {
return match(entry)
} else {
return Mark.matches(entry[0], match)
}
},
}

View File

@@ -2,7 +2,6 @@ import { createDraft, finishDraft, isDraft } from 'immer'
import {
Node,
Editor,
Mark,
Range,
Point,
Text,
@@ -68,17 +67,6 @@ export const GeneralTransforms = {
let selection = editor.selection && createDraft(editor.selection)
switch (op.type) {
case 'add_mark': {
const { path, mark } = op
const node = Node.leaf(editor, path)
if (!Mark.exists(mark, node.marks)) {
node.marks.push(mark)
}
break
}
case 'insert_node': {
const { path, node } = op
const parent = Node.parent(editor, path)
@@ -174,20 +162,6 @@ export const GeneralTransforms = {
break
}
case 'remove_mark': {
const { path, mark } = op
const node = Node.leaf(editor, path)
for (let i = 0; i < node.marks.length; i++) {
if (Mark.matches(node.marks[i], mark)) {
node.marks.splice(i, 1)
break
}
}
break
}
case 'remove_node': {
const { path } = op
const index = path[path.length - 1]
@@ -238,20 +212,6 @@ export const GeneralTransforms = {
break
}
case 'set_mark': {
const { path, properties, newProperties } = op
const node = Node.leaf(editor, path)
for (const mark of node.marks) {
if (Mark.matches(mark, properties)) {
Object.assign(mark, newProperties)
break
}
}
break
}
case 'set_node': {
const { path, newProperties } = op
@@ -260,7 +220,21 @@ export const GeneralTransforms = {
}
const node = Node.get(editor, path)
Object.assign(node, newProperties)
for (const key in newProperties) {
if (key === 'children' || key === 'text') {
throw new Error(`Cannot set the "${key}" property of nodes!`)
}
const value = newProperties[key]
if (value == null) {
delete node[key]
} else {
node[key] = value
}
}
break
}

View File

@@ -1,140 +0,0 @@
import { Editor, Mark, Location, Range } from '../../..'
export const MarkTransforms = {
/**
* Add a set of marks to the text nodes at a location.
*/
addMarks(
editor: Editor,
mark: Mark | Mark[],
options: {
at?: Location
hanging?: boolean
} = {}
) {
Editor.withoutNormalizing(editor, () => {
const at = splitLocation(editor, options)
if (!at) {
return
}
// De-dupe the marks being added to ensure the set is unique.
const marks = Array.isArray(mark) ? mark : [mark]
const set: Mark[] = []
for (const m of marks) {
if (!Mark.exists(m, set)) {
set.push(m)
}
}
for (const [node, path] of Editor.texts(editor, { at })) {
for (const m of set) {
if (!Mark.exists(m, node.marks)) {
editor.apply({ type: 'add_mark', path, mark: m })
}
}
}
})
},
removeMarks(
editor: Editor,
mark: Mark | Mark[],
options: {
at?: Location
hanging?: boolean
} = {}
) {
Editor.withoutNormalizing(editor, () => {
const at = splitLocation(editor, options)
if (at) {
const marks = Array.isArray(mark) ? mark : [mark]
for (const [m, i, node, path] of Editor.marks(editor, { at })) {
if (Mark.exists(m, marks)) {
editor.apply({ type: 'remove_mark', path, mark: m })
}
}
}
})
},
setMarks(
editor: Editor,
mark: Mark | Mark[],
props: Partial<Mark>,
options: {
at?: Location
hanging?: boolean
} = {}
) {
Editor.withoutNormalizing(editor, () => {
const at = splitLocation(editor, options)
if (at) {
const marks = Array.isArray(mark) ? mark : [mark]
for (const [m, i, node, path] of Editor.marks(editor, { at })) {
if (Mark.exists(m, marks)) {
const newProps = {}
for (const k in props) {
if (props[k] !== m[k]) {
newProps[k] = props[k]
}
}
if (Object.keys(newProps).length > 0) {
editor.apply({
type: 'set_mark',
path,
properties: m,
newProperties: newProps,
})
}
}
}
}
})
},
}
/**
* Split the text nodes at a range's edges to prepare for adding/removing marks.
*/
const splitLocation = (
editor: Editor,
options: {
at?: Location
hanging?: boolean
} = {}
): Location | undefined => {
let { at = editor.selection, hanging = false } = options
if (!at) {
return
}
if (Range.isRange(at)) {
if (!hanging) {
at = Editor.unhangRange(editor, at)
}
const rangeRef = Editor.rangeRef(editor, at, { affinity: 'inward' })
const [start, end] = Range.edges(at)
Editor.splitNodes(editor, { at: end, match: 'text' })
Editor.splitNodes(editor, { at: start, match: 'text' })
const range = rangeRef.unref()!
if (options.at == null) {
Editor.select(editor, range)
}
return range
}
return at
}

View File

@@ -270,7 +270,7 @@ export const NodeTransforms = {
// Ensure that the nodes are equivalent, and figure out what the position
// and extra properties of the merge will be.
if (Text.isText(node) && Text.isText(prevNode)) {
const { text, marks, ...rest } = node
const { text, ...rest } = node
position = prevNode.text.length
properties = rest as Partial<Text>
} else if (Element.isElement(node) && Element.isElement(prevNode)) {
@@ -413,7 +413,7 @@ export const NodeTransforms = {
},
/**
* Set new properties on the nodes ...
* Set new properties on the nodes at a location.
*/
setNodes(
@@ -422,12 +422,14 @@ export const NodeTransforms = {
options: {
at?: Location
match?: NodeMatch
mode?: 'all' | 'highest'
hanging?: boolean
split?: boolean
} = {}
) {
Editor.withoutNormalizing(editor, () => {
let { match, at = editor.selection } = options
const { hanging = false } = options
const { hanging = false, mode = 'highest', split = false } = options
if (match == null) {
if (Path.isPath(at)) {
@@ -446,21 +448,29 @@ export const NodeTransforms = {
at = Editor.unhangRange(editor, at)
}
for (const [node, path] of Editor.nodes(editor, {
at,
match,
mode: 'highest',
})) {
if (split && Range.isRange(at)) {
const rangeRef = Editor.rangeRef(editor, at, { affinity: 'inward' })
const [start, end] = Range.edges(at)
Editor.splitNodes(editor, { at: end, match })
Editor.splitNodes(editor, { at: start, match })
at = rangeRef.unref()!
if (options.at == null) {
Editor.select(editor, at)
}
}
for (const [node, path] of Editor.nodes(editor, { at, match, mode })) {
const properties: Partial<Node> = {}
const newProperties: Partial<Node> = {}
// You can't set properties on the editor node.
if (path.length === 0) {
continue
}
for (const k in props) {
if (
k === 'marks' ||
k === 'children' ||
k === 'selection' ||
k === 'text'
) {
if (k === 'children' || k === 'text') {
continue
}
@@ -540,7 +550,7 @@ export const NodeTransforms = {
let after = Editor.after(editor, voidPath)
if (!after) {
const text = { text: '', marks: [] }
const text = { text: '' }
const afterPath = Path.next(voidPath)
Editor.insertNodes(editor, text, { at: afterPath })
after = Editor.point(editor, afterPath)!
@@ -581,7 +591,7 @@ export const NodeTransforms = {
if (always || !beforeRef || !Editor.isEdge(editor, point, path)) {
split = true
const { text, marks, children, ...properties } = node
const { text, children, ...properties } = node
editor.apply({
type: 'split_node',
path,

View File

@@ -1,68 +0,0 @@
import isPlainObject from 'is-plain-object'
import { Path, Text } from '..'
/**
* `Mark` objects represent formatting that is applied to text in a Slate
* document. They appear in leaf text nodes in the document.
*/
export interface Mark {
[key: string]: any
}
export const Mark = {
/**
* Check if a mark exists in a set of marks.
*/
exists(mark: Mark, marks: Mark[]): boolean {
return !!marks.find(f => Mark.matches(f, mark))
},
/**
* Check if a value implements the `Mark` interface.
*/
isMark(value: any): value is Mark {
return isPlainObject(value)
},
/**
* Check if a value is an array of `Mark` objects.
*/
isMarkSet(value: any): value is Mark[] {
return Array.isArray(value) && (value.length === 0 || Mark.isMark(value[0]))
},
/**
* Check if a mark matches set of properties.
*/
matches(mark: Mark, props: Partial<Mark>): boolean {
for (const key in props) {
if (mark[key] !== props[key]) {
return false
}
}
return true
},
}
/**
* `MarkEntry` tuples are returned when iterating through the marks in a text
* node. They include the index of the mark in the text node's marks array, as
* well as the text node and its path in the root node.
*/
export type MarkEntry = [Mark, number, Text, Path]
/**
* `MarkMatch` values are used as shorthands for matching mark objects.
*/
export type MarkMatch =
| Partial<Mark>
| ((entry: MarkEntry) => boolean)
| MarkMatch[]

View File

@@ -1,14 +1,5 @@
import { produce } from 'immer'
import {
Editor,
Element,
ElementEntry,
MarkEntry,
Path,
Range,
Text,
TextEntry,
} from '..'
import { Editor, Element, ElementEntry, Path, Range, Text, TextEntry } from '..'
/**
* The `Node` union type represents all of the different types of nodes that
@@ -372,27 +363,6 @@ export const Node = {
}
},
/**
* Return an iterable of all the marks in all of the text nodes in a root node.
*/
*marks(
root: Node,
options: {
from?: Path
to?: Path
reverse?: boolean
pass?: (node: NodeEntry) => boolean
} = {}
): Iterable<MarkEntry> {
for (const [node, path] of Node.texts(root, options)) {
for (let i = 0; i < node.marks.length; i++) {
const mark = node.marks[i]
yield [mark, i, node, path]
}
}
},
/**
* Return an iterable of all the node entries of a root node. Each entry is
* returned as a `[Node, Path]` tuple, with the path referring to the node's

View File

@@ -1,13 +1,6 @@
import { Mark, Node, Path, Range } from '..'
import { Node, Path, Range } from '..'
import isPlainObject from 'is-plain-object'
type AddMarkOperation = {
type: 'add_mark'
path: Path
mark: Mark
[key: string]: any
}
type InsertNodeOperation = {
type: 'insert_node'
path: Path
@@ -39,13 +32,6 @@ type MoveNodeOperation = {
[key: string]: any
}
type RemoveMarkOperation = {
type: 'remove_mark'
path: Path
mark: Mark
[key: string]: any
}
type RemoveNodeOperation = {
type: 'remove_node'
path: Path
@@ -61,14 +47,6 @@ type RemoveTextOperation = {
[key: string]: any
}
type SetMarkOperation = {
type: 'set_mark'
path: Path
properties: Partial<Mark>
newProperties: Partial<Mark>
[key: string]: any
}
type SetNodeOperation = {
type: 'set_node'
path: Path
@@ -113,11 +91,7 @@ type SplitNodeOperation = {
* collaboration, and other features.
*/
type Operation =
| NodeOperation
| MarkOperation
| SelectionOperation
| TextOperation
type Operation = NodeOperation | SelectionOperation | TextOperation
type NodeOperation =
| InsertNodeOperation
@@ -127,8 +101,6 @@ type NodeOperation =
| SetNodeOperation
| SplitNodeOperation
type MarkOperation = AddMarkOperation | RemoveMarkOperation | SetMarkOperation
type SelectionOperation = SetSelectionOperation
type TextOperation = InsertTextOperation | RemoveTextOperation
@@ -142,14 +114,6 @@ const Operation = {
return Operation.isOperation(value) && value.type.endsWith('_node')
},
/**
* Check of a value is a `MarkOperation` object.
*/
isMarkOperation(value: any): value is MarkOperation {
return Operation.isOperation(value) && value.type.endsWith('_mark')
},
/**
* Check of a value is an `Operation` object.
*/
@@ -160,10 +124,6 @@ const Operation = {
}
switch (value.type) {
case 'add_mark': {
return Path.isPath(value.path) && Mark.isMark(value.mark)
}
case 'insert_node': {
return Path.isPath(value.path) && Node.isNode(value.node)
}
@@ -189,10 +149,6 @@ const Operation = {
return Path.isPath(value.path) && Path.isPath(value.newPath)
}
case 'remove_mark': {
return Path.isPath(value.path) && Mark.isMark(value.mark)
}
case 'remove_node': {
return Path.isPath(value.path) && Node.isNode(value.node)
}
@@ -205,14 +161,6 @@ const Operation = {
)
}
case 'set_mark': {
return (
Path.isPath(value.path) &&
isPlainObject(value.properties) &&
isPlainObject(value.newProperties)
)
}
case 'set_node': {
return (
Path.isPath(value.path) &&
@@ -285,10 +233,6 @@ const Operation = {
inverse(op: Operation): Operation {
switch (op.type) {
case 'add_mark': {
return { ...op, type: 'remove_mark' }
}
case 'insert_node': {
return { ...op, type: 'remove_node' }
}
@@ -317,10 +261,6 @@ const Operation = {
return { ...op, path: inversePath, newPath: inverseNewPath }
}
case 'remove_mark': {
return { ...op, type: 'add_mark' }
}
case 'remove_node': {
return { ...op, type: 'insert_node' }
}
@@ -329,7 +269,6 @@ const Operation = {
return { ...op, type: 'insert_text' }
}
case 'set_mark':
case 'set_node': {
const { properties, newProperties } = op
return { ...op, properties: newProperties, newProperties: properties }
@@ -363,15 +302,12 @@ const Operation = {
}
export {
AddMarkOperation,
InsertNodeOperation,
InsertTextOperation,
MergeNodeOperation,
MoveNodeOperation,
RemoveMarkOperation,
RemoveNodeOperation,
RemoveTextOperation,
SetMarkOperation,
SetNodeOperation,
SetSelectionOperation,
SplitNodeOperation,

View File

@@ -1,29 +1,58 @@
import isPlainObject from 'is-plain-object'
import { Mark, Path } from '..'
import { Path } from '..'
/**
* `Text` objects represent the nodes that contain the actual text content of a
* Slate document along with any formatting marks. They are always leaf nodes in
* the document tree as they cannot contain any children.
* Slate document along with any formatting properties. They are always leaf
* nodes in the document tree as they cannot contain any children.
*/
export interface Text {
text: string
marks: Mark[]
[key: string]: any
}
export const Text = {
/**
* Check if two text nodes are equal.
*/
equals(
text: Text,
another: Text,
options: { loose?: boolean } = {}
): boolean {
const { loose = false } = options
for (const key in text) {
if (loose && key === 'text') {
continue
}
if (text[key] !== another[key]) {
return false
}
}
for (const key in another) {
if (loose && key === 'text') {
continue
}
if (text[key] !== another[key]) {
return false
}
}
return true
},
/**
* Check if a value implements the `Text` interface.
*/
isText(value: any): value is Text {
return (
isPlainObject(value) &&
typeof value.text === 'string' &&
Array.isArray(value.marks)
)
return isPlainObject(value) && typeof value.text === 'string'
},
/**
@@ -38,8 +67,7 @@ export const Text = {
* Check if an text matches set of properties.
*
* Note: this is for matching custom properties, and it does not ensure that
* the `text` property are two nodes equal. However, if `marks` are passed it
* will ensure that the set of marks is exactly equal.
* the `text` property are two nodes equal.
*/
matches(text: Text, props: Partial<Text>): boolean {
@@ -48,30 +76,6 @@ export const Text = {
continue
}
if (key === 'marks' && props.marks != null) {
const existing = text.marks
const { marks } = props
// PERF: If the lengths aren't the same, we know it's not a match.
if (existing.length !== marks.length) {
return false
}
for (const m of existing) {
if (!Mark.exists(m, marks)) {
return false
}
}
for (const m of marks) {
if (!Mark.exists(m, existing)) {
return false
}
}
continue
}
if (text[key] !== props[key]) {
return false
}

View File

@@ -2,7 +2,6 @@ import { Element } from 'slate'
export const input = {
text: '',
marks: [],
}
export const test = value => {

View File

@@ -3,7 +3,6 @@ import { Element } from 'slate'
export const input = [
{
text: '',
marks: [],
},
]

View File

@@ -1,12 +0,0 @@
import { Mark } from 'slate'
export const input = {
element: { children: [], type: 'bold', other: true },
props: { type: 'bold' },
}
export const test = ({ element, props }) => {
return Mark.matches(element, props)
}
export const output = true

View File

@@ -1,12 +0,0 @@
import { Mark } from 'slate'
export const input = {
element: { children: [], type: 'bold' },
props: {},
}
export const test = ({ mark, props }) => {
return Mark.matches(mark, props)
}
export const output = true

View File

@@ -1,12 +0,0 @@
import { Mark } from 'slate'
export const input = {
mark: { type: 'bold' },
marks: [{ type: 'bold' }],
}
export const test = ({ mark, marks }) => {
return Mark.exists(mark, marks)
}
export const output = true

View File

@@ -1,12 +0,0 @@
import { Mark } from 'slate'
export const input = {
mark: { type: 'bold' },
marks: [{ type: 'italic' }],
}
export const test = ({ mark, marks }) => {
return Mark.exists(mark, marks)
}
export const output = false

View File

@@ -1,12 +0,0 @@
import { Mark } from 'slate'
export const input = {
mark: { type: 'bold' },
marks: [{ type: 'bold', other: true }],
}
export const test = ({ mark, marks }) => {
return Mark.exists(mark, marks)
}
export const output = true

View File

@@ -1,12 +0,0 @@
import { Mark } from 'slate'
export const input = {
mark: {},
marks: [{}],
}
export const test = ({ mark, marks }) => {
return Mark.exists(mark, marks)
}
export const output = true

View File

@@ -1,12 +0,0 @@
import { Mark } from 'slate'
export const input = {
mark: {},
marks: [{ type: 'bold' }],
}
export const test = ({ mark, marks }) => {
return Mark.exists(mark, marks)
}
export const output = true

View File

@@ -1,12 +0,0 @@
import { Mark } from 'slate'
export const input = {
mark: {},
marks: [],
}
export const test = ({ mark, marks }) => {
return Mark.exists(mark, marks)
}
export const output = false

View File

@@ -1,9 +0,0 @@
import { Mark } from 'slate'
export const input = true
export const test = value => {
return Mark.isMark(value)
}
export const output = false

View File

@@ -1,11 +0,0 @@
import { Mark } from 'slate'
export const input = {
custom: 'value',
}
export const test = value => {
return Mark.isMark(value)
}
export const output = true

View File

@@ -1,9 +0,0 @@
import { Mark } from 'slate'
export const input = {}
export const test = value => {
return Mark.isMark(value)
}
export const output = true

View File

@@ -1,9 +0,0 @@
import { Mark } from 'slate'
export const input = true
export const test = value => {
return Mark.isMarkSet(value)
}
export const output = false

View File

@@ -1,9 +0,0 @@
import { Mark } from 'slate'
export const input = []
export const test = value => {
return Mark.isMarkSet(value)
}
export const output = true

View File

@@ -1,9 +0,0 @@
import { Mark } from 'slate'
export const input = [{}]
export const test = value => {
return Mark.isMarkSet(value)
}
export const output = true

View File

@@ -1,9 +0,0 @@
import { Mark } from 'slate'
export const input = {}
export const test = value => {
return Mark.isMarkSet(value)
}
export const output = false

View File

@@ -1,12 +0,0 @@
import { Mark } from 'slate'
export const input = {
mark: { type: 'bold' },
props: { type: 'bold' },
}
export const test = ({ mark, props }) => {
return Mark.matches(mark, props)
}
export const output = true

View File

@@ -1,12 +0,0 @@
import { Mark } from 'slate'
export const input = {
mark: { type: 'bold' },
props: { type: 'italic' },
}
export const test = ({ mark, props }) => {
return Mark.matches(mark, props)
}
export const output = false

View File

@@ -1,12 +0,0 @@
import { Mark } from 'slate'
export const input = {
mark: { type: 'bold', other: true },
props: { type: 'bold' },
}
export const test = ({ mark, props }) => {
return Mark.matches(mark, props)
}
export const output = true

View File

@@ -1,12 +0,0 @@
import { Mark } from 'slate'
export const input = {
mark: {},
props: {},
}
export const test = ({ mark, props }) => {
return Mark.matches(mark, props)
}
export const output = true

View File

@@ -1,12 +0,0 @@
import { Mark } from 'slate'
export const input = {
mark: { type: 'bold' },
props: {},
}
export const test = ({ mark, props }) => {
return Mark.matches(mark, props)
}
export const output = true

View File

@@ -2,7 +2,6 @@ import { Node } from 'slate'
export const input = {
text: '',
marks: [],
}
export const test = value => {

View File

@@ -3,7 +3,6 @@ import { Node } from 'slate'
export const input = [
{
text: '',
marks: [],
},
]

View File

@@ -1,22 +0,0 @@
/** @jsx jsx */
import { Node } from 'slate'
import { jsx } from 'slate-hyperscript'
export const input = (
<editor>
<element>
<mark key="a">one</mark>
<mark key="b">two</mark>
</element>
</editor>
)
export const test = value => {
return Array.from(Node.marks(value))
}
export const output = [
[{ key: 'a' }, 0, <mark key="a">one</mark>, [0, 0]],
[{ key: 'b' }, 0, <mark key="b">two</mark>, [0, 1]],
]

View File

@@ -1,19 +0,0 @@
/** @jsx jsx */
import { Node } from 'slate'
import { jsx } from 'slate-hyperscript'
export const input = (
<editor>
<element>
<mark key="a">one</mark>
<mark key="b">two</mark>
</element>
</editor>
)
export const test = value => {
return Array.from(Node.marks(value, { from: [0, 1] }))
}
export const output = [[{ key: 'b' }, 0, <mark key="b">two</mark>, [0, 1]]]

Some files were not shown because too many files have changed in this diff Show More