From 68c04f3fe2f3a889e46efb38e3fa601bcd4b98b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Wed, 13 Mar 2019 12:55:28 +0100 Subject: [PATCH] RichText: change value to have separate keys for line and object formats (#13948) * Add objects and lineFormats * Update RichText * Fix image toolbar * Update format placeholder * lineFormat => lines * concatPair => mergePair * Update selectedFormat checks * Add some extra info to create docs * Move create docs inline * Merge lines and objects * Fix typos * Add getActiveObject unit tests * Update docs * Rebase * Adjust unstableToDom arguments * Remove normaliseFormats from list functions * Update native files * Update native file --- .../src/components/rich-text/format-edit.js | 13 +- .../src/components/rich-text/index.js | 24 +-- .../src/components/rich-text/index.native.js | 15 +- packages/format-library/src/image/index.js | 21 ++- packages/rich-text/README.md | 65 ++++++-- packages/rich-text/src/apply-format.js | 18 +-- packages/rich-text/src/apply-format.native.js | 14 +- packages/rich-text/src/change-list-type.js | 25 ++-- packages/rich-text/src/concat.js | 23 ++- packages/rich-text/src/create.js | 140 ++++++++---------- packages/rich-text/src/get-active-object.js | 20 +++ .../rich-text/src/get-last-child-index.js | 6 +- .../rich-text/src/get-parent-line-index.js | 6 +- packages/rich-text/src/indent-list-items.js | 29 ++-- packages/rich-text/src/index.js | 1 + .../rich-text/src/insert-line-separator.js | 9 +- packages/rich-text/src/insert-object.js | 6 +- packages/rich-text/src/insert.js | 9 +- packages/rich-text/src/join.js | 5 +- packages/rich-text/src/normalise-formats.js | 8 +- .../rich-text/src/normalise-formats.native.js | 36 ----- packages/rich-text/src/outdent-list-items.js | 19 +-- packages/rich-text/src/remove-format.js | 38 +++-- .../rich-text/src/remove-format.native.js | 9 +- packages/rich-text/src/replace.js | 8 +- packages/rich-text/src/slice.js | 11 +- packages/rich-text/src/split.js | 7 +- .../src/test/__snapshots__/to-dom.js.snap | 11 +- packages/rich-text/src/test/apply-format.js | 2 +- .../rich-text/src/test/change-list-type.js | 24 ++- packages/rich-text/src/test/concat.js | 3 + packages/rich-text/src/test/create.js | 1 + .../rich-text/src/test/get-active-object.js | 41 +++++ .../src/test/get-last-child-index.js | 12 +- .../src/test/get-parent-line-index.js | 8 +- packages/rich-text/src/test/helpers/index.js | 61 ++++++-- .../rich-text/src/test/indent-list-items.js | 74 ++++----- .../src/test/insert-line-separator.js | 20 ++- packages/rich-text/src/test/insert-object.js | 7 +- packages/rich-text/src/test/insert.js | 6 + packages/rich-text/src/test/join.js | 6 +- .../rich-text/src/test/outdent-list-items.js | 64 ++++---- packages/rich-text/src/test/replace.js | 7 + packages/rich-text/src/test/slice.js | 4 + packages/rich-text/src/test/split.js | 22 +++ packages/rich-text/src/test/to-dom.js | 2 - packages/rich-text/src/to-dom.js | 4 - packages/rich-text/src/to-html-string.js | 5 +- packages/rich-text/src/to-tree.js | 54 ++++--- packages/rich-text/src/toggle-format.js | 5 +- 50 files changed, 583 insertions(+), 445 deletions(-) create mode 100644 packages/rich-text/src/get-active-object.js delete mode 100644 packages/rich-text/src/normalise-formats.native.js create mode 100644 packages/rich-text/src/test/get-active-object.js diff --git a/packages/block-editor/src/components/rich-text/format-edit.js b/packages/block-editor/src/components/rich-text/format-edit.js index 7505ed1f81a3c..29911206aba83 100644 --- a/packages/block-editor/src/components/rich-text/format-edit.js +++ b/packages/block-editor/src/components/rich-text/format-edit.js @@ -3,7 +3,7 @@ */ import { withSelect } from '@wordpress/data'; import { Fragment } from '@wordpress/element'; -import { getActiveFormat } from '@wordpress/rich-text'; +import { getActiveFormat, getActiveObject } from '@wordpress/rich-text'; const FormatEdit = ( { formatTypes, onChange, value } ) => { return ( @@ -15,13 +15,20 @@ const FormatEdit = ( { formatTypes, onChange, value } ) => { const activeFormat = getActiveFormat( value, name ); const isActive = activeFormat !== undefined; - const activeAttributes = isActive ? activeFormat.attributes || {} : {}; + const activeObject = getActiveObject( value ); + const isObjectActive = activeObject !== undefined; return ( diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 5b91674b9a673..1d714ba9844d3 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -177,10 +177,10 @@ export class RichText extends Component { * @return {Object} The current record (value and selection). */ getRecord() { - const { formats, text } = this.formatToValue( this.props.value ); + const { formats, replacements, text } = this.formatToValue( this.props.value ); const { start, end, selectedFormat } = this.state; - return { formats, text, start, end, selectedFormat }; + return { formats, replacements, text, start, end, selectedFormat }; } createRecord() { @@ -394,13 +394,17 @@ export class RichText extends Component { } let { selectedFormat } = this.state; - const { formats, text, start, end } = this.createRecord(); + const { formats, replacements, text, start, end } = this.createRecord(); if ( this.formatPlaceholder ) { - formats[ this.state.start ] = formats[ this.state.start ] || []; - formats[ this.state.start ].push( this.formatPlaceholder ); - selectedFormat = formats[ this.state.start ].length; - } else if ( selectedFormat ) { + selectedFormat = this.formatPlaceholder.length; + + if ( selectedFormat > 0 ) { + formats[ this.state.start ] = this.formatPlaceholder; + } else { + delete formats[ this.state.start ]; + } + } else if ( selectedFormat > 0 ) { const formatsBefore = formats[ start - 1 ] || []; const formatsAfter = formats[ start ] || []; @@ -411,12 +415,13 @@ export class RichText extends Component { } source = source.slice( 0, selectedFormat ); + formats[ this.state.start ] = source; } else { delete formats[ this.state.start ]; } - const change = { formats, text, start, end, selectedFormat }; + const change = { formats, replacements, text, start, end, selectedFormat }; this.onChange( change, { withoutHistory: true, @@ -936,7 +941,6 @@ export class RichText extends Component { return unstableToDom( { value, multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, prepareEditableTree: this.props.prepareEditableTree, } ).body.innerHTML; } @@ -975,7 +979,6 @@ export class RichText extends Component { return children.fromDOM( unstableToDom( { value, multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, isEditableTree: false, } ).body.childNodes ); } @@ -984,7 +987,6 @@ export class RichText extends Component { return toHTMLString( { value, multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, } ); } diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 55bf4580e5e70..db750a18d41f2 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -98,9 +98,9 @@ export class RichText extends Component { const { formatPlaceholder, start, end } = this.state; // Since we get the text selection from Aztec we need to be in sync with the HTML `value` // Removing leading white spaces using `trim()` should make sure this is the case. - const { formats, text } = this.formatToValue( this.props.value === undefined ? undefined : this.props.value.trimLeft() ); + const { formats, replacements, text } = this.formatToValue( this.props.value === undefined ? undefined : this.props.value.trimLeft() ); - return { formats, formatPlaceholder, text, start, end }; + return { formats, replacements, formatPlaceholder, text, start, end }; } /* @@ -156,13 +156,12 @@ export class RichText extends Component { onSplit( before, after, ...blocks ); } - valueToFormat( { formats, text } ) { - const value = toHTMLString( { - value: { formats, text }, - multilineTag: this.multilineTag, - } ); + valueToFormat( value ) { // remove the outer root tags - return this.removeRootTagsProduceByAztec( value ); + return this.removeRootTagsProduceByAztec( toHTMLString( { + value, + multilineTag: this.multilineTag, + } ) ); } getActiveFormatNames( record ) { diff --git a/packages/format-library/src/image/index.js b/packages/format-library/src/image/index.js index 2dca1c41506b6..ce62a8b6c5a75 100644 --- a/packages/format-library/src/image/index.js +++ b/packages/format-library/src/image/index.js @@ -40,7 +40,7 @@ export const image = { } static getDerivedStateFromProps( props, state ) { - const { activeAttributes: { style } } = props; + const { activeObjectAttributes: { style } } = props; if ( style === state.previousStyle ) { return null; @@ -79,8 +79,8 @@ export const image = { } render() { - const { value, onChange, isActive, activeAttributes } = this.props; - const { style } = activeAttributes; + const { value, onChange, isObjectActive, activeObjectAttributes } = this.props; + const { style } = activeObjectAttributes; // Rerender PositionedAtSelection when the selection changes or when // the width changes. const key = value.start + style; @@ -91,7 +91,7 @@ export const image = { icon={ } title={ __( 'Inline Image' ) } onClick={ this.openModal } - isActive={ isActive } + isActive={ isObjectActive } /> { this.state.modal && } - { isActive && + { isObjectActive && { - const newFormats = value.formats.slice( 0 ); + const newReplacements = value.replacements.slice(); - newFormats[ value.start ] = [ { + newReplacements[ value.start ] = { type: name, - object: true, attributes: { - ...activeAttributes, + ...activeObjectAttributes, style: `width: ${ this.state.width }px;`, }, - } ]; + }; onChange( { ...value, - formats: newFormats, + replacements: newReplacements, } ); event.preventDefault(); diff --git a/packages/rich-text/README.md b/packages/rich-text/README.md index 0ac85d85b640d..90fe67367ddc8 100644 --- a/packages/rich-text/README.md +++ b/packages/rich-text/README.md @@ -61,6 +61,26 @@ called without any input, an empty value will be created. If `multilineTag` will be separated by two newlines. The optional functions can be used to filter out content. +A value will have the following shape, which you are strongly encouraged not +to modify without the use of helper functions: + +```js +{ + text: string, + formats: Array, + replacements: Array, + ?start: number, + ?end: number, +} +``` + +As you can see, text and formatting are separated. `text` holds the text, +including any replacement characters for objects and lines. `formats`, +`objects` and `lines` are all sparse arrays of the same length as `text`. It +holds information about the formatting at the relevant text indices. Finally +`start` and `end` state which text indices are selected. They are only +provided if a `Range` was given. + **Parameters** - **$1** `[Object]`: Optional named arguments. @@ -93,9 +113,23 @@ is no format at the selection. `(Object|undefined)`: Active format object of the specified type, or undefined. +### getActiveObject + +[src/index.js#L11-L11](src/index.js#L11-L11) + +Gets the active object, if there is any. + +**Parameters** + +- **value** `Object`: Value to inspect. + +**Returns** + +`?Object`: Active object, or undefined. + ### getTextContent -[src/index.js#L13-L13](src/index.js#L13-L13) +[src/index.js#L14-L14](src/index.js#L14-L14) Get the textual content of a Rich Text value. This is similar to `Element.textContent`. @@ -110,7 +144,7 @@ Get the textual content of a Rich Text value. This is similar to ### insert -[src/index.js#L21-L21](src/index.js#L21-L21) +[src/index.js#L22-L22](src/index.js#L22-L22) Insert a Rich Text value, an HTML string, or a plain text string, into a Rich Text value at the given `startIndex`. Any content between `startIndex` @@ -130,7 +164,7 @@ none are provided. ### insertObject -[src/index.js#L24-L24](src/index.js#L24-L24) +[src/index.js#L25-L25](src/index.js#L25-L25) Insert a format as an object into a Rich Text value at the given `startIndex`. Any content between `startIndex` and `endIndex` will be @@ -149,7 +183,7 @@ removed. Indices are retrieved from the selection if none are provided. ### isCollapsed -[src/index.js#L14-L14](src/index.js#L14-L14) +[src/index.js#L15-L15](src/index.js#L15-L15) Check if the selection of a Rich Text value is collapsed or not. Collapsed means that no characters are selected, but there is a caret present. If there @@ -166,7 +200,7 @@ is no selection, `undefined` will be returned. This is similar to ### isEmpty -[src/index.js#L15-L15](src/index.js#L15-L15) +[src/index.js#L16-L16](src/index.js#L16-L16) Check if a Rich Text value is Empty, meaning it contains no text or any objects (such as images). @@ -181,7 +215,7 @@ objects (such as images). ### join -[src/index.js#L16-L16](src/index.js#L16-L16) +[src/index.js#L17-L17](src/index.js#L17-L17) Combine an array of Rich Text values into one, optionally separated by `separator`, which can be a Rich Text value, HTML string, or plain text @@ -198,7 +232,7 @@ string. This is similar to `Array.prototype.join`. ### registerFormatType -[src/index.js#L17-L17](src/index.js#L17-L17) +[src/index.js#L18-L18](src/index.js#L18-L18) Registers a new format provided a unique name and an object defining its behavior. @@ -218,7 +252,7 @@ behavior. ### remove -[src/index.js#L19-L19](src/index.js#L19-L19) +[src/index.js#L20-L20](src/index.js#L20-L20) Remove content from a Rich Text value between the given `startIndex` and `endIndex`. Indices are retrieved from the selection if none are provided. @@ -235,7 +269,7 @@ Remove content from a Rich Text value between the given `startIndex` and ### removeFormat -[src/index.js#L18-L18](src/index.js#L18-L18) +[src/index.js#L19-L19](src/index.js#L19-L19) Remove any format object from a Rich Text value by type from the given `startIndex` to the given `endIndex`. Indices are retrieved from the @@ -254,7 +288,7 @@ selection if none are provided. ### replace -[src/index.js#L20-L20](src/index.js#L20-L20) +[src/index.js#L21-L21](src/index.js#L21-L21) Search a Rich Text value and replace the match(es) with `replacement`. This is similar to `String.prototype.replace`. @@ -271,7 +305,7 @@ is similar to `String.prototype.replace`. ### slice -[src/index.js#L25-L25](src/index.js#L25-L25) +[src/index.js#L26-L26](src/index.js#L26-L26) Slice a Rich Text value from `startIndex` to `endIndex`. Indices are retrieved from the selection if none are provided. This is similar to @@ -289,7 +323,7 @@ retrieved from the selection if none are provided. This is similar to ### split -[src/index.js#L26-L26](src/index.js#L26-L26) +[src/index.js#L27-L27](src/index.js#L27-L27) Split a Rich Text value in two at the given `startIndex` and `endIndex`, or split at the given separator. This is similar to `String.prototype.split`. @@ -307,7 +341,7 @@ Indices are retrieved from the selection if none are provided. ### toggleFormat -[src/index.js#L29-L29](src/index.js#L29-L29) +[src/index.js#L30-L30](src/index.js#L30-L30) Toggles a format object to a Rich Text value at the current selection. @@ -322,7 +356,7 @@ Toggles a format object to a Rich Text value at the current selection. ### toHTMLString -[src/index.js#L28-L28](src/index.js#L28-L28) +[src/index.js#L29-L29](src/index.js#L29-L29) Create an HTML string from a Rich Text value. If a `multilineTag` is provided, text separated by a line separator will be wrapped in it. @@ -332,7 +366,6 @@ provided, text separated by a line separator will be wrapped in it. - **$1** `Object`: Named argements. - **$1.value** `Object`: Rich text value. - **$1.multilineTag** `[string]`: Multiline tag. -- **$1.multilineWrapperTags** `[Array]`: Tags where lines can be found if nesting is possible. **Returns** @@ -340,7 +373,7 @@ provided, text separated by a line separator will be wrapped in it. ### unregisterFormatType -[src/index.js#L31-L31](src/index.js#L31-L31) +[src/index.js#L32-L32](src/index.js#L32-L32) Unregisters a format. diff --git a/packages/rich-text/src/apply-format.js b/packages/rich-text/src/apply-format.js index 8134cd8d742a9..c9a59b96e6a65 100644 --- a/packages/rich-text/src/apply-format.js +++ b/packages/rich-text/src/apply-format.js @@ -23,12 +23,12 @@ import { normaliseFormats } from './normalise-formats'; * @return {Object} A new value with the format applied. */ export function applyFormat( - { formats, text, start, end }, + value, format, - startIndex = start, - endIndex = end + startIndex = value.start, + endIndex = value.end ) { - const newFormats = formats.slice( 0 ); + const newFormats = value.formats.slice( 0 ); // The selection is collapsed. if ( startIndex === endIndex ) { @@ -52,14 +52,10 @@ export function applyFormat( // with the format applied. } else { const previousFormat = newFormats[ startIndex - 1 ] || []; - const hasType = find( previousFormat, { type: format.type } ); return { - formats, - text, - start, - end, - formatPlaceholder: hasType ? undefined : format, + ...value, + formatPlaceholder: [ ...previousFormat, format ], }; } } else { @@ -68,7 +64,7 @@ export function applyFormat( } } - return normaliseFormats( { formats: newFormats, text, start, end } ); + return normaliseFormats( { ...value, formats: newFormats } ); } function applyFormats( formats, index, format ) { diff --git a/packages/rich-text/src/apply-format.native.js b/packages/rich-text/src/apply-format.native.js index 9c5a4749ae928..ef641cbaa224d 100644 --- a/packages/rich-text/src/apply-format.native.js +++ b/packages/rich-text/src/apply-format.native.js @@ -23,11 +23,13 @@ import { normaliseFormats } from './normalise-formats'; * @return {Object} A new value with the format applied. */ export function applyFormat( - { formats: currentFormats, formatPlaceholder, text, start, end }, + value, formats, - startIndex = start, - endIndex = end + startIndex = value.start, + endIndex = value.end ) { + const { formats: currentFormats, formatPlaceholder, start } = value; + if ( ! Array.isArray( formats ) ) { formats = [ formats ]; } @@ -40,10 +42,8 @@ export function applyFormat( // Follow the same logic as in getActiveFormat: placeholderFormats has priority over previousFormats const activeFormats = ( placeholderFormats ? placeholderFormats : previousFormats ) || []; return { + ...value, formats: currentFormats, - text, - start, - end, formatPlaceholder: { index: start, formats: mergeFormats( activeFormats, formats ), @@ -57,7 +57,7 @@ export function applyFormat( applyFormats( newFormats, index, formats ); } - return normaliseFormats( { formats: newFormats, text, start, end } ); + return normaliseFormats( { ...value, formats: newFormats } ); } function mergeFormats( formats1, formats2 ) { diff --git a/packages/rich-text/src/change-list-type.js b/packages/rich-text/src/change-list-type.js index 1dfc040657363..e429b91c6cbca 100644 --- a/packages/rich-text/src/change-list-type.js +++ b/packages/rich-text/src/change-list-type.js @@ -3,7 +3,6 @@ */ import { LINE_SEPARATOR } from './special-characters'; -import { normaliseFormats } from './normalise-formats'; import { getLineIndex } from './get-line-index'; import { getParentLineIndex } from './get-parent-line-index'; @@ -20,12 +19,12 @@ import { getParentLineIndex } from './get-parent-line-index'; * @return {Object} The changed value. */ export function changeListType( value, newFormat ) { - const { text, formats, start, end } = value; + const { text, replacements, start, end } = value; const startingLineIndex = getLineIndex( value, start ); - const startLineFormats = formats[ startingLineIndex ] || []; - const endLineFormats = formats[ getLineIndex( value, end ) ] || []; + const startLineFormats = replacements[ startingLineIndex ] || []; + const endLineFormats = replacements[ getLineIndex( value, end ) ] || []; const startIndex = getParentLineIndex( value, startingLineIndex ); - const newFormats = formats.slice( 0 ); + const newReplacements = replacements.slice(); const startCount = startLineFormats.length - 1; const endCount = endLineFormats.length - 1; @@ -36,16 +35,16 @@ export function changeListType( value, newFormat ) { continue; } - if ( ( newFormats[ index ] || [] ).length <= startCount ) { + if ( ( newReplacements[ index ] || [] ).length <= startCount ) { break; } - if ( ! newFormats[ index ] ) { + if ( ! newReplacements[ index ] ) { continue; } changed = true; - newFormats[ index ] = newFormats[ index ].map( ( format, i ) => { + newReplacements[ index ] = newReplacements[ index ].map( ( format, i ) => { return i < startCount || i > endCount ? format : newFormat; } ); } @@ -54,10 +53,8 @@ export function changeListType( value, newFormat ) { return value; } - return normaliseFormats( { - text, - formats: newFormats, - start, - end, - } ); + return { + ...value, + replacements: newReplacements, + }; } diff --git a/packages/rich-text/src/concat.js b/packages/rich-text/src/concat.js index 9f14091f9b1c8..07be12140db91 100644 --- a/packages/rich-text/src/concat.js +++ b/packages/rich-text/src/concat.js @@ -3,6 +3,24 @@ */ import { normaliseFormats } from './normalise-formats'; +import { create } from './create'; + +/** + * Concats a pair of rich text values. Not that this mutates `a` and does NOT + * normalise formats! + * + * @param {Object} a Value to mutate. + * @param {Object} b Value to add read from. + * + * @return {Object} `a`, mutated. + */ +export function mergePair( a, b ) { + a.formats = a.formats.concat( b.formats ); + a.replacements = a.replacements.concat( b.replacements ); + a.text += b.text; + + return a; +} /** * Combine all Rich Text values into one. This is similar to @@ -13,8 +31,5 @@ import { normaliseFormats } from './normalise-formats'; * @return {Object} A new value combining all given records. */ export function concat( ...values ) { - return normaliseFormats( values.reduce( ( accumlator, { formats, text } ) => ( { - text: accumlator.text + text, - formats: accumlator.formats.concat( formats ), - } ) ) ); + return normaliseFormats( values.reduce( mergePair, create() ) ); } diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index d1c1e47d4df06..37bd1189193f2 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -7,9 +7,9 @@ import { select } from '@wordpress/data'; * Internal dependencies */ -import { isEmpty } from './is-empty'; import { isFormatEqual } from './is-format-equal'; import { createElement } from './create-element'; +import { mergePair } from './concat'; import { LINE_SEPARATOR, OBJECT_REPLACEMENT_CHARACTER, @@ -22,7 +22,11 @@ import { const { TEXT_NODE, ELEMENT_NODE } = window.Node; function createEmptyValue() { - return { formats: [], text: '' }; + return { + formats: [], + replacements: [], + text: '', + }; } function simpleFindKey( object, value ) { @@ -96,6 +100,26 @@ function toFormat( { type, attributes } ) { * `multilineTag` will be separated by two newlines. The optional functions can * be used to filter out content. * + * A value will have the following shape, which you are strongly encouraged not + * to modify without the use of helper functions: + * + * ```js + * { + * text: string, + * formats: Array, + * replacements: Array, + * ?start: number, + * ?end: number, + * } + * ``` + * + * As you can see, text and formatting are separated. `text` holds the text, + * including any replacement characters for objects and lines. `formats`, + * `objects` and `lines` are all sparse arrays of the same length as `text`. It + * holds information about the formatting at the relevant text indices. Finally + * `start` and `end` state which text indices are selected. They are only + * provided if a `Range` was given. + * * @param {Object} [$1] Optional named arguments. * @param {Element} [$1.element] Element to create value from. * @param {string} [$1.text] Text to create value from. @@ -120,6 +144,7 @@ export function create( { if ( typeof text === 'string' && text.length > 0 ) { return { formats: Array( text.length ), + replacements: Array( text.length ), text, }; } @@ -291,10 +316,11 @@ function createFromElement( { const text = filterString( node.nodeValue ); range = filterRange( node, range, filterString ); accumulateSelection( accumulator, node, range, { text } ); - accumulator.text += text; // Create a sparse array of the same length as `text`, in which // formats can be added. accumulator.formats.length += text.length; + accumulator.replacements.length += text.length; + accumulator.text += text; continue; } @@ -312,8 +338,7 @@ function createFromElement( { if ( type === 'br' ) { accumulateSelection( accumulator, node, range, createEmptyValue() ); - accumulator.text += '\n'; - accumulator.formats.length += 1; + mergePair( accumulator, create( { text: '\n' } ) ); continue; } @@ -323,22 +348,10 @@ function createFromElement( { type, attributes: getAttributes( { element: node } ), } ); - - let format; - - if ( newFormat ) { - // Reuse the last format if it's equal. - if ( isFormatEqual( newFormat, lastFormat ) ) { - format = lastFormat; - } else { - format = newFormat; - } - } - - let value; + const format = isFormatEqual( newFormat, lastFormat ) ? lastFormat : newFormat; if ( multilineWrapperTags && multilineWrapperTags.indexOf( type ) !== -1 ) { - value = createFromMultilineElement( { + const value = createFromMultilineElement( { element: node, range, multilineTag, @@ -346,64 +359,39 @@ function createFromElement( { currentWrapperTags: [ ...currentWrapperTags, format ], isEditableTree, } ); - format = undefined; - } else { - value = createFromElement( { - element: node, - range, - multilineTag, - multilineWrapperTags, - isEditableTree, - } ); - } - - const text = value.text; - const start = accumulator.text.length; - accumulateSelection( accumulator, node, range, value ); - - // Don't apply the element as formatting if it has no content. - if ( isEmpty( value ) && format && ! format.attributes ) { + accumulateSelection( accumulator, node, range, value ); + mergePair( accumulator, value ); continue; } - const { formats } = accumulator; + const value = createFromElement( { + element: node, + range, + multilineTag, + multilineWrapperTags, + isEditableTree, + } ); - if ( format && format.attributes && text.length === 0 ) { - format.object = true; - accumulator.text += OBJECT_REPLACEMENT_CHARACTER; + accumulateSelection( accumulator, node, range, value ); - if ( formats[ start ] ) { - formats[ start ].unshift( format ); - } else { - formats[ start ] = [ format ]; + if ( ! format ) { + mergePair( accumulator, value ); + } else if ( value.text.length === 0 ) { + if ( format.attributes ) { + mergePair( accumulator, { + formats: [ , ], + replacements: [ format ], + text: OBJECT_REPLACEMENT_CHARACTER, + } ); } } else { - accumulator.text += text; - accumulator.formats.length += text.length; - - let i = value.formats.length; - - // Optimise for speed. - while ( i-- ) { - const formatIndex = start + i; - - if ( format ) { - if ( formats[ formatIndex ] ) { - formats[ formatIndex ].push( format ); - } else { - formats[ formatIndex ] = [ format ]; - } - } - - if ( value.formats[ i ] ) { - if ( formats[ formatIndex ] ) { - formats[ formatIndex ].push( ...value.formats[ i ] ); - } else { - formats[ formatIndex ] = value.formats[ i ]; - } - } - } + mergePair( accumulator, { + ...value, + formats: Array.from( value.formats, ( formats ) => + formats ? [ format, ...formats ] : [ format ] + ), + } ); } } @@ -459,17 +447,17 @@ function createFromMultilineElement( { isEditableTree, } ); - // Multiline value text should be separated by a double line break. + // Multiline value text should be separated by a line separator. if ( index !== 0 || currentWrapperTags.length > 0 ) { - const formats = currentWrapperTags.length > 0 ? [ currentWrapperTags ] : [ , ]; - accumulator.formats = accumulator.formats.concat( formats ); - accumulator.text += LINE_SEPARATOR; + mergePair( accumulator, { + formats: [ , ], + replacements: currentWrapperTags.length > 0 ? [ currentWrapperTags ] : [ , ], + text: LINE_SEPARATOR, + } ); } accumulateSelection( accumulator, node, range, value ); - - accumulator.formats = accumulator.formats.concat( value.formats ); - accumulator.text += value.text; + mergePair( accumulator, value ); } return accumulator; diff --git a/packages/rich-text/src/get-active-object.js b/packages/rich-text/src/get-active-object.js new file mode 100644 index 0000000000000..e324521284597 --- /dev/null +++ b/packages/rich-text/src/get-active-object.js @@ -0,0 +1,20 @@ +/** + * Internal dependencies + */ + +import { OBJECT_REPLACEMENT_CHARACTER } from './special-characters'; + +/** + * Gets the active object, if there is any. + * + * @param {Object} value Value to inspect. + * + * @return {?Object} Active object, or undefined. + */ +export function getActiveObject( { start, end, replacements, text } ) { + if ( start + 1 !== end || text[ start ] !== OBJECT_REPLACEMENT_CHARACTER ) { + return; + } + + return replacements[ start ]; +} diff --git a/packages/rich-text/src/get-last-child-index.js b/packages/rich-text/src/get-last-child-index.js index 976051b10c0a3..0fc4aeb32ff3e 100644 --- a/packages/rich-text/src/get-last-child-index.js +++ b/packages/rich-text/src/get-last-child-index.js @@ -12,8 +12,8 @@ import { LINE_SEPARATOR } from './special-characters'; * * @return {Array} The index of the last child. */ -export function getLastChildIndex( { text, formats }, lineIndex ) { - const lineFormats = formats[ lineIndex ] || []; +export function getLastChildIndex( { text, replacements }, lineIndex ) { + const lineFormats = replacements[ lineIndex ] || []; // Use the given line index in case there are no next children. let childIndex = lineIndex; @@ -24,7 +24,7 @@ export function getLastChildIndex( { text, formats }, lineIndex ) { continue; } - const formatsAtIndex = formats[ index ] || []; + const formatsAtIndex = replacements[ index ] || []; // If the amout of formats is equal or more, store it, then return the // last one if the amount of formats is less. diff --git a/packages/rich-text/src/get-parent-line-index.js b/packages/rich-text/src/get-parent-line-index.js index bd3f72de96519..1b0a0ecb5cf48 100644 --- a/packages/rich-text/src/get-parent-line-index.js +++ b/packages/rich-text/src/get-parent-line-index.js @@ -14,8 +14,8 @@ import { LINE_SEPARATOR } from './special-characters'; * * @return {Array} The parent list line index. */ -export function getParentLineIndex( { text, formats }, lineIndex ) { - const startFormats = formats[ lineIndex ] || []; +export function getParentLineIndex( { text, replacements }, lineIndex ) { + const startFormats = replacements[ lineIndex ] || []; let index = lineIndex; @@ -24,7 +24,7 @@ export function getParentLineIndex( { text, formats }, lineIndex ) { continue; } - const formatsAtIndex = formats[ index ] || []; + const formatsAtIndex = replacements[ index ] || []; if ( formatsAtIndex.length === startFormats.length - 1 ) { return index; diff --git a/packages/rich-text/src/indent-list-items.js b/packages/rich-text/src/indent-list-items.js index 1a61bd7db1536..985d1062c76ed 100644 --- a/packages/rich-text/src/indent-list-items.js +++ b/packages/rich-text/src/indent-list-items.js @@ -3,7 +3,6 @@ */ import { LINE_SEPARATOR } from './special-characters'; -import { normaliseFormats } from './normalise-formats'; import { getLineIndex } from './get-line-index'; /** @@ -14,8 +13,8 @@ import { getLineIndex } from './get-line-index'; * * @return {boolean} The line index. */ -function getTargetLevelLineIndex( { text, formats }, lineIndex ) { - const startFormats = formats[ lineIndex ] || []; +function getTargetLevelLineIndex( { text, replacements }, lineIndex ) { + const startFormats = replacements[ lineIndex ] || []; let index = lineIndex; @@ -24,7 +23,7 @@ function getTargetLevelLineIndex( { text, formats }, lineIndex ) { continue; } - const formatsAtIndex = formats[ index ] || []; + const formatsAtIndex = replacements[ index ] || []; // Return the first line index that is one level higher. If the level is // lower or equal, there is no result. @@ -52,10 +51,10 @@ export function indentListItems( value, rootFormat ) { return value; } - const { text, formats, start, end } = value; + const { text, replacements, end } = value; const previousLineIndex = getLineIndex( value, lineIndex ); - const formatsAtLineIndex = formats[ lineIndex ] || []; - const formatsAtPreviousLineIndex = formats[ previousLineIndex ] || []; + const formatsAtLineIndex = replacements[ lineIndex ] || []; + const formatsAtPreviousLineIndex = replacements[ previousLineIndex ] || []; // The the indentation of the current line is greater than previous line, // then the line cannot be furter indented. @@ -63,7 +62,7 @@ export function indentListItems( value, rootFormat ) { return value; } - const newFormats = formats.slice(); + const newFormats = replacements.slice(); const targetLevelLineIndex = getTargetLevelLineIndex( value, lineIndex ); for ( let index = lineIndex; index < end; index++ ) { @@ -74,12 +73,12 @@ export function indentListItems( value, rootFormat ) { // Get the previous list, and if there's a child list, take over the // formats. If not, duplicate the last level and create a new level. if ( targetLevelLineIndex ) { - const targetFormats = formats[ targetLevelLineIndex ] || []; + const targetFormats = replacements[ targetLevelLineIndex ] || []; newFormats[ index ] = targetFormats.concat( ( newFormats[ index ] || [] ).slice( targetFormats.length - 1 ) ); } else { - const targetFormats = formats[ previousLineIndex ] || []; + const targetFormats = replacements[ previousLineIndex ] || []; const lastformat = targetFormats[ targetFormats.length - 1 ] || rootFormat; newFormats[ index ] = targetFormats.concat( @@ -89,10 +88,8 @@ export function indentListItems( value, rootFormat ) { } } - return normaliseFormats( { - text, - formats: newFormats, - start, - end, - } ); + return { + ...value, + replacements: newFormats, + }; } diff --git a/packages/rich-text/src/index.js b/packages/rich-text/src/index.js index 423046079132b..9d9f462021287 100644 --- a/packages/rich-text/src/index.js +++ b/packages/rich-text/src/index.js @@ -8,6 +8,7 @@ export { charAt } from './char-at'; export { concat } from './concat'; export { create } from './create'; export { getActiveFormat } from './get-active-format'; +export { getActiveObject } from './get-active-object'; export { getSelectionEnd } from './get-selection-end'; export { getSelectionStart } from './get-selection-start'; export { getTextContent } from './get-text-content'; diff --git a/packages/rich-text/src/insert-line-separator.js b/packages/rich-text/src/insert-line-separator.js index e273c9eea9ac4..77434a25ac012 100644 --- a/packages/rich-text/src/insert-line-separator.js +++ b/packages/rich-text/src/insert-line-separator.js @@ -24,15 +24,16 @@ export function insertLineSeparator( ) { const beforeText = getTextContent( value ).slice( 0, startIndex ); const previousLineSeparatorIndex = beforeText.lastIndexOf( LINE_SEPARATOR ); - const previousLineSeparatorFormats = value.formats[ previousLineSeparatorIndex ]; - let formats = [ , ]; + const previousLineSeparatorFormats = value.replacements[ previousLineSeparatorIndex ]; + let replacements = [ , ]; if ( previousLineSeparatorFormats ) { - formats = [ previousLineSeparatorFormats ]; + replacements = [ previousLineSeparatorFormats ]; } const valueToInsert = { - formats, + formats: [ , ], + replacements, text: LINE_SEPARATOR, }; diff --git a/packages/rich-text/src/insert-object.js b/packages/rich-text/src/insert-object.js index fcdfc6f897c2d..7495d6082cbcb 100644 --- a/packages/rich-text/src/insert-object.js +++ b/packages/rich-text/src/insert-object.js @@ -25,11 +25,9 @@ export function insertObject( endIndex ) { const valueToInsert = { + formats: [ , ], + replacements: [ formatToInsert ], text: OBJECT_REPLACEMENT_CHARACTER, - formats: [ [ { - ...formatToInsert, - object: true, - } ] ], }; return insert( value, valueToInsert, startIndex, endIndex ); diff --git a/packages/rich-text/src/insert.js b/packages/rich-text/src/insert.js index cf46305240fb7..4d2353eaba4cb 100644 --- a/packages/rich-text/src/insert.js +++ b/packages/rich-text/src/insert.js @@ -19,11 +19,13 @@ import { normaliseFormats } from './normalise-formats'; * @return {Object} A new value with the value inserted. */ export function insert( - { formats, text, start, end }, + value, valueToInsert, - startIndex = start, - endIndex = end + startIndex = value.start, + endIndex = value.end ) { + const { formats, replacements, text } = value; + if ( typeof valueToInsert === 'string' ) { valueToInsert = create( { text: valueToInsert } ); } @@ -32,6 +34,7 @@ export function insert( return normaliseFormats( { formats: formats.slice( 0, startIndex ).concat( valueToInsert.formats, formats.slice( endIndex ) ), + replacements: replacements.slice( 0, startIndex ).concat( valueToInsert.replacements, replacements.slice( endIndex ) ), text: text.slice( 0, startIndex ) + valueToInsert.text + text.slice( endIndex ), start: index, end: index, diff --git a/packages/rich-text/src/join.js b/packages/rich-text/src/join.js index 7784b5962ca53..cabfc82756016 100644 --- a/packages/rich-text/src/join.js +++ b/packages/rich-text/src/join.js @@ -20,8 +20,9 @@ export function join( values, separator = '' ) { separator = create( { text: separator } ); } - return normaliseFormats( values.reduce( ( accumlator, { formats, text } ) => ( { - text: accumlator.text + separator.text + text, + return normaliseFormats( values.reduce( ( accumlator, { formats, replacements, text } ) => ( { formats: accumlator.formats.concat( separator.formats, formats ), + replacements: accumlator.replacements.concat( separator.replacements, replacements ), + text: accumlator.text + separator.text + text, } ) ) ); } diff --git a/packages/rich-text/src/normalise-formats.js b/packages/rich-text/src/normalise-formats.js index 533df66933886..72c04f818f9a4 100644 --- a/packages/rich-text/src/normalise-formats.js +++ b/packages/rich-text/src/normalise-formats.js @@ -13,13 +13,13 @@ import { isFormatEqual } from './is-format-equal'; /** * Normalises formats: ensures subsequent equal formats have the same reference. * - * @param {Object} value Value to normalise formats of. + * @param {Object} value Value to normalise formats of. * * @return {Object} New value with normalised formats. */ -export function normaliseFormats( { formats, text, start, end } ) { +export function normaliseFormats( value ) { const refs = []; - const newFormats = formats.map( ( formatsAtIndex ) => + const newFormats = value.formats.map( ( formatsAtIndex ) => formatsAtIndex.map( ( format ) => { const equalRef = find( refs, ( ref ) => isFormatEqual( ref, format ) @@ -35,5 +35,5 @@ export function normaliseFormats( { formats, text, start, end } ) { } ) ); - return { formats: newFormats, text, start, end }; + return { ...value, formats: newFormats }; } diff --git a/packages/rich-text/src/normalise-formats.native.js b/packages/rich-text/src/normalise-formats.native.js deleted file mode 100644 index 2a75e343a2c12..0000000000000 --- a/packages/rich-text/src/normalise-formats.native.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Internal dependencies - */ - -import { isFormatEqual } from './is-format-equal'; - -/** - * Normalises formats: ensures subsequent equal formats have the same reference. - * - * @param {Object} value Value to normalise formats of. - * - * @return {Object} New value with normalised formats. - */ -export function normaliseFormats( { formats, formatPlaceholder, text, start, end } ) { - const newFormats = formats.slice( 0 ); - - newFormats.forEach( ( formatsAtIndex, index ) => { - const lastFormatsAtIndex = newFormats[ index - 1 ]; - - if ( lastFormatsAtIndex ) { - const newFormatsAtIndex = formatsAtIndex.slice( 0 ); - - newFormatsAtIndex.forEach( ( format, formatIndex ) => { - const lastFormat = lastFormatsAtIndex[ formatIndex ]; - - if ( isFormatEqual( format, lastFormat ) ) { - newFormatsAtIndex[ formatIndex ] = lastFormat; - } - } ); - - newFormats[ index ] = newFormatsAtIndex; - } - } ); - - return { formats: newFormats, formatPlaceholder, text, start, end }; -} diff --git a/packages/rich-text/src/outdent-list-items.js b/packages/rich-text/src/outdent-list-items.js index 3a493caa9b03a..19fac90515dfb 100644 --- a/packages/rich-text/src/outdent-list-items.js +++ b/packages/rich-text/src/outdent-list-items.js @@ -3,7 +3,6 @@ */ import { LINE_SEPARATOR } from './special-characters'; -import { normaliseFormats } from './normalise-formats'; import { getLineIndex } from './get-line-index'; import { getParentLineIndex } from './get-parent-line-index'; import { getLastChildIndex } from './get-last-child-index'; @@ -16,16 +15,16 @@ import { getLastChildIndex } from './get-last-child-index'; * @return {Object} The changed value. */ export function outdentListItems( value ) { - const { text, formats, start, end } = value; + const { text, replacements, start, end } = value; const startingLineIndex = getLineIndex( value, start ); // Return early if the starting line index cannot be further outdented. - if ( formats[ startingLineIndex ] === undefined ) { + if ( replacements[ startingLineIndex ] === undefined ) { return value; } - const newFormats = formats.slice( 0 ); - const parentFormats = formats[ getParentLineIndex( value, startingLineIndex ) ] || []; + const newFormats = replacements.slice( 0 ); + const parentFormats = replacements[ getParentLineIndex( value, startingLineIndex ) ] || []; const endingLineIndex = getLineIndex( value, end ); const lastChildIndex = getLastChildIndex( value, endingLineIndex ); @@ -51,10 +50,8 @@ export function outdentListItems( value ) { } } - return normaliseFormats( { - text, - formats: newFormats, - start, - end, - } ); + return { + ...value, + replacements: newFormats, + }; } diff --git a/packages/rich-text/src/remove-format.js b/packages/rich-text/src/remove-format.js index be1c92ace7fd4..405cb6265e1e0 100644 --- a/packages/rich-text/src/remove-format.js +++ b/packages/rich-text/src/remove-format.js @@ -2,7 +2,7 @@ * External dependencies */ -import { find } from 'lodash'; +import { find, reject } from 'lodash'; /** * Internal dependencies @@ -23,28 +23,38 @@ import { normaliseFormats } from './normalise-formats'; * @return {Object} A new value with the format applied. */ export function removeFormat( - { formats, text, start, end }, + value, formatType, - startIndex = start, - endIndex = end + startIndex = value.start, + endIndex = value.end ) { - const newFormats = formats.slice( 0 ); + const newFormats = value.formats.slice( 0 ); // If the selection is collapsed, expand start and end to the edges of the // format. if ( startIndex === endIndex ) { const format = find( newFormats[ startIndex ], { type: formatType } ); - while ( find( newFormats[ startIndex ], format ) ) { - filterFormats( newFormats, startIndex, formatType ); - startIndex--; - } - - endIndex++; + if ( format ) { + while ( find( newFormats[ startIndex ], format ) ) { + filterFormats( newFormats, startIndex, formatType ); + startIndex--; + } - while ( find( newFormats[ endIndex ], format ) ) { - filterFormats( newFormats, endIndex, formatType ); endIndex++; + + while ( find( newFormats[ endIndex ], format ) ) { + filterFormats( newFormats, endIndex, formatType ); + endIndex++; + } + } else { + return { + ...value, + formatPlaceholder: reject( + newFormats[ startIndex - 1 ] || [], + { type: formatType } + ), + }; } } else { for ( let i = startIndex; i < endIndex; i++ ) { @@ -54,7 +64,7 @@ export function removeFormat( } } - return normaliseFormats( { formats: newFormats, text, start, end } ); + return normaliseFormats( { ...value, formats: newFormats } ); } function filterFormats( formats, index, formatType ) { diff --git a/packages/rich-text/src/remove-format.native.js b/packages/rich-text/src/remove-format.native.js index b6213f813bb63..5c20260728b71 100644 --- a/packages/rich-text/src/remove-format.native.js +++ b/packages/rich-text/src/remove-format.native.js @@ -23,11 +23,12 @@ import { normaliseFormats } from './normalise-formats'; * @return {Object} A new value with the format applied. */ export function removeFormat( - { formats, formatPlaceholder, text, start, end }, + value, formatType, - startIndex = start, - endIndex = end + startIndex = value.start, + endIndex = value.end ) { + const { formats, formatPlaceholder, start, end } = value; const newFormats = formats.slice( 0 ); let newFormatPlaceholder = null; @@ -55,7 +56,7 @@ export function removeFormat( } } - return normaliseFormats( { formats: newFormats, formatPlaceholder: newFormatPlaceholder, text, start, end } ); + return normaliseFormats( { ...value, formats: newFormats, formatPlaceholder: newFormatPlaceholder } ); } function filterFormats( formats, index, formatType ) { diff --git a/packages/rich-text/src/replace.js b/packages/rich-text/src/replace.js index 110fc186bd638..0cb26cb7431bd 100644 --- a/packages/rich-text/src/replace.js +++ b/packages/rich-text/src/replace.js @@ -20,11 +20,12 @@ import { normaliseFormats } from './normalise-formats'; * * @return {Object} A new value with replacements applied. */ -export function replace( { formats, text, start, end }, pattern, replacement ) { +export function replace( { formats, replacements, text, start, end }, pattern, replacement ) { text = text.replace( pattern, ( match, ...rest ) => { const offset = rest[ rest.length - 2 ]; let newText = replacement; let newFormats; + let newReplacements; if ( typeof newText === 'function' ) { newText = replacement( match, ...rest ); @@ -32,9 +33,11 @@ export function replace( { formats, text, start, end }, pattern, replacement ) { if ( typeof newText === 'object' ) { newFormats = newText.formats; + newReplacements = newText.replacements; newText = newText.text; } else { newFormats = Array( newText.length ); + newReplacements = Array( newText.length ); if ( formats[ offset ] ) { newFormats = newFormats.fill( formats[ offset ] ); @@ -42,6 +45,7 @@ export function replace( { formats, text, start, end }, pattern, replacement ) { } formats = formats.slice( 0, offset ).concat( newFormats, formats.slice( offset + match.length ) ); + replacements = replacements.slice( 0, offset ).concat( newReplacements, replacements.slice( offset + match.length ) ); if ( start ) { start = end = offset + newText.length; @@ -50,5 +54,5 @@ export function replace( { formats, text, start, end }, pattern, replacement ) { return newText; } ); - return normaliseFormats( { formats, text, start, end } ); + return normaliseFormats( { formats, replacements, text, start, end } ); } diff --git a/packages/rich-text/src/slice.js b/packages/rich-text/src/slice.js index bb4313dd61309..b535a445d45f5 100644 --- a/packages/rich-text/src/slice.js +++ b/packages/rich-text/src/slice.js @@ -10,16 +10,19 @@ * @return {Object} A new extracted value. */ export function slice( - { formats, text, start, end }, - startIndex = start, - endIndex = end + value, + startIndex = value.start, + endIndex = value.end ) { + const { formats, replacements, text } = value; + if ( startIndex === undefined || endIndex === undefined ) { - return { formats, text }; + return { ...value }; } return { formats: formats.slice( startIndex, endIndex ), + replacements: replacements.slice( startIndex, endIndex ), text: text.slice( startIndex, endIndex ), }; } diff --git a/packages/rich-text/src/split.js b/packages/rich-text/src/split.js index a300c3ccbcca4..f79a675b1e1f4 100644 --- a/packages/rich-text/src/split.js +++ b/packages/rich-text/src/split.js @@ -15,7 +15,7 @@ import { replace } from './replace'; * * @return {Array} An array of new values. */ -export function split( { formats, text, start, end }, string ) { +export function split( { formats, replacements, text, start, end }, string ) { if ( typeof string !== 'string' ) { return splitAtSelection( ...arguments ); } @@ -26,6 +26,7 @@ export function split( { formats, text, start, end }, string ) { const startIndex = nextStart; const value = { formats: formats.slice( startIndex, startIndex + substring.length ), + replacements: replacements.slice( startIndex, startIndex + substring.length ), text: substring, }; @@ -50,16 +51,18 @@ export function split( { formats, text, start, end }, string ) { } function splitAtSelection( - { formats, text, start, end }, + { formats, replacements, text, start, end }, startIndex = start, endIndex = end ) { const before = { formats: formats.slice( 0, startIndex ), + replacements: replacements.slice( 0, startIndex ), text: text.slice( 0, startIndex ), }; const after = { formats: formats.slice( endIndex ), + replacements: replacements.slice( endIndex ), text: text.slice( endIndex ), start: 0, end: 0, diff --git a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap index a68cdab1c911e..ae6c0db2d5575 100644 --- a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap +++ b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap @@ -36,6 +36,7 @@ exports[`recordToDom should create a value with formatting with attributes 1`] = exports[`recordToDom should create a value with image object 1`] = ` + @@ -45,7 +46,10 @@ exports[`recordToDom should create a value with image object 1`] = ` exports[`recordToDom should create a value with image object and formatting 1`] = ` - + + @@ -57,7 +61,10 @@ exports[`recordToDom should create a value with image object and formatting 1`] exports[`recordToDom should create a value with image object and text after 1`] = ` - + + diff --git a/packages/rich-text/src/test/apply-format.js b/packages/rich-text/src/test/apply-format.js index de4f7b69e5f5e..b8cff3bc47620 100644 --- a/packages/rich-text/src/test/apply-format.js +++ b/packages/rich-text/src/test/apply-format.js @@ -61,7 +61,7 @@ describe( 'applyFormat', () => { }; const expected = { ...record, - formatPlaceholder: a2, + formatPlaceholder: [ a2 ], }; const result = applyFormat( deepFreeze( record ), a2 ); diff --git a/packages/rich-text/src/test/change-list-type.js b/packages/rich-text/src/test/change-list-type.js index 3817c66ba7977..6294368c6427d 100644 --- a/packages/rich-text/src/test/change-list-type.js +++ b/packages/rich-text/src/test/change-list-type.js @@ -17,7 +17,7 @@ describe( 'changeListType', () => { it( 'should only change list type if list item is indented', () => { const record = { - formats: [ , ], + replacements: [ , ], text: '1', start: 1, end: 1, @@ -26,27 +26,25 @@ describe( 'changeListType', () => { expect( result ).toEqual( record ); expect( result ).toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); it( 'should change list type', () => { const record = { - formats: [ , [ ul ] ], + replacements: [ , [ ul ] ], text: `1${ LINE_SEPARATOR }`, start: 2, end: 2, }; const expected = { - formats: [ , [ ol ] ], - text: `1${ LINE_SEPARATOR }`, - start: 2, - end: 2, + ...record, + replacements: [ , [ ol ] ], }; const result = changeListType( deepFreeze( record ), ol ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 1 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 1 ); } ); it( 'should outdent with multiple lines selected', () => { @@ -54,21 +52,19 @@ describe( 'changeListType', () => { const text = `a${ LINE_SEPARATOR }1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }i${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4${ LINE_SEPARATOR }b`; const record = { - formats: [ , [ ul ], , [ ul ], , [ ul, ul ], , [ ul ], , [ ul ], , , , [ ul ], , ], + replacements: [ , [ ul ], , [ ul ], , [ ul, ul ], , [ ul ], , [ ul ], , , , [ ul ], , ], text, start: 4, end: 9, }; const expected = { - formats: [ , [ ol ], , [ ol ], , [ ol, ul ], , [ ol ], , [ ol ], , , , [ ul ], , ], - text, - start: 4, - end: 9, + ...record, + replacements: [ , [ ol ], , [ ol ], , [ ol, ul ], , [ ol ], , [ ol ], , , , [ ul ], , ], }; const result = changeListType( deepFreeze( record ), ol ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 6 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 6 ); } ); } ); diff --git a/packages/rich-text/src/test/concat.js b/packages/rich-text/src/test/concat.js index 9ac2aa2dc7556..ae8253e1a5461 100644 --- a/packages/rich-text/src/test/concat.js +++ b/packages/rich-text/src/test/concat.js @@ -16,14 +16,17 @@ describe( 'concat', () => { it( 'should merge records', () => { const one = { formats: [ , , [ em ] ], + replacements: [ , , , ], text: 'one', }; const two = { formats: [ [ em ], , , ], + replacements: [ , , , ], text: 'two', }; const three = { formats: [ , , [ em ], [ em ], , , ], + replacements: [ , , , , , , ], text: 'onetwo', }; diff --git a/packages/rich-text/src/test/create.js b/packages/rich-text/src/test/create.js index 5128c55273d78..b585d72ec6614 100644 --- a/packages/rich-text/src/test/create.js +++ b/packages/rich-text/src/test/create.js @@ -81,6 +81,7 @@ describe( 'create', () => { expect( value ).toEqual( { formats: [ [ em ], [ em ], [ em, strong ], [ em, strong ] ], + replacements: [ , , , , ], text: 'test', } ); diff --git a/packages/rich-text/src/test/get-active-object.js b/packages/rich-text/src/test/get-active-object.js new file mode 100644 index 0000000000000..35d0f7637c7bc --- /dev/null +++ b/packages/rich-text/src/test/get-active-object.js @@ -0,0 +1,41 @@ +/** + * Internal dependencies + */ + +import { getActiveObject } from '../get-active-object'; +import { OBJECT_REPLACEMENT_CHARACTER } from '../special-characters'; + +describe( 'getActiveObject', () => { + it( 'should return object if selected', () => { + const record = { + replacements: [ { type: 'img' } ], + text: OBJECT_REPLACEMENT_CHARACTER, + start: 0, + end: 1, + }; + + expect( getActiveObject( record ) ).toEqual( { type: 'img' } ); + } ); + + it( 'should return nothing if nothing is selected', () => { + const record = { + replacements: [ { type: 'img' } ], + text: OBJECT_REPLACEMENT_CHARACTER, + start: 0, + end: 0, + }; + + expect( getActiveObject( record ) ).toBe( undefined ); + } ); + + it( 'should return nothing if te selection is not an object', () => { + const record = { + replacements: [ { type: 'em' } ], + text: 'a', + start: 0, + end: 1, + }; + + expect( getActiveObject( record ) ).toBe( undefined ); + } ); +} ); diff --git a/packages/rich-text/src/test/get-last-child-index.js b/packages/rich-text/src/test/get-last-child-index.js index 55c881d356555..f59cec9eeeab1 100644 --- a/packages/rich-text/src/test/get-last-child-index.js +++ b/packages/rich-text/src/test/get-last-child-index.js @@ -10,40 +10,40 @@ import deepFreeze from 'deep-freeze'; import { getLastChildIndex } from '../get-last-child-index'; import { LINE_SEPARATOR } from '../special-characters'; -describe( 'outdentListItems', () => { +describe( 'getLastChildIndex', () => { const ul = { type: 'ul' }; it( 'should return undefined if there is only one line', () => { expect( getLastChildIndex( deepFreeze( { - formats: [ , ], + replacements: [ , ], text: '1', } ), undefined ) ).toBe( undefined ); } ); it( 'should return the last line if no line is indented', () => { expect( getLastChildIndex( deepFreeze( { - formats: [ , ], + replacements: [ , ], text: `1${ LINE_SEPARATOR }`, } ), undefined ) ).toBe( 1 ); } ); it( 'should return the last child index', () => { expect( getLastChildIndex( deepFreeze( { - formats: [ , [ ul ], , [ ul ], , ], + replacements: [ , [ ul ], , [ ul ], , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, } ), undefined ) ).toBe( 3 ); } ); it( 'should return the last child index by sibling', () => { expect( getLastChildIndex( deepFreeze( { - formats: [ , [ ul ], , [ ul ], , ], + replacements: [ , [ ul ], , [ ul ], , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, } ), 1 ) ).toBe( 3 ); } ); it( 'should return the last child index (with further lower indented items)', () => { expect( getLastChildIndex( deepFreeze( { - formats: [ , [ ul ], , , , ], + replacements: [ , [ ul ], , , , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, } ), 1 ) ).toBe( 1 ); } ); diff --git a/packages/rich-text/src/test/get-parent-line-index.js b/packages/rich-text/src/test/get-parent-line-index.js index 4e6a75ffd0a6e..832ee4412dacc 100644 --- a/packages/rich-text/src/test/get-parent-line-index.js +++ b/packages/rich-text/src/test/get-parent-line-index.js @@ -15,28 +15,28 @@ describe( 'getParentLineIndex', () => { it( 'should return undefined if there is only one line', () => { expect( getParentLineIndex( deepFreeze( { - formats: [ , ], + replacements: [ , ], text: '1', } ), undefined ) ).toBe( undefined ); } ); it( 'should return undefined if the list is part of the first root list child', () => { expect( getParentLineIndex( deepFreeze( { - formats: [ , ], + replacements: [ , ], text: `1${ LINE_SEPARATOR }2`, } ), 2 ) ).toBe( undefined ); } ); it( 'should return the line index of the parent list (1)', () => { expect( getParentLineIndex( deepFreeze( { - formats: [ , , , [ ul ], , ], + replacements: [ , , , [ ul ], , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, } ), 3 ) ).toBe( 1 ); } ); it( 'should return the line index of the parent list (2)', () => { expect( getParentLineIndex( deepFreeze( { - formats: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], + replacements: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`, } ), 5 ) ).toBe( undefined ); } ); diff --git a/packages/rich-text/src/test/helpers/index.js b/packages/rich-text/src/test/helpers/index.js index 859e7e3acefc2..16b09bb19b832 100644 --- a/packages/rich-text/src/test/helpers/index.js +++ b/packages/rich-text/src/test/helpers/index.js @@ -4,7 +4,7 @@ export function getSparseArrayLength( array ) { const em = { type: 'em' }; const strong = { type: 'strong' }; -const img = { type: 'img', attributes: { src: '' }, object: true }; +const img = { type: 'img', attributes: { src: '' } }; const a = { type: 'a', attributes: { href: '#' } }; const ul = { type: 'ul' }; const ol = { type: 'ol' }; @@ -25,6 +25,7 @@ export const spec = [ start: 0, end: 0, formats: [], + replacements: [], text: '', }, }, @@ -43,6 +44,7 @@ export const spec = [ start: 0, end: 1, formats: [ , ], + replacements: [ , ], text: ' ', }, }, @@ -61,6 +63,7 @@ export const spec = [ start: 5, end: 5, formats: [ , , , , , , , , , , ], + replacements: [ , , , , , , , , , , ], text: 'test\u00a0 test', }, }, @@ -79,6 +82,7 @@ export const spec = [ start: 0, end: 0, formats: [], + replacements: [], text: '', }, }, @@ -97,6 +101,7 @@ export const spec = [ start: 0, end: 4, formats: [ , , , , ], + replacements: [ , , , , ], text: 'test', }, }, @@ -115,6 +120,7 @@ export const spec = [ start: 0, end: 2, formats: [ , , ], + replacements: [ , , ], text: '🍒', }, }, @@ -133,6 +139,7 @@ export const spec = [ start: 0, end: 2, formats: [ [ em ], [ em ] ], + replacements: [ , , ], text: '🍒', }, }, @@ -151,6 +158,7 @@ export const spec = [ start: 0, end: 4, formats: [ [ em ], [ em ], [ em ], [ em ] ], + replacements: [ , , , , ], text: 'test', }, }, @@ -169,6 +177,7 @@ export const spec = [ start: 0, end: 4, formats: [ [ em, strong ], [ em, strong ], [ em, strong ], [ em, strong ] ], + replacements: [ , , , , ], text: 'test', }, }, @@ -187,6 +196,7 @@ export const spec = [ start: 0, end: 2, formats: [ [ em ], [ em ], [ em ], [ em ] ], + replacements: [ , , , , ], text: 'test', }, }, @@ -205,6 +215,7 @@ export const spec = [ start: 0, end: 4, formats: [ [ a ], [ a ], [ a ], [ a ] ], + replacements: [ , , , , ], text: 'test', }, }, @@ -217,12 +228,13 @@ export const spec = [ endOffset: 1, endContainer: element, } ), - startPath: [ 1, 0 ], - endPath: [ 1, 0 ], + startPath: [ 0, 0 ], + endPath: [ 0, 0 ], record: { start: 0, end: 0, - formats: [ [ img ] ], + formats: [ , ], + replacements: [ img ], text: '\ufffc', }, }, @@ -235,12 +247,13 @@ export const spec = [ endOffset: 1, endContainer: element.querySelector( 'img' ), } ), - startPath: [ 0, 1, 0 ], - endPath: [ 0, 1, 0 ], + startPath: [ 0, 0, 0 ], + endPath: [ 0, 2, 0 ], record: { start: 0, end: 1, - formats: [ [ em, img ] ], + formats: [ [ em ] ], + replacements: [ img ], text: '\ufffc', }, }, @@ -258,7 +271,8 @@ export const spec = [ record: { start: 0, end: 5, - formats: [ , , [ em ], [ em ], [ em, img ] ], + formats: [ , , [ em ], [ em ], [ em ] ], + replacements: [ , , , , img ], text: 'test\ufffc', }, }, @@ -271,12 +285,13 @@ export const spec = [ endOffset: 2, endContainer: element, } ), - startPath: [ 0, 1, 0 ], + startPath: [ 0, 0, 0 ], endPath: [ 1, 2 ], record: { start: 0, end: 5, - formats: [ [ em, img ], [ em ], [ em ], , , ], + formats: [ [ em ], [ em ], [ em ], , , ], + replacements: [ img, , , , , ], text: '\ufffctest', }, }, @@ -295,6 +310,7 @@ export const spec = [ start: 0, end: 0, formats: [ , ], + replacements: [ , ], text: '\n', }, }, @@ -313,6 +329,7 @@ export const spec = [ start: 2, end: 3, formats: [ , , , , , ], + replacements: [ , , , , , ], text: 'te\nst', }, }, @@ -331,6 +348,7 @@ export const spec = [ start: 0, end: 1, formats: [ [ em ] ], + replacements: [ , ], text: '\n', }, }, @@ -347,6 +365,7 @@ export const spec = [ endPath: [ 4, 0 ], record: { formats: [ , , , , ], + replacements: [ , , , , ], text: 'a\n\nb', start: 2, end: 3, @@ -365,6 +384,7 @@ export const spec = [ endPath: [ 2, 0 ], record: { formats: [ , , , , ], + replacements: [ , , , , ], text: 'a\n\nb', start: 2, end: 2, @@ -386,6 +406,7 @@ export const spec = [ start: 0, end: 0, formats: [], + replacements: [], text: '', }, }, @@ -405,6 +426,7 @@ export const spec = [ start: 1, end: 4, formats: [ , , , , , , , ], + replacements: [ , , , , , , , ], text: 'one\u2028two', }, }, @@ -424,7 +446,8 @@ export const spec = [ record: { start: 0, end: 9, - formats: [ , , , [ ul ], , [ ul ], , [ ul, ol ], , [ ul, ol ], , , , , , , , ], + formats: [ , , , , , , , , , , , , , , , , , ], + replacements: [ , , , [ ul ], , [ ul ], , [ ul, ol ], , [ ul, ol ], , , , , , , , ], text: 'one\u2028a\u2028b\u20281\u20282\u2028three', }, }, @@ -445,6 +468,7 @@ export const spec = [ start: 0, end: 0, formats: [], + replacements: [], text: '', }, }, @@ -464,7 +488,8 @@ export const spec = [ record: { start: 1, end: 1, - formats: [ [ ul ] ], + formats: [ , ], + replacements: [ [ ul ] ], text: '\u2028', }, }, @@ -485,6 +510,7 @@ export const spec = [ start: 1, end: 1, formats: [ , , ], + replacements: [ , , ], text: '\u2028\u2028', }, }, @@ -504,6 +530,7 @@ export const spec = [ start: 4, end: 4, formats: [ , , , , ], + replacements: [ , , , , ], text: 'one\u2028', }, }, @@ -524,6 +551,7 @@ export const spec = [ start: 3, end: 3, formats: [ , , , ], + replacements: [ , , , ], text: 'one', }, }, @@ -534,6 +562,7 @@ export const spec = [ endPath: [], record: { formats: [ [ em ], [ em ], [ em ], [ em ], [ em ], [ em ], [ em ] ], + replacements: [ , , , , , , , ], text: 'one\u2028two', }, }, @@ -552,6 +581,7 @@ export const spec = [ start: 0, end: 0, formats: [], + replacements: [], text: '', }, }, @@ -570,6 +600,7 @@ export const spec = [ start: 0, end: 4, formats: [ [ strong ], [ strong ], [ strong ], [ strong ] ], + replacements: [ , , , , ], text: 'test', }, }, @@ -592,6 +623,7 @@ export const specWithRegistration = [ attributes: {}, unregisteredAttributes: {}, } ] ], + replacements: [ , ], text: 'a', }, }, @@ -613,6 +645,7 @@ export const specWithRegistration = [ class: 'test', }, } ] ], + replacements: [ , ], text: 'a', }, }, @@ -634,6 +667,7 @@ export const specWithRegistration = [ class: 'custom-format', }, } ] ], + replacements: [ , ], text: 'a', }, }, @@ -647,6 +681,7 @@ export const specWithRegistration = [ class: 'custom-format', }, } ] ], + replacements: [ , ], text: 'a', }, }, @@ -663,6 +698,7 @@ export const specWithRegistration = [ html: 'a', value: { formats: [ , ], + replacements: [ , ], text: 'a', }, noToHTMLString: true, @@ -685,6 +721,7 @@ export const specWithRegistration = [ attributes: {}, unregisteredAttributes: {}, } ] ], + replacements: [ , ], text: 'a', }, }, diff --git a/packages/rich-text/src/test/indent-list-items.js b/packages/rich-text/src/test/indent-list-items.js index e7f631e5fa8f9..349174247cc42 100644 --- a/packages/rich-text/src/test/indent-list-items.js +++ b/packages/rich-text/src/test/indent-list-items.js @@ -17,7 +17,7 @@ describe( 'indentListItems', () => { it( 'should not indent only item', () => { const record = { - formats: [ , ], + replacements: [ , ], text: '1', start: 1, end: 1, @@ -26,34 +26,32 @@ describe( 'indentListItems', () => { expect( result ).toEqual( record ); expect( result ).toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); it( 'should indent', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }`; const record = { - formats: [ , , ], + replacements: [ , , ], text, start: 2, end: 2, }; const expected = { - formats: [ , [ ul ] ], - text, - start: 2, - end: 2, + ...record, + replacements: [ , [ ul ] ], }; const result = indentListItems( deepFreeze( record ), ul ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 1 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 1 ); } ); it( 'should not indent without target list', () => { const record = { - formats: [ , [ ul ] ], + replacements: [ , [ ul ] ], text: `1${ LINE_SEPARATOR }`, start: 2, end: 2, @@ -62,80 +60,74 @@ describe( 'indentListItems', () => { expect( result ).toEqual( record ); expect( result ).toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 1 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 1 ); } ); it( 'should indent and merge with previous list', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }${ LINE_SEPARATOR }`; const record = { - formats: [ , [ ol ], , ], + replacements: [ , [ ol ], , ], text, start: 3, end: 3, }; const expected = { - formats: [ , [ ol ], [ ol ] ], - text, - start: 3, - end: 3, + ...record, + replacements: [ , [ ol ], [ ol ] ], }; const result = indentListItems( deepFreeze( record ), ul ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should indent already indented item', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`; const record = { - formats: [ , [ ul ], , [ ul ], , ], + replacements: [ , [ ul ], , [ ul ], , ], text, start: 5, end: 5, }; const expected = { - formats: [ , [ ul ], , [ ul, ul ], , ], - text, - start: 5, - end: 5, + ...record, + replacements: [ , [ ul ], , [ ul, ul ], , ], }; const result = indentListItems( deepFreeze( record ), ul ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should indent with multiple lines selected', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`; const record = { - formats: [ , , , [ ul ], , ], + replacements: [ , , , [ ul ], , ], text, start: 2, end: 5, }; const expected = { - formats: [ , [ ul ], , [ ul, ul ], , ], - text, - start: 2, - end: 5, + ...record, + replacements: [ , [ ul ], , [ ul, ul ], , ], }; const result = indentListItems( deepFreeze( record ), ul ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should indent one level at a time', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`; const record = { - formats: [ , [ ul ], , [ ul, ul ], , , , ], + replacements: [ , [ ul ], , [ ul, ul ], , , , ], text, start: 6, end: 6, @@ -144,34 +136,28 @@ describe( 'indentListItems', () => { const result1 = indentListItems( deepFreeze( record ), ul ); expect( result1 ).not.toBe( record ); - expect( getSparseArrayLength( result1.formats ) ).toBe( 3 ); + expect( getSparseArrayLength( result1.replacements ) ).toBe( 3 ); expect( result1 ).toEqual( { - formats: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], - text, - start: 6, - end: 6, + ...record, + replacements: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], } ); const result2 = indentListItems( deepFreeze( result1 ), ul ); expect( result2 ).not.toBe( result1 ); - expect( getSparseArrayLength( result2.formats ) ).toBe( 3 ); + expect( getSparseArrayLength( result2.replacements ) ).toBe( 3 ); expect( result2 ).toEqual( { - formats: [ , [ ul ], , [ ul, ul ], , [ ul, ul ], , ], - text, - start: 6, - end: 6, + ...record, + replacements: [ , [ ul ], , [ ul, ul ], , [ ul, ul ], , ], } ); const result3 = indentListItems( deepFreeze( result2 ), ul ); expect( result3 ).not.toBe( result2 ); - expect( getSparseArrayLength( result3.formats ) ).toBe( 3 ); + expect( getSparseArrayLength( result3.replacements ) ).toBe( 3 ); expect( result3 ).toEqual( { - formats: [ , [ ul ], , [ ul, ul ], , [ ul, ul, ul ], , ], - text, - start: 6, - end: 6, + ...record, + replacements: [ , [ ul ], , [ ul, ul ], , [ ul, ul, ul ], , ], } ); } ); } ); diff --git a/packages/rich-text/src/test/insert-line-separator.js b/packages/rich-text/src/test/insert-line-separator.js index 398b0a2b8834f..497555cc4a01a 100644 --- a/packages/rich-text/src/test/insert-line-separator.js +++ b/packages/rich-text/src/test/insert-line-separator.js @@ -17,12 +17,14 @@ describe( 'insertLineSeparator', () => { it( 'should insert line separator at end', () => { const value = { formats: [ , ], + replacements: [ , ], text: '1', start: 1, end: 1, }; const expected = { formats: [ , , ], + replacements: [ , , ], text: `1${ LINE_SEPARATOR }`, start: 2, end: 2, @@ -31,18 +33,20 @@ describe( 'insertLineSeparator', () => { expect( result ).not.toBe( value ); expect( result ).toEqual( expected ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); it( 'should insert line separator at start', () => { const value = { formats: [ , ], + replacements: [ , ], text: '1', start: 0, end: 0, }; const expected = { formats: [ , , ], + replacements: [ , , ], text: `${ LINE_SEPARATOR }1`, start: 1, end: 1, @@ -51,18 +55,20 @@ describe( 'insertLineSeparator', () => { expect( result ).not.toBe( value ); expect( result ).toEqual( expected ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); it( 'should insert line separator with previous line separator formats', () => { const value = { - formats: [ , , , [ ol ], , ], + formats: [ , , , , , ], + replacements: [ , , , [ ol ], , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a`, start: 5, end: 5, }; const expected = { - formats: [ , , , [ ol ], , [ ol ] ], + formats: [ , , , , , , ], + replacements: [ , , , [ ol ], , [ ol ] ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a${ LINE_SEPARATOR }`, start: 6, end: 6, @@ -71,18 +77,20 @@ describe( 'insertLineSeparator', () => { expect( result ).not.toBe( value ); expect( result ).toEqual( expected ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should insert line separator without formats if previous line separator did not have any', () => { const value = { formats: [ , , , , , ], + replacements: [ , , , , , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a`, start: 5, end: 5, }; const expected = { formats: [ , , , , , , ], + replacements: [ , , , , , , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a${ LINE_SEPARATOR }`, start: 6, end: 6, @@ -91,6 +99,6 @@ describe( 'insertLineSeparator', () => { expect( result ).not.toBe( value ); expect( result ).toEqual( expected ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); } ); diff --git a/packages/rich-text/src/test/insert-object.js b/packages/rich-text/src/test/insert-object.js index 15680372986a9..7c2ba57806ad0 100644 --- a/packages/rich-text/src/test/insert-object.js +++ b/packages/rich-text/src/test/insert-object.js @@ -17,12 +17,14 @@ describe( 'insert', () => { it( 'should delete and insert', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', start: 6, end: 6, }; const expected = { - formats: [ , , [ { ...obj, object: true } ], [ em ], , , , , , , ], + formats: [ , , , [ em ], , , , , , , ], + replacements: [ , , obj, , , , , , , , ], text: `on${ OBJECT_REPLACEMENT_CHARACTER }o three`, start: 3, end: 3, @@ -31,6 +33,7 @@ describe( 'insert', () => { expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.formats ) ).toBe( 1 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 1 ); } ); } ); diff --git a/packages/rich-text/src/test/insert.js b/packages/rich-text/src/test/insert.js index 64de9f44828a2..cae4cc5b85d5f 100644 --- a/packages/rich-text/src/test/insert.js +++ b/packages/rich-text/src/test/insert.js @@ -17,16 +17,19 @@ describe( 'insert', () => { it( 'should delete and insert', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [], text: 'one two three', start: 6, end: 6, }; const toInsert = { formats: [ [ strong ] ], + replacements: [], text: 'a', }; const expected = { formats: [ , , [ strong ], [ em ], , , , , , , ], + replacements: [], text: 'onao three', start: 3, end: 3, @@ -41,16 +44,19 @@ describe( 'insert', () => { it( 'should insert line break with selection', () => { const record = { formats: [ , , ], + replacements: [], text: 'tt', start: 1, end: 1, }; const toInsert = { formats: [ , ], + replacements: [], text: '\n', }; const expected = { formats: [ , , , ], + replacements: [], text: 't\nt', start: 2, end: 2, diff --git a/packages/rich-text/src/test/join.js b/packages/rich-text/src/test/join.js index fb2f20b1b2784..84801125cc2b0 100644 --- a/packages/rich-text/src/test/join.js +++ b/packages/rich-text/src/test/join.js @@ -15,8 +15,9 @@ describe( 'join', () => { const separators = [ ' ', { - text: ' ', formats: [ , ], + replacements: [ , ], + text: ' ', }, ]; @@ -24,14 +25,17 @@ describe( 'join', () => { it( 'should join records with string separator', () => { const one = { formats: [ , , [ em ] ], + replacements: [ , , , ], text: 'one', }; const two = { formats: [ [ em ], , , ], + replacements: [ , , , ], text: 'two', }; const three = { formats: [ , , [ em ], , [ em ], , , ], + replacements: [ , , , , , , , ], text: 'one two', }; const result = join( [ deepFreeze( one ), deepFreeze( two ) ], separator ); diff --git a/packages/rich-text/src/test/outdent-list-items.js b/packages/rich-text/src/test/outdent-list-items.js index c2bf8b30e4766..e4325dfb61c90 100644 --- a/packages/rich-text/src/test/outdent-list-items.js +++ b/packages/rich-text/src/test/outdent-list-items.js @@ -16,7 +16,7 @@ describe( 'outdentListItems', () => { it( 'should not outdent only item', () => { const record = { - formats: [ , ], + replacements: [ , ], text: '1', start: 1, end: 1, @@ -25,138 +25,126 @@ describe( 'outdentListItems', () => { expect( result ).toEqual( record ); expect( result ).toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); it( 'should indent', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }`; const record = { - formats: [ , [ ul ] ], + replacements: [ , [ ul ] ], text, start: 2, end: 2, }; const expected = { - formats: [ , , ], - text, - start: 2, - end: 2, + ...record, + replacements: [ , , ], }; const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); it( 'should outdent two levels deep', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`; const record = { - formats: [ , [ ul ], , [ ul, ul ], , ], + replacements: [ , [ ul ], , [ ul, ul ], , ], text, start: 5, end: 5, }; const expected = { - formats: [ , [ ul ], , [ ul ], , ], - text, - start: 5, - end: 5, + ...record, + replacements: [ , [ ul ], , [ ul ], , ], }; const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should outdent with multiple lines selected', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`; const record = { - formats: [ , [ ul ], , [ ul, ul ], , ], + replacements: [ , [ ul ], , [ ul, ul ], , ], text, start: 2, end: 5, }; const expected = { - formats: [ , , , [ ul ], , ], - text, - start: 2, - end: 5, + ...record, + replacements: [ , , , [ ul ], , ], }; const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 1 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 1 ); } ); it( 'should outdent list item with children', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`; const record = { - formats: [ , [ ul ], , [ ul, ul ], , [ ul, ul ], , ], + replacements: [ , [ ul ], , [ ul, ul ], , [ ul, ul ], , ], text, start: 2, end: 2, }; const expected = { - formats: [ , , , [ ul ], , [ ul ], , ], - text, - start: 2, - end: 2, + ...record, + replacements: [ , , , [ ul ], , [ ul ], , ], }; const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should outdent list based on parent list', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`; const record = { - formats: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], + replacements: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], text, start: 6, end: 6, }; const expected = { - formats: [ , [ ul ], , [ ul, ul ], , , , ], - text, - start: 6, - end: 6, + ...record, + replacements: [ , [ ul ], , [ ul, ul ], , , , ], }; const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should outdent when a selected item is at level 0', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`; const record = { - formats: [ , [ ul ], , , , ], + replacements: [ , [ ul ], , , , ], text, start: 2, end: 5, }; const expected = { - formats: [ , , , , , ], - text, - start: 2, - end: 5, + ...record, + replacements: [ , , , , , ], }; const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); } ); diff --git a/packages/rich-text/src/test/replace.js b/packages/rich-text/src/test/replace.js index f3c7d9aa923e9..6cfd4dcc9e48b 100644 --- a/packages/rich-text/src/test/replace.js +++ b/packages/rich-text/src/test/replace.js @@ -16,12 +16,14 @@ describe( 'replace', () => { it( 'should replace string to string', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', start: 6, end: 6, }; const expected = { formats: [ , , , , [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , ], text: 'one 2 three', start: 5, end: 5, @@ -36,16 +38,19 @@ describe( 'replace', () => { it( 'should replace string to record', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', start: 6, end: 6, }; const replacement = { formats: [ , ], + replacements: [ , ], text: '2', }; const expected = { formats: [ , , , , , , , , , , , ], + replacements: [ , , , , , , , , , , , ], text: 'one 2 three', start: 5, end: 5, @@ -60,12 +65,14 @@ describe( 'replace', () => { it( 'should replace string to function', () => { const record = { formats: [ , , , , , , , , , , , , ], + replacements: [ , , , , , , , , , , , , ], text: 'abc12345#$*%', start: 6, end: 6, }; const expected = { formats: [ , , , , , , , , , , , , , , , , , , ], + replacements: [ , , , , , , , , , , , , , , , , , , ], text: 'abc - 12345 - #$*%', start: 18, end: 18, diff --git a/packages/rich-text/src/test/slice.js b/packages/rich-text/src/test/slice.js index 9d40f7a6de037..b181e5e9be817 100644 --- a/packages/rich-text/src/test/slice.js +++ b/packages/rich-text/src/test/slice.js @@ -16,10 +16,12 @@ describe( 'slice', () => { it( 'should slice', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', }; const expected = { formats: [ , [ em ], [ em ] ], + replacements: [ , , , ], text: ' tw', }; const result = slice( deepFreeze( record ), 3, 6 ); @@ -32,12 +34,14 @@ describe( 'slice', () => { it( 'should slice record', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', start: 3, end: 6, }; const expected = { formats: [ , [ em ], [ em ] ], + replacements: [ , , , ], text: ' tw', }; const result = slice( deepFreeze( record ) ); diff --git a/packages/rich-text/src/test/split.js b/packages/rich-text/src/test/split.js index 1cdfbae9630d2..8eef998e488d6 100644 --- a/packages/rich-text/src/test/split.js +++ b/packages/rich-text/src/test/split.js @@ -18,17 +18,20 @@ describe( 'split', () => { start: 5, end: 10, formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', }; const expected = [ { formats: [ , , , , [ em ], [ em ] ], + replacements: [ , , , , , , ], text: 'one tw', }, { start: 0, end: 0, formats: [ [ em ], , , , , , , ], + replacements: [ , , , , , , , ], text: 'o three', }, ]; @@ -45,6 +48,7 @@ describe( 'split', () => { it( 'should split with selection', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', start: 6, end: 6, @@ -52,10 +56,12 @@ describe( 'split', () => { const expected = [ { formats: [ , , , , [ em ], [ em ] ], + replacements: [ , , , , , , ], text: 'one tw', }, { formats: [ [ em ], , , , , , , ], + replacements: [ , , , , , , , ], text: 'o three', start: 0, end: 0, @@ -74,6 +80,7 @@ describe( 'split', () => { it( 'should split empty', () => { const record = { formats: [], + replacements: [], text: '', start: 0, end: 0, @@ -81,10 +88,12 @@ describe( 'split', () => { const expected = [ { formats: [], + replacements: [], text: '', }, { formats: [], + replacements: [], text: '', start: 0, end: 0, @@ -103,6 +112,7 @@ describe( 'split', () => { it( 'should split multiline', () => { const record = { formats: [ , , , , , , , , , , ], + replacements: [ , , , , , , , , , , ], text: 'test\u2028\u2028test', start: 5, end: 5, @@ -110,10 +120,12 @@ describe( 'split', () => { const expected = [ { formats: [ , , , , ], + replacements: [ , , , , ], text: 'test', }, { formats: [ , , , , ], + replacements: [ , , , , ], text: 'test', start: 0, end: 0, @@ -134,33 +146,39 @@ describe( 'split', () => { start: 6, end: 16, formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , , , , , , , , , , , ], + replacements: [ , , , , , , , , , , , , , , , , , , , , , , , ], text: 'one two three four five', }; const expected = [ { formats: [ , , , ], + replacements: [ , , , ], text: 'one', }, { start: 2, end: 3, formats: [ [ em ], [ em ], [ em ] ], + replacements: [ , , , ], text: 'two', }, { start: 0, end: 5, formats: [ , , , , , ], + replacements: [ , , , , , ], text: 'three', }, { start: 0, end: 2, formats: [ , , , , ], + replacements: [ , , , , ], text: 'four', }, { formats: [ , , , , ], + replacements: [ , , , , ], text: 'five', }, ]; @@ -179,21 +197,25 @@ describe( 'split', () => { start: 5, end: 6, formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', }; const expected = [ { formats: [ , , , ], + replacements: [ , , , ], text: 'one', }, { start: 1, end: 2, formats: [ [ em ], [ em ], [ em ] ], + replacements: [ , , , ], text: 'two', }, { formats: [ , , , , , ], + replacements: [ , , , , , ], text: 'three', }, ]; diff --git a/packages/rich-text/src/test/to-dom.js b/packages/rich-text/src/test/to-dom.js index 1df1e8e25b229..8db8a52e1b443 100644 --- a/packages/rich-text/src/test/to-dom.js +++ b/packages/rich-text/src/test/to-dom.js @@ -24,7 +24,6 @@ describe( 'recordToDom', () => { spec.forEach( ( { description, multilineTag, - multilineWrapperTags, record, startPath, endPath, @@ -33,7 +32,6 @@ describe( 'recordToDom', () => { const { body, selection } = toDom( { value: record, multilineTag, - multilineWrapperTags, } ); expect( body ).toMatchSnapshot(); expect( selection ).toEqual( { startPath, endPath } ); diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js index b81c518468444..9ebe8365a25ba 100644 --- a/packages/rich-text/src/to-dom.js +++ b/packages/rich-text/src/to-dom.js @@ -122,7 +122,6 @@ function prepareFormats( prepareEditableTree = [], value ) { export function toDom( { value, multilineTag, - multilineWrapperTags, prepareEditableTree, isEditableTree = true, } ) { @@ -135,7 +134,6 @@ export function toDom( { formats: prepareFormats( prepareEditableTree, value ), }, multilineTag, - multilineWrapperTags, createEmpty, append, getLastChild, @@ -174,7 +172,6 @@ export function apply( { value, current, multilineTag, - multilineWrapperTags, prepareEditableTree, __unstableDomOnly, } ) { @@ -182,7 +179,6 @@ export function apply( { const { body, selection } = toDom( { value, multilineTag, - multilineWrapperTags, prepareEditableTree, } ); diff --git a/packages/rich-text/src/to-html-string.js b/packages/rich-text/src/to-html-string.js index 0ba36e62510a3..bf6c60fda0442 100644 --- a/packages/rich-text/src/to-html-string.js +++ b/packages/rich-text/src/to-html-string.js @@ -21,16 +21,13 @@ import { toTree } from './to-tree'; * @param {Object} $1 Named argements. * @param {Object} $1.value Rich text value. * @param {string} [$1.multilineTag] Multiline tag. - * @param {Array} [$1.multilineWrapperTags] Tags where lines can be found if - * nesting is possible. * * @return {string} HTML string. */ -export function toHTMLString( { value, multilineTag, multilineWrapperTags } ) { +export function toHTMLString( { value, multilineTag } ) { const tree = toTree( { value, multilineTag, - multilineWrapperTags, createEmpty, append, getLastChild, diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js index c89bdc94a2d06..ef15fe7524b6e 100644 --- a/packages/rich-text/src/to-tree.js +++ b/packages/rich-text/src/to-tree.js @@ -91,7 +91,6 @@ const padding = { export function toTree( { value, multilineTag, - multilineWrapperTags = [], createEmpty, append, getLastChild, @@ -104,7 +103,7 @@ export function toTree( { onEndIndex, isEditableTree, } ) { - const { formats, text, start, end } = value; + const { formats, replacements, text, start, end } = value; const formatsLength = formats.length + 1; const tree = createEmpty(); const multilineFormat = { type: multilineTag }; @@ -138,12 +137,8 @@ export function toTree( { // Set multiline tags in queue for building the tree. if ( multilineTag ) { if ( character === LINE_SEPARATOR ) { - characterFormats = lastSeparatorFormats = ( characterFormats || [] ).reduce( ( accumulator, format ) => { - if ( character === LINE_SEPARATOR && multilineWrapperTags.indexOf( format.type ) !== -1 ) { - accumulator.push( format ); - accumulator.push( multilineFormat ); - } - + characterFormats = lastSeparatorFormats = ( replacements[ i ] || [] ).reduce( ( accumulator, format ) => { + accumulator.push( format, multilineFormat ); return accumulator; }, [ multilineFormat ] ); } else { @@ -196,11 +191,10 @@ export function toTree( { return; } - const { type, attributes, unregisteredAttributes, object } = format; + const { type, attributes, unregisteredAttributes } = format; const boundaryClass = ( isEditableTree && - ! object && character !== LINE_SEPARATOR && format === deepestActiveFormat ); @@ -210,7 +204,6 @@ export function toTree( { type, attributes, unregisteredAttributes, - object, boundaryClass, } ) ); @@ -218,7 +211,7 @@ export function toTree( { remove( pointer ); } - pointer = append( format.object ? parent : newNode, '' ); + pointer = append( newNode, '' ); } ); } @@ -240,22 +233,27 @@ export function toTree( { } } - if ( character !== OBJECT_REPLACEMENT_CHARACTER ) { - if ( character === '\n' ) { - pointer = append( getParent( pointer ), { - type: 'br', - attributes: isEditableTree ? { - 'data-rich-text-line-break': 'true', - } : undefined, - object: true, - } ); - // Ensure pointer is text node. - pointer = append( getParent( pointer ), '' ); - } else if ( ! isText( pointer ) ) { - pointer = append( getParent( pointer ), character ); - } else { - appendText( pointer, character ); - } + if ( character === OBJECT_REPLACEMENT_CHARACTER ) { + pointer = append( getParent( pointer ), fromFormat( { + ...replacements[ i ], + object: true, + } ) ); + // Ensure pointer is text node. + pointer = append( getParent( pointer ), '' ); + } else if ( character === '\n' ) { + pointer = append( getParent( pointer ), { + type: 'br', + attributes: isEditableTree ? { + 'data-rich-text-line-break': 'true', + } : undefined, + object: true, + } ); + // Ensure pointer is text node. + pointer = append( getParent( pointer ), '' ); + } else if ( ! isText( pointer ) ) { + pointer = append( getParent( pointer ), character ); + } else { + appendText( pointer, character ); } if ( onStartIndex && start === i + 1 ) { diff --git a/packages/rich-text/src/toggle-format.js b/packages/rich-text/src/toggle-format.js index 6e7854dcaa662..7545f5d30c9d6 100644 --- a/packages/rich-text/src/toggle-format.js +++ b/packages/rich-text/src/toggle-format.js @@ -14,10 +14,7 @@ import { applyFormat } from './apply-format'; * * @return {Object} A new value with the format applied or removed. */ -export function toggleFormat( - value, - format -) { +export function toggleFormat( value, format ) { if ( getActiveFormat( value, format.type ) ) { return removeFormat( value, format.type ); }