| //Const |
| const NOAH_ARK_CAPACITY = 3; |
| export var EntryType; |
| (function (EntryType) { |
| EntryType[EntryType["Marker"] = 0] = "Marker"; |
| EntryType[EntryType["Element"] = 1] = "Element"; |
| })(EntryType || (EntryType = {})); |
| const MARKER = { type: EntryType.Marker }; |
| //List of formatting elements |
| export class FormattingElementList { |
| constructor(treeAdapter) { |
| this.treeAdapter = treeAdapter; |
| this.entries = []; |
| this.bookmark = null; |
| } |
| //Noah Ark's condition |
| //OPTIMIZATION: at first we try to find possible candidates for exclusion using |
| //lightweight heuristics without thorough attributes check. |
| _getNoahArkConditionCandidates(newElement, neAttrs) { |
| const candidates = []; |
| const neAttrsLength = neAttrs.length; |
| const neTagName = this.treeAdapter.getTagName(newElement); |
| const neNamespaceURI = this.treeAdapter.getNamespaceURI(newElement); |
| for (let i = 0; i < this.entries.length; i++) { |
| const entry = this.entries[i]; |
| if (entry.type === EntryType.Marker) { |
| break; |
| } |
| const { element } = entry; |
| if (this.treeAdapter.getTagName(element) === neTagName && |
| this.treeAdapter.getNamespaceURI(element) === neNamespaceURI) { |
| const elementAttrs = this.treeAdapter.getAttrList(element); |
| if (elementAttrs.length === neAttrsLength) { |
| candidates.push({ idx: i, attrs: elementAttrs }); |
| } |
| } |
| } |
| return candidates; |
| } |
| _ensureNoahArkCondition(newElement) { |
| if (this.entries.length < NOAH_ARK_CAPACITY) |
| return; |
| const neAttrs = this.treeAdapter.getAttrList(newElement); |
| const candidates = this._getNoahArkConditionCandidates(newElement, neAttrs); |
| if (candidates.length < NOAH_ARK_CAPACITY) |
| return; |
| //NOTE: build attrs map for the new element, so we can perform fast lookups |
| const neAttrsMap = new Map(neAttrs.map((neAttr) => [neAttr.name, neAttr.value])); |
| let validCandidates = 0; |
| //NOTE: remove bottommost candidates, until Noah's Ark condition will not be met |
| for (let i = 0; i < candidates.length; i++) { |
| const candidate = candidates[i]; |
| // We know that `candidate.attrs.length === neAttrs.length` |
| if (candidate.attrs.every((cAttr) => neAttrsMap.get(cAttr.name) === cAttr.value)) { |
| validCandidates += 1; |
| if (validCandidates >= NOAH_ARK_CAPACITY) { |
| this.entries.splice(candidate.idx, 1); |
| } |
| } |
| } |
| } |
| //Mutations |
| insertMarker() { |
| this.entries.unshift(MARKER); |
| } |
| pushElement(element, token) { |
| this._ensureNoahArkCondition(element); |
| this.entries.unshift({ |
| type: EntryType.Element, |
| element, |
| token, |
| }); |
| } |
| insertElementAfterBookmark(element, token) { |
| const bookmarkIdx = this.entries.indexOf(this.bookmark); |
| this.entries.splice(bookmarkIdx, 0, { |
| type: EntryType.Element, |
| element, |
| token, |
| }); |
| } |
| removeEntry(entry) { |
| const entryIndex = this.entries.indexOf(entry); |
| if (entryIndex !== -1) { |
| this.entries.splice(entryIndex, 1); |
| } |
| } |
| /** |
| * Clears the list of formatting elements up to the last marker. |
| * |
| * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-list-of-active-formatting-elements-up-to-the-last-marker |
| */ |
| clearToLastMarker() { |
| const markerIdx = this.entries.indexOf(MARKER); |
| if (markerIdx === -1) { |
| this.entries.length = 0; |
| } |
| else { |
| this.entries.splice(0, markerIdx + 1); |
| } |
| } |
| //Search |
| getElementEntryInScopeWithTagName(tagName) { |
| const entry = this.entries.find((entry) => entry.type === EntryType.Marker || this.treeAdapter.getTagName(entry.element) === tagName); |
| return entry && entry.type === EntryType.Element ? entry : null; |
| } |
| getElementEntry(element) { |
| return this.entries.find((entry) => entry.type === EntryType.Element && entry.element === element); |
| } |
| } |