You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DraftTreeInvariants.js.flow 4.8 KiB

3 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @format
  8. * @flow strict-local
  9. * @emails oncall+draft_js
  10. *
  11. * This is unstable and not part of the public API and should not be used by
  12. * production systems. This file may be update/removed without notice.
  13. */
  14. import type { BlockMap } from "./BlockMap";
  15. import type ContentBlockNode from "./ContentBlockNode";
  16. const warning = require("fbjs/lib/warning");
  17. const DraftTreeInvariants = {
  18. /**
  19. * Check if the block is valid
  20. */
  21. isValidBlock(block: ContentBlockNode, blockMap: BlockMap): boolean {
  22. const key = block.getKey(); // is its parent's child
  23. const parentKey = block.getParentKey();
  24. if (parentKey != null) {
  25. const parent = blockMap.get(parentKey);
  26. if (!parent.getChildKeys().includes(key)) {
  27. warning(true, 'Tree is missing parent -> child pointer on %s', key);
  28. return false;
  29. }
  30. } // is its children's parent
  31. const children = block.getChildKeys().map(k => blockMap.get(k));
  32. if (!children.every(c => c.getParentKey() === key)) {
  33. warning(true, 'Tree is missing child -> parent pointer on %s', key);
  34. return false;
  35. } // is its previous sibling's next sibling
  36. const prevSiblingKey = block.getPrevSiblingKey();
  37. if (prevSiblingKey != null) {
  38. const prevSibling = blockMap.get(prevSiblingKey);
  39. if (prevSibling.getNextSiblingKey() !== key) {
  40. warning(true, "Tree is missing nextSibling pointer on %s's prevSibling", key);
  41. return false;
  42. }
  43. } // is its next sibling's previous sibling
  44. const nextSiblingKey = block.getNextSiblingKey();
  45. if (nextSiblingKey != null) {
  46. const nextSibling = blockMap.get(nextSiblingKey);
  47. if (nextSibling.getPrevSiblingKey() !== key) {
  48. warning(true, "Tree is missing prevSibling pointer on %s's nextSibling", key);
  49. return false;
  50. }
  51. } // no 2-node cycles
  52. if (nextSiblingKey !== null && prevSiblingKey !== null) {
  53. if (prevSiblingKey === nextSiblingKey) {
  54. warning(true, 'Tree has a two-node cycle at %s', key);
  55. return false;
  56. }
  57. } // if it's a leaf node, it has text but no children
  58. if (block.text != '') {
  59. if (block.getChildKeys().size > 0) {
  60. warning(true, 'Leaf node %s has children', key);
  61. return false;
  62. }
  63. }
  64. return true;
  65. },
  66. /**
  67. * Checks that this is a connected tree on all the blocks
  68. * starting from the first block, traversing nextSibling and child pointers
  69. * should be a tree (preorder traversal - parent, then children)
  70. * num of connected node === number of blocks
  71. */
  72. isConnectedTree(blockMap: BlockMap): boolean {
  73. // exactly one node has no previous sibling + no parent
  74. const eligibleFirstNodes = blockMap.toArray().filter(block => block.getParentKey() == null && block.getPrevSiblingKey() == null);
  75. if (eligibleFirstNodes.length !== 1) {
  76. warning(true, 'Tree is not connected. More or less than one first node');
  77. return false;
  78. }
  79. const firstNode = eligibleFirstNodes.shift();
  80. let nodesSeen = 0;
  81. let currentKey = firstNode.getKey();
  82. const visitedStack = [];
  83. while (currentKey != null) {
  84. const currentNode = blockMap.get(currentKey);
  85. const childKeys = currentNode.getChildKeys();
  86. const nextSiblingKey = currentNode.getNextSiblingKey(); // if the node has children, add parent's next sibling to stack and go to children
  87. if (childKeys.size > 0) {
  88. if (nextSiblingKey != null) {
  89. visitedStack.unshift(nextSiblingKey);
  90. }
  91. const children = childKeys.map(k => blockMap.get(k));
  92. const firstNode = children.find(block => block.getPrevSiblingKey() == null);
  93. if (firstNode == null) {
  94. warning(true, '%s has no first child', currentKey);
  95. return false;
  96. }
  97. currentKey = firstNode.getKey(); // TODO(T32490138): Deal with multi-node cycles here
  98. } else {
  99. if (currentNode.getNextSiblingKey() != null) {
  100. currentKey = currentNode.getNextSiblingKey();
  101. } else {
  102. currentKey = visitedStack.shift();
  103. }
  104. }
  105. nodesSeen++;
  106. }
  107. if (nodesSeen !== blockMap.size) {
  108. warning(true, 'Tree is not connected. %s nodes were seen instead of %s', nodesSeen, blockMap.size);
  109. return false;
  110. }
  111. return true;
  112. },
  113. /**
  114. * Checks that the block map is a connected tree with valid blocks
  115. */
  116. isValidTree(blockMap: BlockMap): boolean {
  117. const blocks = blockMap.toArray();
  118. if (!blocks.every(block => this.isValidBlock(block, blockMap))) {
  119. return false;
  120. }
  121. return this.isConnectedTree(blockMap);
  122. }
  123. };
  124. module.exports = DraftTreeInvariants;