UNPKG

10.5 kBJavaScriptView Raw
1import { TAG_ID as $, NS, isNumberedHeader } from '../common/html.js';
2//Element utils
3const IMPLICIT_END_TAG_REQUIRED = new Set([$.DD, $.DT, $.LI, $.OPTGROUP, $.OPTION, $.P, $.RB, $.RP, $.RT, $.RTC]);
4const IMPLICIT_END_TAG_REQUIRED_THOROUGHLY = new Set([
5 ...IMPLICIT_END_TAG_REQUIRED,
6 $.CAPTION,
7 $.COLGROUP,
8 $.TBODY,
9 $.TD,
10 $.TFOOT,
11 $.TH,
12 $.THEAD,
13 $.TR,
14]);
15const SCOPING_ELEMENT_NS = new Map([
16 [$.APPLET, NS.HTML],
17 [$.CAPTION, NS.HTML],
18 [$.HTML, NS.HTML],
19 [$.MARQUEE, NS.HTML],
20 [$.OBJECT, NS.HTML],
21 [$.TABLE, NS.HTML],
22 [$.TD, NS.HTML],
23 [$.TEMPLATE, NS.HTML],
24 [$.TH, NS.HTML],
25 [$.ANNOTATION_XML, NS.MATHML],
26 [$.MI, NS.MATHML],
27 [$.MN, NS.MATHML],
28 [$.MO, NS.MATHML],
29 [$.MS, NS.MATHML],
30 [$.MTEXT, NS.MATHML],
31 [$.DESC, NS.SVG],
32 [$.FOREIGN_OBJECT, NS.SVG],
33 [$.TITLE, NS.SVG],
34]);
35const NAMED_HEADERS = [$.H1, $.H2, $.H3, $.H4, $.H5, $.H6];
36const TABLE_ROW_CONTEXT = [$.TR, $.TEMPLATE, $.HTML];
37const TABLE_BODY_CONTEXT = [$.TBODY, $.TFOOT, $.THEAD, $.TEMPLATE, $.HTML];
38const TABLE_CONTEXT = [$.TABLE, $.TEMPLATE, $.HTML];
39const TABLE_CELLS = [$.TD, $.TH];
40//Stack of open elements
41export class OpenElementStack {
42 get currentTmplContentOrNode() {
43 return this._isInTemplate() ? this.treeAdapter.getTemplateContent(this.current) : this.current;
44 }
45 constructor(document, treeAdapter, handler) {
46 this.treeAdapter = treeAdapter;
47 this.handler = handler;
48 this.items = [];
49 this.tagIDs = [];
50 this.stackTop = -1;
51 this.tmplCount = 0;
52 this.currentTagId = $.UNKNOWN;
53 this.current = document;
54 }
55 //Index of element
56 _indexOf(element) {
57 return this.items.lastIndexOf(element, this.stackTop);
58 }
59 //Update current element
60 _isInTemplate() {
61 return this.currentTagId === $.TEMPLATE && this.treeAdapter.getNamespaceURI(this.current) === NS.HTML;
62 }
63 _updateCurrentElement() {
64 this.current = this.items[this.stackTop];
65 this.currentTagId = this.tagIDs[this.stackTop];
66 }
67 //Mutations
68 push(element, tagID) {
69 this.stackTop++;
70 this.items[this.stackTop] = element;
71 this.current = element;
72 this.tagIDs[this.stackTop] = tagID;
73 this.currentTagId = tagID;
74 if (this._isInTemplate()) {
75 this.tmplCount++;
76 }
77 this.handler.onItemPush(element, tagID, true);
78 }
79 pop() {
80 const popped = this.current;
81 if (this.tmplCount > 0 && this._isInTemplate()) {
82 this.tmplCount--;
83 }
84 this.stackTop--;
85 this._updateCurrentElement();
86 this.handler.onItemPop(popped, true);
87 }
88 replace(oldElement, newElement) {
89 const idx = this._indexOf(oldElement);
90 this.items[idx] = newElement;
91 if (idx === this.stackTop) {
92 this.current = newElement;
93 }
94 }
95 insertAfter(referenceElement, newElement, newElementID) {
96 const insertionIdx = this._indexOf(referenceElement) + 1;
97 this.items.splice(insertionIdx, 0, newElement);
98 this.tagIDs.splice(insertionIdx, 0, newElementID);
99 this.stackTop++;
100 if (insertionIdx === this.stackTop) {
101 this._updateCurrentElement();
102 }
103 this.handler.onItemPush(this.current, this.currentTagId, insertionIdx === this.stackTop);
104 }
105 popUntilTagNamePopped(tagName) {
106 let targetIdx = this.stackTop + 1;
107 do {
108 targetIdx = this.tagIDs.lastIndexOf(tagName, targetIdx - 1);
109 } while (targetIdx > 0 && this.treeAdapter.getNamespaceURI(this.items[targetIdx]) !== NS.HTML);
110 this.shortenToLength(targetIdx < 0 ? 0 : targetIdx);
111 }
112 shortenToLength(idx) {
113 while (this.stackTop >= idx) {
114 const popped = this.current;
115 if (this.tmplCount > 0 && this._isInTemplate()) {
116 this.tmplCount -= 1;
117 }
118 this.stackTop--;
119 this._updateCurrentElement();
120 this.handler.onItemPop(popped, this.stackTop < idx);
121 }
122 }
123 popUntilElementPopped(element) {
124 const idx = this._indexOf(element);
125 this.shortenToLength(idx < 0 ? 0 : idx);
126 }
127 popUntilPopped(tagNames, targetNS) {
128 const idx = this._indexOfTagNames(tagNames, targetNS);
129 this.shortenToLength(idx < 0 ? 0 : idx);
130 }
131 popUntilNumberedHeaderPopped() {
132 this.popUntilPopped(NAMED_HEADERS, NS.HTML);
133 }
134 popUntilTableCellPopped() {
135 this.popUntilPopped(TABLE_CELLS, NS.HTML);
136 }
137 popAllUpToHtmlElement() {
138 //NOTE: here we assume that the root <html> element is always first in the open element stack, so
139 //we perform this fast stack clean up.
140 this.tmplCount = 0;
141 this.shortenToLength(1);
142 }
143 _indexOfTagNames(tagNames, namespace) {
144 for (let i = this.stackTop; i >= 0; i--) {
145 if (tagNames.includes(this.tagIDs[i]) && this.treeAdapter.getNamespaceURI(this.items[i]) === namespace) {
146 return i;
147 }
148 }
149 return -1;
150 }
151 clearBackTo(tagNames, targetNS) {
152 const idx = this._indexOfTagNames(tagNames, targetNS);
153 this.shortenToLength(idx + 1);
154 }
155 clearBackToTableContext() {
156 this.clearBackTo(TABLE_CONTEXT, NS.HTML);
157 }
158 clearBackToTableBodyContext() {
159 this.clearBackTo(TABLE_BODY_CONTEXT, NS.HTML);
160 }
161 clearBackToTableRowContext() {
162 this.clearBackTo(TABLE_ROW_CONTEXT, NS.HTML);
163 }
164 remove(element) {
165 const idx = this._indexOf(element);
166 if (idx >= 0) {
167 if (idx === this.stackTop) {
168 this.pop();
169 }
170 else {
171 this.items.splice(idx, 1);
172 this.tagIDs.splice(idx, 1);
173 this.stackTop--;
174 this._updateCurrentElement();
175 this.handler.onItemPop(element, false);
176 }
177 }
178 }
179 //Search
180 tryPeekProperlyNestedBodyElement() {
181 //Properly nested <body> element (should be second element in stack).
182 return this.stackTop >= 1 && this.tagIDs[1] === $.BODY ? this.items[1] : null;
183 }
184 contains(element) {
185 return this._indexOf(element) > -1;
186 }
187 getCommonAncestor(element) {
188 const elementIdx = this._indexOf(element) - 1;
189 return elementIdx >= 0 ? this.items[elementIdx] : null;
190 }
191 isRootHtmlElementCurrent() {
192 return this.stackTop === 0 && this.tagIDs[0] === $.HTML;
193 }
194 //Element in scope
195 hasInScope(tagName) {
196 for (let i = this.stackTop; i >= 0; i--) {
197 const tn = this.tagIDs[i];
198 const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
199 if (tn === tagName && ns === NS.HTML) {
200 return true;
201 }
202 if (SCOPING_ELEMENT_NS.get(tn) === ns) {
203 return false;
204 }
205 }
206 return true;
207 }
208 hasNumberedHeaderInScope() {
209 for (let i = this.stackTop; i >= 0; i--) {
210 const tn = this.tagIDs[i];
211 const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
212 if (isNumberedHeader(tn) && ns === NS.HTML) {
213 return true;
214 }
215 if (SCOPING_ELEMENT_NS.get(tn) === ns) {
216 return false;
217 }
218 }
219 return true;
220 }
221 hasInListItemScope(tagName) {
222 for (let i = this.stackTop; i >= 0; i--) {
223 const tn = this.tagIDs[i];
224 const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
225 if (tn === tagName && ns === NS.HTML) {
226 return true;
227 }
228 if (((tn === $.UL || tn === $.OL) && ns === NS.HTML) || SCOPING_ELEMENT_NS.get(tn) === ns) {
229 return false;
230 }
231 }
232 return true;
233 }
234 hasInButtonScope(tagName) {
235 for (let i = this.stackTop; i >= 0; i--) {
236 const tn = this.tagIDs[i];
237 const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
238 if (tn === tagName && ns === NS.HTML) {
239 return true;
240 }
241 if ((tn === $.BUTTON && ns === NS.HTML) || SCOPING_ELEMENT_NS.get(tn) === ns) {
242 return false;
243 }
244 }
245 return true;
246 }
247 hasInTableScope(tagName) {
248 for (let i = this.stackTop; i >= 0; i--) {
249 const tn = this.tagIDs[i];
250 const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
251 if (ns !== NS.HTML) {
252 continue;
253 }
254 if (tn === tagName) {
255 return true;
256 }
257 if (tn === $.TABLE || tn === $.TEMPLATE || tn === $.HTML) {
258 return false;
259 }
260 }
261 return true;
262 }
263 hasTableBodyContextInTableScope() {
264 for (let i = this.stackTop; i >= 0; i--) {
265 const tn = this.tagIDs[i];
266 const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
267 if (ns !== NS.HTML) {
268 continue;
269 }
270 if (tn === $.TBODY || tn === $.THEAD || tn === $.TFOOT) {
271 return true;
272 }
273 if (tn === $.TABLE || tn === $.HTML) {
274 return false;
275 }
276 }
277 return true;
278 }
279 hasInSelectScope(tagName) {
280 for (let i = this.stackTop; i >= 0; i--) {
281 const tn = this.tagIDs[i];
282 const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
283 if (ns !== NS.HTML) {
284 continue;
285 }
286 if (tn === tagName) {
287 return true;
288 }
289 if (tn !== $.OPTION && tn !== $.OPTGROUP) {
290 return false;
291 }
292 }
293 return true;
294 }
295 //Implied end tags
296 generateImpliedEndTags() {
297 while (IMPLICIT_END_TAG_REQUIRED.has(this.currentTagId)) {
298 this.pop();
299 }
300 }
301 generateImpliedEndTagsThoroughly() {
302 while (IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId)) {
303 this.pop();
304 }
305 }
306 generateImpliedEndTagsWithExclusion(exclusionId) {
307 while (this.currentTagId !== exclusionId && IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId)) {
308 this.pop();
309 }
310 }
311}
312//# sourceMappingURL=open-element-stack.js.map
\No newline at end of file