• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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