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