mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-02-01 13:18:29 +01:00
36ed4397d8
#### Is this adding or improving a _feature_ or fixing a _bug_? Debt. #### What's the new behavior? This removes almost all existing deprecations from previous API changes, to save on filesize and reduce complexity in the codebase going forward. It also changes from using the `slate-dev-logger` to using the Facebook-inspired `slate-dev-warning` which can be compiled out of production builds with [`babel-plugin-dev-expression`](https://github.com/4Catalyzer/babel-plugin-dev-expression) to save even further on file size. The only deprecations it keeps are in the `fromJSON` methods for data model changes like `.kind` and `.leaves` which may still may not have been migrated in databases, since this is a bigger pain point. #### Have you checked that...? * [x] The new code matches the existing patterns and styles. * [x] The tests pass with `yarn test`. * [x] The linter passes with `yarn lint`. (Fix errors with `yarn prettier`.) * [x] The relevant examples still work. (Run examples with `yarn watch`.) #### Does this fix any issues or need any specific reviewers? Fixes: #1922 Fixes: #2105 Fixes: #646 Fixes: #2109 Fixes: #2107 Fixes: #2018
431 lines
9.1 KiB
JavaScript
431 lines
9.1 KiB
JavaScript
import React from 'react'
|
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
import typeOf from 'type-of'
|
|
import { Node, Value } from 'slate'
|
|
import { Record } from 'immutable'
|
|
|
|
/**
|
|
* String.
|
|
*
|
|
* @type {String}
|
|
*/
|
|
|
|
const String = new Record({
|
|
object: 'string',
|
|
text: '',
|
|
})
|
|
|
|
/**
|
|
* A rule to (de)serialize text nodes. This is automatically added to the HTML
|
|
* serializer so that users don't have to worry about text-level serialization.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
|
|
const TEXT_RULE = {
|
|
deserialize(el) {
|
|
if (el.tagName && el.tagName.toLowerCase() === 'br') {
|
|
return {
|
|
object: 'text',
|
|
leaves: [
|
|
{
|
|
object: 'leaf',
|
|
text: '\n',
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
if (el.nodeName == '#text') {
|
|
if (el.nodeValue && el.nodeValue.match(/<!--.*?-->/)) return
|
|
|
|
return {
|
|
object: 'text',
|
|
leaves: [
|
|
{
|
|
object: 'leaf',
|
|
text: el.nodeValue,
|
|
},
|
|
],
|
|
}
|
|
}
|
|
},
|
|
|
|
serialize(obj, children) {
|
|
if (obj.object === 'string') {
|
|
return children.split('\n').reduce((array, text, i) => {
|
|
if (i != 0) array.push(<br key={i} />)
|
|
array.push(text)
|
|
return array
|
|
}, [])
|
|
}
|
|
},
|
|
}
|
|
|
|
/**
|
|
* A default `parseHtml` function that returns the `<body>` using `DOMParser`.
|
|
*
|
|
* @param {String} html
|
|
* @return {Object}
|
|
*/
|
|
|
|
function defaultParseHtml(html) {
|
|
if (typeof DOMParser === 'undefined') {
|
|
throw new Error(
|
|
'The native `DOMParser` global which the `Html` serializer uses by default is not present in this environment. You must supply the `options.parseHtml` function instead.'
|
|
)
|
|
}
|
|
|
|
const parsed = new DOMParser().parseFromString(html, 'text/html')
|
|
const { body } = parsed
|
|
// COMPAT: in IE 11 body is null if html is an empty string
|
|
return body || window.document.createElement('body')
|
|
}
|
|
|
|
/**
|
|
* HTML serializer.
|
|
*
|
|
* @type {Html}
|
|
*/
|
|
|
|
class Html {
|
|
/**
|
|
* Create a new serializer with `rules`.
|
|
*
|
|
* @param {Object} options
|
|
* @property {Array} rules
|
|
* @property {String|Object|Block} defaultBlock
|
|
* @property {Function} parseHtml
|
|
*/
|
|
|
|
constructor(options = {}) {
|
|
let {
|
|
defaultBlock = 'paragraph',
|
|
parseHtml = defaultParseHtml,
|
|
rules = [],
|
|
} = options
|
|
|
|
defaultBlock = Node.createProperties(defaultBlock)
|
|
|
|
this.rules = [...rules, TEXT_RULE]
|
|
this.defaultBlock = defaultBlock
|
|
this.parseHtml = parseHtml
|
|
}
|
|
|
|
/**
|
|
* Deserialize pasted HTML.
|
|
*
|
|
* @param {String} html
|
|
* @param {Object} options
|
|
* @property {Boolean} toRaw
|
|
* @return {Value}
|
|
*/
|
|
|
|
deserialize = (html, options = {}) => {
|
|
const { toJSON = false } = options
|
|
const { defaultBlock, parseHtml } = this
|
|
const fragment = parseHtml(html)
|
|
const children = Array.from(fragment.childNodes)
|
|
let nodes = this.deserializeElements(children)
|
|
|
|
// COMPAT: ensure that all top-level inline nodes are wrapped into a block.
|
|
nodes = nodes.reduce((memo, node, i, original) => {
|
|
if (node.object == 'block') {
|
|
memo.push(node)
|
|
return memo
|
|
}
|
|
|
|
if (i > 0 && original[i - 1].object != 'block') {
|
|
const block = memo[memo.length - 1]
|
|
block.nodes.push(node)
|
|
return memo
|
|
}
|
|
|
|
const block = {
|
|
object: 'block',
|
|
data: {},
|
|
...defaultBlock,
|
|
nodes: [node],
|
|
}
|
|
|
|
memo.push(block)
|
|
return memo
|
|
}, [])
|
|
|
|
// TODO: pretty sure this is no longer needed.
|
|
if (nodes.length == 0) {
|
|
nodes = [
|
|
{
|
|
object: 'block',
|
|
data: {},
|
|
...defaultBlock,
|
|
nodes: [
|
|
{
|
|
object: 'text',
|
|
leaves: [
|
|
{
|
|
object: 'leaf',
|
|
text: '',
|
|
marks: [],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]
|
|
}
|
|
|
|
const json = {
|
|
object: 'value',
|
|
document: {
|
|
object: 'document',
|
|
data: {},
|
|
nodes,
|
|
},
|
|
}
|
|
|
|
const ret = toJSON ? json : Value.fromJSON(json)
|
|
return ret
|
|
}
|
|
|
|
/**
|
|
* Deserialize an array of DOM elements.
|
|
*
|
|
* @param {Array} elements
|
|
* @return {Array}
|
|
*/
|
|
|
|
deserializeElements = (elements = []) => {
|
|
let nodes = []
|
|
|
|
elements.filter(this.cruftNewline).forEach(element => {
|
|
const node = this.deserializeElement(element)
|
|
|
|
switch (typeOf(node)) {
|
|
case 'array':
|
|
nodes = nodes.concat(node)
|
|
break
|
|
case 'object':
|
|
nodes.push(node)
|
|
break
|
|
}
|
|
})
|
|
|
|
return nodes
|
|
}
|
|
|
|
/**
|
|
* Deserialize a DOM element.
|
|
*
|
|
* @param {Object} element
|
|
* @return {Any}
|
|
*/
|
|
|
|
deserializeElement = element => {
|
|
let node
|
|
|
|
if (!element.tagName) {
|
|
element.tagName = ''
|
|
}
|
|
|
|
const next = elements => {
|
|
if (Object.prototype.toString.call(elements) == '[object NodeList]') {
|
|
elements = Array.from(elements)
|
|
}
|
|
|
|
switch (typeOf(elements)) {
|
|
case 'array':
|
|
return this.deserializeElements(elements)
|
|
case 'object':
|
|
return this.deserializeElement(elements)
|
|
case 'null':
|
|
case 'undefined':
|
|
return
|
|
default:
|
|
throw new Error(
|
|
`The \`next\` argument was called with invalid children: "${elements}".`
|
|
)
|
|
}
|
|
}
|
|
|
|
for (const rule of this.rules) {
|
|
if (!rule.deserialize) continue
|
|
const ret = rule.deserialize(element, next)
|
|
const type = typeOf(ret)
|
|
|
|
if (
|
|
type != 'array' &&
|
|
type != 'object' &&
|
|
type != 'null' &&
|
|
type != 'undefined'
|
|
) {
|
|
throw new Error(
|
|
`A rule returned an invalid deserialized representation: "${node}".`
|
|
)
|
|
}
|
|
|
|
if (ret === undefined) {
|
|
continue
|
|
} else if (ret === null) {
|
|
return null
|
|
} else if (ret.object == 'mark') {
|
|
node = this.deserializeMark(ret)
|
|
} else {
|
|
node = ret
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
return node || next(element.childNodes)
|
|
}
|
|
|
|
/**
|
|
* Deserialize a `mark` object.
|
|
*
|
|
* @param {Object} mark
|
|
* @return {Array}
|
|
*/
|
|
|
|
deserializeMark = mark => {
|
|
const { type, data } = mark
|
|
|
|
const applyMark = node => {
|
|
if (node.object == 'mark') {
|
|
return this.deserializeMark(node)
|
|
} else if (node.object == 'text') {
|
|
node.leaves = node.leaves.map(leaf => {
|
|
leaf.marks = leaf.marks || []
|
|
leaf.marks.push({ type, data })
|
|
return leaf
|
|
})
|
|
} else if (node.nodes) {
|
|
node.nodes = node.nodes.map(applyMark)
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
return mark.nodes.reduce((nodes, node) => {
|
|
const ret = applyMark(node)
|
|
if (Array.isArray(ret)) return nodes.concat(ret)
|
|
nodes.push(ret)
|
|
return nodes
|
|
}, [])
|
|
}
|
|
|
|
/**
|
|
* Serialize a `value` object into an HTML string.
|
|
*
|
|
* @param {Value} value
|
|
* @param {Object} options
|
|
* @property {Boolean} render
|
|
* @return {String|Array}
|
|
*/
|
|
|
|
serialize = (value, options = {}) => {
|
|
const { document } = value
|
|
const elements = document.nodes.map(this.serializeNode).filter(el => el)
|
|
if (options.render === false) return elements
|
|
|
|
const html = renderToStaticMarkup(<body>{elements}</body>)
|
|
const inner = html.slice(6, -7)
|
|
return inner
|
|
}
|
|
|
|
/**
|
|
* Serialize a `node`.
|
|
*
|
|
* @param {Node} node
|
|
* @return {String}
|
|
*/
|
|
|
|
serializeNode = node => {
|
|
if (node.object === 'text') {
|
|
const leaves = node.getLeaves()
|
|
return leaves.map(this.serializeLeaf)
|
|
}
|
|
|
|
const children = node.nodes.map(this.serializeNode)
|
|
|
|
for (const rule of this.rules) {
|
|
if (!rule.serialize) continue
|
|
const ret = rule.serialize(node, children)
|
|
if (ret === null) return
|
|
if (ret) return addKey(ret)
|
|
}
|
|
|
|
throw new Error(`No serializer defined for node of type "${node.type}".`)
|
|
}
|
|
|
|
/**
|
|
* Serialize a `leaf`.
|
|
*
|
|
* @param {Leaf} leaf
|
|
* @return {String}
|
|
*/
|
|
|
|
serializeLeaf = leaf => {
|
|
const string = new String({ text: leaf.text })
|
|
const text = this.serializeString(string)
|
|
|
|
return leaf.marks.reduce((children, mark) => {
|
|
for (const rule of this.rules) {
|
|
if (!rule.serialize) continue
|
|
const ret = rule.serialize(mark, children)
|
|
if (ret === null) return
|
|
if (ret) return addKey(ret)
|
|
}
|
|
|
|
throw new Error(`No serializer defined for mark of type "${mark.type}".`)
|
|
}, text)
|
|
}
|
|
|
|
/**
|
|
* Serialize a `string`.
|
|
*
|
|
* @param {String} string
|
|
* @return {String}
|
|
*/
|
|
|
|
serializeString = string => {
|
|
for (const rule of this.rules) {
|
|
if (!rule.serialize) continue
|
|
const ret = rule.serialize(string, string.text)
|
|
if (ret) return ret
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Filter out cruft newline nodes inserted by the DOM parser.
|
|
*
|
|
* @param {Object} element
|
|
* @return {Boolean}
|
|
*/
|
|
|
|
cruftNewline = element => {
|
|
return !(element.nodeName === '#text' && element.nodeValue == '\n')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a unique key to a React `element`.
|
|
*
|
|
* @param {Element} element
|
|
* @return {Element}
|
|
*/
|
|
|
|
let key = 0
|
|
|
|
function addKey(element) {
|
|
return React.cloneElement(element, { key: key++ })
|
|
}
|
|
|
|
/**
|
|
* Export.
|
|
*
|
|
* @type {Html}
|
|
*/
|
|
|
|
export default Html
|