From 18cdd1868338e26dec02ca3792fc853dc5d0ddd1 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 7 Jul 2023 13:41:39 +0300 Subject: [PATCH 1/4] [CARE-1965] Refactor some of lists normalizer (without changing logic) --- .../normalizations/normalizeListChildren.ts | 35 ++++++------------- .../normalizeListItemChildren.ts | 4 +-- .../normalizations/normalizeSiblingLists.ts | 6 ++-- 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/packages/slate-lists/src/normalizations/normalizeListChildren.ts b/packages/slate-lists/src/normalizations/normalizeListChildren.ts index 4a6f50425..06130b209 100644 --- a/packages/slate-lists/src/normalizations/normalizeListChildren.ts +++ b/packages/slate-lists/src/normalizations/normalizeListChildren.ts @@ -1,5 +1,5 @@ import type { Editor, NodeEntry } from 'slate'; -import { Element, Node, Text, Transforms } from 'slate'; +import { Node, Text, Transforms } from 'slate'; import type { ListsSchema } from '../types'; @@ -18,22 +18,15 @@ export function normalizeListChildren( return false; } - let normalized = false; - const children = Array.from(Node.children(editor, path)); - children.forEach(([childNode, childPath]) => { - if (normalized) { - // Make sure at most 1 normalization operation is done at a time. - return; - } - + for (const [childNode, childPath] of children) { if (Text.isText(childNode)) { // This can happen during pasting // When pasting from MS Word there may be weird text nodes with some whitespace // characters. They're not expected to be deserialized so we remove them. - if (!childNode.text.trim()) { + if (childNode.text.trim() === '') { if (children.length > 1) { Transforms.removeNodes(editor, { at: childPath }); } else { @@ -41,8 +34,7 @@ export function normalizeListChildren( // to avoid never-ending normalization (Slate will insert empty text node). Transforms.removeNodes(editor, { at: path }); } - normalized = true; - return; + return true; } Transforms.wrapNodes( @@ -52,33 +44,26 @@ export function normalizeListChildren( }), { at: childPath }, ); - normalized = true; - return; - } - - if (!Element.isElement(childNode)) { - return; + return true; } if (schema.isListItemTextNode(childNode)) { Transforms.wrapNodes(editor, schema.createListItemNode(), { at: childPath }); - normalized = true; - return; + return true; } if (schema.isListNode(childNode)) { // Wrap it into a list item so that `normalizeOrphanNestedList` can take care of it. Transforms.wrapNodes(editor, schema.createListItemNode(), { at: childPath }); - normalized = true; - return; + return true; } if (!schema.isListItemNode(childNode)) { Transforms.setNodes(editor, schema.createListItemTextNode(), { at: childPath }); Transforms.wrapNodes(editor, schema.createListItemNode(), { at: childPath }); - normalized = true; + return true; } - }); + } - return normalized; + return false; } diff --git a/packages/slate-lists/src/normalizations/normalizeListItemChildren.ts b/packages/slate-lists/src/normalizations/normalizeListItemChildren.ts index 01629f061..ca22e4440 100644 --- a/packages/slate-lists/src/normalizations/normalizeListItemChildren.ts +++ b/packages/slate-lists/src/normalizations/normalizeListItemChildren.ts @@ -18,9 +18,7 @@ export function normalizeListItemChildren( const children = Array.from(Node.children(editor, path)); - for (let childIndex = 0; childIndex < children.length; ++childIndex) { - const [childNode, childPath] = children[childIndex]; - + for (const [childIndex, [childNode, childPath]] of children.entries()) { if (Text.isText(childNode) || editor.isInline(childNode)) { const listItemText = schema.createListItemTextNode({ children: [childNode], diff --git a/packages/slate-lists/src/normalizations/normalizeSiblingLists.ts b/packages/slate-lists/src/normalizations/normalizeSiblingLists.ts index b9aa9cc66..91750c309 100644 --- a/packages/slate-lists/src/normalizations/normalizeSiblingLists.ts +++ b/packages/slate-lists/src/normalizations/normalizeSiblingLists.ts @@ -22,9 +22,9 @@ export function normalizeSiblingLists( const [, path] = entry; const nextSibling = getNextSibling(editor, path); - if (!nextSibling) { - return false; + if (nextSibling) { + return mergeListWithPreviousSiblingList(editor, schema, nextSibling); } - return mergeListWithPreviousSiblingList(editor, schema, nextSibling); + return false; } From 1756f2ef4d13447f7233b39ef480c364558edfa6 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 7 Jul 2023 13:42:21 +0300 Subject: [PATCH 2/4] [CARE-1965] Add a test for infinite normalization loop --- .../src/modules/editor/Editor.test.tsx | 24 ++++++++++++++ .../expected/list-normalization-4.json | 18 +++++++++++ .../__tests__/input/list-normalization-4.json | 32 +++++++++++++++++++ .../src/modules/editor/test-utils.ts | 6 +++- 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 packages/slate-editor/src/modules/editor/__tests__/expected/list-normalization-4.json create mode 100644 packages/slate-editor/src/modules/editor/__tests__/input/list-normalization-4.json diff --git a/packages/slate-editor/src/modules/editor/Editor.test.tsx b/packages/slate-editor/src/modules/editor/Editor.test.tsx index 94c2fc1c5..2d941b792 100644 --- a/packages/slate-editor/src/modules/editor/Editor.test.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.test.tsx @@ -321,6 +321,30 @@ describe('Editor', () => { expect(editor.children).toMatchObject(JSON.parse(expected)); }); + /** + * @see CARE-1965 + */ + it('should normalize list-items directly nested into another list-item', () => { + const editor = createEditor( + + + + + + + , + ); + + const input = readTestFile('input/list-normalization-4.json'); + const expected = readTestFile('expected/list-normalization-4.json'); + + editor.children = JSON.parse(input).children; + + Editor.normalize(editor, { force: true }); + + expect(editor.children).toMatchObject(JSON.parse(expected)); + }); + /** * @see CARE-1320 */ diff --git a/packages/slate-editor/src/modules/editor/__tests__/expected/list-normalization-4.json b/packages/slate-editor/src/modules/editor/__tests__/expected/list-normalization-4.json new file mode 100644 index 000000000..2501f6da6 --- /dev/null +++ b/packages/slate-editor/src/modules/editor/__tests__/expected/list-normalization-4.json @@ -0,0 +1,18 @@ +[ + { + "type": "paragraph", + "children": [ + { + "text": "Hello" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "" + } + ] + } +] diff --git a/packages/slate-editor/src/modules/editor/__tests__/input/list-normalization-4.json b/packages/slate-editor/src/modules/editor/__tests__/input/list-normalization-4.json new file mode 100644 index 000000000..ae08a15fb --- /dev/null +++ b/packages/slate-editor/src/modules/editor/__tests__/input/list-normalization-4.json @@ -0,0 +1,32 @@ +{ + "type": "document", + "children": [ + { + "type": "list-item", + "children": [ + { + "type": "list-item-text", + "children": [ + { + "text": "Hello" + } + ] + }, + { + "type": "list-item", + "children": [ + { + "type": "list-item-text", + "children": [ + { + "text": "" + } + ] + } + ] + } + ] + } + ], + "version": "0.50" +} diff --git a/packages/slate-editor/src/modules/editor/test-utils.ts b/packages/slate-editor/src/modules/editor/test-utils.ts index 363be823f..1a59f4fd5 100644 --- a/packages/slate-editor/src/modules/editor/test-utils.ts +++ b/packages/slate-editor/src/modules/editor/test-utils.ts @@ -4,6 +4,7 @@ import type { Editor } from 'slate'; import type { EditorEventMap } from '#modules/events'; import { withEvents } from '#modules/events'; +import { hierarchySchema, withNodesHierarchy } from '#modules/nodes-hierarchy'; import { coverage, createDelayedResolve, oembedInfo } from '#modules/tests'; import { createEditor as createBaseEditor } from './createEditor'; @@ -102,5 +103,8 @@ export function getAllExtensions() { } export function createEditor(input: JSX.Element) { - return createBaseEditor(input as unknown as Editor, getAllExtensions, [withEvents(events)]); + return createBaseEditor(input as unknown as Editor, getAllExtensions, [ + withEvents(events), + withNodesHierarchy(hierarchySchema), + ]); } From 8e2194e18187850060a306b424df0946e9ae327e Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 7 Jul 2023 14:44:26 +0300 Subject: [PATCH 3/4] [CARE-1965] Change how `normalizeOrphanListItem` & `normalizeOrphanListItemText` works --- packages/slate-lists/src/lib/index.ts | 2 + .../src/lib/isContainingTextNodes.ts | 6 +++ .../slate-lists/src/lib/isElementOrEditor.ts | 8 ++++ .../normalizations/normalizeOrphanListItem.ts | 40 ++++++++++--------- .../normalizeOrphanListItemText.ts | 35 +++++++++------- 5 files changed, 57 insertions(+), 34 deletions(-) create mode 100644 packages/slate-lists/src/lib/isContainingTextNodes.ts create mode 100644 packages/slate-lists/src/lib/isElementOrEditor.ts diff --git a/packages/slate-lists/src/lib/index.ts b/packages/slate-lists/src/lib/index.ts index 1cb72a73f..53dc17f60 100644 --- a/packages/slate-lists/src/lib/index.ts +++ b/packages/slate-lists/src/lib/index.ts @@ -10,6 +10,8 @@ export { getParentListItem } from './getParentListItem'; export { getPrevSibling } from './getPrevSibling'; export { isAtStartOfListItem } from './isAtStartOfListItem'; export { isAtEmptyListItem } from './isAtEmptyListItem'; +export { isContainingTextNodes } from './isContainingTextNodes'; +export { isElementOrEditor } from './isElementOrEditor'; export { isInList } from './isInList'; export { isListItemContainingText } from './isListItemContainingText'; export { pickSubtreesRoots } from './pickSubtreesRoots'; diff --git a/packages/slate-lists/src/lib/isContainingTextNodes.ts b/packages/slate-lists/src/lib/isContainingTextNodes.ts new file mode 100644 index 000000000..21b928d32 --- /dev/null +++ b/packages/slate-lists/src/lib/isContainingTextNodes.ts @@ -0,0 +1,6 @@ +import type { Element } from 'slate'; +import { Text } from 'slate'; + +export function isContainingTextNodes(element: Element): boolean { + return element.children.some(Text.isText); +} diff --git a/packages/slate-lists/src/lib/isElementOrEditor.ts b/packages/slate-lists/src/lib/isElementOrEditor.ts new file mode 100644 index 000000000..e539a2442 --- /dev/null +++ b/packages/slate-lists/src/lib/isElementOrEditor.ts @@ -0,0 +1,8 @@ +import type { Editor, Element, Node } from 'slate'; + +/** + * The Slate's `Element.isElement()` is explicitly excluding `Editor`. + */ +export function isElementOrEditor(node: Node): node is Element | Editor { + return 'children' in node; +} diff --git a/packages/slate-lists/src/normalizations/normalizeOrphanListItem.ts b/packages/slate-lists/src/normalizations/normalizeOrphanListItem.ts index 93226e172..96780fafa 100644 --- a/packages/slate-lists/src/normalizations/normalizeOrphanListItem.ts +++ b/packages/slate-lists/src/normalizations/normalizeOrphanListItem.ts @@ -1,7 +1,7 @@ -import type { Node, NodeEntry } from 'slate'; -import { Editor, Transforms } from 'slate'; +import type { Node, NodeEntry, Editor } from 'slate'; +import { Element, Transforms } from 'slate'; -import { getParentList } from '../lib'; +import { isContainingTextNodes, isElementOrEditor } from '../lib'; import type { ListsSchema } from '../types'; /** @@ -17,22 +17,24 @@ export function normalizeOrphanListItem( schema: ListsSchema, [node, path]: NodeEntry, ): boolean { - if (!schema.isListItemNode(node)) { - // This function does not know how to normalize other nodes. - return false; + if (isElementOrEditor(node) && !schema.isListNode(node)) { + // We look for "list-item" nodes that are NOT under a "list" node + for (const [index, child] of node.children.entries()) { + if (Element.isElement(child) && schema.isListItemNode(child)) { + if (isContainingTextNodes(child)) { + Transforms.setNodes(editor, schema.createDefaultTextNode(), { + at: [...path, index], + }); + } else { + Transforms.unwrapNodes(editor, { + at: [...path, index], + mode: 'highest', + }); + } + return true; + } + } } - const parentList = getParentList(editor, schema, path); - - if (parentList) { - // If there is a parent "list", then the fix does not apply. - return false; - } - - Editor.withoutNormalizing(editor, () => { - Transforms.unwrapNodes(editor, { at: path }); - Transforms.setNodes(editor, schema.createDefaultTextNode(), { at: path }); - }); - - return true; + return false; } diff --git a/packages/slate-lists/src/normalizations/normalizeOrphanListItemText.ts b/packages/slate-lists/src/normalizations/normalizeOrphanListItemText.ts index c76f54922..8f51949aa 100644 --- a/packages/slate-lists/src/normalizations/normalizeOrphanListItemText.ts +++ b/packages/slate-lists/src/normalizations/normalizeOrphanListItemText.ts @@ -1,7 +1,7 @@ import type { Editor, Node, NodeEntry } from 'slate'; -import { Transforms } from 'slate'; +import { Element, Transforms } from 'slate'; -import { getParentListItem } from '../lib'; +import { isContainingTextNodes, isElementOrEditor } from '../lib'; import type { ListsSchema } from '../types'; /** @@ -17,19 +17,24 @@ export function normalizeOrphanListItemText( schema: ListsSchema, [node, path]: NodeEntry, ): boolean { - if (!schema.isListItemTextNode(node)) { - // This function does not know how to normalize other nodes. - return false; + if (isElementOrEditor(node) && !schema.isListItemNode(node)) { + // We look for "list-item-text" nodes that are NOT under a "list-item" node + for (const [index, child] of node.children.entries()) { + if (Element.isElement(child) && schema.isListItemTextNode(child)) { + if (isContainingTextNodes(child)) { + Transforms.setNodes(editor, schema.createDefaultTextNode(), { + at: [...path, index], + }); + } else { + Transforms.unwrapNodes(editor, { + at: [...path, index], + mode: 'highest', + }); + } + return true; + } + } } - const parentListItem = getParentListItem(editor, schema, path); - - if (parentListItem) { - // If there is a parent "list-item", then the fix does not apply. - return false; - } - - Transforms.setNodes(editor, schema.createDefaultTextNode(), { at: path }); - - return true; + return false; } From 7e19ac2421c29d64e76794d662fd7cf1d441194b Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 7 Jul 2023 14:45:37 +0300 Subject: [PATCH 4/4] [CARE-1965] Re-add `canvas` dev dependency This reverts #451 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 76a7044dd..9dd309053 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "babel-plugin-module-resolver": "^5.0.0", "babel-plugin-transform-remove-imports": "^1.7.0", "branch-pipe": "^1.0.1", + "canvas": "^2.11.2", "concurrently": "^6.4.0", "eslint": "^8.18.0", "eslint-config-prettier": "^8.5.0",