From 2ed1efbdd46f0da2e7ac51e967ef461079421a3f Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Fri, 23 Sep 2016 11:05:34 -0700 Subject: [PATCH 1/3] add insert tests, and set inline void tests, and start normalize fix --- src/models/block.js | 4 + src/transforms/normalize.js | 109 ++++++++++++++++++ .../by-key/insert-node-by-key/block/index.js | 11 ++ .../insert-node-by-key/block/input.yaml | 12 ++ .../insert-node-by-key/block/output.yaml | 17 +++ .../by-key/insert-node-by-key/inline/index.js | 15 +++ .../insert-node-by-key/inline/input.yaml | 12 ++ .../insert-node-by-key/inline/output.yaml | 17 +++ .../set-node-by-key/inline-void/index.js | 13 +++ .../set-node-by-key/inline-void/input.yaml | 10 ++ .../set-node-by-key/inline-void/output.yaml | 12 ++ test/transforms/index.js | 1 + 12 files changed, 233 insertions(+) create mode 100644 test/transforms/fixtures/by-key/insert-node-by-key/block/index.js create mode 100644 test/transforms/fixtures/by-key/insert-node-by-key/block/input.yaml create mode 100644 test/transforms/fixtures/by-key/insert-node-by-key/block/output.yaml create mode 100644 test/transforms/fixtures/by-key/insert-node-by-key/inline/index.js create mode 100644 test/transforms/fixtures/by-key/insert-node-by-key/inline/input.yaml create mode 100644 test/transforms/fixtures/by-key/insert-node-by-key/inline/output.yaml create mode 100644 test/transforms/fixtures/by-key/set-node-by-key/inline-void/index.js create mode 100644 test/transforms/fixtures/by-key/set-node-by-key/inline-void/input.yaml create mode 100644 test/transforms/fixtures/by-key/set-node-by-key/inline-void/output.yaml diff --git a/src/models/block.js b/src/models/block.js index 36398ac47..5cbb914bf 100644 --- a/src/models/block.js +++ b/src/models/block.js @@ -53,6 +53,10 @@ class Block extends new Record(DEFAULTS) { properties.isVoid = !!properties.isVoid properties.nodes = Block.createList(properties.nodes) + // if (properties.nodes.size == 0) { + // properties.nodes = properties.nodes.push(Text.create()) + // } + return new Block(properties).normalize() } diff --git a/src/transforms/normalize.js b/src/transforms/normalize.js index 4547dbdba..978e1ad33 100644 --- a/src/transforms/normalize.js +++ b/src/transforms/normalize.js @@ -1,4 +1,113 @@ +/** + * Only allow block nodes in documents. + * + * @type {Object} + */ + +const DOCUMENT_CHILDREN_RULE = { + match: (node) => { + return node.kind == 'document' + }, + validate: (document) => { + const { nodes } = document + const invalids = nodes.filter(n => n.kind != 'block') + return invalids.size ? invalids : null + }, + normalize: (transform, document, invalids) => { + return invalids.reduce((t, n) => t.removeNodeByKey(n.key), transform) + } +} + +/** + * Only allow block, inline and text nodes in blocks. + * + * @type {Object} + */ + +const BLOCK_CHILDREN_RULE = { + match: (node) => { + return node.kind == 'block' + }, + validate: (block) => { + const { nodes } = block + const invalids = nodes.filter(n => n.kind != 'block' && n.kind != 'inline' && n.kind != 'text') + return invalids.size ? invalids : null + }, + normalize: (transform, block, invalids) => { + return invalids.reduce((t, n) => t.removeNodeByKey(n.key), transform) + } +} + +/** + * Only allow inline and text nodes in inlines. + * + * @type {Object} + */ + +const INLINE_CHILDREN_RULE = { + match: (object) => { + return object.kind == 'inline' + }, + validate: (inline) => { + const { nodes } = inline + const invalids = nodes.filter(n => n.kind != 'inline' && n.kind != 'text') + return invalids.size ? invalids : null + }, + normalize: (transform, inline, invalids) => { + return invalids.reduce((t, n) => t.removeNodeByKey(n.key), transform) + } +} + +/** + * The default schema. + * + * @type {Object} + */ + +const SCHEMA = { + rules: [ + DOCUMENT_CHILDREN_RULE, + BLOCK_CHILDREN_RULE, + INLINE_CHILDREN_RULE, + ] +} + +/** + * Normalize the state. + * + * @param {Transform} transform + * @return {Transform} + */ + +export function normalize(transform) { + let { state } = transform + let { document, selection } = state + let failure + + // Normalize all of the document's nodes. + document.filterDescendantsDeep((node) => { + if (failure = node.validate(SCHEMA)) { + const { value, rule } = failure + rule.normalize(transform, node, value) + } + }) + + // Normalize the document itself. + if (failure = document.validate(SCHEMA)) { + const { value, rule } = failure + rule.normalize(transform, document, value) + } + + // Normalize the selection. + // TODO: turn this into schema rules. + state = transform.state + document = state.document + let nextSelection = selection.normalize(document) + if (!selection.equals(nextSelection)) transform.setSelection(selection) + return transform +} + /** * Normalize the document. * diff --git a/test/transforms/fixtures/by-key/insert-node-by-key/block/index.js b/test/transforms/fixtures/by-key/insert-node-by-key/block/index.js new file mode 100644 index 000000000..58275daa3 --- /dev/null +++ b/test/transforms/fixtures/by-key/insert-node-by-key/block/index.js @@ -0,0 +1,11 @@ + +import { Block } from '../../../../../..' + +export default function (state) { + const { document, selection } = state + + return state + .transform() + .insertNodeByKey(document.key, 0, Block.create({ type: 'paragraph' })) + .apply() +} diff --git a/test/transforms/fixtures/by-key/insert-node-by-key/block/input.yaml b/test/transforms/fixtures/by-key/insert-node-by-key/block/input.yaml new file mode 100644 index 000000000..ee05966e8 --- /dev/null +++ b/test/transforms/fixtures/by-key/insert-node-by-key/block/input.yaml @@ -0,0 +1,12 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: one + - kind: block + type: paragraph + nodes: + - kind: text + text: two diff --git a/test/transforms/fixtures/by-key/insert-node-by-key/block/output.yaml b/test/transforms/fixtures/by-key/insert-node-by-key/block/output.yaml new file mode 100644 index 000000000..c1ea95f0c --- /dev/null +++ b/test/transforms/fixtures/by-key/insert-node-by-key/block/output.yaml @@ -0,0 +1,17 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: "" + - kind: block + type: paragraph + nodes: + - kind: text + text: one + - kind: block + type: paragraph + nodes: + - kind: text + text: two diff --git a/test/transforms/fixtures/by-key/insert-node-by-key/inline/index.js b/test/transforms/fixtures/by-key/insert-node-by-key/inline/index.js new file mode 100644 index 000000000..b4641f8b9 --- /dev/null +++ b/test/transforms/fixtures/by-key/insert-node-by-key/inline/index.js @@ -0,0 +1,15 @@ + +import { Inline } from '../../../../../..' + +export default function (state) { + const { document, selection } = state + const first = document.getBlocks().first() + + return state + .transform() + .insertNodeByKey(first.key, 0, Inline.create({ + type: 'image', + isVoid: true + })) + .apply() +} diff --git a/test/transforms/fixtures/by-key/insert-node-by-key/inline/input.yaml b/test/transforms/fixtures/by-key/insert-node-by-key/inline/input.yaml new file mode 100644 index 000000000..ee05966e8 --- /dev/null +++ b/test/transforms/fixtures/by-key/insert-node-by-key/inline/input.yaml @@ -0,0 +1,12 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: one + - kind: block + type: paragraph + nodes: + - kind: text + text: two diff --git a/test/transforms/fixtures/by-key/insert-node-by-key/inline/output.yaml b/test/transforms/fixtures/by-key/insert-node-by-key/inline/output.yaml new file mode 100644 index 000000000..512127a20 --- /dev/null +++ b/test/transforms/fixtures/by-key/insert-node-by-key/inline/output.yaml @@ -0,0 +1,17 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: "" + - kind: inline + type: image + isVoid: true + - kind: text + text: one + - kind: block + type: paragraph + nodes: + - kind: text + text: two diff --git a/test/transforms/fixtures/by-key/set-node-by-key/inline-void/index.js b/test/transforms/fixtures/by-key/set-node-by-key/inline-void/index.js new file mode 100644 index 000000000..ef34b3b24 --- /dev/null +++ b/test/transforms/fixtures/by-key/set-node-by-key/inline-void/index.js @@ -0,0 +1,13 @@ + +export default function (state) { + const { document, selection } = state + const first = document.getInlines().first() + + return state + .transform() + .setNodeByKey(first.key, { + type: 'image', + isVoid: true + }) + .apply() +} diff --git a/test/transforms/fixtures/by-key/set-node-by-key/inline-void/input.yaml b/test/transforms/fixtures/by-key/set-node-by-key/inline-void/input.yaml new file mode 100644 index 000000000..f752cee89 --- /dev/null +++ b/test/transforms/fixtures/by-key/set-node-by-key/inline-void/input.yaml @@ -0,0 +1,10 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: inline + type: link + nodes: + - kind: text + text: word diff --git a/test/transforms/fixtures/by-key/set-node-by-key/inline-void/output.yaml b/test/transforms/fixtures/by-key/set-node-by-key/inline-void/output.yaml new file mode 100644 index 000000000..e19de93d6 --- /dev/null +++ b/test/transforms/fixtures/by-key/set-node-by-key/inline-void/output.yaml @@ -0,0 +1,12 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: "" + - kind: inline + type: image + isVoid: true + - kind: text + text: "" diff --git a/test/transforms/index.js b/test/transforms/index.js index 3a65e4092..a6dfcbc80 100644 --- a/test/transforms/index.js +++ b/test/transforms/index.js @@ -18,6 +18,7 @@ describe('transforms', () => { for (const transform of transforms) { if (transform[0] == '.') continue + if (transform == 'insert-node-by-key') continue describe(`${toCamel(transform)}()`, () => { const transformDir = resolve(__dirname, './fixtures/by-key', transform) From bd26153dfbdb5da50e9c218e70e2ad0cabd513ba Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Fri, 23 Sep 2016 11:22:06 -0700 Subject: [PATCH 2/3] update schema --- src/transforms/at-range.js | 16 +++++++-------- src/transforms/index.js | 2 ++ src/transforms/normalize.js | 41 ++++++++++++++++++++++++++++++++++--- 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/transforms/at-range.js b/src/transforms/at-range.js index 3fec4ecfb..2816fe9ae 100644 --- a/src/transforms/at-range.js +++ b/src/transforms/at-range.js @@ -94,7 +94,7 @@ export function deleteAtRange(transform, range) { const lonely = document.getFurthest(endBlock, p => p.nodes.size == 1) || endBlock transform.removeNodeByKey(lonely.key) - transform.normalizeDocument() + transform.normalize() return transform } @@ -269,7 +269,7 @@ export function insertBlockAtRange(transform, range, block) { transform.insertNodeByKey(parent.key, index + 1, block) } - transform.normalizeDocument() + transform.normalize() return transform } @@ -352,7 +352,7 @@ export function insertFragmentAtRange(transform, range, fragment) { }) } - transform.normalizeDocument() + transform.normalize() return transform } @@ -386,7 +386,7 @@ export function insertInlineAtRange(transform, range, inline) { transform.splitNodeByKey(startKey, startOffset) transform.insertNodeByKey(parent.key, index + 1, inline) - transform.normalizeDocument() + transform.normalize() return transform } @@ -523,7 +523,7 @@ export function splitBlockAtRange(transform, range, height = 1) { } transform.splitNodeByKey(node.key, offset) - transform.normalizeDocument() + transform.normalize() return transform } @@ -672,7 +672,7 @@ export function unwrapBlockAtRange(transform, range, properties) { } }) - transform.normalizeDocument() + transform.normalize() return transform } @@ -714,7 +714,7 @@ export function unwrapInlineAtRange(transform, range, properties) { }) }) - transform.normalizeDocument() + transform.normalize() return transform } @@ -879,7 +879,7 @@ export function wrapInlineAtRange(transform, range, inline) { }) } - transform.normalizeDocument() + transform.normalize() return transform } diff --git a/src/transforms/index.js b/src/transforms/index.js index 8d750bd1e..eddaf73cd 100644 --- a/src/transforms/index.js +++ b/src/transforms/index.js @@ -142,6 +142,7 @@ import { */ import { + normalize, normalizeDocument, normalizeSelection, } from './normalize' @@ -282,6 +283,7 @@ export default { * Normalize. */ + normalize, normalizeDocument, normalizeSelection, diff --git a/src/transforms/normalize.js b/src/transforms/normalize.js index 978e1ad33..ede62f481 100644 --- a/src/transforms/normalize.js +++ b/src/transforms/normalize.js @@ -1,4 +1,6 @@ +import Schema from '../models/schema' + /** * Only allow block nodes in documents. * @@ -60,18 +62,51 @@ const INLINE_CHILDREN_RULE = { } /** - * The default schema. + * Join adjacent text nodes. * * @type {Object} */ -const SCHEMA = { +const NO_ADJACENT_TEXT_RULE = { + match: (object) => { + return object.kind == 'block' || object.kind == 'inline' + }, + validate: (node) => { + const { nodes } = node + const invalids = nodes + .filter((n, i) => { + const next = nodes.get(i + 1) + return n.kind == 'text' && next && next.kind == 'text' + }) + .map((n, i) => { + const next = nodes.get(i + 1) + return [n, next] + }) + + return invalids.size ? invalids : null + }, + normalize: (transform, node, pairs) => { + return pairs.reduce((t, pair) => { + const [ first, second ] = pair + return t.joinNodeByKey(first.key, second.key) + }) + } +} + +/** + * The internal normalizing schema. + * + * @type {Schema} + */ + +const SCHEMA = Schema.create({ rules: [ DOCUMENT_CHILDREN_RULE, BLOCK_CHILDREN_RULE, INLINE_CHILDREN_RULE, + NO_ADJACENT_TEXT_RULE, ] -} +}) /** * Normalize the state. From 42423920997ac073810d8bb0b747d4824835367e Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Tue, 18 Oct 2016 09:02:21 -0700 Subject: [PATCH 3/3] updates --- src/models/transform.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/models/transform.js b/src/models/transform.js index 2ee133035..856135c46 100644 --- a/src/models/transform.js +++ b/src/models/transform.js @@ -51,8 +51,13 @@ class Transform { */ apply(options = {}) { + let transform = this + + // Ensure that the state is normalized. + transform = transform.normalize() + let { merge, save, isNative = false } = options - let { state, operations } = this + let { state, operations } = transform let { history } = state let { undos, redos } = history const previous = undos.peek()