'use strict'; | |
//Const | |
var NOAH_ARK_CAPACITY = 3; | |
//List of formatting elements | |
var FormattingElementList = module.exports = function (treeAdapter) { | |
this.length = 0; | |
this.entries = []; | |
this.treeAdapter = treeAdapter; | |
this.bookmark = null; | |
}; | |
//Entry types | |
FormattingElementList.MARKER_ENTRY = 'MARKER_ENTRY'; | |
FormattingElementList.ELEMENT_ENTRY = 'ELEMENT_ENTRY'; | |
//Noah Ark's condition | |
//OPTIMIZATION: at first we try to find possible candidates for exclusion using | |
//lightweight heuristics without thorough attributes check. | |
FormattingElementList.prototype._getNoahArkConditionCandidates = function (newElement) { | |
var candidates = []; | |
if (this.length >= NOAH_ARK_CAPACITY) { | |
var neAttrsLength = this.treeAdapter.getAttrList(newElement).length, | |
neTagName = this.treeAdapter.getTagName(newElement), | |
neNamespaceURI = this.treeAdapter.getNamespaceURI(newElement); | |
for (var i = this.length - 1; i >= 0; i--) { | |
var entry = this.entries[i]; | |
if (entry.type === FormattingElementList.MARKER_ENTRY) | |
break; | |
var element = entry.element, | |
elementAttrs = this.treeAdapter.getAttrList(element); | |
if (this.treeAdapter.getTagName(element) === neTagName && | |
this.treeAdapter.getNamespaceURI(element) === neNamespaceURI && | |
elementAttrs.length === neAttrsLength) { | |
candidates.push({idx: i, attrs: elementAttrs}); | |
} | |
} | |
} | |
return candidates.length < NOAH_ARK_CAPACITY ? [] : candidates; | |
}; | |
FormattingElementList.prototype._ensureNoahArkCondition = function (newElement) { | |
var candidates = this._getNoahArkConditionCandidates(newElement), | |
cLength = candidates.length; | |
if (cLength) { | |
var neAttrs = this.treeAdapter.getAttrList(newElement), | |
neAttrsLength = neAttrs.length, | |
neAttrsMap = {}; | |
//NOTE: build attrs map for the new element so we can perform fast lookups | |
for (var i = 0; i < neAttrsLength; i++) { | |
var neAttr = neAttrs[i]; | |
neAttrsMap[neAttr.name] = neAttr.value; | |
} | |
for (var i = 0; i < neAttrsLength; i++) { | |
for (var j = 0; j < cLength; j++) { | |
var cAttr = candidates[j].attrs[i]; | |
if (neAttrsMap[cAttr.name] !== cAttr.value) { | |
candidates.splice(j, 1); | |
cLength--; | |
} | |
if (candidates.length < NOAH_ARK_CAPACITY) | |
return; | |
} | |
} | |
//NOTE: remove bottommost candidates until Noah's Ark condition will not be met | |
for (var i = cLength - 1; i >= NOAH_ARK_CAPACITY - 1; i--) { | |
this.entries.splice(candidates[i].idx, 1); | |
this.length--; | |
} | |
} | |
}; | |
//Mutations | |
FormattingElementList.prototype.insertMarker = function () { | |
this.entries.push({type: FormattingElementList.MARKER_ENTRY}); | |
this.length++; | |
}; | |
FormattingElementList.prototype.pushElement = function (element, token) { | |
this._ensureNoahArkCondition(element); | |
this.entries.push({ | |
type: FormattingElementList.ELEMENT_ENTRY, | |
element: element, | |
token: token | |
}); | |
this.length++; | |
}; | |
FormattingElementList.prototype.insertElementAfterBookmark = function (element, token) { | |
var bookmarkIdx = this.length - 1; | |
for (; bookmarkIdx >= 0; bookmarkIdx--) { | |
if (this.entries[bookmarkIdx] === this.bookmark) | |
break; | |
} | |
this.entries.splice(bookmarkIdx + 1, 0, { | |
type: FormattingElementList.ELEMENT_ENTRY, | |
element: element, | |
token: token | |
}); | |
this.length++; | |
}; | |
FormattingElementList.prototype.removeEntry = function (entry) { | |
for (var i = this.length - 1; i >= 0; i--) { | |
if (this.entries[i] === entry) { | |
this.entries.splice(i, 1); | |
this.length--; | |
break; | |
} | |
} | |
}; | |
FormattingElementList.prototype.clearToLastMarker = function () { | |
while (this.length) { | |
var entry = this.entries.pop(); | |
this.length--; | |
if (entry.type === FormattingElementList.MARKER_ENTRY) | |
break; | |
} | |
}; | |
//Search | |
FormattingElementList.prototype.getElementEntryInScopeWithTagName = function (tagName) { | |
for (var i = this.length - 1; i >= 0; i--) { | |
var entry = this.entries[i]; | |
if (entry.type === FormattingElementList.MARKER_ENTRY) | |
return null; | |
if (this.treeAdapter.getTagName(entry.element) === tagName) | |
return entry; | |
} | |
return null; | |
}; | |
FormattingElementList.prototype.getElementEntry = function (element) { | |
for (var i = this.length - 1; i >= 0; i--) { | |
var entry = this.entries[i]; | |
if (entry.type === FormattingElementList.ELEMENT_ENTRY && entry.element == element) | |
return entry; | |
} | |
return null; | |
}; |