diff --git a/docs/reference/models/transform.md b/docs/reference/models/transform.md index 19c5abd67..ef7fe3584 100644 --- a/docs/reference/models/transform.md +++ b/docs/reference/models/transform.md @@ -59,6 +59,7 @@ Transform methods can either operate on the [`Document`](./document.md), the [`S - [`splitNodeByKey`](#splitnodebykey) - [`unwrapInlineByKey`](#unwrapinlinebykey) - [`unwrapBlockByKey`](#unwrapblockbykey) + - [`unwrapNodeByKey`](#unwrapnodebykey) - [`wrapBlockByKey`](#wrapblockbykey) - [`wrapInlineByKey`](#wrapinlinebykey) - [Document Transforms](#document-transforms) @@ -333,6 +334,11 @@ Unwrap all inner content of an [`Inline`](./inline.md) node that match `properti Unwrap all inner content of a [`Block`](./block.md) node that match `properties`. For convenience, you can pass a `type` string or `properties` object. +### `unwrapNodeByKey` +`unwrapNodeByKey(key: String) => Transform` + +Unwrap a single node from its parent. If the node is surrounded with siblings, its parent will be split. If the node is the only child, the parent is removed, and simply replaced by the node itself. Cannot unwrap a root node. + ### `wrapBlockByKey` `wrapBlockByKey(key: String, properties: Object) => Transform`
`wrapBlockByKey(key: String, type: String) => Transform` diff --git a/src/transforms/by-key.js b/src/transforms/by-key.js index 2cd3eccc8..a3298e709 100644 --- a/src/transforms/by-key.js +++ b/src/transforms/by-key.js @@ -276,7 +276,7 @@ export function splitNodeByKey(transform, key, offset, options = {}) { let { document } = state const path = document.getPath(key) - transform.splitNodeOperation(path, offset) + transform.splitNodeAtOffsetOperation(path, offset) if (normalize) { const parent = document.getParent(key) @@ -324,6 +324,63 @@ export function unwrapBlockByKey(transform, key, properties, options) { transform.unwrapBlockAtRange(range, properties, options) } +/** + * Unwrap a single node from its parent. + * + * If the node is surrounded with siblings, its parent will be + * split. If the node is the only child, the parent is removed, and + * simply replaced by the node itself. Cannot unwrap a root node. + * + * @param {Transform} transform + * @param {String} key + * @param {Object} options + * @property {Boolean} normalize + */ + +export function unwrapNodeByKey(transform, key, options = {}) { + const { normalize = true } = options + const { state } = transform + const { document } = state + const parent = document.getParent(key) + const node = parent.getChild(key) + + const index = parent.nodes.indexOf(node) + const isFirst = index === 0 + const isLast = index === parent.nodes.size - 1 + + const parentParent = document.getParent(parent.key) + const parentIndex = parentParent.nodes.indexOf(parent) + + + if (parent.nodes.size === 1) { + // Remove the parent + transform.removeNodeByKey(parent.key, { normalize: false }) + // and replace it by the node itself + transform.insertNodeByKey(parentParent.key, parentIndex, node, options) + } + + else if (isFirst) { + // Just move the node before its parent + transform.moveNodeByKey(key, parentParent.key, parentIndex, options) + } + + else if (isLast) { + // Just move the node after its parent + transform.moveNodeByKey(key, parentParent.key, parentIndex + 1, options) + } + + else { + const parentPath = document.getPath(parent.key) + // Split the parent + transform.splitNodeOperation(parentPath, index) + // Extract the node in between the splitted parent + transform.moveNodeByKey(key, parentParent.key, parentIndex + 1, { normalize: false }) + + if (normalize) { + transform.normalizeNodeByKey(parentParent.key, SCHEMA) + } + } +} /** * Wrap a node in an inline with `properties`. diff --git a/src/transforms/index.js b/src/transforms/index.js index 11c33f9b2..280901a63 100644 --- a/src/transforms/index.js +++ b/src/transforms/index.js @@ -29,6 +29,7 @@ import { setMarkOperation, setNodeOperation, setSelectionOperation, + splitNodeAtOffsetOperation, splitNodeOperation, } from './operations' @@ -114,6 +115,7 @@ import { splitNodeByKey, unwrapInlineByKey, unwrapBlockByKey, + unwrapNodeByKey, wrapBlockByKey, wrapInlineByKey, } from './by-key' @@ -211,6 +213,7 @@ export default { setMarkOperation, setNodeOperation, setSelectionOperation, + splitNodeAtOffsetOperation, splitNodeOperation, /** @@ -290,6 +293,7 @@ export default { splitNodeByKey, unwrapInlineByKey, unwrapBlockByKey, + unwrapNodeByKey, wrapBlockByKey, wrapInlineByKey, diff --git a/src/transforms/operations.js b/src/transforms/operations.js index 923919192..b6121989b 100644 --- a/src/transforms/operations.js +++ b/src/transforms/operations.js @@ -432,7 +432,7 @@ export function setSelectionOperation(transform, properties, options = {}) { * @param {Number} offset */ -export function splitNodeOperation(transform, path, offset) { +export function splitNodeAtOffsetOperation(transform, path, offset) { const inversePath = path.slice() inversePath[path.length - 1] += 1 @@ -440,13 +440,45 @@ export function splitNodeOperation(transform, path, offset) { type: 'join_node', path: inversePath, withPath: path, - deep: true // we need to join nodes recursively + // we will split down to the text nodes, so we must join nodes recursively + deep: true }] const operation = { type: 'split_node', path, offset, + count: null, + inverse, + } + + transform.applyOperation(operation) +} + +/** + * Split a node by `path` after its 'count' child. + * + * @param {Transform} transform + * @param {Array} path + * @param {Number} count + */ + +export function splitNodeOperation(transform, path, count) { + const inversePath = path.slice() + inversePath[path.length - 1] += 1 + + const inverse = [{ + type: 'join_node', + path: inversePath, + withPath: path, + deep: false + }] + + const operation = { + type: 'split_node', + path, + offset: null, + count, inverse, } diff --git a/test/transforms/fixtures/by-key/unwrap-node-by-key/first-sibling/index.js b/test/transforms/fixtures/by-key/unwrap-node-by-key/first-sibling/index.js new file mode 100644 index 000000000..536c233d8 --- /dev/null +++ b/test/transforms/fixtures/by-key/unwrap-node-by-key/first-sibling/index.js @@ -0,0 +1,7 @@ + +export default function (state) { + return state + .transform() + .unwrapNodeByKey('to-unwrap') + .apply() +} diff --git a/test/transforms/fixtures/by-key/unwrap-node-by-key/first-sibling/input.yaml b/test/transforms/fixtures/by-key/unwrap-node-by-key/first-sibling/input.yaml new file mode 100644 index 000000000..76ac4dd3a --- /dev/null +++ b/test/transforms/fixtures/by-key/unwrap-node-by-key/first-sibling/input.yaml @@ -0,0 +1,16 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + key: 'to-unwrap' + nodes: + - kind: text + text: word1 + - kind: block + type: paragraph + nodes: + - kind: text + text: word2 diff --git a/test/transforms/fixtures/by-key/unwrap-node-by-key/first-sibling/output.yaml b/test/transforms/fixtures/by-key/unwrap-node-by-key/first-sibling/output.yaml new file mode 100644 index 000000000..c6de08c0c --- /dev/null +++ b/test/transforms/fixtures/by-key/unwrap-node-by-key/first-sibling/output.yaml @@ -0,0 +1,15 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word1 + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word2 diff --git a/test/transforms/fixtures/by-key/unwrap-node-by-key/last-sibling/index.js b/test/transforms/fixtures/by-key/unwrap-node-by-key/last-sibling/index.js new file mode 100644 index 000000000..536c233d8 --- /dev/null +++ b/test/transforms/fixtures/by-key/unwrap-node-by-key/last-sibling/index.js @@ -0,0 +1,7 @@ + +export default function (state) { + return state + .transform() + .unwrapNodeByKey('to-unwrap') + .apply() +} diff --git a/test/transforms/fixtures/by-key/unwrap-node-by-key/last-sibling/input.yaml b/test/transforms/fixtures/by-key/unwrap-node-by-key/last-sibling/input.yaml new file mode 100644 index 000000000..8f064ed90 --- /dev/null +++ b/test/transforms/fixtures/by-key/unwrap-node-by-key/last-sibling/input.yaml @@ -0,0 +1,16 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word1 + - kind: block + type: paragraph + key: 'to-unwrap' + nodes: + - kind: text + text: word2 diff --git a/test/transforms/fixtures/by-key/unwrap-node-by-key/last-sibling/output.yaml b/test/transforms/fixtures/by-key/unwrap-node-by-key/last-sibling/output.yaml new file mode 100644 index 000000000..b5ce4de3d --- /dev/null +++ b/test/transforms/fixtures/by-key/unwrap-node-by-key/last-sibling/output.yaml @@ -0,0 +1,15 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word1 + - kind: block + type: paragraph + nodes: + - kind: text + text: word2 diff --git a/test/transforms/fixtures/by-key/unwrap-node-by-key/single-block/index.js b/test/transforms/fixtures/by-key/unwrap-node-by-key/single-block/index.js new file mode 100644 index 000000000..536c233d8 --- /dev/null +++ b/test/transforms/fixtures/by-key/unwrap-node-by-key/single-block/index.js @@ -0,0 +1,7 @@ + +export default function (state) { + return state + .transform() + .unwrapNodeByKey('to-unwrap') + .apply() +} diff --git a/test/transforms/fixtures/by-key/unwrap-node-by-key/single-block/input.yaml b/test/transforms/fixtures/by-key/unwrap-node-by-key/single-block/input.yaml new file mode 100644 index 000000000..fee4eb30f --- /dev/null +++ b/test/transforms/fixtures/by-key/unwrap-node-by-key/single-block/input.yaml @@ -0,0 +1,19 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + key: 'to-unwrap' + type: paragraph + nodes: + - kind: text + text: word + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word diff --git a/test/transforms/fixtures/by-key/unwrap-node-by-key/single-block/output.yaml b/test/transforms/fixtures/by-key/unwrap-node-by-key/single-block/output.yaml new file mode 100644 index 000000000..c5a0f1a30 --- /dev/null +++ b/test/transforms/fixtures/by-key/unwrap-node-by-key/single-block/output.yaml @@ -0,0 +1,15 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word diff --git a/test/transforms/fixtures/by-key/unwrap-node-by-key/with-siblings/index.js b/test/transforms/fixtures/by-key/unwrap-node-by-key/with-siblings/index.js new file mode 100644 index 000000000..536c233d8 --- /dev/null +++ b/test/transforms/fixtures/by-key/unwrap-node-by-key/with-siblings/index.js @@ -0,0 +1,7 @@ + +export default function (state) { + return state + .transform() + .unwrapNodeByKey('to-unwrap') + .apply() +} diff --git a/test/transforms/fixtures/by-key/unwrap-node-by-key/with-siblings/input.yaml b/test/transforms/fixtures/by-key/unwrap-node-by-key/with-siblings/input.yaml new file mode 100644 index 000000000..eac9dbc1b --- /dev/null +++ b/test/transforms/fixtures/by-key/unwrap-node-by-key/with-siblings/input.yaml @@ -0,0 +1,21 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word1 + - kind: block + type: paragraph + key: 'to-unwrap' + nodes: + - kind: text + text: word2 + - kind: block + type: paragraph + nodes: + - kind: text + text: word3 diff --git a/test/transforms/fixtures/by-key/unwrap-node-by-key/with-siblings/output.yaml b/test/transforms/fixtures/by-key/unwrap-node-by-key/with-siblings/output.yaml new file mode 100644 index 000000000..eed18bd3d --- /dev/null +++ b/test/transforms/fixtures/by-key/unwrap-node-by-key/with-siblings/output.yaml @@ -0,0 +1,23 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word1 + - kind: block + type: paragraph + nodes: + - kind: text + text: word2 + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word3 diff --git a/test/transforms/fixtures/on-history/undo/unwrap-node-by-key/index.js b/test/transforms/fixtures/on-history/undo/unwrap-node-by-key/index.js new file mode 100644 index 000000000..911da2ab6 --- /dev/null +++ b/test/transforms/fixtures/on-history/undo/unwrap-node-by-key/index.js @@ -0,0 +1,11 @@ + +export default function (state) { + return state + .transform() + .unwrapNodeByKey('to-unwrap') + .apply() + + .transform() + .undo() + .apply() +} diff --git a/test/transforms/fixtures/on-history/undo/unwrap-node-by-key/input.yaml b/test/transforms/fixtures/on-history/undo/unwrap-node-by-key/input.yaml new file mode 100644 index 000000000..eac9dbc1b --- /dev/null +++ b/test/transforms/fixtures/on-history/undo/unwrap-node-by-key/input.yaml @@ -0,0 +1,21 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word1 + - kind: block + type: paragraph + key: 'to-unwrap' + nodes: + - kind: text + text: word2 + - kind: block + type: paragraph + nodes: + - kind: text + text: word3 diff --git a/test/transforms/fixtures/on-history/undo/unwrap-node-by-key/output.yaml b/test/transforms/fixtures/on-history/undo/unwrap-node-by-key/output.yaml new file mode 100644 index 000000000..a99daffea --- /dev/null +++ b/test/transforms/fixtures/on-history/undo/unwrap-node-by-key/output.yaml @@ -0,0 +1,20 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word1 + - kind: block + type: paragraph + nodes: + - kind: text + text: word2 + - kind: block + type: paragraph + nodes: + - kind: text + text: word3