From 8692d1a98ab2addb16a4280d514f7e9cefb89262 Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Sat, 13 Aug 2016 16:18:07 -0700 Subject: [PATCH] working on moving components into the schema --- lib/components/editor.js | 36 +++-- lib/models/mark.js | 11 ++ lib/models/node.js | 126 +++++++++++------- lib/models/schema.js | 115 +++++++--------- lib/models/state.js | 36 ++++- lib/models/text.js | 14 +- lib/plugins/core.js | 126 +++++++++--------- package.json | 4 +- .../default-no-block-void-text/index.js | 2 - .../default-no-block-void-text/input.yaml | 8 -- .../default-no-block-void-text/output.yaml | 5 - .../default-no-inline-void-text/index.js | 2 - .../default-no-inline-void-text/input.yaml | 11 -- .../default-no-inline-void-text/output.yaml | 8 -- test/schema/index.js | 7 +- 15 files changed, 273 insertions(+), 238 deletions(-) delete mode 100644 test/schema/fixtures/default-no-block-void-text/index.js delete mode 100644 test/schema/fixtures/default-no-block-void-text/input.yaml delete mode 100644 test/schema/fixtures/default-no-block-void-text/output.yaml delete mode 100644 test/schema/fixtures/default-no-inline-void-text/index.js delete mode 100644 test/schema/fixtures/default-no-inline-void-text/input.yaml delete mode 100644 test/schema/fixtures/default-no-inline-void-text/output.yaml diff --git a/lib/components/editor.js b/lib/components/editor.js index 82a2ab1cc..7b2197b5a 100644 --- a/lib/components/editor.js +++ b/lib/components/editor.js @@ -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 + } + } /** diff --git a/lib/models/mark.js b/lib/models/mark.js index 22761c57d..f61db8fba 100644 --- a/lib/models/mark.js +++ b/lib/models/mark.js @@ -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) + } + } /** diff --git a/lib/models/node.js b/lib/models/node.js index 7c38699bc..0c0b50d73 100644 --- a/lib/models/node.js +++ b/lib/models/node.js @@ -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. */ diff --git a/lib/models/schema.js b/lib/models/schema.js index 588c65bdd..4d1c10fa9 100644 --- a/lib/models/schema.js +++ b/lib/models/schema.js @@ -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, diff --git a/lib/models/state.js b/lib/models/state.js index fba38c24b..479f51bf3 100644 --- a/lib/models/state.js +++ b/lib/models/state.js @@ -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. * diff --git a/lib/models/text.js b/lib/models/text.js index 5c47e60e8..0d0f7ab24 100644 --- a/lib/models/text.js +++ b/lib/models/text.js @@ -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' ]) /** diff --git a/lib/plugins/core.js b/lib/plugins/core.js index d48780a67..3f61cea8e 100644 --- a/lib/plugins/core.js +++ b/lib/plugins/core.js @@ -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 } } diff --git a/package.json b/package.json index 43af37141..5499b0e30 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/test/schema/fixtures/default-no-block-void-text/index.js b/test/schema/fixtures/default-no-block-void-text/index.js deleted file mode 100644 index 9a676e861..000000000 --- a/test/schema/fixtures/default-no-block-void-text/index.js +++ /dev/null @@ -1,2 +0,0 @@ - -export default {} diff --git a/test/schema/fixtures/default-no-block-void-text/input.yaml b/test/schema/fixtures/default-no-block-void-text/input.yaml deleted file mode 100644 index 224990dfb..000000000 --- a/test/schema/fixtures/default-no-block-void-text/input.yaml +++ /dev/null @@ -1,8 +0,0 @@ - -nodes: - - kind: block - type: default - isVoid: true - nodes: - - kind: text - text: one diff --git a/test/schema/fixtures/default-no-block-void-text/output.yaml b/test/schema/fixtures/default-no-block-void-text/output.yaml deleted file mode 100644 index e80ded17a..000000000 --- a/test/schema/fixtures/default-no-block-void-text/output.yaml +++ /dev/null @@ -1,5 +0,0 @@ - -nodes: - - kind: block - type: default - isVoid: true diff --git a/test/schema/fixtures/default-no-inline-void-text/index.js b/test/schema/fixtures/default-no-inline-void-text/index.js deleted file mode 100644 index 9a676e861..000000000 --- a/test/schema/fixtures/default-no-inline-void-text/index.js +++ /dev/null @@ -1,2 +0,0 @@ - -export default {} diff --git a/test/schema/fixtures/default-no-inline-void-text/input.yaml b/test/schema/fixtures/default-no-inline-void-text/input.yaml deleted file mode 100644 index 970dd66a4..000000000 --- a/test/schema/fixtures/default-no-inline-void-text/input.yaml +++ /dev/null @@ -1,11 +0,0 @@ - -nodes: - - kind: block - type: default - nodes: - - kind: inline - type: default - isVoid: true - nodes: - - kind: text - text: one diff --git a/test/schema/fixtures/default-no-inline-void-text/output.yaml b/test/schema/fixtures/default-no-inline-void-text/output.yaml deleted file mode 100644 index 5060e5507..000000000 --- a/test/schema/fixtures/default-no-inline-void-text/output.yaml +++ /dev/null @@ -1,8 +0,0 @@ - -nodes: - - kind: block - type: default - nodes: - - kind: inline - type: default - isVoid: true diff --git a/test/schema/index.js b/test/schema/index.js index 046569ba3..16ae590ad 100644 --- a/test/schema/index.js +++ b/test/schema/index.js @@ -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 = const output = Raw.serialize(state, { terse: true }) strictEqual(strip(output), strip(expected)) })