DFS Helper (#639)

This commit is contained in:
Gerard Rovira
2021-09-23 10:43:23 +01:00
committed by acywatson
parent f30beec4ad
commit 1bf44d2a6f
10 changed files with 206 additions and 47 deletions

View File

@ -30,6 +30,7 @@ module.name_mapper='^outline/HistoryHelpers' -> '<PROJECT_ROOT>/packages/outline
module.name_mapper='^outline/SelectionHelpers' -> '<PROJECT_ROOT>/packages/outline/src/helpers/OutlineSelectionHelpers.js'
module.name_mapper='^outline/TextHelpers' -> '<PROJECT_ROOT>/packages/outline/src/helpers/OutlineTextHelpers.js'
module.name_mapper='^outline/KeyHelpers' -> '<PROJECT_ROOT>/packages/outline/src/helpers/OutlineKeyHelpers.js'
module.name_mapper='^outline/NodeHelpers' -> '<PROJECT_ROOT>/packages/outline/src/helpers/OutlineNodeHelpers.js'
module.name_mapper='^outline-react/OutlineTreeView' -> '<PROJECT_ROOT>/packages/outline-react/src/OutlineTreeView.js'
module.name_mapper='^outline-react/useOutlineEditor' -> '<PROJECT_ROOT>/packages/outline-react/src/useOutlineEditor.js'

View File

@ -45,6 +45,8 @@ module.exports = {
'<rootDir>/packages/outline/src/helpers/OutlineTextHelpers.js',
'^outline/KeyHelpers$':
'<rootDir>/packages/outline/src/helpers/OutlineKeyHelpers.js',
'^outline/NodeHelpers$':
'<rootDir>/packages/outline/src/helpers/OutlineNodeHelpers.js',
'^shared/getDOMTextNodeFromElement$':
'<rootDir>/packages/shared/src/getDOMTextNodeFromElement.js',
'^shared/isImmutableOrInert$':

View File

@ -15,6 +15,7 @@ module.exports = {
'outline/TextHelpers': 'outline/dist/OutlineTextHelpers',
'outline/HistoryHelpers': 'outline/dist/OutlineHistoryHelpers',
'outline/KeyHelpers': 'outline/dist/OutlineKeyHelpers',
'outline/NodeHelpers': 'outline/dist/OutlineNodeHelpers',
// Outline React
'outline-react/OutlineTreeView': 'outline-react/dist/OutlineTreeView',
'outline-react/useOutlineEditor': 'outline-react/dist/useOutlineEditor',

View File

@ -12,19 +12,8 @@ import React from 'react';
import ReactDOM from 'react-dom';
import ReactTestUtils from 'react-dom/test-utils';
import {BlockNode, createEditor, createTextNode} from 'outline';
class TestBlockNode extends BlockNode {
static clone(node: BlockNode) {
return new TestBlockNode(node.__key);
}
createDOM() {
return document.createElement('div');
}
updateDOM() {
return false;
}
}
import {createEditor, createTextNode} from 'outline';
import {createTestBlockNode} from '../utils';
describe('OutlineBlockNode tests', () => {
let container = null;
@ -76,7 +65,7 @@ describe('OutlineBlockNode tests', () => {
// Insert initial block
await update((view) => {
const block = new TestBlockNode();
const block = createTestBlockNode();
const text = createTextNode('Foo');
const text2 = createTextNode('Bar');
// Prevent text nodes from combining.
@ -96,7 +85,7 @@ describe('OutlineBlockNode tests', () => {
describe('getChildren()', () => {
test('no children', async () => {
await update(() => {
const block = new TestBlockNode();
const block = createTestBlockNode();
const children = block.getChildren();
expect(children).toHaveLength(0);
expect(children).toEqual([]);
@ -121,8 +110,8 @@ describe('OutlineBlockNode tests', () => {
test('nested', async () => {
await update((view) => {
const block = new TestBlockNode();
const innerBlock = new TestBlockNode();
const block = createTestBlockNode();
const innerBlock = createTestBlockNode();
const text = createTextNode('Foo');
text.select(0, 0);
const text2 = createTextNode('Bar');
@ -140,7 +129,7 @@ describe('OutlineBlockNode tests', () => {
expect(children).toHaveLength(4);
expect(children).toEqual([text, text2, text3, text4]);
const innerInnerBlock = new TestBlockNode();
const innerInnerBlock = createTestBlockNode();
const text5 = createTextNode('More');
const text6 = createTextNode('Stuff');
innerInnerBlock.append(text5);
@ -168,7 +157,7 @@ describe('OutlineBlockNode tests', () => {
test('empty', async () => {
await update(() => {
const block = new TestBlockNode();
const block = createTestBlockNode();
expect(block.getFirstChild()).toBe(null);
});
});
@ -185,7 +174,7 @@ describe('OutlineBlockNode tests', () => {
test('empty', async () => {
await update(() => {
const block = new TestBlockNode();
const block = createTestBlockNode();
expect(block.getLastChild()).toBe(null);
});
});
@ -202,15 +191,15 @@ describe('OutlineBlockNode tests', () => {
test('empty', async () => {
await update(() => {
const block = new TestBlockNode();
const block = createTestBlockNode();
expect(block.getTextContent()).toBe('');
});
});
test('nested', async () => {
await update((view) => {
const block = new TestBlockNode();
const innerBlock = new TestBlockNode();
const block = createTestBlockNode();
const innerBlock = createTestBlockNode();
const text = createTextNode('Foo');
text.select(0, 0);
const text2 = createTextNode('Bar');
@ -228,7 +217,7 @@ describe('OutlineBlockNode tests', () => {
expect(block.getTextContent()).toEqual('FooBar\n\nQux');
expect(block.getTextContent(true)).toEqual('FooBarBaz\n\nQux');
const innerInnerBlock = new TestBlockNode();
const innerInnerBlock = createTestBlockNode();
const text5 = createTextNode('More');
text5.makeInert();
const text6 = createTextNode('Stuff');

View File

@ -7,8 +7,8 @@
*/
import {createCodeNode, CodeNode} from 'outline/CodeNode';
import {ParagraphNode} from 'outline/ParagraphNode';
import {TextNode} from 'outline';
import {createParagraphNode} from 'outline/ParagraphNode';
import {createTextNode} from 'outline';
import {initializeUnitTest} from '../utils';
const editorConfig = Object.freeze({
@ -60,8 +60,8 @@ describe('OutlineCodeNode tests', () => {
const {editor} = testEnv;
await editor.update((view) => {
const root = view.getRoot();
const paragraphNode = new ParagraphNode();
const textNode = new TextNode('foo');
const paragraphNode = createParagraphNode();
const textNode = createTextNode('foo');
paragraphNode.append(textNode);
root.append(paragraphNode);
textNode.select(0, 0);

View File

@ -22,22 +22,7 @@ import {
import {createParagraphNode, ParagraphNode} from 'outline/ParagraphNode';
import useOutlineRichText from 'outline-react/useOutlineRichText';
import {getNodeByKey} from '../../core/OutlineNode';
class TestBlockNode extends BlockNode {
static clone(node: TestBlockNode) {
return new TestBlockNode(node.__key);
}
createDOM() {
return document.createElement('div');
}
updateDOM() {
return false;
}
}
function createTestBlockNode() {
return new TestBlockNode();
}
import {createTestBlockNode} from '../utils';
function sanitizeHTML(html) {
// Remove zero width characters

View File

@ -6,15 +6,15 @@
*
*/
import type {OutlineEditor} from 'outline';
import React from 'react';
import ReactDOM from 'react-dom';
import ReactTestUtils from 'react-dom/test-utils';
import {createEditor} from 'outline';
import {createEditor, BlockNode} from 'outline';
import {resetRandomKey} from '../../core/OutlineUtils';
import type {OutlineEditor} from 'outline';
type TestEnv = {
editor: OutlineEditor | null,
container: HTMLDivElement | null,
@ -80,3 +80,19 @@ export const initializeUnitTest = (runTests: (testEnv: TestEnv) => void) => {
runTests(testEnv);
};
export class TestBlockNode extends BlockNode {
static clone(node: BlockNode) {
return new TestBlockNode(node.__key);
}
createDOM() {
return document.createElement('div');
}
updateDOM() {
return false;
}
}
export function createTestBlockNode(): TestBlockNode {
return new TestBlockNode();
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/
import type {OutlineNode} from 'outline';
import {isBlockNode} from 'outline';
export function dfs(
startingNode: OutlineNode,
nextNode: (OutlineNode) => OutlineNode | null,
) {
let node = startingNode;
nextNode(node);
while (node !== null) {
if (isBlockNode(node) && node.getChildrenSize() > 0) {
node = node.getFirstChild();
} else {
// Find immediate sibling or nearest parent sibling
let sibling = null;
while (sibling === null && node !== null) {
sibling = node.getNextSibling();
if (sibling === null) {
node = node.getParent();
} else {
node = sibling;
}
}
}
if (node !== null) {
node = nextNode(node);
}
}
}

View File

@ -0,0 +1,120 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/
import type {OutlineEditor, View, NodeKey, OutlineNode} from 'outline';
import {
initializeUnitTest,
createTestBlockNode,
} from '../../../__tests__/utils';
import {dfs} from '../../OutlineNodeHelpers';
import {createParagraphNode, isParagraphNode} from 'outline/ParagraphNode';
import {createTextNode} from 'outline';
describe('OutlineNodeHelpers tests', () => {
initializeUnitTest((testEnv) => {
/**
* R
* P1 P2
* B1 B2 T4 T5 B3
* T1 T2 T3 T6
*
* DFS: R, P1, B1, T1, B2, T2, T3, P2, T4, T5, B3, T6
*/
test('DFS node order', async () => {
const editor: OutlineEditor = testEnv.editor;
let expectedKeys: Array<NodeKey> = [];
await editor.update((view: View) => {
const root = view.getRoot();
const paragraph1 = createParagraphNode();
const paragraph2 = createParagraphNode();
const block1 = createTestBlockNode();
const block2 = createTestBlockNode();
const block3 = createTestBlockNode();
const text1 = createTextNode('text1');
const text2 = createTextNode('text2');
const text3 = createTextNode('text3');
const text4 = createTextNode('text4');
const text5 = createTextNode('text5');
const text6 = createTextNode('text6');
root.append(paragraph1);
root.append(paragraph2);
paragraph1.append(block1);
paragraph1.append(block2);
paragraph2.append(text4);
paragraph2.append(text5);
text5.toggleBold(); // Prevent from merging with text 4
paragraph2.append(block3);
block1.append(text1);
block2.append(text2);
block2.append(text3);
text3.toggleBold(); // Prevent from merging with text2
block3.append(text6);
expectedKeys = [
root.getKey(),
paragraph1.getKey(),
block1.getKey(),
text1.getKey(),
block2.getKey(),
text2.getKey(),
text3.getKey(),
paragraph2.getKey(),
text4.getKey(),
text5.getKey(),
block3.getKey(),
text6.getKey(),
];
});
const dfsKeys = [];
await editor.update((view: View) => {
const root = view.getRoot();
dfs(root, (node: OutlineNode) => {
dfsKeys.push(node.getKey());
return node;
});
});
expect(dfsKeys).toEqual(expectedKeys);
});
test('Skip some DFS nodes', async () => {
const editor: OutlineEditor = testEnv.editor;
let expectedKeys: Array<NodeKey> = [];
await editor.update((view: View) => {
const root = view.getRoot();
const paragraph1 = createParagraphNode();
const block1 = createTestBlockNode();
const block2 = createTestBlockNode();
const block3 = createTestBlockNode();
root.append(paragraph1);
paragraph1.append(block1);
paragraph1.append(block2);
paragraph1.append(block3);
expectedKeys = [root.getKey(), paragraph1.getKey(), block3.getKey()];
});
const dfsKeys = [];
await editor.update((view: View) => {
const root = view.getRoot();
dfs(root, (node: OutlineNode) => {
dfsKeys.push(node.getKey());
if (isParagraphNode(node)) {
return (
node.getLastChild() && node.getLastChild().getPreviousSibling()
);
}
return node;
});
});
expect(dfsKeys).toEqual(expectedKeys);
});
});
});

View File

@ -143,6 +143,12 @@ async function build(name, inputFile, outputFile) {
'packages/outline/src/helpers/OutlineKeyHelpers',
),
},
{
find: isWWW ? 'Outline/NodeHelpers' : 'outline/NodeHelpers',
replacement: path.resolve(
'packages/outline/src/helpers/OutlineNodeHelpers',
),
},
{
find: isWWW ? 'Outline/TextHelpers' : 'outline/TextHelpers',
replacement: path.resolve(