1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-12 18:24:03 +02:00

change void nodes to have a single space, prevent text-less inlines

This commit is contained in:
Ian Storm Taylor
2016-07-27 16:21:55 -07:00
parent 807d9f93e4
commit 42cbcb7e8d
40 changed files with 128 additions and 140 deletions

View File

@@ -158,9 +158,16 @@ class Leaf extends React.Component {
} }
renderText() { renderText() {
const { text } = this.props const { text, parent } = this.props
if (!text) return <br />
// If the text is empty, we need to render a <br/> to get the block to have
// the proper height.
if (text == '') return <br />
// COMPAT: Browsers will collapse trailing new lines, so we need to add an
// extra trailing new lines to prevent that.
if (text.charAt(text.length - 1) == '\n') return `${text}\n` if (text.charAt(text.length - 1) == '\n') return `${text}\n`
return text return text
} }

View File

@@ -59,18 +59,18 @@ class Node extends React.Component {
} }
/** /**
* Render a `node`. * Render a `child` node.
* *
* @param {Node} node * @param {Node} child
* @return {Element} element * @return {Element} element
*/ */
renderNode = (node) => { renderNode = (child) => {
const { editor, renderDecorations, renderMark, renderNode, state } = this.props const { editor, renderDecorations, renderMark, renderNode, state } = this.props
return ( return (
<Node <Node
key={node.key} key={child.key}
node={node} node={child}
state={state} state={state}
editor={editor} editor={editor}
renderDecorations={renderDecorations} renderDecorations={renderDecorations}

View File

@@ -141,7 +141,6 @@ class Void extends React.Component {
return ( return (
<Leaf <Leaf
ref={this.renderLeafRefs}
renderMark={this.renderLeafMark} renderMark={this.renderLeafMark}
key={offsetKey} key={offsetKey}
state={state} state={state}

View File

@@ -85,11 +85,7 @@ class Block extends new Record(DEFAULTS) {
*/ */
get isEmpty() { get isEmpty() {
return ( return this.text == ''
this.nodes.size == 1 &&
this.nodes.first().kind == 'text' &&
this.length == 0
)
} }
/** /**

View File

@@ -52,7 +52,17 @@ class Document extends new Record(DEFAULTS) {
} }
/** /**
* Get the length of the concatenated text of the node. * Is the document empty?
*
* @return {Boolean} isEmpty
*/
get isEmpty() {
return this.text == ''
}
/**
* Get the length of the concatenated text of the document.
* *
* @return {Number} length * @return {Number} length
*/ */

View File

@@ -85,11 +85,7 @@ class Inline extends new Record(DEFAULTS) {
*/ */
get isEmpty() { get isEmpty() {
return ( return this.text == ''
this.nodes.size == 1 &&
this.nodes.first().kind == 'text' &&
this.length == 0
)
} }
/** /**

View File

@@ -882,28 +882,34 @@ const Node = {
// Map this node's descendants, ensuring... // Map this node's descendants, ensuring...
node = node.mapDescendants((desc) => { node = node.mapDescendants((desc) => {
if (removals.has(desc.key)) return desc
// ...that there are no duplicate keys. // ...that there are no duplicate keys.
if (keys.has(desc.key)) desc = desc.set('key', uid()) if (keys.has(desc.key)) desc = desc.set('key', uid())
keys = keys.add(desc.key) keys = keys.add(desc.key)
// ...that void nodes contain no text. // ...that void nodes contain a single space of content.
if (desc.isVoid && desc.length) { if (desc.isVoid && desc.text != ' ') {
let text = desc.getTexts().first() desc = desc.merge({
let characters = text.characters.clear() nodes: Text.createList([{
text = text.merge({ characters }) characters: Character.createList([{ text: ' ' }])
const nodes = desc.nodes.clear().push(text) }])
desc = desc.merge({ nodes }) })
} }
// ...that no block or inline node is empty. // ...that no block or inline has no text node inside it.
if (desc.kind != 'text' && desc.nodes.size == 0) { if (desc.kind != 'text' && desc.nodes.size == 0) {
const text = Text.create() const text = Text.create()
const nodes = desc.nodes.push(text) const nodes = desc.nodes.push(text)
desc = desc.merge({ nodes }) desc = desc.merge({ nodes })
} }
if (desc.kind == 'text' && !removals.has(desc.key)) { // ...that no inline node is empty.
if (desc.kind == 'inline' && desc.text == '') {
removals = removals.add(desc.key)
}
if (desc.kind == 'text') {
let next = node.getNextSibling(desc) let next = node.getNextSibling(desc)
// ...that there are no adjacent text nodes. // ...that there are no adjacent text nodes.
@@ -919,7 +925,9 @@ const Node = {
// ...that there are no extra empty text nodes. // ...that there are no extra empty text nodes.
else if (desc.length == 0) { else if (desc.length == 0) {
const parent = node.getParent(desc) const parent = node.getParent(desc)
if (parent.nodes.size > 1) removals = removals.add(desc.key) if (!removals.has(parent.key) && parent.nodes.size > 1) {
removals = removals.add(desc.key)
}
} }
} }

View File

@@ -21,9 +21,7 @@ const Range = new Record({
const DEFAULTS = { const DEFAULTS = {
characters: new List(), characters: new List(),
decorations: null, key: null
key: null,
cache: null
} }
/** /**
@@ -43,8 +41,6 @@ class Text extends new Record(DEFAULTS) {
if (properties instanceof Text) return properties if (properties instanceof Text) return properties
properties.key = properties.key || uid(4) properties.key = properties.key || uid(4)
properties.characters = Character.createList(properties.characters) properties.characters = Character.createList(properties.characters)
properties.decorations = null
properties.cache = null
return new Text(properties) return new Text(properties)
} }
@@ -77,7 +73,7 @@ class Text extends new Record(DEFAULTS) {
*/ */
get isEmpty() { get isEmpty() {
return this.length == 0 return this.text == ''
} }
/** /**

View File

@@ -401,6 +401,15 @@ const Transforms = {
range = range.collapseToStart() range = range.collapseToStart()
} }
const { startKey, endKey, startOffset, endOffset } = range
// If the range is inside a void, abort.
const block = doc.getClosestBlock(startKey)
if (block && block.isVoid) return doc
const inline = doc.getClosestInline(startKey)
if (inline && inline.isVoid) return doc
// Allow for passing a type string. // Allow for passing a type string.
if (typeof node == 'string') node = { type: node } if (typeof node == 'string') node = { type: node }
@@ -411,7 +420,6 @@ const Transforms = {
doc = doc.splitTextAtRange(range) doc = doc.splitTextAtRange(range)
// Insert the node between the split text nodes. // Insert the node between the split text nodes.
const { startKey, endKey, startOffset, endOffset } = range
const startText = doc.getDescendant(startKey) const startText = doc.getDescendant(startKey)
let parent = doc.getParent(startKey) let parent = doc.getParent(startKey)
const nodes = parent.nodes.takeUntil(n => n == startText) const nodes = parent.nodes.takeUntil(n => n == startText)

View File

@@ -72,7 +72,7 @@
"lint": "eslint --ignore-pattern 'build.js' '{examples,lib}/**/*.js'", "lint": "eslint --ignore-pattern 'build.js' '{examples,lib}/**/*.js'",
"prepublish": "npm run dist", "prepublish": "npm run dist",
"start": "http-server ./examples", "start": "http-server ./examples",
"test": "mocha --compilers js:babel-core/register --require source-map-support/register --reporter spec ./test/server.js" "test": "mocha --compilers js:babel-core/register --reporter spec ./test/server.js"
}, },
"keywords": [ "keywords": [
"canvas", "canvas",

View File

@@ -4,4 +4,6 @@ nodes:
isVoid: true isVoid: true
data: {} data: {}
nodes: nodes:
- characters: [] - characters:
- text: " "
marks: []

View File

@@ -4,8 +4,10 @@ nodes:
isVoid: false isVoid: false
data: {} data: {}
nodes: nodes:
- type: link - type: link
isVoid: true isVoid: true
data: {} data: {}
nodes: nodes:
- characters: [] - characters:
- text: " "
marks: []

View File

@@ -4,4 +4,6 @@ nodes:
isVoid: true isVoid: true
data: {} data: {}
nodes: nodes:
- characters: [] - characters:
- text: " "
marks: []

View File

@@ -8,4 +8,6 @@ nodes:
isVoid: true isVoid: true
data: {} data: {}
nodes: nodes:
- characters: [] - characters:
- text: " "
marks: []

View File

@@ -4,4 +4,6 @@ nodes:
isVoid: true isVoid: true
data: {} data: {}
nodes: nodes:
- characters: [] - characters:
- text: " "
marks: []

View File

@@ -4,8 +4,10 @@ nodes:
isVoid: false isVoid: false
data: {} data: {}
nodes: nodes:
- type: link - type: link
isVoid: true isVoid: true
data: {} data: {}
nodes: nodes:
- characters: [] - characters:
- text: " "
marks: []

View File

@@ -11,5 +11,5 @@ document:
- kind: text - kind: text
ranges: ranges:
- kind: range - kind: range
text: "" text: " "
marks: [] marks: []

View File

@@ -16,5 +16,5 @@ document:
- kind: text - kind: text
ranges: ranges:
- kind: range - kind: range
text: "" text: " "
marks: [] marks: []

View File

@@ -60,7 +60,7 @@ describe('serializers', () => {
const innerDir = resolve(dir, test) const innerDir = resolve(dir, test)
const expected = readMetadata.sync(resolve(innerDir, 'output.yaml')) const expected = readMetadata.sync(resolve(innerDir, 'output.yaml'))
const input = fs.readFileSync(resolve(innerDir, 'input.txt'), 'utf8') const input = fs.readFileSync(resolve(innerDir, 'input.txt'), 'utf8')
const state = Plain.deserialize(input.trim()) const state = Plain.deserialize(input.replace(/\n$/m, ''))
const json = state.document.toJS() const json = state.document.toJS()
strictEqual(strip(json), expected) strictEqual(strip(json), expected)
}) })
@@ -77,7 +77,7 @@ describe('serializers', () => {
const input = require(resolve(innerDir, 'input.js')).default const input = require(resolve(innerDir, 'input.js')).default
const expected = fs.readFileSync(resolve(innerDir, 'output.txt'), 'utf8') const expected = fs.readFileSync(resolve(innerDir, 'output.txt'), 'utf8')
const serialized = Plain.serialize(input) const serialized = Plain.serialize(input)
strictEqual(serialized, expected.trim()) strictEqual(serialized, expected.replace(/\n$/m, ''))
}) })
} }
}) })

View File

@@ -12,6 +12,9 @@ export default function (state) {
return state return state
.transform() .transform()
.insertInlineAtRange(range, 'hashtag') .insertInlineAtRange(range, {
type: 'hashtag',
isVoid: true
})
.apply() .apply()
} }

View File

@@ -7,6 +7,4 @@ nodes:
text: word text: word
- kind: inline - kind: inline
type: hashtag type: hashtag
nodes: isVoid: true
- kind: text
text: ""

View File

@@ -12,6 +12,9 @@ export default function (state) {
return state return state
.transform() .transform()
.insertBlockAtRange(range, 'image') .insertInlineAtRange(range, {
type: 'hashtag',
isVoid: true
})
.apply() .apply()
} }

View File

@@ -5,13 +5,8 @@ nodes:
nodes: nodes:
- kind: text - kind: text
text: wo text: wo
- kind: block - kind: inline
type: image type: hashtag
nodes: isVoid: true
- kind: text
text: ""
- kind: block
type: paragraph
nodes:
- kind: text - kind: text
text: rd text: rd

View File

@@ -12,6 +12,9 @@ export default function (state) {
return state return state
.transform() .transform()
.insertBlockAtRange(range, 'image') .insertInlineAtRange(range, {
type: 'hashtag',
isVoid: true
})
.apply() .apply()
} }

View File

@@ -1,12 +1,10 @@
nodes: nodes:
- kind: block
type: image
nodes:
- kind: text
text: ""
- kind: block - kind: block
type: paragraph type: paragraph
nodes: nodes:
- kind: inline
type: hashtag
isVoid: true
- kind: text - kind: text
text: word text: word

View File

@@ -12,6 +12,9 @@ export default function (state) {
return state return state
.transform() .transform()
.insertBlockAtRange(range, 'image') .insertInlineAtRange(range, {
type: 'hashtag',
isVoid: true
})
.apply() .apply()
} }

View File

@@ -1,7 +1,8 @@
nodes: nodes:
- kind: block - kind: block
type: image type: paragraph
nodes: nodes:
- kind: text - kind: inline
text: "" type: hashtag
isVoid: true

View File

@@ -12,6 +12,9 @@ export default function (state) {
return state return state
.transform() .transform()
.insertBlockAtRange(range, 'image') .insertInlineAtRange(range, {
type: 'hashtag',
isVoid: true
})
.apply() .apply()
} }

View File

@@ -3,6 +3,3 @@ nodes:
- kind: block - kind: block
type: image type: image
isVoid: true isVoid: true
nodes:
- kind: text
text: ""

View File

@@ -3,8 +3,3 @@ nodes:
- kind: block - kind: block
type: image type: image
isVoid: true isVoid: true
- kind: block
type: image
nodes:
- kind: text
text: ""

View File

@@ -1,12 +0,0 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
text: ""
- kind: block
type: paragraph
nodes:
- kind: text
text: word

View File

@@ -1,5 +1,5 @@
import { Block } from '../../../../..' import { Inline } from '../../../../..'
export default function (state) { export default function (state) {
const { document, selection } = state const { document, selection } = state
@@ -14,6 +14,9 @@ export default function (state) {
return state return state
.transform() .transform()
.insertBlockAtRange(range, Block.create({ type: 'image' })) .insertInlineAtRange(range, Inline.create({
type: 'image',
isVoid: true
}))
.apply() .apply()
} }

View File

@@ -3,5 +3,8 @@ nodes:
- kind: block - kind: block
type: paragraph type: paragraph
nodes: nodes:
- kind: inline
type: image
isVoid: true
- kind: text - kind: text
text: word text: word

View File

@@ -1,17 +0,0 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertBlockAtRange(range, { type: 'image' })
.apply()
}

View File

@@ -1,12 +0,0 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
text: ""
- kind: block
type: paragraph
nodes:
- kind: text
text: word

View File

@@ -11,11 +11,6 @@ nodes:
- kind: block - kind: block
type: paragraph type: paragraph
nodes: nodes:
- kind: inline
type: one
nodes:
- kind: text
text: ""
- kind: inline - kind: inline
type: two type: two
nodes: nodes:

View File

@@ -11,11 +11,6 @@ nodes:
- kind: block - kind: block
type: paragraph type: paragraph
nodes: nodes:
- kind: inline
type: one
nodes:
- kind: text
text: ""
- kind: inline - kind: inline
type: two type: two
nodes: nodes: