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