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.

181 lines
5.2 KiB

  1. 'use strict';
  2. //Const
  3. const NOAH_ARK_CAPACITY = 3;
  4. //List of formatting elements
  5. class FormattingElementList {
  6. constructor(treeAdapter) {
  7. this.length = 0;
  8. this.entries = [];
  9. this.treeAdapter = treeAdapter;
  10. this.bookmark = null;
  11. }
  12. //Noah Ark's condition
  13. //OPTIMIZATION: at first we try to find possible candidates for exclusion using
  14. //lightweight heuristics without thorough attributes check.
  15. _getNoahArkConditionCandidates(newElement) {
  16. const candidates = [];
  17. if (this.length >= NOAH_ARK_CAPACITY) {
  18. const neAttrsLength = this.treeAdapter.getAttrList(newElement).length;
  19. const neTagName = this.treeAdapter.getTagName(newElement);
  20. const neNamespaceURI = this.treeAdapter.getNamespaceURI(newElement);
  21. for (let i = this.length - 1; i >= 0; i--) {
  22. const entry = this.entries[i];
  23. if (entry.type === FormattingElementList.MARKER_ENTRY) {
  24. break;
  25. }
  26. const element = entry.element;
  27. const elementAttrs = this.treeAdapter.getAttrList(element);
  28. const isCandidate =
  29. this.treeAdapter.getTagName(element) === neTagName &&
  30. this.treeAdapter.getNamespaceURI(element) === neNamespaceURI &&
  31. elementAttrs.length === neAttrsLength;
  32. if (isCandidate) {
  33. candidates.push({ idx: i, attrs: elementAttrs });
  34. }
  35. }
  36. }
  37. return candidates.length < NOAH_ARK_CAPACITY ? [] : candidates;
  38. }
  39. _ensureNoahArkCondition(newElement) {
  40. const candidates = this._getNoahArkConditionCandidates(newElement);
  41. let cLength = candidates.length;
  42. if (cLength) {
  43. const neAttrs = this.treeAdapter.getAttrList(newElement);
  44. const neAttrsLength = neAttrs.length;
  45. const neAttrsMap = Object.create(null);
  46. //NOTE: build attrs map for the new element so we can perform fast lookups
  47. for (let i = 0; i < neAttrsLength; i++) {
  48. const neAttr = neAttrs[i];
  49. neAttrsMap[neAttr.name] = neAttr.value;
  50. }
  51. for (let i = 0; i < neAttrsLength; i++) {
  52. for (let j = 0; j < cLength; j++) {
  53. const cAttr = candidates[j].attrs[i];
  54. if (neAttrsMap[cAttr.name] !== cAttr.value) {
  55. candidates.splice(j, 1);
  56. cLength--;
  57. }
  58. if (candidates.length < NOAH_ARK_CAPACITY) {
  59. return;
  60. }
  61. }
  62. }
  63. //NOTE: remove bottommost candidates until Noah's Ark condition will not be met
  64. for (let i = cLength - 1; i >= NOAH_ARK_CAPACITY - 1; i--) {
  65. this.entries.splice(candidates[i].idx, 1);
  66. this.length--;
  67. }
  68. }
  69. }
  70. //Mutations
  71. insertMarker() {
  72. this.entries.push({ type: FormattingElementList.MARKER_ENTRY });
  73. this.length++;
  74. }
  75. pushElement(element, token) {
  76. this._ensureNoahArkCondition(element);
  77. this.entries.push({
  78. type: FormattingElementList.ELEMENT_ENTRY,
  79. element: element,
  80. token: token
  81. });
  82. this.length++;
  83. }
  84. insertElementAfterBookmark(element, token) {
  85. let bookmarkIdx = this.length - 1;
  86. for (; bookmarkIdx >= 0; bookmarkIdx--) {
  87. if (this.entries[bookmarkIdx] === this.bookmark) {
  88. break;
  89. }
  90. }
  91. this.entries.splice(bookmarkIdx + 1, 0, {
  92. type: FormattingElementList.ELEMENT_ENTRY,
  93. element: element,
  94. token: token
  95. });
  96. this.length++;
  97. }
  98. removeEntry(entry) {
  99. for (let i = this.length - 1; i >= 0; i--) {
  100. if (this.entries[i] === entry) {
  101. this.entries.splice(i, 1);
  102. this.length--;
  103. break;
  104. }
  105. }
  106. }
  107. clearToLastMarker() {
  108. while (this.length) {
  109. const entry = this.entries.pop();
  110. this.length--;
  111. if (entry.type === FormattingElementList.MARKER_ENTRY) {
  112. break;
  113. }
  114. }
  115. }
  116. //Search
  117. getElementEntryInScopeWithTagName(tagName) {
  118. for (let i = this.length - 1; i >= 0; i--) {
  119. const entry = this.entries[i];
  120. if (entry.type === FormattingElementList.MARKER_ENTRY) {
  121. return null;
  122. }
  123. if (this.treeAdapter.getTagName(entry.element) === tagName) {
  124. return entry;
  125. }
  126. }
  127. return null;
  128. }
  129. getElementEntry(element) {
  130. for (let i = this.length - 1; i >= 0; i--) {
  131. const entry = this.entries[i];
  132. if (entry.type === FormattingElementList.ELEMENT_ENTRY && entry.element === element) {
  133. return entry;
  134. }
  135. }
  136. return null;
  137. }
  138. }
  139. //Entry types
  140. FormattingElementList.MARKER_ENTRY = 'MARKER_ENTRY';
  141. FormattingElementList.ELEMENT_ENTRY = 'ELEMENT_ENTRY';
  142. module.exports = FormattingElementList;