182 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			182 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
'use strict';
 | 
						|
 | 
						|
//Const
 | 
						|
const NOAH_ARK_CAPACITY = 3;
 | 
						|
 | 
						|
//List of formatting elements
 | 
						|
class FormattingElementList {
 | 
						|
    constructor(treeAdapter) {
 | 
						|
        this.length = 0;
 | 
						|
        this.entries = [];
 | 
						|
        this.treeAdapter = treeAdapter;
 | 
						|
        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) {
 | 
						|
        const candidates = [];
 | 
						|
 | 
						|
        if (this.length >= NOAH_ARK_CAPACITY) {
 | 
						|
            const neAttrsLength = this.treeAdapter.getAttrList(newElement).length;
 | 
						|
            const neTagName = this.treeAdapter.getTagName(newElement);
 | 
						|
            const neNamespaceURI = this.treeAdapter.getNamespaceURI(newElement);
 | 
						|
 | 
						|
            for (let i = this.length - 1; i >= 0; i--) {
 | 
						|
                const entry = this.entries[i];
 | 
						|
 | 
						|
                if (entry.type === FormattingElementList.MARKER_ENTRY) {
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
 | 
						|
                const element = entry.element;
 | 
						|
                const elementAttrs = this.treeAdapter.getAttrList(element);
 | 
						|
 | 
						|
                const isCandidate =
 | 
						|
                    this.treeAdapter.getTagName(element) === neTagName &&
 | 
						|
                    this.treeAdapter.getNamespaceURI(element) === neNamespaceURI &&
 | 
						|
                    elementAttrs.length === neAttrsLength;
 | 
						|
 | 
						|
                if (isCandidate) {
 | 
						|
                    candidates.push({ idx: i, attrs: elementAttrs });
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return candidates.length < NOAH_ARK_CAPACITY ? [] : candidates;
 | 
						|
    }
 | 
						|
 | 
						|
    _ensureNoahArkCondition(newElement) {
 | 
						|
        const candidates = this._getNoahArkConditionCandidates(newElement);
 | 
						|
        let cLength = candidates.length;
 | 
						|
 | 
						|
        if (cLength) {
 | 
						|
            const neAttrs = this.treeAdapter.getAttrList(newElement);
 | 
						|
            const neAttrsLength = neAttrs.length;
 | 
						|
            const neAttrsMap = Object.create(null);
 | 
						|
 | 
						|
            //NOTE: build attrs map for the new element so we can perform fast lookups
 | 
						|
            for (let i = 0; i < neAttrsLength; i++) {
 | 
						|
                const neAttr = neAttrs[i];
 | 
						|
 | 
						|
                neAttrsMap[neAttr.name] = neAttr.value;
 | 
						|
            }
 | 
						|
 | 
						|
            for (let i = 0; i < neAttrsLength; i++) {
 | 
						|
                for (let j = 0; j < cLength; j++) {
 | 
						|
                    const 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 (let i = cLength - 1; i >= NOAH_ARK_CAPACITY - 1; i--) {
 | 
						|
                this.entries.splice(candidates[i].idx, 1);
 | 
						|
                this.length--;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    //Mutations
 | 
						|
    insertMarker() {
 | 
						|
        this.entries.push({ type: FormattingElementList.MARKER_ENTRY });
 | 
						|
        this.length++;
 | 
						|
    }
 | 
						|
 | 
						|
    pushElement(element, token) {
 | 
						|
        this._ensureNoahArkCondition(element);
 | 
						|
 | 
						|
        this.entries.push({
 | 
						|
            type: FormattingElementList.ELEMENT_ENTRY,
 | 
						|
            element: element,
 | 
						|
            token: token
 | 
						|
        });
 | 
						|
 | 
						|
        this.length++;
 | 
						|
    }
 | 
						|
 | 
						|
    insertElementAfterBookmark(element, token) {
 | 
						|
        let 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++;
 | 
						|
    }
 | 
						|
 | 
						|
    removeEntry(entry) {
 | 
						|
        for (let i = this.length - 1; i >= 0; i--) {
 | 
						|
            if (this.entries[i] === entry) {
 | 
						|
                this.entries.splice(i, 1);
 | 
						|
                this.length--;
 | 
						|
                break;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    clearToLastMarker() {
 | 
						|
        while (this.length) {
 | 
						|
            const entry = this.entries.pop();
 | 
						|
 | 
						|
            this.length--;
 | 
						|
 | 
						|
            if (entry.type === FormattingElementList.MARKER_ENTRY) {
 | 
						|
                break;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    //Search
 | 
						|
    getElementEntryInScopeWithTagName(tagName) {
 | 
						|
        for (let i = this.length - 1; i >= 0; i--) {
 | 
						|
            const entry = this.entries[i];
 | 
						|
 | 
						|
            if (entry.type === FormattingElementList.MARKER_ENTRY) {
 | 
						|
                return null;
 | 
						|
            }
 | 
						|
 | 
						|
            if (this.treeAdapter.getTagName(entry.element) === tagName) {
 | 
						|
                return entry;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return null;
 | 
						|
    }
 | 
						|
 | 
						|
    getElementEntry(element) {
 | 
						|
        for (let i = this.length - 1; i >= 0; i--) {
 | 
						|
            const entry = this.entries[i];
 | 
						|
 | 
						|
            if (entry.type === FormattingElementList.ELEMENT_ENTRY && entry.element === element) {
 | 
						|
                return entry;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return null;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
//Entry types
 | 
						|
FormattingElementList.MARKER_ENTRY = 'MARKER_ENTRY';
 | 
						|
FormattingElementList.ELEMENT_ENTRY = 'ELEMENT_ENTRY';
 | 
						|
 | 
						|
module.exports = FormattingElementList;
 |