1 | 'use strict';
|
---|
2 |
|
---|
3 | //Const
|
---|
4 | const NOAH_ARK_CAPACITY = 3;
|
---|
5 |
|
---|
6 | //List of formatting elements
|
---|
7 | class 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
|
---|
178 | FormattingElementList.MARKER_ENTRY = 'MARKER_ENTRY';
|
---|
179 | FormattingElementList.ELEMENT_ENTRY = 'ELEMENT_ENTRY';
|
---|
180 |
|
---|
181 | module.exports = FormattingElementList;
|
---|