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

Improve normalize suppression to make less verbose and safer (#1549)

* change normalization can be set with setOperationFlag, and changes can be executed in sequence with automatic suppression with guaranteed document normalization at the end.

* responded to developer feedback by renaming execute to withMutations (to mirror immutable JS), implemented tests, updated documentation

* fixed typos discovered in review.

* fixed missing normalize flag usages and added withMutations to the schemas guide

* responded to developer feedback

* fixed lint errors and cleaned up code

* readd missing tests

* getFlag now allows options to override the change flags

* removed normalize restoration asserts from unit tests

* unit test cleanup
This commit is contained in:
CameronAckermanSEL
2018-01-26 11:32:37 -08:00
committed by Ian Storm Taylor
parent 00165a3155
commit ef5106e30f
11 changed files with 382 additions and 33 deletions

View File

@@ -28,7 +28,7 @@ const Changes = {}
Changes.addMarkAtRange = (change, range, mark, options = {}) => {
if (range.isCollapsed) return
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const { startKey, startOffset, endKey, endOffset } = range
@@ -77,7 +77,7 @@ Changes.deleteAtRange = (change, range, options = {}) => {
// when you undo a delete, the expanded selection will be retained.
change.snapshotSelection()
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
let { startKey, startOffset, endKey, endOffset } = range
let { document } = value
@@ -350,7 +350,7 @@ Changes.deleteWordBackwardAtRange = (change, range, options) => {
*/
Changes.deleteBackwardAtRange = (change, range, n = 1, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const { startKey, focusOffset } = range
@@ -536,7 +536,7 @@ Changes.deleteWordForwardAtRange = (change, range, options) => {
*/
Changes.deleteForwardAtRange = (change, range, n = 1, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const { startKey, focusOffset } = range
@@ -665,7 +665,7 @@ Changes.deleteForwardAtRange = (change, range, n = 1, options = {}) => {
Changes.insertBlockAtRange = (change, range, block, options = {}) => {
block = Block.create(block)
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
if (range.isExpanded) {
change.deleteAtRange(range)
@@ -717,7 +717,7 @@ Changes.insertBlockAtRange = (change, range, block, options = {}) => {
*/
Changes.insertFragmentAtRange = (change, range, fragment, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
// If the range is expanded, delete it first.
if (range.isExpanded) {
@@ -830,7 +830,7 @@ Changes.insertFragmentAtRange = (change, range, fragment, options = {}) => {
*/
Changes.insertInlineAtRange = (change, range, inline, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
inline = Inline.create(inline)
if (range.isExpanded) {
@@ -908,7 +908,7 @@ Changes.insertTextAtRange = (change, range, text, marks, options = {}) => {
Changes.removeMarkAtRange = (change, range, mark, options = {}) => {
if (range.isCollapsed) return
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const texts = document.getTextsAtRange(range)
@@ -938,7 +938,7 @@ Changes.removeMarkAtRange = (change, range, mark, options = {}) => {
*/
Changes.setBlockAtRange = (change, range, properties, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const blocks = document.getBlocksAtRange(range)
@@ -959,7 +959,7 @@ Changes.setBlockAtRange = (change, range, properties, options = {}) => {
*/
Changes.setInlineAtRange = (change, range, properties, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const inlines = document.getInlinesAtRange(range)
@@ -980,7 +980,7 @@ Changes.setInlineAtRange = (change, range, properties, options = {}) => {
*/
Changes.splitBlockAtRange = (change, range, height = 1, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
if (range.isExpanded) {
change.deleteAtRange(range, { normalize })
@@ -1014,7 +1014,7 @@ Changes.splitBlockAtRange = (change, range, height = 1, options = {}) => {
*/
Changes.splitInlineAtRange = (change, range, height = Infinity, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
if (range.isExpanded) {
change.deleteAtRange(range, { normalize })
@@ -1053,7 +1053,7 @@ Changes.toggleMarkAtRange = (change, range, mark, options = {}) => {
mark = Mark.create(mark)
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const marks = document.getActiveMarksAtRange(range)
@@ -1079,7 +1079,7 @@ Changes.toggleMarkAtRange = (change, range, mark, options = {}) => {
Changes.unwrapBlockAtRange = (change, range, properties, options = {}) => {
properties = Node.createProperties(properties)
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
let { document } = value
const blocks = document.getBlocksAtRange(range)
@@ -1171,7 +1171,7 @@ Changes.unwrapBlockAtRange = (change, range, properties, options = {}) => {
Changes.unwrapInlineAtRange = (change, range, properties, options = {}) => {
properties = Node.createProperties(properties)
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const texts = document.getTextsAtRange(range)
@@ -1218,7 +1218,7 @@ Changes.wrapBlockAtRange = (change, range, block, options = {}) => {
block = Block.create(block)
block = block.set('nodes', block.nodes.clear())
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
@@ -1288,7 +1288,7 @@ Changes.wrapBlockAtRange = (change, range, block, options = {}) => {
Changes.wrapInlineAtRange = (change, range, inline, options = {}) => {
const { value } = change
let { document } = value
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { startKey, startOffset, endKey, endOffset } = range
if (range.isCollapsed) {
@@ -1397,7 +1397,7 @@ Changes.wrapInlineAtRange = (change, range, inline, options = {}) => {
*/
Changes.wrapTextAtRange = (change, range, prefix, suffix = prefix, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { startKey, endKey } = range
const start = range.collapseToStart()
let end = range.collapseToEnd()

View File

@@ -26,7 +26,7 @@ const Changes = {}
Changes.addMarkByKey = (change, key, offset, length, mark, options = {}) => {
mark = Mark.create(mark)
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const path = document.getPath(key)
@@ -84,7 +84,7 @@ Changes.addMarkByKey = (change, key, offset, length, mark, options = {}) => {
*/
Changes.insertFragmentByKey = (change, key, index, fragment, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
fragment.nodes.forEach((node, i) => {
change.insertNodeByKey(key, index + i, node)
@@ -107,7 +107,7 @@ Changes.insertFragmentByKey = (change, key, index, fragment, options = {}) => {
*/
Changes.insertNodeByKey = (change, key, index, node, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const path = document.getPath(key)
@@ -137,7 +137,8 @@ Changes.insertNodeByKey = (change, key, index, node, options = {}) => {
*/
Changes.insertTextByKey = (change, key, offset, text, marks, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const path = document.getPath(key)
@@ -169,7 +170,7 @@ Changes.insertTextByKey = (change, key, offset, text, marks, options = {}) => {
*/
Changes.mergeNodeByKey = (change, key, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const path = document.getPath(key)
@@ -208,7 +209,7 @@ Changes.mergeNodeByKey = (change, key, options = {}) => {
*/
Changes.moveNodeByKey = (change, key, newKey, newIndex, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const path = document.getPath(key)
@@ -241,7 +242,7 @@ Changes.moveNodeByKey = (change, key, newKey, newIndex, options = {}) => {
Changes.removeMarkByKey = (change, key, offset, length, mark, options = {}) => {
mark = Mark.create(mark)
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const path = document.getPath(key)
@@ -319,7 +320,7 @@ Changes.removeAllMarksByKey = (change, key, options = {}) => {
*/
Changes.removeNodeByKey = (change, key, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const path = document.getPath(key)
@@ -350,7 +351,7 @@ Changes.removeNodeByKey = (change, key, options = {}) => {
*/
Changes.removeTextByKey = (change, key, offset, length, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const path = document.getPath(key)
@@ -408,7 +409,7 @@ Changes.removeTextByKey = (change, key, offset, length, options = {}) => {
Changes.replaceNodeByKey = (change, key, newNode, options = {}) => {
newNode = Node.create(newNode)
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const node = document.getNode(key)
@@ -436,7 +437,7 @@ Changes.replaceNodeByKey = (change, key, newNode, options = {}) => {
Changes.setMarkByKey = (change, key, offset, length, mark, properties, options = {}) => {
mark = Mark.create(mark)
properties = Mark.createProperties(properties)
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const path = document.getPath(key)
@@ -469,7 +470,7 @@ Changes.setMarkByKey = (change, key, offset, length, mark, properties, options =
Changes.setNodeByKey = (change, key, properties, options = {}) => {
properties = Node.createProperties(properties)
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const path = document.getPath(key)
@@ -534,7 +535,7 @@ Changes.splitDescendantsByKey = (change, key, textKey, textOffset, options = {})
return
}
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
@@ -611,7 +612,7 @@ Changes.unwrapBlockByKey = (change, key, properties, options) => {
*/
Changes.unwrapNodeByKey = (change, key, options = {}) => {
const { normalize = true } = options
const normalize = change.getFlag('normalize', options)
const { value } = change
const { document } = value
const parent = document.getParent(key)

View File

@@ -48,7 +48,10 @@ class Change {
const { value } = attrs
this.value = value
this.operations = new List()
this.flags = pick(attrs, ['merge', 'save'])
this.flags = {
normalize: true,
...pick(attrs, ['merge', 'save', 'normalize'])
}
}
/**
@@ -140,6 +143,27 @@ class Change {
return this
}
/**
* Applies a series of change mutations and defers normalization until the end.
*
* @param {Function} customChange - function that accepts a change object and executes change operations
* @return {Change}
*/
withoutNormalization(customChange) {
const original = this.flags.normalize
this.setOperationFlag('normalize', false)
try {
customChange(this)
// if the change function worked then run normalization
this.normalizeDocument()
} finally {
// restore the flag to whatever it was
this.setOperationFlag('normalize', original)
}
return this
}
/**
* Set an operation flag by `key` to `value`.
*
@@ -153,6 +177,21 @@ class Change {
return this
}
/**
* Get the `value` of the specified flag by its `key`. Optionally accepts an `options`
* object with override flags.
*
* @param {String} key
* @param {Object} options
* @return {Change}
*/
getFlag(key, options = {}) {
return options[key] !== undefined ?
options[key] :
this.flags[key]
}
/**
* Unset an operation flag by `key`.
*

View File

@@ -15,6 +15,7 @@ describe('slate', () => {
require('./changes')
require('./history')
require('./operations')
require('./models')
})
/**

View File

@@ -0,0 +1,48 @@
/** @jsx h */
import h from '../../helpers/h'
export const flags = { normalize: false }
export const schema = {
blocks: {
paragraph: {},
item: {
parent: { types: ['list'] },
nodes: [
{ objects: ['text'] }
]
},
list: {},
}
}
export const customChange = (change) => {
// this change function and schema are designed such that if
// validation takes place before both wrapBlock calls complete
// the node gets deleted by the default schema
// and causes a test failure
let target = change.value.document.nodes.get(0)
change.wrapBlockByKey(target.key, 'item')
target = change.value.document.nodes.get(0)
change.wrapBlockByKey(target.key, 'list')
}
export const input = (
<value>
<document>
<paragraph />
</document>
</value>
)
export const output = (
<value>
<document>
<list>
<item />
</list>
</document>
</value>
)

View File

@@ -0,0 +1,48 @@
/** @jsx h */
import h from '../../helpers/h'
export const flags = { normalize: true }
export const schema = {
blocks: {
paragraph: {},
item: {
parent: { types: ['list'] },
nodes: [
{ objects: ['text'] }
]
},
list: {},
}
}
export const customChange = (change) => {
// this change function and schema are designed such that if
// validation takes place before both wrapBlock calls complete
// the node gets deleted by the default schema
// and causes a test failure
let target = change.value.document.nodes.get(0)
change.wrapBlockByKey(target.key, 'item')
target = change.value.document.nodes.get(0)
change.wrapBlockByKey(target.key, 'list')
}
export const input = (
<value>
<document>
<paragraph />
</document>
</value>
)
export const output = (
<value>
<document>
<list>
<item />
</list>
</document>
</value>
)

View File

@@ -0,0 +1,40 @@
/** @jsx h */
import h from '../../helpers/h'
export const flags = { }
export const schema = {
blocks: {
paragraph: {},
item: {
parent: { types: ['list'] },
nodes: [
{ objects: ['text'] }
]
},
list: {},
}
}
export const customChange = (change) => {
// see if we can break the expected validation sequence by toggling
// the normalization option
const target = change.value.document.nodes.get(0)
change.wrapBlockByKey(target.key, 'item', { normalize: true })
}
export const input = (
<value>
<document>
<paragraph />
</document>
</value>
)
export const output = (
<value>
<document />
</value>
)

View File

@@ -0,0 +1,47 @@
/** @jsx h */
import h from '../../helpers/h'
export const flags = { }
export const schema = {
blocks: {
paragraph: {},
item: {
parent: { types: ['list'] },
nodes: [
{ objects: ['text'] }
]
},
list: {},
}
}
export const customChange = (change) => {
// this change function and schema are designed such that if
// validation takes place before both wrapBlock calls complete
// the node gets deleted by the default schema
// and causes a test failure
let target = change.value.document.nodes.get(0)
change.wrapBlockByKey(target.key, 'item')
target = change.value.document.nodes.get(0)
change.wrapBlockByKey(target.key, 'list')
}
export const input = (
<value>
<document>
<paragraph />
</document>
</value>
)
export const output = (
<value>
<document>
<list>
<item />
</list>
</document>
</value>
)

View File

@@ -0,0 +1,34 @@
import assert from 'assert'
import fs from 'fs'
import { Schema } from '../..'
import { basename, extname, resolve } from 'path'
/**
* Tests.
*/
describe('models', () => {
describe('change', () => {
describe('withoutNormalization', () => {
const testsDir = resolve(__dirname, 'change')
const tests = fs.readdirSync(testsDir).filter(t => t[0] != '.').map(t => basename(t, extname(t)))
for (const test of tests) {
it(test, async () => {
const module = require(resolve(testsDir, test))
const { input, output, schema, flags, customChange } = module
const s = Schema.create(schema)
const expected = output.toJSON()
const actual = input
.change(flags)
.setValue({ schema: s })
.withoutNormalization(customChange)
.value.toJSON()
assert.deepEqual(actual, expected)
})
}
})
})
})