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

refactor transforms to be modularized

This commit is contained in:
Ian Storm Taylor
2016-08-16 12:10:01 -07:00
parent 0413d93305
commit 183abf8f7c
17 changed files with 2387 additions and 2174 deletions

View File

@@ -29,6 +29,12 @@ import Html from './serializers/html'
import Plain from './serializers/plain'
import Raw from './serializers/raw'
/**
* Transforms.
*/
import Transforms from './transforms'
/**
* Utils.
*/
@@ -37,6 +43,8 @@ import findDOMNode from './utils/find-dom-node'
/**
* Export.
*
* @type {Object}
*/
export {
@@ -55,6 +63,7 @@ export {
Selection,
State,
Text,
Transforms,
findDOMNode
}
@@ -74,5 +83,6 @@ export default {
Selection,
State,
Text,
Transforms,
findDOMNode
}

View File

@@ -6,9 +6,9 @@ import Document from './document'
import Inline from './inline'
import Mark from './mark'
import Selection from './selection'
import Transforms from './transforms'
import Text from './text'
import direction from 'direction'
import isInRange from '../utils/is-in-range'
import includes from 'lodash/includes'
import memoize from '../utils/memoize'
import uid from '../utils/uid'
@@ -1198,38 +1198,6 @@ function normalizeKey(key) {
return key.key
}
/**
* Check if an `index` of a `text` node is in a `range`.
*
* @param {Number} index
* @param {Text} text
* @param {Selection} range
* @return {Set} characters
*/
function isInRange(index, text, range) {
const { startKey, startOffset, endKey, endOffset } = range
let matcher
if (text.key == startKey && text.key == endKey) {
return startOffset <= index && index < endOffset
} else if (text.key == startKey) {
return startOffset <= index
} else if (text.key == endKey) {
return index < endOffset
} else {
return true
}
}
/**
* Transforms.
*/
for (const key in Transforms) {
Node[key] = Transforms[key]
}
/**
* Export.
*/

View File

@@ -399,837 +399,6 @@ class State extends new Record(DEFAULTS) {
return new Transform({ state })
}
/**
* Add a `mark` to the characters in the current selection.
*
* @param {Mark} mark
* @return {State} state
*/
addMark(mark) {
mark = normalizeMark(mark)
let state = this
let { cursorMarks, document, selection } = state
// If the selection is collapsed, add the mark to the cursor instead.
if (selection.isCollapsed) {
const marks = document.getMarksAtRange(selection)
state = state.merge({ cursorMarks: marks.add(mark) })
return state
}
document = document.addMarkAtRange(selection, mark)
state = state.merge({ document })
return state
}
/**
* Move the selection to the start of the previous block.
*
* @return {State} state
*/
collapseToStartOfPreviousBlock() {
let state = this
let { document, selection } = state
let blocks = document.getBlocksAtRange(selection)
let block = blocks.first()
if (!block) return state
let previous = document.getPreviousBlock(block)
if (!previous) return state
selection = selection.collapseToStartOf(previous)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the end of the previous block.
*
* @return {State} state
*/
collapseToEndOfPreviousBlock() {
let state = this
let { document, selection } = state
let blocks = document.getBlocksAtRange(selection)
let block = blocks.first()
if (!block) return state
let previous = document.getPreviousBlock(block)
if (!previous) return state
selection = selection.collapseToEndOf(previous)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the start of the next block.
*
* @return {State} state
*/
collapseToStartOfNextBlock() {
let state = this
let { document, selection } = state
let blocks = document.getBlocksAtRange(selection)
let block = blocks.last()
if (!block) return state
let next = document.getNextBlock(block)
if (!next) return state
selection = selection.collapseToStartOf(next)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the end of the next block.
*
* @return {State} state
*/
collapseToEndOfNextBlock() {
let state = this
let { document, selection } = state
let blocks = document.getBlocksAtRange(selection)
let block = blocks.last()
if (!block) return state
let next = document.getNextBlock(block)
if (!next) return state
selection = selection.collapseToEndOf(next)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the start of the previous text.
*
* @return {State} state
*/
collapseToStartOfPreviousText() {
let state = this
let { document, selection } = state
let texts = document.getTextsAtRange(selection)
let text = texts.first()
if (!text) return state
let previous = document.getPreviousText(text)
if (!previous) return state
selection = selection.collapseToStartOf(previous)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the end of the previous text.
*
* @return {State} state
*/
collapseToEndOfPreviousText() {
let state = this
let { document, selection } = state
let texts = document.getTextsAtRange(selection)
let text = texts.first()
if (!text) return state
let previous = document.getPreviousText(text)
if (!previous) return state
selection = selection.collapseToEndOf(previous)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the start of the next text.
*
* @return {State} state
*/
collapseToStartOfNextText() {
let state = this
let { document, selection } = state
let texts = document.getTextsAtRange(selection)
let text = texts.last()
if (!text) return state
let next = document.getNextText(text)
if (!next) return state
selection = selection.collapseToStartOf(next)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the end of the next text.
*
* @return {State} state
*/
collapseToEndOfNextText() {
let state = this
let { document, selection } = state
let texts = document.getTextsAtRange(selection)
let text = texts.last()
if (!text) return state
let next = document.getNextText(text)
if (!next) return state
selection = selection.collapseToEndOf(next)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Delete at the current selection.
*
* @return {State} state
*/
delete() {
let state = this
let { document, selection } = state
let after
// When collapsed, there's nothing to do.
if (selection.isCollapsed) return state
// Determine what the selection will be after deleting.
const { startText } = this
const { startKey, startOffset, endKey, endOffset } = selection
const block = document.getClosestBlock(startText)
const highest = block.getHighestChild(startText)
const previous = block.getPreviousSibling(highest)
const next = block.getNextSibling(highest)
if (
previous &&
startOffset == 0 &&
(endKey != startKey || endOffset == startText.length)
) {
if (previous.kind == 'text') {
if (next && next.kind == 'text') {
after = selection.merge({
anchorKey: previous.key,
anchorOffset: previous.length,
focusKey: previous.key,
focusOffset: previous.length
})
} else {
after = selection.collapseToEndOf(previous)
}
} else {
const last = previous.getTexts().last()
after = selection.collapseToEndOf(last)
}
}
else {
after = selection.collapseToStart()
}
// Delete and update the selection.
document = document.deleteAtRange(selection)
selection = after
state = state.merge({ document, selection })
return state
}
/**
* Delete backward `n` characters at the current selection.
*
* @param {Number} n (optional)
* @return {State} state
*/
deleteBackward(n = 1) {
let state = this
let { document, selection } = state
let after = selection
// Determine what the selection should be after deleting.
const { startKey } = selection
const startNode = document.getDescendant(startKey)
if (selection.isExpanded) {
after = selection.collapseToStart()
}
else if (selection.isAtStartOf(document)) {
after = selection
}
else if (selection.isAtStartOf(startNode)) {
const previous = document.getPreviousText(startNode)
const prevBlock = document.getClosestBlock(previous)
const prevInline = document.getClosestInline(previous)
if (prevBlock && prevBlock.isVoid) {
after = selection
} else if (prevInline && prevInline.isVoid) {
after = selection
} else {
after = selection.collapseToEndOf(previous)
}
}
else if (selection.isAtEndOf(startNode) && startNode.length == 1) {
const block = document.getClosestBlock(startKey)
const highest = block.getHighestChild(startKey)
const previous = block.getPreviousSibling(highest)
const next = block.getNextSibling(highest)
if (previous) {
if (previous.kind == 'text') {
if (next && next.kind == 'text') {
after = selection.merge({
anchorKey: previous.key,
anchorOffset: previous.length,
focusKey: previous.key,
focusOffset: previous.length
})
} else {
after = selection.collapseToEndOf(previous)
}
} else {
const last = previous.getTexts().last()
after = selection.collapseToEndOf(last)
}
} else {
after = selection.moveBackward(n)
}
}
else {
after = selection.moveBackward(n)
}
// Delete backward and then update the selection.
document = document.deleteBackwardAtRange(selection, n)
selection = after
state = state.merge({ document, selection })
return state
}
/**
* Delete forward `n` characters at the current selection.
*
* @param {Number} n (optional)
* @return {State} state
*/
deleteForward(n = 1) {
let state = this
let { document, selection, startText } = state
let { startKey, startOffset } = selection
let after = selection
// Determine what the selection should be after deleting.
const block = document.getClosestBlock(startKey)
const inline = document.getClosestInline(startKey)
const highest = block.getHighestChild(startKey)
const previous = block.getPreviousSibling(highest)
const next = block.getNextSibling(highest)
if (selection.isExpanded) {
after = selection.collapseToStart()
}
else if ((block && block.isVoid) || (inline && inline.isVoid)) {
const nextText = document.getNextText(startKey)
const prevText = document.getPreviousText(startKey)
after = next
? selection.collapseToStartOf(nextText)
: selection.collapseToEndOf(prevText)
}
else if (previous && startOffset == 0 && startText.length == 1) {
if (previous.kind == 'text') {
if (next && next.kind == 'text') {
after = selection.merge({
anchorKey: previous.key,
anchorOffset: previous.length,
focusKey: previous.key,
focusOffset: previous.length
})
} else {
after = selection.collapseToEndOf(previous)
}
} else {
const last = previous.getTexts().last()
after = selection.collapseToEndOf(last)
}
}
// Delete forward and then update the selection.
document = document.deleteForwardAtRange(selection, n)
selection = after
state = state.merge({ document, selection })
return state
}
/**
* Insert a `block` at the current selection.
*
* @param {String || Object || Block} block
* @return {State} state
*/
insertBlock(block) {
let state = this
let { document, selection } = state
let after = selection
// Insert the block
document = document.insertBlockAtRange(selection, block)
// Determine what the selection should be after inserting.
const keys = state.document.getTexts().map(text => text.key)
const text = document.getTexts().find(n => !keys.includes(n.key))
selection = selection.collapseToEndOf(text)
// Update the document and selection.
state = state.merge({ document, selection })
return state
}
/**
* Insert a `fragment` at the current selection.
*
* @param {Document} fragment
* @return {State} state
*/
insertFragment(fragment) {
let state = this
let { document, selection } = state
let after = selection
// If there's nothing in the fragment, do nothing.
if (!fragment.length) return state
// Lookup some nodes for determining the selection next.
const lastText = fragment.getTexts().last()
const lastInline = fragment.getClosestInline(lastText)
const beforeTexts = document.getTexts()
// Insert the fragment.
document = document.insertFragmentAtRange(selection, fragment)
// Determine what the selection should be after inserting.
const keys = beforeTexts.map(text => text.key)
const text = document.getTexts().findLast(n => !keys.includes(n.key))
const previousText = text ? document.getPreviousText(text) : null
if (text && lastInline && previousText) {
after = selection.collapseToEndOf(previousText)
}
else if (text && lastInline) {
after = selection.collapseToStart()
}
else if (text) {
after = selection
.collapseToStartOf(text)
.moveForward(lastText.length)
}
else {
after = selection
.collapseToStart()
.moveForward(lastText.length)
}
// Update the document and selection.
selection = after
state = state.merge({ document, selection })
return state
}
/**
* Insert a `inline` at the current selection.
*
* @param {String || Object || Block} inline
* @return {State} state
*/
insertInline(inline) {
let state = this
let { document, selection, startText } = state
let after = selection
const hasVoid = document.hasVoidParent(startText)
// Insert the inline
document = document.insertInlineAtRange(selection, inline)
// Determine what the selection should be after inserting.
if (hasVoid) {
selection = selection
}
else {
const keys = state.document.getTexts().map(text => text.key)
const text = document.getTexts().find(n => !keys.includes(n.key))
selection = selection.collapseToEndOf(text)
}
// Update the document and selection.
state = state.merge({ document, selection })
return state
}
/**
* Insert a `text` string at the current selection.
*
* @param {String} text
* @param {Set} marks (optional)
* @return {State} state
*/
insertText(text, marks) {
let state = this
let { cursorMarks, document, selection } = state
let after
const isVoid = document.hasVoidParent(state.startText)
// Determine what the selection should be after inserting.
if (isVoid) {
after = selection
}
else if (selection.isExpanded) {
after = selection.collapseToStart().moveForward(text.length)
}
else {
after = selection.moveForward(text.length)
}
// Insert the text and update the selection.
document = document.insertTextAtRange(selection, text, marks || cursorMarks)
selection = after
state = state.merge({ document, selection })
return state
}
/**
* Move the selection to a specific anchor and focus point.
*
* @param {Object} properties
* @return {State} state
*/
moveTo(properties) {
let state = this
let { document, selection } = state
// Allow for passing a `Selection` object.
if (properties instanceof Selection) {
properties = {
anchorKey: properties.anchorKey,
anchorOffset: properties.anchorOffset,
focusKey: properties.focusKey,
focusOffset: properties.focusOffset,
isFocused: properties.isFocused
}
}
// Pass in properties, and force `isBackward` to be re-resolved.
selection = selection.merge({
...properties,
isBackward: null
})
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Set `properties` of the block nodes in the current selection.
*
* @param {Object} properties
* @return {State} state
*/
setBlock(properties) {
let state = this
let { document, selection } = state
document = document.setBlockAtRange(selection, properties)
state = state.merge({ document })
return state
}
/**
* Set `properties` of the inline nodes in the current selection.
*
* @param {Object} properties
* @return {State} state
*/
setInline(properties) {
let state = this
let { document, selection } = state
document = document.setInlineAtRange(selection, properties)
state = state.merge({ document })
return state
}
/**
* Split the block node at the current selection, to optional `depth`.
*
* @param {Number} depth (optional)
* @return {State} state
*/
splitBlock(depth = 1) {
let state = this
let { document, selection } = state
// Split the document.
document = document.splitBlockAtRange(selection, depth)
// Determine what the selection should be after splitting.
const { startKey } = selection
const startNode = document.getDescendant(startKey)
const nextNode = document.getNextText(startNode)
selection = selection.collapseToStartOf(nextNode)
state = state.merge({ document, selection })
return state
}
/**
* Split the inline nodes at the current selection, to optional `depth`.
*
* @param {Number} depth (optional)
* @return {State} state
*/
splitInline(depth = Infinity) {
let state = this
let { document, selection } = state
// Split the document.
document = document.splitInlineAtRange(selection, depth)
// Determine what the selection should be after splitting.
const { startKey } = selection
const inlineParent = document.getClosestInline(startKey)
if (inlineParent) {
const startNode = document.getDescendant(startKey)
const nextNode = document.getNextText(startNode)
selection = selection.collapseToStartOf(nextNode)
}
state = state.merge({ document, selection })
return state
}
/**
* Remove a `mark` from the characters in the current selection.
*
* @param {Mark} mark
* @return {State} state
*/
removeMark(mark) {
mark = normalizeMark(mark)
let state = this
let { cursorMarks, document, selection } = state
// If the selection is collapsed, remove the mark from the cursor instead.
if (selection.isCollapsed) {
const marks = document.getMarksAtRange(selection)
state = state.merge({ cursorMarks: marks.remove(mark) })
return state
}
document = document.removeMarkAtRange(selection, mark)
state = state.merge({ document })
return state
}
/**
* Add or remove a `mark` from the characters in the current selection,
* depending on whether it's already there.
*
* @param {Mark} mark
* @return {State} state
*/
toggleMark(mark) {
mark = normalizeMark(mark)
let state = this
let { marks, document, selection } = state
const exists = marks.some(m => m.equals(mark))
return exists
? state.removeMark(mark)
: state.addMark(mark)
}
/**
* Wrap the block nodes in the current selection with a new block node with
* `properties`.
*
* @param {Object or String} properties
* @return {State} state
*/
wrapBlock(properties) {
let state = this
let { document, selection } = state
document = document.wrapBlockAtRange(selection, properties)
state = state.merge({ document })
return state
}
/**
* Unwrap the current selection from a block parent with `properties`.
*
* @param {Object or String} properties
* @return {State} state
*/
unwrapBlock(properties) {
let state = this
let { document, selection } = state
document = document.unwrapBlockAtRange(selection, properties)
state = state.merge({ document, selection })
return state
}
/**
* Wrap the current selection in new inline nodes with `properties`.
*
* @param {Object or String} properties
* @return {State} state
*/
wrapInline(properties) {
let state = this
let { document, selection } = state
const { startKey } = selection
const previous = document.getPreviousText(startKey)
document = document.wrapInlineAtRange(selection, properties)
// Determine what the selection should be after wrapping.
if (selection.isCollapsed) {
selection = selection
}
else if (selection.startOffset == 0) {
const text = previous
? document.getNextText(previous)
: document.getTexts().first()
selection = selection.moveToRangeOf(text)
}
else if (selection.startKey == selection.endKey) {
const text = document.getNextText(selection.startKey)
selection = selection.moveToRangeOf(text)
}
else {
const anchor = document.getNextText(selection.anchorKey)
const focus = document.getDescendant(selection.focusKey)
selection = selection.merge({
anchorKey: anchor.key,
anchorOffset: 0,
focusKey: focus.key,
focusOffset: selection.focusOffset
})
}
state = state.merge({ document, selection })
return state
}
/**
* Wrap the current selection with prefix/suffix.
*
* @param {String} prefix
* @param {String} suffix
* @return {State} state
*/
wrapText(prefix, suffix = prefix) {
let state = this
let { document, selection } = state
let { anchorOffset, anchorKey, focusOffset, focusKey, isBackward } = selection
let after
// Determine what the selection should be after wrapping.
if (anchorKey == focusKey) {
after = selection.moveForward(prefix.length)
}
else {
after = selection.merge({
anchorOffset: isBackward ? anchorOffset : anchorOffset + prefix.length,
focusOffset: isBackward ? focusOffset + prefix.length : focusOffset
})
}
// Wrap the text and update the state.
document = document.wrapTextAtRange(selection, prefix, suffix)
selection = after
state = state.merge({ document, selection })
return state
}
/**
* Unwrap the current selection from an inline parent with `properties`.
*
* @param {Object or String} properties
* @return {State} state
*/
unwrapInline(properties) {
let state = this
let { document, selection } = state
document = document.unwrapInlineAtRange(selection, properties)
state = state.merge({ document, selection })
return state
}
}
/**
* Normalize a `mark` argument, which can be a string or plain object too.
*
* @param {Mark or String or Object} mark
* @return {Mark}
*/
function normalizeMark(mark) {
if (typeof mark == 'string') {
return Mark.create({ type: mark })
} else {
return Mark.create(mark)
}
}
/**

View File

@@ -1,4 +1,5 @@
import Transforms from '../transforms'
import includes from 'lodash/includes'
import xor from 'lodash/xor'
import { List, Record } from 'immutable'
@@ -23,39 +24,13 @@ const Step = new Record({
})
/**
* Document range transforms.
* Defaults.
*/
const DOCUMENT_RANGE_TRANSFORMS = [
'deleteAtRange',
'deleteBackwardAtRange',
'deleteForwardAtRange',
'insertBlockAtRange',
'insertFragmentAtRange',
'insertInlineAtRange',
'insertTextAtRange',
'addMarkAtRange',
'setBlockAtRange',
'setInlineAtRange',
'splitBlockAtRange',
'splitInlineAtRange',
'removeMarkAtRange',
'toggleMarkAtRange',
'unwrapBlockAtRange',
'unwrapInlineAtRange',
'wrapBlockAtRange',
'wrapInlineAtRange',
'wrapTextAtRange'
]
/**
* Document node transforms.
*/
const DOCUMENT_NODE_TRANSFORMS = [
'removeNodeByKey',
'setNodeByKey',
]
const DEFAULT_PROPERTIES = {
state: null,
steps: new List()
}
/**
* Selection transforms.
@@ -77,40 +52,7 @@ const SELECTION_TRANSFORMS = [
'moveBackward',
'moveForward',
'moveToOffsets',
'moveToRangeOf'
]
/**
* State-level document transforms.
*/
const STATE_DOCUMENT_TRANSFORMS = [
'delete',
'deleteBackward',
'deleteForward',
'insertBlock',
'insertFragment',
'insertInline',
'insertText',
'addMark',
'setBlock',
'setInline',
'splitBlock',
'splitInline',
'removeMark',
'toggleMark',
'unwrapBlock',
'unwrapInline',
'wrapBlock',
'wrapInline',
'wrapText'
]
/**
* State selection transforms.
*/
const STATE_SELECTION_TRANSFORMS = [
'moveToRangeOf',
'collapseToEndOfNextBlock',
'collapseToEndOfNextText',
'collapseToEndOfPreviousBlock',
@@ -122,33 +64,6 @@ const STATE_SELECTION_TRANSFORMS = [
'moveTo',
]
/**
* All state-level transforms.
*/
const STATE_TRANSFORMS = []
.concat(STATE_DOCUMENT_TRANSFORMS)
.concat(STATE_SELECTION_TRANSFORMS)
/**
* All transforms.
*/
const TRANSFORMS = []
.concat(DOCUMENT_RANGE_TRANSFORMS)
.concat(DOCUMENT_NODE_TRANSFORMS)
.concat(SELECTION_TRANSFORMS)
.concat(STATE_TRANSFORMS)
/**
* Defaults.
*/
const DEFAULT_PROPERTIES = {
state: null,
steps: new List()
}
/**
* Transform.
*/
@@ -224,37 +139,14 @@ class Transform extends new Record(DEFAULT_PROPERTIES) {
applyStep(state, step) {
const { type, args } = step
const transform = Transforms[type]
if (includes(DOCUMENT_RANGE_TRANSFORMS, type)) {
let { document, selection } = state
let [ range, ...rest ] = args
range = range.normalize(document)
document = document[type](range, ...rest)
selection = selection.normalize(document)
state = state.merge({ document, selection })
return state
if (!transform) {
throw new Error(`Unknown transform type: "${type}".`)
}
else if (includes(DOCUMENT_NODE_TRANSFORMS, type)) {
let { document, selection } = state
document = document[type](...args)
selection = selection.normalize(document)
state = state.merge({ document, selection })
return state
}
else if (includes(SELECTION_TRANSFORMS, type)) {
let { document, selection } = state
selection = selection[type](...args)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
else if (includes(STATE_TRANSFORMS, type)) {
state = state[type](...args)
return state
}
state = transform(state, ...args)
return state
}
/**
@@ -271,13 +163,7 @@ class Transform extends new Record(DEFAULT_PROPERTIES) {
const previous = undos.peek()
// If the only steps applied are selection transforms, don't snapshot.
const onlySelections = steps.every((step) => {
return (
includes(SELECTION_TRANSFORMS, step.type) ||
includes(STATE_SELECTION_TRANSFORMS, step.type)
)
})
const onlySelections = steps.every(step => includes(SELECTION_TRANSFORMS, step.type))
if (onlySelections) return false
// If there isn't a previous state, snapshot.
@@ -392,7 +278,7 @@ class Transform extends new Record(DEFAULT_PROPERTIES) {
* Add a step-creating method for each of the transforms.
*/
TRANSFORMS.forEach((type) => {
Object.keys(Transforms).forEach((type) => {
Transform.prototype[type] = function (...args) {
let transform = this
let { steps } = transform

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,612 @@
import normalizeMark from '../utils/normalize-mark'
import {
addMarkAtRange,
deleteAtRange,
deleteBackwardAtRange,
deleteForwardAtRange,
insertBlockAtRange,
insertFragmentAtRange,
insertInlineAtRange,
insertTextAtRange,
removeMarkAtRange,
setBlockAtRange,
setInlineAtRange,
splitBlockAtRange,
splitInlineAtRange,
splitTextAtRange,
toggleMarkAtRange,
unwrapBlockAtRange,
unwrapInlineAtRange,
wrapBlockAtRange,
wrapInlineAtRange,
wrapTextAtRange,
} from './at-range'
/**
* Add a `mark` to the characters in the current selection.
*
* @param {State} state
* @param {Mark} mark
* @return {State} state
*/
export function addMark(state, mark) {
mark = normalizeMark(mark)
let { cursorMarks, document, selection } = state
// If the selection is collapsed, add the mark to the cursor instead.
if (selection.isCollapsed) {
const marks = document.getMarksAtRange(selection)
state = state.merge({ cursorMarks: marks.add(mark) })
return state
}
return addMarkAtRange(state, selection, mark)
}
/**
* Delete at the current selection.
*
* @param {State} state
* @return {State}
*/
export function _delete(state) {
let { document, selection } = state
let after
// When collapsed, there's nothing to do.
if (selection.isCollapsed) return state
// Determine what the selection will be after deleting.
const { startText } = state
const { startKey, startOffset, endKey, endOffset } = selection
const block = document.getClosestBlock(startText)
const highest = block.getHighestChild(startText)
const previous = block.getPreviousSibling(highest)
const next = block.getNextSibling(highest)
if (
previous &&
startOffset == 0 &&
(endKey != startKey || endOffset == startText.length)
) {
if (previous.kind == 'text') {
if (next && next.kind == 'text') {
after = selection.merge({
anchorKey: previous.key,
anchorOffset: previous.length,
focusKey: previous.key,
focusOffset: previous.length
})
} else {
after = selection.collapseToEndOf(previous)
}
} else {
const last = previous.getTexts().last()
after = selection.collapseToEndOf(last)
}
}
else {
after = selection.collapseToStart()
}
// Delete and update the selection.
state = deleteAtRange(state, selection)
state = state.merge({ selection: after })
return state
}
/**
* Delete backward `n` characters at the current selection.
*
* @param {State} state
* @param {Number} n (optional)
* @return {State}
*/
export function deleteBackward(state, n = 1) {
let { document, selection } = state
let after = selection
// Determine what the selection should be after deleting.
const { startKey } = selection
const startNode = document.getDescendant(startKey)
if (selection.isExpanded) {
after = selection.collapseToStart()
}
else if (selection.isAtStartOf(document)) {
after = selection
}
else if (selection.isAtStartOf(startNode)) {
const previous = document.getPreviousText(startNode)
const prevBlock = document.getClosestBlock(previous)
const prevInline = document.getClosestInline(previous)
if (prevBlock && prevBlock.isVoid) {
after = selection
} else if (prevInline && prevInline.isVoid) {
after = selection
} else {
after = selection.collapseToEndOf(previous)
}
}
else if (selection.isAtEndOf(startNode) && startNode.length == 1) {
const block = document.getClosestBlock(startKey)
const highest = block.getHighestChild(startKey)
const previous = block.getPreviousSibling(highest)
const next = block.getNextSibling(highest)
if (previous) {
if (previous.kind == 'text') {
if (next && next.kind == 'text') {
after = selection.merge({
anchorKey: previous.key,
anchorOffset: previous.length,
focusKey: previous.key,
focusOffset: previous.length
})
} else {
after = selection.collapseToEndOf(previous)
}
} else {
const last = previous.getTexts().last()
after = selection.collapseToEndOf(last)
}
} else {
after = selection.moveBackward(n)
}
}
else {
after = selection.moveBackward(n)
}
// Delete backward and then update the selection.
state = deleteBackwardAtRange(state, selection, n)
state = state.merge({ selection: after })
return state
}
/**
* Delete forward `n` characters at the current selection.
*
* @param {State} state
* @param {Number} n (optional)
* @return {State}
*/
export function deleteForward(state, n = 1) {
let { document, selection, startText } = state
let { startKey, startOffset } = selection
let after = selection
// Determine what the selection should be after deleting.
const block = document.getClosestBlock(startKey)
const inline = document.getClosestInline(startKey)
const highest = block.getHighestChild(startKey)
const previous = block.getPreviousSibling(highest)
const next = block.getNextSibling(highest)
if (selection.isExpanded) {
after = selection.collapseToStart()
}
else if ((block && block.isVoid) || (inline && inline.isVoid)) {
const nextText = document.getNextText(startKey)
const prevText = document.getPreviousText(startKey)
after = next
? selection.collapseToStartOf(nextText)
: selection.collapseToEndOf(prevText)
}
else if (previous && startOffset == 0 && startText.length == 1) {
if (previous.kind == 'text') {
if (next && next.kind == 'text') {
after = selection.merge({
anchorKey: previous.key,
anchorOffset: previous.length,
focusKey: previous.key,
focusOffset: previous.length
})
} else {
after = selection.collapseToEndOf(previous)
}
} else {
const last = previous.getTexts().last()
after = selection.collapseToEndOf(last)
}
}
// Delete forward and then update the selection.
state = deleteForwardAtRange(state, selection, n)
state = state.merge({ selection: after })
return state
}
/**
* Insert a `block` at the current selection.
*
* @param {State} state
* @param {String || Object || Block} block
* @return {State}
*/
export function insertBlock(state, block) {
let { document, selection } = state
const keys = document.getTexts().map(text => text.key)
// Insert the block
state = insertBlockAtRange(state, selection, block)
document = state.document
selection = state.selection
// Determine what the selection should be after inserting.
const text = document.getTexts().find(n => !keys.includes(n.key))
selection = selection.collapseToEndOf(text)
// Update the document and selection.
state = state.merge({ selection })
return state
}
/**
* Insert a `fragment` at the current selection.
*
* @param {State} state
* @param {Document} fragment
* @return {State}
*/
export function insertFragment(state, fragment) {
let { document, selection } = state
let after = selection
// If there's nothing in the fragment, do nothing.
if (!fragment.length) return state
// Lookup some nodes for determining the selection next.
const lastText = fragment.getTexts().last()
const lastInline = fragment.getClosestInline(lastText)
const beforeTexts = document.getTexts()
// Insert the fragment.
state = insertFragmentAtRange(state, selection, fragment)
document = state.document
selection = state.selection
// Determine what the selection should be after inserting.
const keys = beforeTexts.map(text => text.key)
const text = document.getTexts().findLast(n => !keys.includes(n.key))
const previousText = text ? document.getPreviousText(text) : null
if (text && lastInline && previousText) {
after = selection.collapseToEndOf(previousText)
}
else if (text && lastInline) {
after = selection.collapseToStart()
}
else if (text) {
after = selection
.collapseToStartOf(text)
.moveForward(lastText.length)
}
else {
after = selection
.collapseToStart()
.moveForward(lastText.length)
}
// Update the document and selection.
selection = after
state = state.merge({ document, selection })
return state
}
/**
* Insert a `inline` at the current selection.
*
* @param {State} state
* @param {String || Object || Block} inline
* @return {State}
*/
export function insertInline(state, inline) {
let { document, selection, startText } = state
const hasVoid = document.hasVoidParent(startText)
const keys = document.getTexts().map(text => text.key)
// Insert the inline
state = insertInlineAtRange(state, selection, inline)
document = state.document
selection = state.selection
// Determine what the selection should be after inserting.
if (hasVoid) {
selection = selection
}
else {
const text = document.getTexts().find(n => !keys.includes(n.key))
selection = selection.collapseToEndOf(text)
}
// Update the document and selection.
state = state.merge({ document, selection })
return state
}
/**
* Insert a `text` string at the current selection.
*
* @param {State} state
* @param {String} text
* @param {Set} marks (optional)
* @return {State}
*/
export function insertText(state, text, marks) {
let { cursorMarks, document, selection } = state
let after
const isVoid = document.hasVoidParent(state.startText)
// Determine what the selection should be after inserting.
if (isVoid) {
after = selection
}
else if (selection.isExpanded) {
after = selection.collapseToStart().moveForward(text.length)
}
else {
after = selection.moveForward(text.length)
}
// Insert the text and update the selection.
state = insertTextAtRange(state, selection, text, marks || cursorMarks)
state = state.merge({ selection: after })
return state
}
/**
* Set `properties` of the block nodes in the current selection.
*
* @param {State} state
* @param {Object} properties
* @return {State}
*/
export function setBlock(state, properties) {
return setBlockAtRange(state, state.selection, properties)
}
/**
* Set `properties` of the inline nodes in the current selection.
*
* @param {State} state
* @param {Object} properties
* @return {State}
*/
export function setInline(state, properties) {
return setInlineAtRange(state, state.selection, properties)
}
/**
* Split the block node at the current selection, to optional `depth`.
*
* @param {State} state
* @param {Number} depth (optional)
* @return {State}
*/
export function splitBlock(state, depth = 1) {
state = splitBlockAtRange(state, state.selection, depth)
let { document, selection } = state
// Determine what the selection should be after splitting.
const { startKey } = selection
const startNode = document.getDescendant(startKey)
const nextNode = document.getNextText(startNode)
selection = selection.collapseToStartOf(nextNode)
state = state.merge({ selection })
return state
}
/**
* Split the inline nodes at the current selection, to optional `depth`.
*
* @param {State} state
* @param {Number} depth (optional)
* @return {State}
*/
export function splitInline(state, depth = Infinity) {
let { document, selection } = state
// Split the document.
state = splitInlineAtRange(state, selection, depth)
document = state.document
selection = state.selection
// Determine what the selection should be after splitting.
const { startKey } = selection
const inlineParent = document.getClosestInline(startKey)
if (inlineParent) {
const startNode = document.getDescendant(startKey)
const nextNode = document.getNextText(startNode)
selection = selection.collapseToStartOf(nextNode)
}
state = state.merge({ document, selection })
return state
}
/**
* Remove a `mark` from the characters in the current selection.
*
* @param {State} state
* @param {Mark} mark
* @return {State}
*/
export function removeMark(state, mark) {
mark = normalizeMark(mark)
let { cursorMarks, document, selection } = state
// If the selection is collapsed, remove the mark from the cursor instead.
if (selection.isCollapsed) {
const marks = document.getMarksAtRange(selection)
state = state.merge({ cursorMarks: marks.remove(mark) })
return state
}
return removeMarkAtRange(state, state.selection, mark)
}
/**
* Add or remove a `mark` from the characters in the current selection,
* depending on whether it's already there.
*
* @param {State} state
* @param {Mark} mark
* @return {State}
*/
export function toggleMark(state, mark) {
mark = normalizeMark(mark)
const exists = state.marks.some(m => m.equals(mark))
return exists
? removeMark(state, mark)
: addMark(state, mark)
}
/**
* Unwrap the current selection from a block parent with `properties`.
*
* @param {State} state
* @param {Object or String} properties
* @return {State}
*/
export function unwrapBlock(state, properties) {
return unwrapBlockAtRange(state, state.selection, properties)
}
/**
* Unwrap the current selection from an inline parent with `properties`.
*
* @param {State} state
* @param {Object or String} properties
* @return {State}
*/
export function unwrapInline(state, properties) {
return unwrapInlineAtRange(state, state.selection, properties)
}
/**
* Wrap the block nodes in the current selection with a new block node with
* `properties`.
*
* @param {State} state
* @param {Object or String} properties
* @return {State}
*/
export function wrapBlock(state, properties) {
return wrapBlockAtRange(state, state.selection, properties)
}
/**
* Wrap the current selection in new inline nodes with `properties`.
*
* @param {State} state
* @param {Object or String} properties
* @return {State}
*/
export function wrapInline(state, properties) {
let { document, selection } = state
const { startKey } = selection
const previous = document.getPreviousText(startKey)
state = wrapInlineAtRange(state, selection, properties)
document = state.document
selection = state.selection
// Determine what the selection should be after wrapping.
if (selection.isCollapsed) {
selection = selection
}
else if (selection.startOffset == 0) {
const text = previous
? document.getNextText(previous)
: document.getTexts().first()
selection = selection.moveToRangeOf(text)
}
else if (selection.startKey == selection.endKey) {
const text = document.getNextText(selection.startKey)
selection = selection.moveToRangeOf(text)
}
else {
const anchor = document.getNextText(selection.anchorKey)
const focus = document.getDescendant(selection.focusKey)
selection = selection.merge({
anchorKey: anchor.key,
anchorOffset: 0,
focusKey: focus.key,
focusOffset: selection.focusOffset
})
}
state = state.merge({ selection })
return state
}
/**
* Wrap the current selection with prefix/suffix.
*
* @param {State} state
* @param {String} prefix
* @param {String} suffix
* @return {State}
*/
export function wrapText(state, prefix, suffix = prefix) {
let { document, selection } = state
let { anchorOffset, anchorKey, focusOffset, focusKey, isBackward } = selection
let after
// Determine what the selection should be after wrapping.
if (anchorKey == focusKey) {
after = selection.moveForward(prefix.length)
}
else {
after = selection.merge({
anchorOffset: isBackward ? anchorOffset : anchorOffset + prefix.length,
focusOffset: isBackward ? focusOffset + prefix.length : focusOffset
})
}
// Wrap the text and update the state.
state = wrapTextAtRange(state, selection, prefix, suffix)
state = state.merge({ selection: after })
return state
}

1117
lib/transforms/at-range.js Normal file

File diff suppressed because it is too large Load Diff

View File

37
lib/transforms/by-key.js Normal file
View File

@@ -0,0 +1,37 @@
import normalizeProperties from '../utils/normalize-node-or-mark-properties'
/**
* Remove a node by `key`.
*
* @param {State} state
* @param {String} key
* @return {State} state
*/
export function removeNodeByKey(state, key) {
let { document } = state
document = document.removeDescendant(key)
document = document.normalize()
state = state.merge({ document })
return state
}
/**
* Set `properties` on a node by `key`.
*
* @param {State} state
* @param {String} key
* @param {Object or String} properties
* @return {State} state
*/
export function setNodeByKey(state, key, properties) {
properties = normalizeProperties(properties)
let { document } = state
let descendant = document.assertDescendant(key)
descendant = descendant.merge(properties)
document = document.updateDescendant(descendant)
state = state.merge({ document })
return state
}

188
lib/transforms/index.js Normal file
View File

@@ -0,0 +1,188 @@
/**
* At range.
*/
import {
deleteAtRange,
deleteBackwardAtRange,
deleteForwardAtRange,
insertBlockAtRange,
insertFragmentAtRange,
insertInlineAtRange,
insertTextAtRange,
addMarkAtRange,
setBlockAtRange,
setInlineAtRange,
splitBlockAtRange,
splitInlineAtRange,
removeMarkAtRange,
toggleMarkAtRange,
unwrapBlockAtRange,
unwrapInlineAtRange,
wrapBlockAtRange,
wrapInlineAtRange,
wrapTextAtRange,
} from './at-range'
/**
* At current range.
*/
import {
_delete,
deleteBackward,
deleteForward,
insertBlock,
insertFragment,
insertInline,
insertText,
addMark,
setBlock,
setInline,
splitBlock,
splitInline,
removeMark,
toggleMark,
unwrapBlock,
unwrapInline,
wrapBlock,
wrapInline,
wrapText,
} from './at-current-range'
/**
* By key.
*/
import {
removeNodeByKey,
setNodeByKey,
} from './by-key'
/**
* On selection.
*/
import {
blur,
collapseToAnchor,
collapseToEnd,
collapseToEndOf,
collapseToEndOfNextBlock,
collapseToEndOfNextText,
collapseToEndOfPreviousBlock,
collapseToEndOfPreviousText,
collapseToFocus,
collapseToStart,
collapseToStartOf,
collapseToStartOfNextBlock,
collapseToStartOfNextText,
collapseToStartOfPreviousBlock,
collapseToStartOfPreviousText,
extendBackward,
extendForward,
extendToEndOf,
extendToStartOf,
focus,
moveBackward,
moveForward,
moveTo,
moveToOffsets,
moveToRangeOf,
} from './on-selection'
/**
* Export.
*
* @type {Object}
*/
export default {
/**
* At range.
*/
deleteAtRange,
deleteBackwardAtRange,
deleteForwardAtRange,
insertBlockAtRange,
insertFragmentAtRange,
insertInlineAtRange,
insertTextAtRange,
addMarkAtRange,
setBlockAtRange,
setInlineAtRange,
splitBlockAtRange,
splitInlineAtRange,
removeMarkAtRange,
toggleMarkAtRange,
unwrapBlockAtRange,
unwrapInlineAtRange,
wrapBlockAtRange,
wrapInlineAtRange,
wrapTextAtRange,
/**
* At current range.
*/
delete: _delete,
deleteBackward,
deleteForward,
insertBlock,
insertFragment,
insertInline,
insertText,
addMark,
setBlock,
setInline,
splitBlock,
splitInline,
removeMark,
toggleMark,
unwrapBlock,
unwrapInline,
wrapBlock,
wrapInline,
wrapText,
/**
* By key.
*/
removeNodeByKey,
setNodeByKey,
/**
* On selection.
*/
blur,
collapseToAnchor,
collapseToEnd,
collapseToEndOf,
collapseToEndOfNextBlock,
collapseToEndOfNextText,
collapseToEndOfPreviousBlock,
collapseToEndOfPreviousText,
collapseToFocus,
collapseToStart,
collapseToStartOf,
collapseToStartOfNextBlock,
collapseToStartOfNextText,
collapseToStartOfPreviousBlock,
collapseToStartOfPreviousText,
extendBackward,
extendForward,
extendToEndOf,
extendToStartOf,
focus,
moveBackward,
moveForward,
moveTo,
moveToOffsets,
moveToRangeOf,
}

View File

@@ -0,0 +1,211 @@
import Selection from '../models/selection'
/**
* Move the selection to the end of the previous block.
*
* @param {State} state
* @return {State}
*/
export function collapseToEndOfPreviousBlock(state) {
let { document, selection } = state
let blocks = document.getBlocksAtRange(selection)
let block = blocks.first()
if (!block) return state
let previous = document.getPreviousBlock(block)
if (!previous) return state
selection = selection.collapseToEndOf(previous)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the start of the previous block.
*
* @param {State} state
* @return {State}
*/
export function collapseToStartOfPreviousBlock(state) {
let { document, selection } = state
let blocks = document.getBlocksAtRange(selection)
let block = blocks.first()
if (!block) return state
let previous = document.getPreviousBlock(block)
if (!previous) return state
selection = selection.collapseToStartOf(previous)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the start of the next block.
*
* @param {State} state
* @return {State}
*/
export function collapseToStartOfNextBlock(state) {
let { document, selection } = state
let blocks = document.getBlocksAtRange(selection)
let block = blocks.last()
if (!block) return state
let next = document.getNextBlock(block)
if (!next) return state
selection = selection.collapseToStartOf(next)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the end of the next block.
*
* @param {State} state
* @return {State}
*/
export function collapseToEndOfNextBlock(state) {
let { document, selection } = state
let blocks = document.getBlocksAtRange(selection)
let block = blocks.last()
if (!block) return state
let next = document.getNextBlock(block)
if (!next) return state
selection = selection.collapseToEndOf(next)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the start of the previous text.
*
* @param {State} state
* @return {State}
*/
export function collapseToStartOfPreviousText(state) {
let { document, selection } = state
let texts = document.getTextsAtRange(selection)
let text = texts.first()
if (!text) return state
let previous = document.getPreviousText(text)
if (!previous) return state
selection = selection.collapseToStartOf(previous)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the end of the previous text.
*
* @param {State} state
* @return {State}
*/
export function collapseToEndOfPreviousText(state) {
let { document, selection } = state
let texts = document.getTextsAtRange(selection)
let text = texts.first()
if (!text) return state
let previous = document.getPreviousText(text)
if (!previous) return state
selection = selection.collapseToEndOf(previous)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the start of the next text.
*
* @param {State} state
* @return {State}
*/
export function collapseToStartOfNextText(state) {
let { document, selection } = state
let texts = document.getTextsAtRange(selection)
let text = texts.last()
if (!text) return state
let next = document.getNextText(text)
if (!next) return state
selection = selection.collapseToStartOf(next)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the end of the next text.
*
* @param {State} state
* @return {State}
*/
export function collapseToEndOfNextText(state) {
let { document, selection } = state
let texts = document.getTextsAtRange(selection)
let text = texts.last()
if (!text) return state
let next = document.getNextText(text)
if (!next) return state
selection = selection.collapseToEndOf(next)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to a specific anchor and focus point.
*
* @param {State} state
* @param {Object} properties
* @return {State}
*/
export function moveTo(state, properties) {
let { document, selection } = state
// Allow for passing a `Selection` object.
if (properties instanceof Selection) {
properties = {
anchorKey: properties.anchorKey,
anchorOffset: properties.anchorOffset,
focusKey: properties.focusKey,
focusOffset: properties.focusOffset,
isFocused: properties.isFocused
}
}
// Pass in properties, and force `isBackward` to be re-resolved.
selection = selection.merge({
...properties,
isBackward: null
})
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}

13
lib/transforms/options.js Normal file
View File

@@ -0,0 +1,13 @@
/**
* Mark the transforms
*/
/**
* Mark the transform as not being "distinct", in that it by itself should not
* create a new save boundary.
*
* @param {State} state
* @param
*/

32
lib/utils/is-in-range.js Normal file
View File

@@ -0,0 +1,32 @@
/**
* Check if an `index` of a `text` node is in a `range`.
*
* @param {Number} index
* @param {Text} text
* @param {Selection} range
* @return {Set} characters
*/
function isInRange(index, text, range) {
const { startKey, startOffset, endKey, endOffset } = range
let matcher
if (text.key == startKey && text.key == endKey) {
return startOffset <= index && index < endOffset
} else if (text.key == startKey) {
return startOffset <= index
} else if (text.key == endKey) {
return index < endOffset
} else {
return true
}
}
/**
* Export.
*
* @type {Function}
*/
export default isInRange

View File

@@ -0,0 +1,35 @@
import Block from '../models/block'
import normalizeProperties from './normalize-node-or-mark-properties'
import typeOf from 'type-of'
/**
* Normalize a `block` argument, which can be a string or plain object too.
*
* @param {Block or String or Object} block
* @return {Block}
*/
function normalizeBlock(block) {
if (block instanceof Block) return block
const type = typeOf(block)
switch (type) {
case 'string':
case 'object': {
return Block.create(normalizeProperties(block))
}
default: {
throw new Error(`A \`block\` argument must be a block, an object or a string, but you passed: "${type}".`)
}
}
}
/**
* Export.
*
* @type {Function}
*/
export default normalizeBlock

View File

@@ -0,0 +1,35 @@
import Inline from '../models/inline'
import normalizeProperties from './normalize-node-or-mark-properties'
import typeOf from 'type-of'
/**
* Normalize an `inline` argument, which can be a string or plain object too.
*
* @param {Inline or String or Object} inline
* @return {Inline}
*/
function normalizeInline(inline) {
if (inline instanceof Inline) return inline
const type = typeOf(inline)
switch (type) {
case 'string':
case 'object': {
return Inline.create(normalizeProperties(inline))
}
default: {
throw new Error(`An \`inline\` argument must be an inline, an object or a string, but you passed: "${type}".`)
}
}
}
/**
* Export.
*
* @type {Function}
*/
export default normalizeInline

View File

@@ -0,0 +1,35 @@
import Mark from '../models/mark'
import typeOf from 'type-of'
import normalizeProperties from './normalize-node-or-mark-properties'
/**
* Normalize a `mark` argument, which can be a string or plain object too.
*
* @param {Mark or String or Object} mark
* @return {Mark}
*/
function normalizeMark(mark) {
if (mark instanceof Mark) return mark
const type = typeOf(mark)
switch (type) {
case 'string':
case 'object': {
return Mark.create(normalizeProperties(mark))
}
default: {
throw new Error(`A \`mark\` argument must be a mark, an object or a string, but you passed: "${type}".`)
}
}
}
/**
* Export.
*
* @type {Function}
*/
export default normalizeMark

View File

@@ -0,0 +1,47 @@
import Data from '../models/data'
import typeOf from 'type-of'
/**
* Normalize the `properties` of a node or mark, which can be either a type
* string or a dictionary of properties. If it's a dictionary, `data` is
* optional and shouldn't be set if null or undefined.
*
* @param {String or Object} properties
* @return {Object}
*/
function normalizeNodeOrMarkProperties(properties = {}) {
const ret = {}
const type = typeOf(properties)
switch (type) {
case 'string': {
ret.type = properties
break
}
case 'object': {
for (const key in properties) {
if (key == 'data') {
if (properties[key] != null) ret[key] = Data.create(properties[key])
} else {
ret[key] = properties[key]
}
}
break
}
default: {
throw new Error(`A \`properties\` argument must be an object or a string, but you passed: "${type}".`)
}
}
return ret
}
/**
* Export.
*
* @type {Function}
*/
export default normalizeNodeOrMarkProperties