1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-20 14:11:35 +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

@@ -5,14 +5,13 @@ Slate works with pure JSON objects. All it requires is that those JSON objects c
```ts ```ts
interface Text { interface Text {
text: string text: string
marks: Mark[]
[key: string]: any [key: string]: any
} }
``` ```
Which means it must have a `text` property with a string of content, and a `marks` property with an array of formatting marks (or an empty array). Which means it must have a `text` property with a string of content.
But other custom properties are also allowed, and completely up to you. This lets you tailor your data to your specific domain and use case, without Slate getting in the way. But **any** other custom properties are also allowed, and completely up to you. This lets you tailor your data to your specific domain and use case, adding whatever formatting logic you'd like, without Slate getting in the way.
This interface-based approach separates Slate from most other rich-text editors which require you to work with their hand-rolled "model" classes, and makes it much easier to reason about. It also means that it avoids startup time penalties related to "initializing" the data model. This interface-based approach separates Slate from most other rich-text editors which require you to work with their hand-rolled "model" classes, and makes it much easier to reason about. It also means that it avoids startup time penalties related to "initializing" the data model.

View File

@@ -16,7 +16,6 @@ const editor = {
children: [ children: [
{ {
text: 'A line of text!', text: 'A line of text!',
marks: [],
}, },
], ],
}, },
@@ -127,18 +126,17 @@ Text nodes are the lowest-level nodes in the tree, containing the text content o
```ts ```ts
interface Text { interface Text {
text: string text: string
marks: Mark[]
[key: string]: any [key: string]: any
} }
``` ```
We'll cover `Mark` objects shortly, but for now you can get an idea for them: For example, a string of bold text:
```js ```js
const text = { const text = {
text: 'A string of bold text', text: 'A string of bold text',
marks: [{ type: 'bold' }], bold: true,
} }
``` ```
Text nodes too can contain any custom properties you want, although it is rarer to need to do that. Text nodes too can contain any custom properties you want, and that's how you implement custom formatting like **bold**, _italic_, `code`, etc.

View File

@@ -20,7 +20,6 @@ const editor = {
children: [ children: [
{ {
text: 'A line of text!', text: 'A line of text!',
marks: [],
}, },
], ],
}, },
@@ -107,7 +106,6 @@ const editor = {
children: [ children: [
{ {
text: 'A line of text!', text: 'A line of text!',
marks: [],
}, },
], ],
}, },

View File

@@ -1,6 +1,6 @@
# Commands # Commands
While editing rich-text content, your users will be doing things like inserting text, deleteing text, splitting paragraphs, adding marks, etc. These edits are expressed using two concepts: commands and operations. While editing rich-text content, your users will be doing things like inserting text, deleteing text, splitting paragraphs, adding formatting, etc. These edits are expressed using two concepts: commands and operations.
Commands are the high-level actions that represent a specific intent of the user. Their interface is simply: Commands are the high-level actions that represent a specific intent of the user. Their interface is simply:
@@ -25,12 +25,11 @@ editor.exec({
}) })
editor.exec({ editor.exec({
type: 'add_mark', type: 'insert_break',
mark: { type: 'bold' },
}) })
``` ```
But you can (and will!) also define your own custom commands that model your domain. For example, you might want to define a `wrap_quote` command, or a `insert_image` command depending on what types of content you allow. But you can (and will!) also define your own custom commands that model your domain. For example, you might want to define a `wrap_quote` command, or a `insert_image` command, or a `format_bold` command depending on what types of content you allow.
Commands always describe an action to be taken as if the user themselves was performing the action. For that reason, they never need to define a location to perform the command, because they always act on the user's current selection. Commands always describe an action to be taken as if the user themselves was performing the action. For that reason, they never need to define a location to perform the command, because they always act on the user's current selection.

View File

@@ -1,49 +0,0 @@
# Formatting: Marks and Decorations
We've already seen how `Element` objects can be extended with custom properties to add semantic meaning to your rich-text documents. But there are other kinds of formatting too.
## `Mark`
Marks are the lowest level of formatting that is applied directly to the text nodes of your document, for things like **bold**, _italic_ and `code`. Their interface is:
```ts
interface Mark {
[key: string]: any
}
```
Which means that they are entirely composed of your domain-specific custom properties. For simple cases, it can suffice to use a `type` string:
```js
const bold = { type: 'bold' }
const italic = { type: 'italic' }
```
There are multiple techniques you might choose to format or style text. You can implement styling based on inlines or marks. Unlike inlines, marks do not affect the structure of the nodes in the document. Marks simply attach themselves to the characters.
Marks may be easier to reason about and manipulate because marks do not affect the structure of the document and are associated to the characters. Marks can be applied to characters no matter how the characters are nested in the document. If you can express it as a `Range`, you can add marks to it. Working with marks instead of inlines does not require you to edit the document's structure, split existing nodes, determine where nodes are in the hierarchy, or other more complex interactions.
When marks are rendered, the characters are grouped into "leaves" of text that each contain the same set of marks applied to them. One disadvantage of marks is that you cannot guarantee how a set of marks will be ordered.
This limitation with respect to the ordering of marks is similar to the DOM, where this is invalid:
```html
<em>t<strong>e</em>x</strong>t
```
Because the elements in the above example do not properly close themselves they are invalid. Instead, you would write the above HTML as follows:
```html
<em>t</em><strong><em>e</em>x</strong>t
```
If you happened to add another overlapping section of `<strike>` to that text, you might have to rearrange the closing tags yet again. Rendering marks in Slate is similar—you can't guarantee that even though a word has one mark applied that that mark will be contiguous, because it depends on how it overlaps with other marks.
Of course, this mark ordering stuff sounds pretty complex. But, you do not have to think about it much, as long as you use marks and inlines for their intended purposes:
- Marks represent **unordered**, character-level formatting.
- Inlines represent **contiguous**, semantic elements in the document.
## Decorations
Decorations are similar to marks, with each one applying to a range of content. However, they are computed at render-time based on the content itself. This is helpful for dynamic formatting like syntax highlighting or search keywords, where changes to the content (or some external data) has the potential to change the formatting.

View File

@@ -17,7 +17,6 @@ editor.apply({
path: [0, 0], path: [0, 0],
node: { node: {
text: 'A line of text!', text: 'A line of text!',
marks: [],
}, },
}) })

View File

@@ -95,8 +95,8 @@ for (const [element, path] of Editor.elements(editor, { at: range })) {
// ... // ...
} }
// Iterate over every mark in every text node in the current selection. // Iterate over every point in every text node in the current selection.
for (const [mark, index, text, path] of Editor.marks(editor)) { for (const [point] of Editor.positions(editor)) {
// ... // ...
} }
``` ```
@@ -110,8 +110,8 @@ Editor.insertNodes(editor, [element], { at: path })
// Split the nodes in half at a specific point. // Split the nodes in half at a specific point.
Editor.splitNodes(editor, { at: point }) Editor.splitNodes(editor, { at: point })
// Add a mark to all the text in a range. // Add a quote format to all the block nodes in the selection.
Editor.addMarks(editor, mark, { at: range }) Editor.setNodes(editor, { type: 'quote' })
``` ```
The editor-specific helpers are the ones you'll use most often when working with Slate editors, so it pays to become very familiar with them. The editor-specific helpers are the ones you'll use most often when working with Slate editors, so it pays to become very familiar with them.

View File

@@ -13,8 +13,7 @@ const withImages = editor => {
editor.exec = command => { editor.exec = command => {
if (command.type === 'insert_image') { if (command.type === 'insert_image') {
const { url } = command const { url } = command
const text = { text: '', marks: [] } const element = { type: 'image', url, children: [{ text: '' }] }
const element = { type: 'image', url, children: [text] }
Editor.insertNodes(editor) Editor.insertNodes(editor)
} else { } else {
exec(command) exec(command)

View File

@@ -2,7 +2,7 @@
One of the best parts of Slate is that it's built with React, so it fits right into your existing application. It doesn't re-invent its own view layer that you have to learn. It tries to keep everything as React-y as possible. One of the best parts of Slate is that it's built with React, so it fits right into your existing application. It doesn't re-invent its own view layer that you have to learn. It tries to keep everything as React-y as possible.
To that end, Slate gives you control over the rendering behavior of your custom elements and custom marks in your rich-text domain. To that end, Slate gives you control over the rendering behavior of your custom nodes and properties in your rich-text domain.
You can define these behaviors by passing "render props" to the top-level `<Editable>` component. You can define these behaviors by passing "render props" to the top-level `<Editable>` component.
@@ -54,24 +54,58 @@ const renderElement = useCallback(props => {
}, []) }, [])
``` ```
The same thing goes for a custom `renderMark` prop: ## Leaves
When text-level formatting is rendered, the characters are grouped into "leaves" of text that each contain the same formatting applied to them.
To customize the rendering of each leaf, you use a custom `renderLeaf` prop:
```jsx ```jsx
const renderMark = useCallback(({ attributes, children, mark }) => { const renderLeaf = useCallback(({ attributes, children, leaf }) => {
switch (mark.type) { return (
case 'bold': <span
return <strong {...attributes}>{children}</strong> {...attributes}
case 'italic': style={{
return <em {...attributes}>{children}</em> fontWeight: leaf.bold ? 'bold' : 'normal',
} fontStyle: leaf.italic ? 'italic' : 'normal',
}}
>
{children}
</span>
)
}, []) }, [])
``` ```
> 🤖 Be aware though that marks aren't guaranteed to be "contiguous". Which means even though a **word** is bolded, it's not guaranteed to render as a single `<strong>` element. If some of its characters are also italic, it might be split up into multiple elements—one `<strong>wo</strong>` and one `<em><strong>rd</strong></em>`. Notice though how we've handled it slightly differently than `renderElement`. Since text formatting texts to be fairly simple, we've opted to ditch the `switch` statement and just toggle on/off a few styles instead. (But there's nothing preventing you from using custom components if you'd like!)
One disadvantage of text-level formatting is that you cannot guarantee that any given format is "contiguous"—meaning that it stays as a single leaf. This limitation with respect to leaves is similar to the DOM, where this is invalid:
```html
<em>t<strong>e</em>x</strong>t
```
Because the elements in the above example do not properly close themselves they are invalid. Instead, you would write the above HTML as follows:
```html
<em>t</em><strong><em>e</em>x</strong>t
```
If you happened to add another overlapping section of `<strike>` to that text, you might have to rearrange the closing tags yet again. Rendering leaves in Slate is similar—you can't guarantee that even though a word has one type of formatting applied to it that that leaf will be contiguous, because it depends on how it overlaps with other formatting.
Of course, this leaf stuff sounds pretty complex. But, you do not have to think about it much, as long as you use text-level formatting and element-level formatting for their intended purposes:
- Text properties are for **non-contiguous**, character-level formatting.
- Element properties are for **contiguous**, semantic elements in the document.
## Decorations
Decorations are another type of text-level formatting. They are similar to regular old custom properties, except each one applies to a `Range` of the document instead of being associated with a given text node.
However, decorations are computed at **render-time** based on the content itself. This is helpful for dynamic formatting like syntax highlighting or search keywords, where changes to the content (or some external data) has the potential to change the formatting.
## Toolbars, Menus, Overlays, and more! ## Toolbars, Menus, Overlays, and more!
In addition to controlling the rendering of elements and marks inside Slate, you can also retrieve the current editor context from inside other components using the `useSlate` hook. In addition to controlling the rendering of nodes inside Slate, you can also retrieve the current editor context from inside other components using the `useSlate` hook.
That way other components can execute commands, query the editor state, or anything else. That way other components can execute commands, query the editor state, or anything else.

View File

@@ -68,9 +68,13 @@ We now use the `beforeinput` event almost exclusively. Instead of having relying
The core history logic has now finally been extracted into a standalone plugin. This makes it much easier for people to implement their own custom history behaviors. And it ensures that plugins have enough control to augment the editor in complex ways, because the history requires it. The core history logic has now finally been extracted into a standalone plugin. This makes it much easier for people to implement their own custom history behaviors. And it ensures that plugins have enough control to augment the editor in complex ways, because the history requires it.
### Mark-less
Marks have been removed the Slate data model. Now that we have the ability to define custom properties right on the nodes themselves, you can model marks as custom properties of text nodes. For example bold can be modelled simply as a `bold: true` property.
### Annotation-less ### Annotation-less
Similarly, annotations have been removed from Slate's core. They can be fully implemented now in userland by defining custom operations and rendering annotated ranges using decorations. But most cases should be using plain marks or plain decorations anyways. There were not that many use cases that benefitted from annotations. Similarly, annotations have been removed from Slate's core. They can be fully implemented now in userland by defining custom operations and rendering annotated ranges using decorations. But most cases should be using custom text node properties or decorations anyways. There were not that many use cases that benefitted from annotations.
## Reductions ## Reductions
@@ -79,23 +83,23 @@ One of the goals was to dramatically simplify a lot of the logic in Slate to mak
To give you a sense for the change in total lines of code: To give you a sense for the change in total lines of code:
``` ```
slate 8,436 -> 4,038 (48%) slate 8,436 -> 3,724 (48%)
slate-react 3,905 -> 715 (18%) slate-react 3,905 -> 627 (18%)
slate-base64-serializer 38 -> 0 slate-base64-serializer 38 -> 0
slate-dev-benchmark 340 -> 0 slate-dev-benchmark 340 -> 0
slate-dev-environment 102 -> 0 slate-dev-environment 102 -> 0
slate-dev-test-utils 44 -> 0 slate-dev-test-utils 44 -> 0
slate-history 0 -> 201 slate-history 0 -> 209
slate-hotkeys 62 -> 0 slate-hotkeys 62 -> 0
slate-html-serializer 253 -> 0 slate-html-serializer 253 -> 0
slate-hyperscript 447 -> 410 slate-hyperscript 447 -> 345
slate-plain-serializer 56 -> 0 slate-plain-serializer 56 -> 0
slate-prop-types 62 -> 0 slate-prop-types 62 -> 0
slate-react-placeholder 62 -> 0 slate-react-placeholder 62 -> 0
slate-schema 0 -> 504 slate-schema 0 -> 439
total 13,807 -> 5,868 (43%) total 13,807 -> 5,344 (39%)
``` ```
It's quite a big difference! And that doesn't even include the dependencies that were shed in the process too. It's quite a big difference! And that doesn't even include the dependencies that were shed in the process too.

View File

@@ -93,7 +93,6 @@ const defaultValue = [
children: [ children: [
{ {
text: 'A line of text in a paragraph.', text: 'A line of text in a paragraph.',
marks: [],
}, },
], ],
}, },

View File

@@ -41,7 +41,7 @@ const App = () => {
} }
``` ```
And now, we'll edit the `onKeyDown` handler to make it so that when you press `control-B`, it will add a "bold" mark to the currently selected text: And now, we'll edit the `onKeyDown` handler to make it so that when you press `control-B`, it will add a `bold` format to the currently selected text:
```js ```js
const App = () => { const App = () => {
@@ -78,10 +78,16 @@ const App = () => {
break break
} }
// When "B" is pressed, add a bold mark to the text. // When "B" is pressed, bold the text in the selection.
case 'b': { case 'b': {
event.preventDefault() event.preventDefault()
Editor.addMarks(editor, { type: 'bold' }) Editor.setNodes(
editor,
{ bold: true },
// Apply it to text nodes, and split the text node up if the
// selection is overlapping only part of it.
{ match: 'text', split: true }
)
break break
} }
} }
@@ -94,18 +100,25 @@ const App = () => {
Okay, so we've got the hotkey handler setup... but! If you happen to now try selecting text and hitting `Ctrl-B`, you won't notice any change. That's because we haven't told Slate how to render a "bold" mark. Okay, so we've got the hotkey handler setup... but! If you happen to now try selecting text and hitting `Ctrl-B`, you won't notice any change. That's because we haven't told Slate how to render a "bold" mark.
For every mark type you want to add to your schema, you need to give Slate a "renderer" for that mark, just like elements. So let's define our `bold` mark: For every format you want to add to your schema, Slate will break up the text content into "leaves", and you need to tell Slate how to read it, just like for elements. So let's define a `Leaf` component:
```js ```js
// Define a React component to render bold text with. // Define a React component to render leaves with bold text.
const BoldMark = props => { const Leaf = props => {
return <strong {...props.attributes}>{props.children}</strong> return (
<span
{...props.attributes}
style={{ fontWeight: leaf.bold ? 'bold' : 'normal' }}
>
{props.children}
</span>
)
} }
``` ```
Pretty familiar, right? Pretty familiar, right?
And now, let's tell Slate about that mark. To do that, we'll pass in the `renderMark` prop to our editor. Also, let's allow our mark to be toggled by changing `addMark` to `toggleMark`. And now, let's tell Slate about that leaf. To do that, we'll pass in the `renderLeaf` prop to our editor. Also, let's allow our formatting to be toggled by adding active-checking logic.
```js ```js
const App = () => { const App = () => {
@@ -119,21 +132,17 @@ const App = () => {
} }
}, []) }, [])
// Define a mark rendering function that is memoized with `useCallback`. // Define a leaf rendering function that is memoized with `useCallback`.
const renderMark = useCallback(props => { const renderLeaf = useCallback(props => {
switch (props.mark.type) { return <Leaf {...props} />
case 'bold': {
return <BoldMark {...props} />
}
}
}, []) }, [])
return ( return (
<Slate editor={editor} defaultValue={defaultValue}> <Slate editor={editor} defaultValue={defaultValue}>
<Editable <Editable
renderElement={renderElement} renderElement={renderElement}
// Pass in the `renderMark` function. // Pass in the `renderLeaf` function.
renderMark={renderMark} renderLeaf={renderLeaf}
onKeyDown={event => { onKeyDown={event => {
if (!event.ctrlKey) { if (!event.ctrlKey) {
return return
@@ -154,7 +163,11 @@ const App = () => {
case 'b': { case 'b': {
event.preventDefault() event.preventDefault()
Editor.addMarks(editor, { type: 'bold' }) Editor.setNodes(
editor,
{ bold: true },
{ match: 'text', split: true }
)
break break
} }
} }
@@ -164,8 +177,15 @@ const App = () => {
) )
} }
const BoldMark = props => { const Leaf = props => {
return <strong {...props.attributes}>{props.children}</strong> return (
<span
{...props.attributes}
style={{ fontWeight: leaf.bold ? 'bold' : 'normal' }}
>
{props.children}
</span>
)
} }
``` ```

View File

@@ -22,19 +22,15 @@ const App = () => {
} }
}, []) }, [])
const renderMark = useCallback(props => { const renderLeaf = useCallback(props => {
switch (props.mark.type) { return <Leaf {...props} />
case 'bold': {
return <BoldMark {...props} />
}
}
}, []) }, [])
return ( return (
<Slate editor={editor} defaultValue={defaultValue}> <Slate editor={editor} defaultValue={defaultValue}>
<Editable <Editable
renderElement={renderElement} renderElement={renderElement}
renderMark={renderMark} renderLeaf={renderLeaf}
onKeyDown={event => { onKeyDown={event => {
if (!event.ctrlKey) { if (!event.ctrlKey) {
return return
@@ -55,7 +51,11 @@ const App = () => {
case 'b': { case 'b': {
event.preventDefault() event.preventDefault()
Editor.addMarks(editor, { type: 'bold' }) Editor.setNodes(
editor,
{ bold: true },
{ match: 'text', split: true }
)
break break
} }
} }
@@ -66,7 +66,7 @@ const App = () => {
} }
``` ```
It has the concept of "code blocks" and "bold marks". But these things are all defined in one-off cases inside the `onKeyDown` handler. If you wanted to reuse that logic elsewhere you'd need to extract it. It has the concept of "code blocks" and "bold formatting". But these things are all defined in one-off cases inside the `onKeyDown` handler. If you wanted to reuse that logic elsewhere you'd need to extract it.
We can instead implement these domain-specific concepts by extending the `editor` object: We can instead implement these domain-specific concepts by extending the `editor` object:
@@ -88,19 +88,15 @@ const App = () => {
} }
}, []) }, [])
const renderMark = useCallback(props => { const renderLeaf = useCallback(props => {
switch (props.mark.type) { return <Leaf {...props} />
case 'bold': {
return <BoldMark {...props} />
}
}
}, []) }, [])
return ( return (
<Slate editor={editor} defaultValue={defaultValue}> <Slate editor={editor} defaultValue={defaultValue}>
<Editable <Editable
renderElement={renderElement} renderElement={renderElement}
renderMark={renderMark} renderLeaf={renderLeaf}
onKeyDown={event => { onKeyDown={event => {
if (!event.ctrlKey) { if (!event.ctrlKey) {
return return
@@ -121,7 +117,11 @@ const App = () => {
case 'b': { case 'b': {
event.preventDefault() event.preventDefault()
Editor.addMarks(editor, [{ type: 'bold' }]) Editor.setNodes(
editor,
{ bold: true },
{ match: 'text', split: true }
)
break break
} }
} }
@@ -141,22 +141,19 @@ const withCustom = editor => {
const { exec } = editor const { exec } = editor
editor.exec = command => { editor.exec = command => {
// Define a command to toggle the bold mark formatting. // Define a command to toggle the bold formatting.
if (command.type === 'toggle_bold_mark') { if (command.type === 'toggle_bold_mark') {
const isActive = CustomEditor.isBoldMarkActive(editor) const isActive = CustomEditor.isBoldMarkActive(editor)
// Delegate to the existing `add_mark` and `remove_mark` commands, so that Editor.setNodes(
// other plugins can override them if they need to still. editor,
editor.exec({ { bold: isActive ? null : true },
type: isActive ? 'remove_mark' : 'add_mark', { match: 'text', split: true }
mark: { type: 'bold' }, )
})
} }
// Define a command to toggle the code block formatting. // Define a command to toggle the code block formatting.
else if (command.type === 'toggle_code_block') { else if (command.type === 'toggle_code_block') {
const isActive = CustomEditor.isCodeBlockActive(editor) const isActive = CustomEditor.isCodeBlockActive(editor)
// There is no `set_nodes` command, so we can transform the editor
// directly using the helper instead.
Editor.setNodes( Editor.setNodes(
editor, editor,
{ type: isActive ? null : 'code' }, { type: isActive ? null : 'code' },
@@ -173,24 +170,24 @@ const withCustom = editor => {
return editor return editor
} }
// Define our own custom set of helpers for common queries. // Define our own custom set of helpers for active-checking queries.
const CustomEditor = { const CustomEditor = {
isBoldMarkActive(editor) { isBoldMarkActive(editor) {
const [mark] = Editor.marks(editor, { const [match] = Editor.nodes(editor, {
match: { type: 'bold' }, match: { bold: true },
mode: 'universal', mode: 'universal',
}) })
return !!mark return !!match
}, },
isCodeBlockActive(editor) { isCodeBlockActive(editor) {
const [node] = Editor.nodes(editor, { const [match] = Editor.nodes(editor, {
match: { type: 'code' }, match: { type: 'code' },
mode: 'highest', mode: 'highest',
}) })
return !!node return !!match
}, },
} }
@@ -205,19 +202,15 @@ const App = () => {
} }
}, []) }, [])
const renderMark = useCallback(props => { const renderLeaf = useCallback(props => {
switch (props.mark.type) { return <Leaf {...props} />
case 'bold': {
return <BoldMark {...props} />
}
}
}, []) }, [])
return ( return (
<Slate editor={editor} defaultValue={defaultValue}> <Slate editor={editor} defaultValue={defaultValue}>
<Editable <Editable
renderElement={renderElement} renderElement={renderElement}
renderMark={renderMark} renderLeaf={renderLeaf}
// Replace the `onKeyDown` logic with our new commands. // Replace the `onKeyDown` logic with our new commands.
onKeyDown={event => { onKeyDown={event => {
if (!event.ctrlKey) { if (!event.ctrlKey) {
@@ -258,12 +251,8 @@ const App = () => {
} }
}, []) }, [])
const renderMark = useCallback(props => { const renderLeaf = useCallback(props => {
switch (props.mark.type) { return <Leaf {...props} />
case 'bold': {
return <BoldMark {...props} />
}
}
}, []) }, [])
return ( return (
@@ -290,7 +279,7 @@ const App = () => {
<Editable <Editable
editor={editor} editor={editor}
renderElement={renderElement} renderElement={renderElement}
renderMark={renderMark} renderLeaf={renderLeaf}
onKeyDown={event => { onKeyDown={event => {
if (!event.ctrlKey) { if (!event.ctrlKey) {
return return

View File

@@ -16,7 +16,6 @@ const defaultValue = [
children: [ children: [
{ {
text: 'A line of text in a paragraph.', text: 'A line of text in a paragraph.',
marks: [],
}, },
], ],
}, },
@@ -44,7 +43,6 @@ const defaultValue = [
children: [ children: [
{ {
text: 'A line of text in a paragraph.', text: 'A line of text in a paragraph.',
marks: [],
}, },
], ],
}, },
@@ -80,7 +78,6 @@ const defaultValue = existingValue || [
children: [ children: [
{ {
text: 'A line of text in a paragraph.', text: 'A line of text in a paragraph.',
marks: [],
}, },
], ],
}, },
@@ -129,7 +126,7 @@ const deserialize = string => {
// Return a value array of children derived by splitting the string. // Return a value array of children derived by splitting the string.
return string.split('\n').map(line => { return string.split('\n').map(line => {
return { return {
children: [{ text: line, marks: [] }], children: [{ text: line }],
} }
}) })
} }

View File

@@ -12,7 +12,7 @@
"fix": "yarn fix:prettier && yarn fix:eslint", "fix": "yarn fix:prettier && yarn fix:eslint",
"fix:eslint": "yarn lint:eslint --fix", "fix:eslint": "yarn lint:eslint --fix",
"fix:prettier": "yarn lint:prettier --write", "fix:prettier": "yarn lint:prettier --write",
"lint": "yarn lint:eslint && yarn lint:prettier", "lint": "yarn lint:eslint",
"lint:eslint": "eslint \"./{packages,site}/**/*.{js,jsx,ts,tsx}\"", "lint:eslint": "eslint \"./{packages,site}/**/*.{js,jsx,ts,tsx}\"",
"lint:prettier": "prettier --list-different \"**/*.{css,md,js,jsx,json,ts,tsx}\"", "lint:prettier": "prettier --list-different \"**/*.{css,md,js,jsx,json,ts,tsx}\"",
"open": "open http://localhost:3000", "open": "open http://localhost:3000",

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

View File

@@ -1,5 +1,5 @@
import isPlainObject from 'is-plain-object' import isPlainObject from 'is-plain-object'
import { Element, Mark } from 'slate' import { Element } from 'slate'
import { import {
createAnchor, createAnchor,
createCursor, createCursor,
@@ -7,7 +7,6 @@ import {
createElement, createElement,
createFocus, createFocus,
createFragment, createFragment,
createMark,
createSelection, createSelection,
createText, createText,
} from './creators' } from './creators'
@@ -23,7 +22,6 @@ const DEFAULT_CREATORS = {
element: createElement, element: createElement,
focus: createFocus, focus: createFocus,
fragment: createFragment, fragment: createFragment,
mark: createMark,
selection: createSelection, selection: createSelection,
text: createText, text: createText,
} }
@@ -54,16 +52,13 @@ const createHyperscript = (
options: { options: {
creators?: HyperscriptCreators creators?: HyperscriptCreators
elements?: HyperscriptShorthands elements?: HyperscriptShorthands
marks?: HyperscriptShorthands
} = {} } = {}
) => { ) => {
const { elements = {}, marks = {} } = options const { elements = {} } = options
const elementCreators = normalizeElements(elements) const elementCreators = normalizeElements(elements)
const markCreators = normalizeMarks(marks)
const creators = { const creators = {
...DEFAULT_CREATORS, ...DEFAULT_CREATORS,
...elementCreators, ...elementCreators,
...markCreators,
...options.creators, ...options.creators,
} }
@@ -132,32 +127,4 @@ const normalizeElements = (elements: HyperscriptShorthands) => {
return creators 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 } 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. * A weak map to hold anchor tokens.
@@ -23,23 +23,17 @@ export class Token {}
*/ */
export class AnchorToken extends Token { export class AnchorToken extends Token {
focused: boolean
marks: Mark[] | null
offset?: number offset?: number
path?: Path path?: Path
constructor( constructor(
props: { props: {
focused?: boolean
marks?: Mark[] | null
offset?: number offset?: number
path?: Path path?: Path
} = {} } = {}
) { ) {
super() super()
const { focused = true, marks = null, offset, path } = props const { offset, path } = props
this.focused = focused
this.marks = marks
this.offset = offset this.offset = offset
this.path = path this.path = path
} }
@@ -50,23 +44,17 @@ export class AnchorToken extends Token {
*/ */
export class FocusToken extends Token { export class FocusToken extends Token {
focused: boolean
marks: Mark[] | null
offset?: number offset?: number
path?: Path path?: Path
constructor( constructor(
props: { props: {
focused?: boolean
marks?: Mark[] | null
offset?: number offset?: number
path?: Path path?: Path
} = {} } = {}
) { ) {
super() super()
const { focused = true, marks = null, offset, path } = props const { offset, path } = props
this.focused = focused
this.marks = marks
this.offset = offset this.offset = offset
this.path = path this.path = path
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,6 @@ export const output = {
children: [ children: [
{ {
text: 'one', 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: [ children: [
{ {
text: '', text: '',
marks: [],
}, },
], ],
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,12 +13,7 @@ import {
NODE_TO_INDEX, NODE_TO_INDEX,
KEY_TO_ELEMENT, KEY_TO_ELEMENT,
} from '../utils/weak-maps' } from '../utils/weak-maps'
import { import { RenderElementProps, RenderLeafProps } from './editable'
RenderDecorationProps,
RenderElementProps,
RenderMarkProps,
} from './editable'
import { isRangeListEqual } from '../utils/leaf'
/** /**
* Element. * Element.
@@ -28,18 +23,16 @@ const Element = (props: {
decorate: (entry: NodeEntry) => Range[] decorate: (entry: NodeEntry) => Range[]
decorations: Range[] decorations: Range[]
element: SlateElement element: SlateElement
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
renderElement?: (props: RenderElementProps) => JSX.Element renderElement?: (props: RenderElementProps) => JSX.Element
renderMark?: (props: RenderMarkProps) => JSX.Element renderLeaf?: (props: RenderLeafProps) => JSX.Element
selection: Range | null selection: Range | null
}) => { }) => {
const { const {
decorate, decorate,
decorations, decorations,
element, element,
renderDecoration,
renderElement = (p: RenderElementProps) => <DefaultElement {...p} />, renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,
renderMark, renderLeaf,
selection, selection,
} = props } = props
const ref = useRef<HTMLElement>(null) const ref = useRef<HTMLElement>(null)
@@ -53,9 +46,8 @@ const Element = (props: {
decorate={decorate} decorate={decorate}
decorations={decorations} decorations={decorations}
node={element} node={element}
renderDecoration={renderDecoration}
renderElement={renderElement} renderElement={renderElement}
renderMark={renderMark} renderLeaf={renderLeaf}
selection={selection} selection={selection}
/> />
) )
@@ -141,9 +133,8 @@ const MemoizedElement = React.memo(Element, (prev, next) => {
return ( return (
prev.decorate === next.decorate && prev.decorate === next.decorate &&
prev.element === next.element && prev.element === next.element &&
prev.renderDecoration === next.renderDecoration &&
prev.renderElement === next.renderElement && prev.renderElement === next.renderElement &&
prev.renderMark === next.renderMark && prev.renderLeaf === next.renderLeaf &&
isRangeListEqual(prev.decorations, next.decorations) && isRangeListEqual(prev.decorations, next.decorations) &&
(prev.selection === next.selection || (prev.selection === next.selection ||
(!!prev.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 export default MemoizedElement

View File

@@ -2,9 +2,8 @@ import React from 'react'
import { Text, Element } from 'slate' import { Text, Element } from 'slate'
import String from './string' import String from './string'
import { Leaf as SlateLeaf } from '../utils/leaf'
import { PLACEHOLDER_SYMBOL } from '../utils/weak-maps' 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. * Individual leaves in a text node with unique formatting.
@@ -12,10 +11,9 @@ import { RenderDecorationProps, RenderMarkProps } from './editable'
const Leaf = (props: { const Leaf = (props: {
isLast: boolean isLast: boolean
leaf: SlateLeaf leaf: Text
parent: Element parent: Element
renderDecoration?: (props: RenderDecorationProps) => JSX.Element renderLeaf?: (props: RenderLeafProps) => JSX.Element
renderMark?: (props: RenderMarkProps) => JSX.Element
text: Text text: Text
}) => { }) => {
const { const {
@@ -23,117 +21,64 @@ const Leaf = (props: {
isLast, isLast,
text, text,
parent, parent,
renderDecoration = (props: RenderDecorationProps) => ( renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
<DefaultDecoration {...props} />
),
renderMark = (props: RenderMarkProps) => <DefaultMark {...props} />,
} = props } = props
let children = ( let children = (
<String isLast={isLast} leaf={leaf} parent={parent} text={text} /> <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 // COMPAT: Having the `data-` attributes on these leaf elements ensures that
// in certain misbehaving browsers they aren't weirdly cloned/destroyed by // in certain misbehaving browsers they aren't weirdly cloned/destroyed by
// contenteditable behaviors. (2019/05/08) // contenteditable behaviors. (2019/05/08)
for (const mark of leaf.marks) { const attributes: {
const ret = renderMark({ 'data-slate-leaf': true
children, } = {
leaf, 'data-slate-leaf': true,
mark,
text,
attributes: {
'data-slate-mark': true,
},
})
if (ret) {
children = ret
}
} }
for (const decoration of leaf.decorations) { return renderLeaf({ attributes, children, leaf, text })
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>
} }
const MemoizedLeaf = React.memo(Leaf, (prev, next) => { const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
return ( return (
next.parent === prev.parent && next.parent === prev.parent &&
next.isLast === prev.isLast && next.isLast === prev.isLast &&
next.renderDecoration === prev.renderDecoration && next.renderLeaf === prev.renderLeaf &&
next.renderMark === prev.renderMark &&
next.text === prev.text && 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 const { attributes, children } = props
return <span {...attributes}>{children}</span> 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 export default MemoizedLeaf

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
export * from './components/editable' export * from './components/editable'
export { DefaultElement } from './components/element' 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-editor'
export * from './hooks/use-focused' export * from './hooks/use-focused'
export * from './hooks/use-read-only' 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' 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 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 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 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. * 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_DRAGGING: WeakMap<Editor, boolean> = new WeakMap()
export const IS_CLICKING: WeakMap<Editor, boolean> = new WeakMap() export const IS_CLICKING: WeakMap<Editor, boolean> = new WeakMap()
/** export const PLACEHOLDER_SYMBOL = (Symbol('placeholder') as unknown) as string
* Symbols.
*/
export const PLACEHOLDER_SYMBOL = Symbol('placeholder')

View File

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

View File

@@ -2,47 +2,14 @@ import {
NodeEntry, NodeEntry,
Node, Node,
Text, Text,
Mark,
Editor, Editor,
MarkEntry,
AncestorEntry, AncestorEntry,
Descendant, Descendant,
DescendantEntry, DescendantEntry,
} from 'slate' } from 'slate'
import { MarkError, NodeError } from './errors' import { NodeError } from './errors'
import { NodeRule, MarkRule, ChildValidation } from './rules' import { NodeRule, 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,
}
}
}
}
}
/** /**
* Check a node object. * 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) { if ('text' in v && v.text != null) {
const text = Node.text(node) 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 { export interface ChildInvalidError {
code: 'child_invalid' code: 'child_invalid'
@@ -56,23 +56,6 @@ export interface NodeTextInvalidError {
text: string 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 { export interface ParentInvalidError {
code: 'parent_invalid' code: 'parent_invalid'
node: Ancestor node: Ancestor
@@ -86,19 +69,16 @@ export interface PreviousSiblingInvalidError {
path: Path path: Path
} }
export type MarkError = MarkPropertyInvalidError
export type NodeError = export type NodeError =
| ChildInvalidError | ChildInvalidError
| ChildMaxInvalidError | ChildMaxInvalidError
| ChildMinInvalidError | ChildMinInvalidError
| FirstChildInvalidError | FirstChildInvalidError
| LastChildInvalidError | LastChildInvalidError
| MarkInvalidError
| NextSiblingInvalidError | NextSiblingInvalidError
| NodePropertyInvalidError | NodePropertyInvalidError
| NodeTextInvalidError | NodeTextInvalidError
| ParentInvalidError | ParentInvalidError
| PreviousSiblingInvalidError | PreviousSiblingInvalidError
export type SchemaError = MarkError | NodeError export type SchemaError = NodeError

View File

@@ -1,16 +1,5 @@
import { Editor, NodeMatch, MarkMatch } from 'slate' import { Editor, NodeMatch } from 'slate'
import { NodeError, MarkError } from './errors' import { NodeError } from './errors'
export interface MarkValidation {
properties?: Record<string, any>
}
export interface MarkRule {
for: 'mark'
match: MarkMatch
validate: MarkValidation
normalize: (editor: Editor, error: MarkError) => void
}
export interface ChildValidation { export interface ChildValidation {
match?: NodeMatch match?: NodeMatch
@@ -22,7 +11,6 @@ export interface NodeValidation {
children?: ChildValidation[] children?: ChildValidation[]
first?: NodeMatch first?: NodeMatch
last?: NodeMatch last?: NodeMatch
marks?: MarkMatch
next?: NodeMatch next?: NodeMatch
parent?: NodeMatch parent?: NodeMatch
previous?: NodeMatch previous?: NodeMatch
@@ -37,4 +25,4 @@ export interface NodeRule {
normalize: (editor: Editor, error: NodeError) => void 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 { Editor, Text, NodeEntry } from 'slate'
import { NodeRule, SchemaRule, MarkRule } from './rules' import { NodeRule, SchemaRule } from './rules'
import { NodeError } from './errors' import { NodeError } from './errors'
import { checkNode, checkAncestor } from './checkers' import { checkNode, checkAncestor } from './checkers'
@@ -14,23 +14,16 @@ export const withSchema = (
rules: SchemaRule[] = [] rules: SchemaRule[] = []
): Editor => { ): Editor => {
const { normalizeNode } = editor const { normalizeNode } = editor
const markRules: MarkRule[] = [] const nodeRules: NodeRule[] = rules
const nodeRules: NodeRule[] = []
const parentRules: NodeRule[] = [] const parentRules: NodeRule[] = []
for (const rule of rules) { for (const rule of rules) {
if (rule.for === 'mark') { if (
markRules.push(rule) 'parent' in rule.validate ||
} else { 'next' in rule.validate ||
nodeRules.push(rule) 'previous' in rule.validate
) {
if ( parentRules.push(rule)
'parent' in rule.validate ||
'next' in rule.validate ||
'previous' in rule.validate
) {
parentRules.push(rule)
}
} }
} }
@@ -136,12 +129,6 @@ export const withSchema = (
break break
} }
case 'mark_invalid': {
const { mark, path } = error
Editor.removeMarks(editor, [mark], { at: path })
break
}
case 'parent_invalid': { case 'parent_invalid': {
const { path, index } = error const { path, index } = error
const childPath = path.concat(index) 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)) { if (Command.isCoreCommand(command)) {
switch (command.type) { switch (command.type) {
case 'add_mark': {
Editor.addMarks(editor, command.mark)
break
}
case 'delete_backward': { case 'delete_backward': {
if (selection && Range.isCollapsed(selection)) { if (selection && Range.isCollapsed(selection)) {
Editor.delete(editor, { unit: command.unit, reverse: true }) Editor.delete(editor, { unit: command.unit, reverse: true })
@@ -136,11 +131,6 @@ export const createEditor = (): Editor => {
Editor.insertText(editor, command.text) Editor.insertText(editor, command.text)
break 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. // Ensure that block and inline nodes have at least one text child.
if (Element.isElement(node) && node.children.length === 0) { if (Element.isElement(node) && node.children.length === 0) {
const child = { text: '', marks: [] } const child = { text: '' }
Editor.insertNodes(editor, child, { at: path.concat(0) }) Editor.insertNodes(editor, child, { at: path.concat(0) })
return return
} }
@@ -194,23 +184,23 @@ export const createEditor = (): Editor => {
// Ensure that inline nodes are surrounded by text nodes. // Ensure that inline nodes are surrounded by text nodes.
if (editor.isInline(child)) { if (editor.isInline(child)) {
if (prev == null || !Text.isText(prev)) { if (prev == null || !Text.isText(prev)) {
const newChild = { text: '', marks: [] } const newChild = { text: '' }
Editor.insertNodes(editor, newChild, { at: path.concat(n) }) Editor.insertNodes(editor, newChild, { at: path.concat(n) })
n++ n++
continue continue
} }
if (isLast) { if (isLast) {
const newChild = { text: '', marks: [] } const newChild = { text: '' }
Editor.insertNodes(editor, newChild, { at: path.concat(n + 1) }) Editor.insertNodes(editor, newChild, { at: path.concat(n + 1) })
n++ n++
continue continue
} }
} }
} else { } 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 (prev != null && Text.isText(prev)) {
if (Text.matches(child, prev)) { if (Text.equals(child, prev, { loose: true })) {
Editor.mergeNodes(editor, { at: path.concat(n) }) Editor.mergeNodes(editor, { at: path.concat(n) })
n-- n--
continue continue
@@ -238,11 +228,8 @@ export const createEditor = (): Editor => {
const getDirtyPaths = (op: Operation) => { const getDirtyPaths = (op: Operation) => {
switch (op.type) { switch (op.type) {
case 'add_mark':
case 'insert_text': case 'insert_text':
case 'remove_mark':
case 'remove_text': case 'remove_text':
case 'set_mark':
case 'set_node': { case 'set_node': {
const { path } = op const { path } = op
return Path.levels(path) return Path.levels(path)

View File

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

View File

@@ -1,5 +1,5 @@
import isPlainObject from 'is-plain-object' 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. * `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' 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. * Check if a value is a `CoreCommand` object.
*/ */
isCoreCommand(value: any): value is CoreCommand { isCoreCommand(value: any): value is CoreCommand {
return ( return (
Command.isAddMarkCommand(value) ||
Command.isDeleteBackwardCommand(value) || Command.isDeleteBackwardCommand(value) ||
Command.isDeleteForwardCommand(value) || Command.isDeleteForwardCommand(value) ||
Command.isDeleteFragmentCommand(value) || Command.isDeleteFragmentCommand(value) ||
Command.isInsertTextCommand(value) || Command.isInsertTextCommand(value) ||
Command.isInsertFragmentCommand(value) || Command.isInsertFragmentCommand(value) ||
Command.isInsertBreakCommand(value) || Command.isInsertBreakCommand(value)
Command.isRemoveMarkCommand(value)
) )
}, },
@@ -124,27 +110,6 @@ export const Command = {
typeof value.text === 'string' 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 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 * The `CoreCommand` union is a set of all of the commands that are recognized
* by Slate's "core" out of the box. * by Slate's "core" out of the box.
*/ */
export type CoreCommand = export type CoreCommand =
| AddMarkCommand
| DeleteBackwardCommand | DeleteBackwardCommand
| DeleteForwardCommand | DeleteForwardCommand
| DeleteFragmentCommand | DeleteFragmentCommand
@@ -233,4 +188,3 @@ export type CoreCommand =
| InsertFragmentCommand | InsertFragmentCommand
| InsertNodeCommand | InsertNodeCommand
| InsertTextCommand | InsertTextCommand
| RemoveMarkCommand

View File

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

View File

@@ -9,8 +9,6 @@ import {
Element, Element,
ElementEntry, ElementEntry,
Location, Location,
Mark,
MarkEntry,
Node, Node,
NodeEntry, NodeEntry,
NodeMatch, NodeMatch,
@@ -21,46 +19,8 @@ import {
Text, Text,
TextEntry, TextEntry,
} from '../../..' } from '../../..'
import { MarkMatch } from '../../mark'
export const LocationQueries = { 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. * Get the point after a location.
*/ */
@@ -305,116 +265,6 @@ export const LocationQueries = {
yield* levels 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. * 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 { import {
Node, Node,
Editor, Editor,
Mark,
Range, Range,
Point, Point,
Text, Text,
@@ -68,17 +67,6 @@ export const GeneralTransforms = {
let selection = editor.selection && createDraft(editor.selection) let selection = editor.selection && createDraft(editor.selection)
switch (op.type) { 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': { case 'insert_node': {
const { path, node } = op const { path, node } = op
const parent = Node.parent(editor, path) const parent = Node.parent(editor, path)
@@ -174,20 +162,6 @@ export const GeneralTransforms = {
break 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': { case 'remove_node': {
const { path } = op const { path } = op
const index = path[path.length - 1] const index = path[path.length - 1]
@@ -238,20 +212,6 @@ export const GeneralTransforms = {
break 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': { case 'set_node': {
const { path, newProperties } = op const { path, newProperties } = op
@@ -260,7 +220,21 @@ export const GeneralTransforms = {
} }
const node = Node.get(editor, path) 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 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 // Ensure that the nodes are equivalent, and figure out what the position
// and extra properties of the merge will be. // and extra properties of the merge will be.
if (Text.isText(node) && Text.isText(prevNode)) { if (Text.isText(node) && Text.isText(prevNode)) {
const { text, marks, ...rest } = node const { text, ...rest } = node
position = prevNode.text.length position = prevNode.text.length
properties = rest as Partial<Text> properties = rest as Partial<Text>
} else if (Element.isElement(node) && Element.isElement(prevNode)) { } 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( setNodes(
@@ -422,12 +422,14 @@ export const NodeTransforms = {
options: { options: {
at?: Location at?: Location
match?: NodeMatch match?: NodeMatch
mode?: 'all' | 'highest'
hanging?: boolean hanging?: boolean
split?: boolean
} = {} } = {}
) { ) {
Editor.withoutNormalizing(editor, () => { Editor.withoutNormalizing(editor, () => {
let { match, at = editor.selection } = options let { match, at = editor.selection } = options
const { hanging = false } = options const { hanging = false, mode = 'highest', split = false } = options
if (match == null) { if (match == null) {
if (Path.isPath(at)) { if (Path.isPath(at)) {
@@ -446,21 +448,29 @@ export const NodeTransforms = {
at = Editor.unhangRange(editor, at) at = Editor.unhangRange(editor, at)
} }
for (const [node, path] of Editor.nodes(editor, { if (split && Range.isRange(at)) {
at, const rangeRef = Editor.rangeRef(editor, at, { affinity: 'inward' })
match, const [start, end] = Range.edges(at)
mode: 'highest', 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 properties: Partial<Node> = {}
const newProperties: 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) { for (const k in props) {
if ( if (k === 'children' || k === 'text') {
k === 'marks' ||
k === 'children' ||
k === 'selection' ||
k === 'text'
) {
continue continue
} }
@@ -540,7 +550,7 @@ export const NodeTransforms = {
let after = Editor.after(editor, voidPath) let after = Editor.after(editor, voidPath)
if (!after) { if (!after) {
const text = { text: '', marks: [] } const text = { text: '' }
const afterPath = Path.next(voidPath) const afterPath = Path.next(voidPath)
Editor.insertNodes(editor, text, { at: afterPath }) Editor.insertNodes(editor, text, { at: afterPath })
after = Editor.point(editor, afterPath)! after = Editor.point(editor, afterPath)!
@@ -581,7 +591,7 @@ export const NodeTransforms = {
if (always || !beforeRef || !Editor.isEdge(editor, point, path)) { if (always || !beforeRef || !Editor.isEdge(editor, point, path)) {
split = true split = true
const { text, marks, children, ...properties } = node const { text, children, ...properties } = node
editor.apply({ editor.apply({
type: 'split_node', type: 'split_node',
path, 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 { produce } from 'immer'
import { import { Editor, Element, ElementEntry, Path, Range, Text, TextEntry } from '..'
Editor,
Element,
ElementEntry,
MarkEntry,
Path,
Range,
Text,
TextEntry,
} from '..'
/** /**
* The `Node` union type represents all of the different types of nodes that * 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 * 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 * 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' import isPlainObject from 'is-plain-object'
type AddMarkOperation = {
type: 'add_mark'
path: Path
mark: Mark
[key: string]: any
}
type InsertNodeOperation = { type InsertNodeOperation = {
type: 'insert_node' type: 'insert_node'
path: Path path: Path
@@ -39,13 +32,6 @@ type MoveNodeOperation = {
[key: string]: any [key: string]: any
} }
type RemoveMarkOperation = {
type: 'remove_mark'
path: Path
mark: Mark
[key: string]: any
}
type RemoveNodeOperation = { type RemoveNodeOperation = {
type: 'remove_node' type: 'remove_node'
path: Path path: Path
@@ -61,14 +47,6 @@ type RemoveTextOperation = {
[key: string]: any [key: string]: any
} }
type SetMarkOperation = {
type: 'set_mark'
path: Path
properties: Partial<Mark>
newProperties: Partial<Mark>
[key: string]: any
}
type SetNodeOperation = { type SetNodeOperation = {
type: 'set_node' type: 'set_node'
path: Path path: Path
@@ -113,11 +91,7 @@ type SplitNodeOperation = {
* collaboration, and other features. * collaboration, and other features.
*/ */
type Operation = type Operation = NodeOperation | SelectionOperation | TextOperation
| NodeOperation
| MarkOperation
| SelectionOperation
| TextOperation
type NodeOperation = type NodeOperation =
| InsertNodeOperation | InsertNodeOperation
@@ -127,8 +101,6 @@ type NodeOperation =
| SetNodeOperation | SetNodeOperation
| SplitNodeOperation | SplitNodeOperation
type MarkOperation = AddMarkOperation | RemoveMarkOperation | SetMarkOperation
type SelectionOperation = SetSelectionOperation type SelectionOperation = SetSelectionOperation
type TextOperation = InsertTextOperation | RemoveTextOperation type TextOperation = InsertTextOperation | RemoveTextOperation
@@ -142,14 +114,6 @@ const Operation = {
return Operation.isOperation(value) && value.type.endsWith('_node') 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. * Check of a value is an `Operation` object.
*/ */
@@ -160,10 +124,6 @@ const Operation = {
} }
switch (value.type) { switch (value.type) {
case 'add_mark': {
return Path.isPath(value.path) && Mark.isMark(value.mark)
}
case 'insert_node': { case 'insert_node': {
return Path.isPath(value.path) && Node.isNode(value.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) return Path.isPath(value.path) && Path.isPath(value.newPath)
} }
case 'remove_mark': {
return Path.isPath(value.path) && Mark.isMark(value.mark)
}
case 'remove_node': { case 'remove_node': {
return Path.isPath(value.path) && Node.isNode(value.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': { case 'set_node': {
return ( return (
Path.isPath(value.path) && Path.isPath(value.path) &&
@@ -285,10 +233,6 @@ const Operation = {
inverse(op: Operation): Operation { inverse(op: Operation): Operation {
switch (op.type) { switch (op.type) {
case 'add_mark': {
return { ...op, type: 'remove_mark' }
}
case 'insert_node': { case 'insert_node': {
return { ...op, type: 'remove_node' } return { ...op, type: 'remove_node' }
} }
@@ -317,10 +261,6 @@ const Operation = {
return { ...op, path: inversePath, newPath: inverseNewPath } return { ...op, path: inversePath, newPath: inverseNewPath }
} }
case 'remove_mark': {
return { ...op, type: 'add_mark' }
}
case 'remove_node': { case 'remove_node': {
return { ...op, type: 'insert_node' } return { ...op, type: 'insert_node' }
} }
@@ -329,7 +269,6 @@ const Operation = {
return { ...op, type: 'insert_text' } return { ...op, type: 'insert_text' }
} }
case 'set_mark':
case 'set_node': { case 'set_node': {
const { properties, newProperties } = op const { properties, newProperties } = op
return { ...op, properties: newProperties, newProperties: properties } return { ...op, properties: newProperties, newProperties: properties }
@@ -363,15 +302,12 @@ const Operation = {
} }
export { export {
AddMarkOperation,
InsertNodeOperation, InsertNodeOperation,
InsertTextOperation, InsertTextOperation,
MergeNodeOperation, MergeNodeOperation,
MoveNodeOperation, MoveNodeOperation,
RemoveMarkOperation,
RemoveNodeOperation, RemoveNodeOperation,
RemoveTextOperation, RemoveTextOperation,
SetMarkOperation,
SetNodeOperation, SetNodeOperation,
SetSelectionOperation, SetSelectionOperation,
SplitNodeOperation, SplitNodeOperation,

View File

@@ -1,29 +1,58 @@
import isPlainObject from 'is-plain-object' 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 * `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 * Slate document along with any formatting properties. They are always leaf
* the document tree as they cannot contain any children. * nodes in the document tree as they cannot contain any children.
*/ */
export interface Text { export interface Text {
text: string text: string
marks: Mark[]
[key: string]: any [key: string]: any
} }
export const Text = { 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. * Check if a value implements the `Text` interface.
*/ */
isText(value: any): value is Text { isText(value: any): value is Text {
return ( return isPlainObject(value) && typeof value.text === 'string'
isPlainObject(value) &&
typeof value.text === 'string' &&
Array.isArray(value.marks)
)
}, },
/** /**
@@ -38,8 +67,7 @@ export const Text = {
* Check if an text matches set of properties. * Check if an text matches set of properties.
* *
* Note: this is for matching custom properties, and it does not ensure that * 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 * the `text` property are two nodes equal.
* will ensure that the set of marks is exactly equal.
*/ */
matches(text: Text, props: Partial<Text>): boolean { matches(text: Text, props: Partial<Text>): boolean {
@@ -48,30 +76,6 @@ export const Text = {
continue 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]) { if (text[key] !== props[key]) {
return false return false
} }

View File

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

View File

@@ -3,7 +3,6 @@ import { Element } from 'slate'
export const input = [ export const input = [
{ {
text: '', 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

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