Skip to content
This repository has been archived by the owner on Feb 6, 2023. It is now read-only.

Commit

Permalink
Add utility methods for parent-child & sibling pointer updates
Browse files Browse the repository at this point in the history
Summary:
When the tree is updated, we need to make sure that we correctly mirror the parent-child & prevSibling-nextSibling links. The utility methods in this diff help make these pointer updates more resilient.

This diff introduces methods to add:
- a child to a parent (either as the first or last child)
- a prev <-> next sibling relationship

These functions will only respect the local invariants on the nodes being updated, and can leave the rest of the tree in an inconsistent state that requires a few more updates (see the test file for examples of this).

In the next diff, I'll be codifying some common paradigms (such as creating a new parent node & adding an existing node as  its child) which will use the methods implemented in this diff to simplify the logic. The paradigm-based operations will preserve the tree invariant after the operation.

Differential Revision: D9601093

fbshipit-source-id: 3152ffc24a49e7a591958aada3333f331aaef9dd
  • Loading branch information
niveditc authored and facebook-github-bot committed Aug 31, 2018
1 parent c5b785a commit 0cb80b7
Show file tree
Hide file tree
Showing 3 changed files with 865 additions and 0 deletions.
114 changes: 114 additions & 0 deletions src/model/modifier/exploration/DraftTreeOperations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @format
* @flow strict-local
*
* This is unstable and not part of the public API and should not be used by
* production systems. This file may be update/removed without notice.
*/
import type {BlockMap} from 'BlockMap';

const invariant = require('invariant');

const DraftTreeOperations = {
/**
* This is a utility method for setting B as a first/last child of A, ensuring
* that parent <-> child operations are correctly mirrored
*
* The block map returned by this method may not be a valid tree (siblings are
* unaffected)
*/
updateParentChild(
blockMap: BlockMap,
parentKey: string,
childKey: string,
position: 'first' | 'last',
): BlockMap {
const parent = blockMap.get(parentKey);
const child = blockMap.get(childKey);
invariant(
parent != null && child != null,
'parent & child should exist in the block map',
);
const existingChildren = parent.getChildKeys();
const newBlocks = {};
// add as parent's child
newBlocks[parentKey] = parent.merge({
children:
position === 'first'
? existingChildren.unshift(childKey)
: existingChildren.push(childKey),
});
// add as child's parent
if (existingChildren.count() !== 0) {
// link child as sibling to the existing children
switch (position) {
case 'first':
const nextSiblingKey = existingChildren.first();
newBlocks[childKey] = child.merge({
parent: parentKey,
nextSibling: nextSiblingKey,
prevSibling: null,
});
newBlocks[nextSiblingKey] = blockMap.get(nextSiblingKey).merge({
prevSibling: childKey,
});
break;
case 'last':
const prevSiblingKey = existingChildren.last();
newBlocks[childKey] = child.merge({
parent: parentKey,
prevSibling: prevSiblingKey,
nextSibling: null,
});
newBlocks[prevSiblingKey] = blockMap.get(prevSiblingKey).merge({
nextSibling: childKey,
});
break;
}
} else {
newBlocks[childKey] = child.merge({
parent: parentKey,
prevSibling: null,
nextSibling: null,
});
}
return blockMap.merge(newBlocks);
},

/**
* This is a utility method for setting B as the next sibling of A, ensuring
* that sibling operations are correctly mirrored
*
* The block map returned by this method may not be a valid tree (parent/child/
* other siblings are unaffected)
*/
updateSibling(
blockMap: BlockMap,
prevKey: string,
nextKey: string,
): BlockMap {
const prevSibling = blockMap.get(prevKey);
const nextSibling = blockMap.get(nextKey);
invariant(
prevSibling != null && nextSibling != null,
'siblings should exist in the block map',
);
const newBlocks = {};
newBlocks[prevKey] = prevSibling.merge({
nextSibling: nextKey,
});
newBlocks[nextKey] = nextSibling.merge({
prevSibling: prevKey,
});
return blockMap.merge(newBlocks);
},
};

module.exports = DraftTreeOperations;
159 changes: 159 additions & 0 deletions src/model/modifier/exploration/__tests__/DraftTreeOperations-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails oncall+ui_infra
* @flow strict-local
* @format
*/

'use strict';

jest.disableAutomock();

const ContentBlockNode = require('ContentBlockNode');
const DraftTreeOperations = require('DraftTreeOperations');

const Immutable = require('immutable');
const blockMap1 = Immutable.OrderedMap({
A: new ContentBlockNode({
key: 'A',
parent: null,
text: 'alpha',
children: Immutable.List([]),
prevSibling: null,
nextSibling: 'X',
}),
X: new ContentBlockNode({
key: 'X',
parent: null,
text: '',
children: Immutable.List(['B', 'C']),
prevSibling: 'A',
nextSibling: 'D',
}),
B: new ContentBlockNode({
key: 'B',
parent: 'X',
text: 'beta',
children: Immutable.List([]),
prevSibling: null,
nextSibling: 'C',
}),
C: new ContentBlockNode({
key: 'C',
parent: 'X',
text: 'charlie',
children: Immutable.List([]),
prevSibling: 'B',
nextSibling: null,
}),
D: new ContentBlockNode({
key: 'D',
parent: null,
text: 'delta',
children: Immutable.List([]),
prevSibling: 'X',
nextSibling: null,
}),
});

test('test adding a last child to parent', () => {
let newBlockMap = DraftTreeOperations.updateParentChild(
blockMap1,
'X',
'D',
'last',
);
newBlockMap = newBlockMap.merge({
X: newBlockMap.get('X').merge({
nextSibling: null,
}),
});
expect(newBlockMap).toMatchSnapshot();
});

test('test adding a first child to parent', () => {
let newBlockMap = DraftTreeOperations.updateParentChild(
blockMap1,
'X',
'D',
'first',
);
newBlockMap = newBlockMap.merge({
X: newBlockMap.get('X').merge({
nextSibling: null,
}),
});
expect(newBlockMap).toMatchSnapshot();
});

test('test adding a sibling', () => {
let newBlockMap = DraftTreeOperations.updateSibling(blockMap1, 'D', 'C');
newBlockMap = newBlockMap.merge({
B: newBlockMap.get('B').merge({
nextSibling: null,
}),
C: newBlockMap.get('C').merge({
parent: null,
}),
X: newBlockMap.get('X').merge({
children: ['B'],
}),
});
expect(newBlockMap).toMatchSnapshot();
});

const blockMap2 = Immutable.OrderedMap({
A: new ContentBlockNode({
key: 'A',
parent: null,
text: 'alpha',
children: Immutable.List([]),
prevSibling: null,
nextSibling: 'X',
}),
X: new ContentBlockNode({
key: 'X',
parent: null,
text: '',
children: Immutable.List([]),
prevSibling: 'A',
nextSibling: 'B',
}),
B: new ContentBlockNode({
key: 'B',
parent: null,
text: 'beta',
children: Immutable.List([]),
prevSibling: 'X',
nextSibling: 'C',
}),
C: new ContentBlockNode({
key: 'C',
parent: null,
text: 'charlie',
children: Immutable.List([]),
prevSibling: 'B',
nextSibling: null,
}),
});

test('test adding an only child to parent', () => {
let newBlockMap = DraftTreeOperations.updateParentChild(
blockMap2,
'X',
'C',
'first',
);
newBlockMap = newBlockMap.merge({
B: newBlockMap.get('B').merge({
nextSibling: null,
}),
});
expect(newBlockMap).toMatchSnapshot();
});
Loading

0 comments on commit 0cb80b7

Please sign in to comment.