1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-30 18:39:51 +02:00

working on moving components into the schema

This commit is contained in:
Ian Storm Taylor
2016-08-13 16:18:07 -07:00
parent f4b584a00b
commit 8692d1a98a
15 changed files with 273 additions and 238 deletions

View File

@@ -93,8 +93,8 @@ class Editor extends React.Component {
super(props)
this.tmp = {}
this.state = {}
this.state.schema = Schema.create(props.schema)
this.state.plugins = this.resolvePlugins(props)
this.state.schema = this.resolveSchema(this.state.plugins)
this.state.state = this.onBeforeChange(props.state)
// Mix in the event handlers.
@@ -112,16 +112,10 @@ class Editor extends React.Component {
*/
componentWillReceiveProps = (props) => {
if (props.schema != this.props.schema) {
this.setState({
schema: Schema.create(props.schema)
})
}
if (props.plugins != this.props.plugins) {
this.setState({
plugins: this.resolvePlugins(props)
})
const plugins = this.resolvePlugins(props)
const schema = this.resolveSchema(plugins)
this.setState({ plugins, schema })
}
this.setState({
@@ -269,6 +263,7 @@ class Editor extends React.Component {
renderDecorations={this.renderDecorations}
renderMark={this.renderMark}
renderNode={this.renderNode}
schema={this.state.schema}
spellCheck={this.props.spellCheck}
state={this.state.state}
style={this.props.style}
@@ -361,6 +356,27 @@ class Editor extends React.Component {
]
}
/**
* Resolve the editor's schema from a set of `plugins`.
*
* @param {Array} plugins
* @return {Schema}
*/
resolveSchema = (plugins) => {
let rules = []
for (const plugin of plugins) {
if (!plugin.schema) continue
debugger
const schema = Schema.create(plugin.schema)
rules = rules.concat(schema.rules)
}
const schema = Schema.create({ rules })
return schema
}
}
/**

View File

@@ -53,6 +53,17 @@ class Mark extends new Record(DEFAULTS) {
return 'mark'
}
/**
* Get the component for the node from a `schema`.
*
* @param {Schema} schema
* @return {Component || Void}
*/
getComponent(schema) {
return schema.__getComponent(this)
}
}
/**

View File

@@ -338,6 +338,17 @@ const Node = {
}
},
/**
* Get the component for the node from a `schema`.
*
* @param {Schema} schema
* @return {Component || Void}
*/
getComponent(schema) {
return schema.__getComponent(this)
},
/**
* Get a descendant node by `key`.
*
@@ -1071,10 +1082,74 @@ const Node = {
updateDescendant(node) {
this.assertDescendant(node)
return this.mapDescendants(d => d.key == node.key ? node : d)
},
/**
* Validate the node against a `schema`.
*
* @param {Schema} schema
* @return {Object || Void}
*/
validate(schema) {
return schema.__validate(this)
}
}
/**
* Memoize read methods.
*/
memoize(Node, [
'assertChild',
'assertDescendant',
'findDescendant',
'filterDescendants',
'getBlocks',
'getBlocksAtRange',
'getCharactersAtRange',
'getChild',
'getChildrenAfter',
'getChildrenAfterIncluding',
'getChildrenBefore',
'getChildrenBeforeIncluding',
'getChildrenBetween',
'getChildrenBetweenIncluding',
'getClosest',
'getClosestBlock',
'getClosestInline',
'getComponent',
'getDescendant',
'getDepth',
'getFragmentAtRange',
'getFurthest',
'getFurthestBlock',
'getFurthestInline',
'getHighestChild',
'getHighestOnlyChildParent',
'getInlinesAtRange',
'getMarksAtRange',
'getNextBlock',
'getNextSibling',
'getNextText',
'getOffset',
'getOffsetAtRange',
'getParent',
'getPreviousSibling',
'getPreviousText',
'getPreviousBlock',
'getTextAtOffset',
'getTextDirection',
'getTexts',
'getTextsAtRange',
'hasChild',
'hasDescendant',
'hasVoidParent',
'isInlineSplitAtRange',
'validate'
])
/**
* Normalize a `key`, from a key string or a node.
*
@@ -1119,57 +1194,6 @@ for (const key in Transforms) {
Node[key] = Transforms[key]
}
/**
* Memoize read methods.
*/
memoize(Node, [
'assertChild',
'assertDescendant',
'findDescendant',
'filterDescendants',
'getBlocks',
'getBlocksAtRange',
'getCharactersAtRange',
'getChildrenAfter',
'getChildrenAfterIncluding',
'getChildrenBefore',
'getChildrenBeforeIncluding',
'getChildrenBetween',
'getChildrenBetweenIncluding',
'getClosest',
'getClosestBlock',
'getClosestInline',
'getChild',
'getDescendant',
'getDepth',
'getFragmentAtRange',
'getFurthest',
'getFurthestBlock',
'getFurthestInline',
'getHighestChild',
'getHighestOnlyChildParent',
'getInlinesAtRange',
'getMarksAtRange',
'getNextBlock',
'getNextSibling',
'getNextText',
'getOffset',
'getOffsetAtRange',
'getParent',
'getPreviousSibling',
'getPreviousText',
'getPreviousBlock',
'getTextAtOffset',
'getTextDirection',
'getTexts',
'getTextsAtRange',
'hasChild',
'hasDescendant',
'hasVoidParent',
'isInlineSplitAtRange'
])
/**
* Export.
*/

View File

@@ -65,7 +65,7 @@ const CHECKS = {
const { nodes } = object
if (!nodes) return
const invalids = nodes.filterNot((child) => {
return value.some(fail => !fail(child))
return value.some(match => match(child))
})
if (invalids.size) return invalids
@@ -75,7 +75,7 @@ const CHECKS = {
const { nodes } = object
if (!nodes) return
const invalids = nodes.filterNot((child) => {
return value.every(fail => fail(child))
return value.every(match => !match(child))
})
if (invalids.size) return invalids
@@ -86,10 +86,10 @@ const CHECKS = {
if (!nodes) return
if (nodes.size != value.length) return nodes
const invalids = nodes.filter((child, i) => {
const fail = value[i]
if (!fail) return true
return fail(child)
const invalids = nodes.filterNot((child, i) => {
const match = value[i]
if (!match) return false
return match(child)
})
if (invalids.size) return invalids
@@ -138,62 +138,47 @@ class Schema extends new Record(DEFAULTS) {
}
/**
* Transform a `state` to abide by the schema.
* Return the component for an `object`.
*
* @param {State} state
* @return {State}
* This method is private, because it should always be called on one of the
* often-changing immutable objects instead, since it will be memoized for
* much better performance.
*
* @param {Mixed} object
* @return {Component || Void}
*/
transform(state) {
const { document } = state
let transform = state.transform()
let failure
document.filterDescendantsDeep((node) => {
if (failure = this.validateNode(node)) {
const { reason, rule } = failure
transform = rule.transform(transform, node, reason)
}
})
if (failure = this.validateNode(document)) {
const { reason, rule } = failure
transform = rule.transform(transform, document, reason)
}
const next = transform.apply({ snapshot: false })
return next
__getComponent(object) {
const match = this.rules.find(rule => rule.match(object) && rule.component)
if (!match) return
return match.component
}
/**
* Validate a `state` against the schema.
* Validate an `object` against the schema, returning the failing rule and
* reason if the object is invalid, or void if it's valid.
*
* @param {State} state
* @return {State}
*/
validate(state) {
return !!state.document.findDescendant(node => this.validateNode(node))
}
/**
* Validate a `node` against the schema, returning the rule that was not met
* if the node is invalid, or null if the rule was valid.
* This method is private, because it should always be called on one of the
* often-changing immutable objects instead, since it will be memoized for
* much better performance.
*
* @param {Node} node
* @param {Mixed} object
* @return {Object || Void}
*/
validateNode(node) {
__validate(object) {
let reason
let match = this.rules
.filter(rule => !rule.match(node))
.find((rule) => {
reason = rule.validate(node)
if (reason) return true
})
if (!match) return
const match = this.rules.find((rule) => {
if (!rule.match(object)) return
debugger
if (!rule.validate) return
debugger
reason = rule.validate(object)
return reason
})
if (!reason) return
return {
rule: match,
@@ -203,16 +188,6 @@ class Schema extends new Record(DEFAULTS) {
}
/**
* Memoize read methods.
*/
memoize(Schema.prototype, [
'transform',
'validate',
'validateNode',
])
/**
* Normalize the `properties` of a schema.
*
@@ -267,9 +242,7 @@ function normalizeProperties(properties) {
}
return {
rules: rules
.concat(RULES)
.map(normalizeRule)
rules: rules.map(normalizeRule)
}
}
@@ -317,10 +290,7 @@ function normalizeValidate(validate) {
switch (typeOf(validate)) {
case 'function': return validate
case 'boolean': return () => validate
case 'object': return normalizeSpec(validate)
default: {
throw new Error(`Invalid \`validate\` spec: "${validate}".`)
}
case 'object': return normalizeSpec(validate, true)
}
}
@@ -334,9 +304,6 @@ function normalizeValidate(validate) {
function normalizeTransform(transform) {
switch (typeOf(transform)) {
case 'function': return transform
default: {
throw new Error(`Invalid \`transform\` spec: "${transform}".`)
}
}
}
@@ -344,10 +311,11 @@ function normalizeTransform(transform) {
* Normalize a `spec` object.
*
* @param {Object} obj
* @param {Boolean} giveReason
* @return {Boolean}
*/
function normalizeSpec(obj) {
function normalizeSpec(obj, giveReason) {
const spec = { ...obj }
if (spec.exactlyOf) spec.exactlyOf = spec.exactlyOf.map(normalizeSpec)
if (spec.anyOf) spec.anyOf = spec.anyOf.map(normalizeSpec)
@@ -356,11 +324,18 @@ function normalizeSpec(obj) {
return (node) => {
for (const key in CHECKS) {
const value = spec[key]
debugger
if (value == null) continue
debugger
const fail = CHECKS[key]
const failure = fail(node, value)
if (!giveReason) {
return !failure
}
if (failure != undefined) {
return {
type: key,

View File

@@ -59,7 +59,7 @@ class State extends new Record(DEFAULTS) {
}
/**
* Is there undoable events?
* Are there undoable events?
*
* @return {Boolean} hasUndos
*/
@@ -69,7 +69,7 @@ class State extends new Record(DEFAULTS) {
}
/**
* Is there redoable events?
* Are there redoable events?
*
* @return {Boolean} hasRedos
*/
@@ -358,6 +358,38 @@ class State extends new Record(DEFAULTS) {
return this.document.getTextsAtRange(this.selection)
}
/**
* Normalize a state against a `schema`.
*
* @param {Schema} schema
* @return {State}
*/
normalize(schema) {
const state = this
const { document, selection } = this
let transform = this.transform()
let failure
document.filterDescendantsDeep((node) => {
if (failure = node.validate(schema)) {
const { reason, rule } = failure
debugger
transform = rule.transform(transform, node, reason)
}
})
if (failure = document.validate(schema)) {
const { reason, rule } = failure
debugger
transform = rule.transform(transform, document, reason)
}
return transform.steps.size
? transform.apply({ snapshot: false })
: state
}
/**
* Return a new `Transform` with the current state as a starting point.
*

View File

@@ -228,6 +228,17 @@ class Text extends new Record(DEFAULTS) {
return this.merge({ characters })
}
/**
* Validate the node against a `schema`.
*
* @param {Schema} schema
* @return {Object || Void}
*/
validate(schema) {
return schema.__validate(this)
}
}
/**
@@ -238,7 +249,8 @@ memoize(Text.prototype, [
'getDecoratedCharacters',
'getDecoratedRanges',
'getRanges',
'getRangesForCharacters'
'getRangesForCharacters',
'validate'
])
/**

View File

@@ -73,66 +73,66 @@ function Plugin(options = {}) {
)
}
// /**
// * The default schema.
// *
// * @type {Object}
// */
/**
* The default schema.
*
* @type {Object}
*/
// const DEFAULT_SCHEMA = {
// rules: [
// {
// match: {
// kind: 'document'
// },
// validate: {
// anyOf: [
// { kind: 'block' }
// ]
// },
// transform: (transform, match, reason) => {
// return reason.value.reduce((tr, node) => {
// return tr.removeNodeByKey(node.key)
// }, transform)
// }
// },
// {
// match: {
// kind: 'block'
// },
// validate: {
// anyOf: [
// { kind: 'block' },
// { kind: 'inline' },
// { kind: 'text' },
// ]
// },
// transform: (transform, match, reason) => {
// return reason.value.reduce((tr, node) => {
// return tr.removeNodeByKey(node.key)
// }, transform)
// },
// component: DEFAULT_BLOCK
// },
// {
// match: {
// kind: 'inline'
// },
// validate: {
// anyOf: [
// { kind: 'inline' },
// { kind: 'text' },
// ]
// },
// transform: (transform, match, reason) => {
// return reason.value.reduce((tr, node) => {
// return tr.removeNodeByKey(node.key)
// }, transform)
// },
// component: DEFAULT_INLINE
// },
// ]
// }
const DEFAULT_SCHEMA = {
rules: [
{
match: {
kind: 'document'
},
validate: {
anyOf: [
{ kind: 'block' }
]
},
transform: (transform, match, reason) => {
return reason.value.reduce((tr, node) => {
return tr.removeNodeByKey(node.key)
}, transform)
}
},
{
match: {
kind: 'block'
},
validate: {
anyOf: [
{ kind: 'block' },
{ kind: 'inline' },
{ kind: 'text' },
]
},
transform: (transform, match, reason) => {
return reason.value.reduce((tr, node) => {
return tr.removeNodeByKey(node.key)
}, transform)
},
component: DEFAULT_BLOCK
},
{
match: {
kind: 'inline'
},
validate: {
anyOf: [
{ kind: 'inline' },
{ kind: 'text' },
]
},
transform: (transform, match, reason) => {
return reason.value.reduce((tr, node) => {
return tr.removeNodeByKey(node.key)
}, transform)
},
component: DEFAULT_INLINE
},
]
}
/**
* On before change, enforce the editor's schema.
@@ -144,9 +144,8 @@ function Plugin(options = {}) {
function onBeforeChange(state, editor) {
if (state.isNative) return state
return editor
.getSchema()
.transform(state)
const schema = editor.getSchema()
return state.normalize(schema)
}
/**
@@ -700,7 +699,8 @@ function Plugin(options = {}) {
onKeyDown,
onPaste,
onSelect,
renderNode
renderNode,
schema: DEFAULT_SCHEMA
}
}

View File

@@ -84,8 +84,8 @@
"open": "open http://localhost:8080/dev.html",
"prepublish": "npm run dist",
"start": "http-server ./examples",
"test": "npm-run-all lint dist:npm test:server",
"test:server": "mocha --compilers js:babel-core/register --reporter spec ./test/server.js",
"test": "npm-run-all lint dist:npm tests",
"tests": "mocha --compilers js:babel-core/register --reporter spec ./test/server.js",
"watch": "npm-run-all --parallel --print-label watch:dist watch:examples start",
"watch:dist": "babel --watch --out-dir ./dist ./lib",
"watch:examples": "watchify --debug --transform babelify ./examples/index.js -o ./examples/build.dev.js"

View File

@@ -1,2 +0,0 @@
export default {}

View File

@@ -1,8 +0,0 @@
nodes:
- kind: block
type: default
isVoid: true
nodes:
- kind: text
text: one

View File

@@ -1,5 +0,0 @@
nodes:
- kind: block
type: default
isVoid: true

View File

@@ -1,2 +0,0 @@
export default {}

View File

@@ -1,11 +0,0 @@
nodes:
- kind: block
type: default
nodes:
- kind: inline
type: default
isVoid: true
nodes:
- kind: text
text: one

View File

@@ -1,8 +0,0 @@
nodes:
- kind: block
type: default
nodes:
- kind: inline
type: default
isVoid: true

View File

@@ -1,8 +1,9 @@
import React from 'react'
import fs from 'fs'
import strip from '../helpers/strip-dynamic'
import readMetadata from 'read-metadata'
import { Raw, Schema } from '../..'
import { Raw, Editor, Schema } from '../..'
import { strictEqual } from '../helpers/assert-json'
import { resolve } from 'path'
@@ -22,8 +23,8 @@ describe('schema', () => {
const expected = readMetadata.sync(resolve(dir, 'output.yaml'))
const schema = Schema.create(require(dir))
let state = Raw.deserialize(input, { terse: true })
state = schema.transform(state)
const state = Raw.deserialize(input, { terse: true })
const editor = <Editor state={state} schema={schema} />
const output = Raw.serialize(state, { terse: true })
strictEqual(strip(output), strip(expected))
})