1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-31 10:51:44 +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) super(props)
this.tmp = {} this.tmp = {}
this.state = {} this.state = {}
this.state.schema = Schema.create(props.schema)
this.state.plugins = this.resolvePlugins(props) this.state.plugins = this.resolvePlugins(props)
this.state.schema = this.resolveSchema(this.state.plugins)
this.state.state = this.onBeforeChange(props.state) this.state.state = this.onBeforeChange(props.state)
// Mix in the event handlers. // Mix in the event handlers.
@@ -112,16 +112,10 @@ class Editor extends React.Component {
*/ */
componentWillReceiveProps = (props) => { componentWillReceiveProps = (props) => {
if (props.schema != this.props.schema) {
this.setState({
schema: Schema.create(props.schema)
})
}
if (props.plugins != this.props.plugins) { if (props.plugins != this.props.plugins) {
this.setState({ const plugins = this.resolvePlugins(props)
plugins: this.resolvePlugins(props) const schema = this.resolveSchema(plugins)
}) this.setState({ plugins, schema })
} }
this.setState({ this.setState({
@@ -269,6 +263,7 @@ class Editor extends React.Component {
renderDecorations={this.renderDecorations} renderDecorations={this.renderDecorations}
renderMark={this.renderMark} renderMark={this.renderMark}
renderNode={this.renderNode} renderNode={this.renderNode}
schema={this.state.schema}
spellCheck={this.props.spellCheck} spellCheck={this.props.spellCheck}
state={this.state.state} state={this.state.state}
style={this.props.style} 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' 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`. * Get a descendant node by `key`.
* *
@@ -1071,10 +1082,74 @@ const Node = {
updateDescendant(node) { updateDescendant(node) {
this.assertDescendant(node) this.assertDescendant(node)
return this.mapDescendants(d => d.key == node.key ? node : d) 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. * Normalize a `key`, from a key string or a node.
* *
@@ -1119,57 +1194,6 @@ for (const key in Transforms) {
Node[key] = Transforms[key] 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. * Export.
*/ */

View File

@@ -65,7 +65,7 @@ const CHECKS = {
const { nodes } = object const { nodes } = object
if (!nodes) return if (!nodes) return
const invalids = nodes.filterNot((child) => { const invalids = nodes.filterNot((child) => {
return value.some(fail => !fail(child)) return value.some(match => match(child))
}) })
if (invalids.size) return invalids if (invalids.size) return invalids
@@ -75,7 +75,7 @@ const CHECKS = {
const { nodes } = object const { nodes } = object
if (!nodes) return if (!nodes) return
const invalids = nodes.filterNot((child) => { const invalids = nodes.filterNot((child) => {
return value.every(fail => fail(child)) return value.every(match => !match(child))
}) })
if (invalids.size) return invalids if (invalids.size) return invalids
@@ -86,10 +86,10 @@ const CHECKS = {
if (!nodes) return if (!nodes) return
if (nodes.size != value.length) return nodes if (nodes.size != value.length) return nodes
const invalids = nodes.filter((child, i) => { const invalids = nodes.filterNot((child, i) => {
const fail = value[i] const match = value[i]
if (!fail) return true if (!match) return false
return fail(child) return match(child)
}) })
if (invalids.size) return invalids 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 * This method is private, because it should always be called on one of the
* @return {State} * often-changing immutable objects instead, since it will be memoized for
* much better performance.
*
* @param {Mixed} object
* @return {Component || Void}
*/ */
transform(state) { __getComponent(object) {
const { document } = state const match = this.rules.find(rule => rule.match(object) && rule.component)
let transform = state.transform() if (!match) return
let failure return match.component
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
} }
/** /**
* 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 * This method is private, because it should always be called on one of the
* @return {State} * often-changing immutable objects instead, since it will be memoized for
*/ * much better performance.
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.
* *
* @param {Node} node * @param {Mixed} object
* @return {Object || Void} * @return {Object || Void}
*/ */
validateNode(node) { __validate(object) {
let reason 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 { return {
rule: match, 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. * Normalize the `properties` of a schema.
* *
@@ -267,9 +242,7 @@ function normalizeProperties(properties) {
} }
return { return {
rules: rules rules: rules.map(normalizeRule)
.concat(RULES)
.map(normalizeRule)
} }
} }
@@ -317,10 +290,7 @@ function normalizeValidate(validate) {
switch (typeOf(validate)) { switch (typeOf(validate)) {
case 'function': return validate case 'function': return validate
case 'boolean': return () => validate case 'boolean': return () => validate
case 'object': return normalizeSpec(validate) case 'object': return normalizeSpec(validate, true)
default: {
throw new Error(`Invalid \`validate\` spec: "${validate}".`)
}
} }
} }
@@ -334,9 +304,6 @@ function normalizeValidate(validate) {
function normalizeTransform(transform) { function normalizeTransform(transform) {
switch (typeOf(transform)) { switch (typeOf(transform)) {
case 'function': return transform case 'function': return transform
default: {
throw new Error(`Invalid \`transform\` spec: "${transform}".`)
}
} }
} }
@@ -344,10 +311,11 @@ function normalizeTransform(transform) {
* Normalize a `spec` object. * Normalize a `spec` object.
* *
* @param {Object} obj * @param {Object} obj
* @param {Boolean} giveReason
* @return {Boolean} * @return {Boolean}
*/ */
function normalizeSpec(obj) { function normalizeSpec(obj, giveReason) {
const spec = { ...obj } const spec = { ...obj }
if (spec.exactlyOf) spec.exactlyOf = spec.exactlyOf.map(normalizeSpec) if (spec.exactlyOf) spec.exactlyOf = spec.exactlyOf.map(normalizeSpec)
if (spec.anyOf) spec.anyOf = spec.anyOf.map(normalizeSpec) if (spec.anyOf) spec.anyOf = spec.anyOf.map(normalizeSpec)
@@ -356,11 +324,18 @@ function normalizeSpec(obj) {
return (node) => { return (node) => {
for (const key in CHECKS) { for (const key in CHECKS) {
const value = spec[key] const value = spec[key]
debugger
if (value == null) continue if (value == null) continue
debugger
const fail = CHECKS[key] const fail = CHECKS[key]
const failure = fail(node, value) const failure = fail(node, value)
if (!giveReason) {
return !failure
}
if (failure != undefined) { if (failure != undefined) {
return { return {
type: key, 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 * @return {Boolean} hasUndos
*/ */
@@ -69,7 +69,7 @@ class State extends new Record(DEFAULTS) {
} }
/** /**
* Is there redoable events? * Are there redoable events?
* *
* @return {Boolean} hasRedos * @return {Boolean} hasRedos
*/ */
@@ -358,6 +358,38 @@ class State extends new Record(DEFAULTS) {
return this.document.getTextsAtRange(this.selection) 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. * 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 }) 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', 'getDecoratedCharacters',
'getDecoratedRanges', 'getDecoratedRanges',
'getRanges', 'getRanges',
'getRangesForCharacters' 'getRangesForCharacters',
'validate'
]) ])
/** /**

View File

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

View File

@@ -84,8 +84,8 @@
"open": "open http://localhost:8080/dev.html", "open": "open http://localhost:8080/dev.html",
"prepublish": "npm run dist", "prepublish": "npm run dist",
"start": "http-server ./examples", "start": "http-server ./examples",
"test": "npm-run-all lint dist:npm test:server", "test": "npm-run-all lint dist:npm tests",
"test:server": "mocha --compilers js:babel-core/register --reporter spec ./test/server.js", "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": "npm-run-all --parallel --print-label watch:dist watch:examples start",
"watch:dist": "babel --watch --out-dir ./dist ./lib", "watch:dist": "babel --watch --out-dir ./dist ./lib",
"watch:examples": "watchify --debug --transform babelify ./examples/index.js -o ./examples/build.dev.js" "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 fs from 'fs'
import strip from '../helpers/strip-dynamic' import strip from '../helpers/strip-dynamic'
import readMetadata from 'read-metadata' import readMetadata from 'read-metadata'
import { Raw, Schema } from '../..' import { Raw, Editor, Schema } from '../..'
import { strictEqual } from '../helpers/assert-json' import { strictEqual } from '../helpers/assert-json'
import { resolve } from 'path' import { resolve } from 'path'
@@ -22,8 +23,8 @@ describe('schema', () => {
const expected = readMetadata.sync(resolve(dir, 'output.yaml')) const expected = readMetadata.sync(resolve(dir, 'output.yaml'))
const schema = Schema.create(require(dir)) const schema = Schema.create(require(dir))
let state = Raw.deserialize(input, { terse: true }) const state = Raw.deserialize(input, { terse: true })
state = schema.transform(state) const editor = <Editor state={state} schema={schema} />
const output = Raw.serialize(state, { terse: true }) const output = Raw.serialize(state, { terse: true })
strictEqual(strip(output), strip(expected)) strictEqual(strip(output), strip(expected))
}) })