You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

482 lines
12 KiB

  1. 'use strict';
  2. const HTML = require('../common/html');
  3. //Aliases
  4. const $ = HTML.TAG_NAMES;
  5. const NS = HTML.NAMESPACES;
  6. //Element utils
  7. //OPTIMIZATION: Integer comparisons are low-cost, so we can use very fast tag name length filters here.
  8. //It's faster than using dictionary.
  9. function isImpliedEndTagRequired(tn) {
  10. switch (tn.length) {
  11. case 1:
  12. return tn === $.P;
  13. case 2:
  14. return tn === $.RB || tn === $.RP || tn === $.RT || tn === $.DD || tn === $.DT || tn === $.LI;
  15. case 3:
  16. return tn === $.RTC;
  17. case 6:
  18. return tn === $.OPTION;
  19. case 8:
  20. return tn === $.OPTGROUP;
  21. }
  22. return false;
  23. }
  24. function isImpliedEndTagRequiredThoroughly(tn) {
  25. switch (tn.length) {
  26. case 1:
  27. return tn === $.P;
  28. case 2:
  29. return (
  30. tn === $.RB ||
  31. tn === $.RP ||
  32. tn === $.RT ||
  33. tn === $.DD ||
  34. tn === $.DT ||
  35. tn === $.LI ||
  36. tn === $.TD ||
  37. tn === $.TH ||
  38. tn === $.TR
  39. );
  40. case 3:
  41. return tn === $.RTC;
  42. case 5:
  43. return tn === $.TBODY || tn === $.TFOOT || tn === $.THEAD;
  44. case 6:
  45. return tn === $.OPTION;
  46. case 7:
  47. return tn === $.CAPTION;
  48. case 8:
  49. return tn === $.OPTGROUP || tn === $.COLGROUP;
  50. }
  51. return false;
  52. }
  53. function isScopingElement(tn, ns) {
  54. switch (tn.length) {
  55. case 2:
  56. if (tn === $.TD || tn === $.TH) {
  57. return ns === NS.HTML;
  58. } else if (tn === $.MI || tn === $.MO || tn === $.MN || tn === $.MS) {
  59. return ns === NS.MATHML;
  60. }
  61. break;
  62. case 4:
  63. if (tn === $.HTML) {
  64. return ns === NS.HTML;
  65. } else if (tn === $.DESC) {
  66. return ns === NS.SVG;
  67. }
  68. break;
  69. case 5:
  70. if (tn === $.TABLE) {
  71. return ns === NS.HTML;
  72. } else if (tn === $.MTEXT) {
  73. return ns === NS.MATHML;
  74. } else if (tn === $.TITLE) {
  75. return ns === NS.SVG;
  76. }
  77. break;
  78. case 6:
  79. return (tn === $.APPLET || tn === $.OBJECT) && ns === NS.HTML;
  80. case 7:
  81. return (tn === $.CAPTION || tn === $.MARQUEE) && ns === NS.HTML;
  82. case 8:
  83. return tn === $.TEMPLATE && ns === NS.HTML;
  84. case 13:
  85. return tn === $.FOREIGN_OBJECT && ns === NS.SVG;
  86. case 14:
  87. return tn === $.ANNOTATION_XML && ns === NS.MATHML;
  88. }
  89. return false;
  90. }
  91. //Stack of open elements
  92. class OpenElementStack {
  93. constructor(document, treeAdapter) {
  94. this.stackTop = -1;
  95. this.items = [];
  96. this.current = document;
  97. this.currentTagName = null;
  98. this.currentTmplContent = null;
  99. this.tmplCount = 0;
  100. this.treeAdapter = treeAdapter;
  101. }
  102. //Index of element
  103. _indexOf(element) {
  104. let idx = -1;
  105. for (let i = this.stackTop; i >= 0; i--) {
  106. if (this.items[i] === element) {
  107. idx = i;
  108. break;
  109. }
  110. }
  111. return idx;
  112. }
  113. //Update current element
  114. _isInTemplate() {
  115. return this.currentTagName === $.TEMPLATE && this.treeAdapter.getNamespaceURI(this.current) === NS.HTML;
  116. }
  117. _updateCurrentElement() {
  118. this.current = this.items[this.stackTop];
  119. this.currentTagName = this.current && this.treeAdapter.getTagName(this.current);
  120. this.currentTmplContent = this._isInTemplate() ? this.treeAdapter.getTemplateContent(this.current) : null;
  121. }
  122. //Mutations
  123. push(element) {
  124. this.items[++this.stackTop] = element;
  125. this._updateCurrentElement();
  126. if (this._isInTemplate()) {
  127. this.tmplCount++;
  128. }
  129. }
  130. pop() {
  131. this.stackTop--;
  132. if (this.tmplCount > 0 && this._isInTemplate()) {
  133. this.tmplCount--;
  134. }
  135. this._updateCurrentElement();
  136. }
  137. replace(oldElement, newElement) {
  138. const idx = this._indexOf(oldElement);
  139. this.items[idx] = newElement;
  140. if (idx === this.stackTop) {
  141. this._updateCurrentElement();
  142. }
  143. }
  144. insertAfter(referenceElement, newElement) {
  145. const insertionIdx = this._indexOf(referenceElement) + 1;
  146. this.items.splice(insertionIdx, 0, newElement);
  147. if (insertionIdx === ++this.stackTop) {
  148. this._updateCurrentElement();
  149. }
  150. }
  151. popUntilTagNamePopped(tagName) {
  152. while (this.stackTop > -1) {
  153. const tn = this.currentTagName;
  154. const ns = this.treeAdapter.getNamespaceURI(this.current);
  155. this.pop();
  156. if (tn === tagName && ns === NS.HTML) {
  157. break;
  158. }
  159. }
  160. }
  161. popUntilElementPopped(element) {
  162. while (this.stackTop > -1) {
  163. const poppedElement = this.current;
  164. this.pop();
  165. if (poppedElement === element) {
  166. break;
  167. }
  168. }
  169. }
  170. popUntilNumberedHeaderPopped() {
  171. while (this.stackTop > -1) {
  172. const tn = this.currentTagName;
  173. const ns = this.treeAdapter.getNamespaceURI(this.current);
  174. this.pop();
  175. if (
  176. tn === $.H1 ||
  177. tn === $.H2 ||
  178. tn === $.H3 ||
  179. tn === $.H4 ||
  180. tn === $.H5 ||
  181. (tn === $.H6 && ns === NS.HTML)
  182. ) {
  183. break;
  184. }
  185. }
  186. }
  187. popUntilTableCellPopped() {
  188. while (this.stackTop > -1) {
  189. const tn = this.currentTagName;
  190. const ns = this.treeAdapter.getNamespaceURI(this.current);
  191. this.pop();
  192. if (tn === $.TD || (tn === $.TH && ns === NS.HTML)) {
  193. break;
  194. }
  195. }
  196. }
  197. popAllUpToHtmlElement() {
  198. //NOTE: here we assume that root <html> element is always first in the open element stack, so
  199. //we perform this fast stack clean up.
  200. this.stackTop = 0;
  201. this._updateCurrentElement();
  202. }
  203. clearBackToTableContext() {
  204. while (
  205. (this.currentTagName !== $.TABLE && this.currentTagName !== $.TEMPLATE && this.currentTagName !== $.HTML) ||
  206. this.treeAdapter.getNamespaceURI(this.current) !== NS.HTML
  207. ) {
  208. this.pop();
  209. }
  210. }
  211. clearBackToTableBodyContext() {
  212. while (
  213. (this.currentTagName !== $.TBODY &&
  214. this.currentTagName !== $.TFOOT &&
  215. this.currentTagName !== $.THEAD &&
  216. this.currentTagName !== $.TEMPLATE &&
  217. this.currentTagName !== $.HTML) ||
  218. this.treeAdapter.getNamespaceURI(this.current) !== NS.HTML
  219. ) {
  220. this.pop();
  221. }
  222. }
  223. clearBackToTableRowContext() {
  224. while (
  225. (this.currentTagName !== $.TR && this.currentTagName !== $.TEMPLATE && this.currentTagName !== $.HTML) ||
  226. this.treeAdapter.getNamespaceURI(this.current) !== NS.HTML
  227. ) {
  228. this.pop();
  229. }
  230. }
  231. remove(element) {
  232. for (let i = this.stackTop; i >= 0; i--) {
  233. if (this.items[i] === element) {
  234. this.items.splice(i, 1);
  235. this.stackTop--;
  236. this._updateCurrentElement();
  237. break;
  238. }
  239. }
  240. }
  241. //Search
  242. tryPeekProperlyNestedBodyElement() {
  243. //Properly nested <body> element (should be second element in stack).
  244. const element = this.items[1];
  245. return element && this.treeAdapter.getTagName(element) === $.BODY ? element : null;
  246. }
  247. contains(element) {
  248. return this._indexOf(element) > -1;
  249. }
  250. getCommonAncestor(element) {
  251. let elementIdx = this._indexOf(element);
  252. return --elementIdx >= 0 ? this.items[elementIdx] : null;
  253. }
  254. isRootHtmlElementCurrent() {
  255. return this.stackTop === 0 && this.currentTagName === $.HTML;
  256. }
  257. //Element in scope
  258. hasInScope(tagName) {
  259. for (let i = this.stackTop; i >= 0; i--) {
  260. const tn = this.treeAdapter.getTagName(this.items[i]);
  261. const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  262. if (tn === tagName && ns === NS.HTML) {
  263. return true;
  264. }
  265. if (isScopingElement(tn, ns)) {
  266. return false;
  267. }
  268. }
  269. return true;
  270. }
  271. hasNumberedHeaderInScope() {
  272. for (let i = this.stackTop; i >= 0; i--) {
  273. const tn = this.treeAdapter.getTagName(this.items[i]);
  274. const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  275. if (
  276. (tn === $.H1 || tn === $.H2 || tn === $.H3 || tn === $.H4 || tn === $.H5 || tn === $.H6) &&
  277. ns === NS.HTML
  278. ) {
  279. return true;
  280. }
  281. if (isScopingElement(tn, ns)) {
  282. return false;
  283. }
  284. }
  285. return true;
  286. }
  287. hasInListItemScope(tagName) {
  288. for (let i = this.stackTop; i >= 0; i--) {
  289. const tn = this.treeAdapter.getTagName(this.items[i]);
  290. const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  291. if (tn === tagName && ns === NS.HTML) {
  292. return true;
  293. }
  294. if (((tn === $.UL || tn === $.OL) && ns === NS.HTML) || isScopingElement(tn, ns)) {
  295. return false;
  296. }
  297. }
  298. return true;
  299. }
  300. hasInButtonScope(tagName) {
  301. for (let i = this.stackTop; i >= 0; i--) {
  302. const tn = this.treeAdapter.getTagName(this.items[i]);
  303. const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  304. if (tn === tagName && ns === NS.HTML) {
  305. return true;
  306. }
  307. if ((tn === $.BUTTON && ns === NS.HTML) || isScopingElement(tn, ns)) {
  308. return false;
  309. }
  310. }
  311. return true;
  312. }
  313. hasInTableScope(tagName) {
  314. for (let i = this.stackTop; i >= 0; i--) {
  315. const tn = this.treeAdapter.getTagName(this.items[i]);
  316. const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  317. if (ns !== NS.HTML) {
  318. continue;
  319. }
  320. if (tn === tagName) {
  321. return true;
  322. }
  323. if (tn === $.TABLE || tn === $.TEMPLATE || tn === $.HTML) {
  324. return false;
  325. }
  326. }
  327. return true;
  328. }
  329. hasTableBodyContextInTableScope() {
  330. for (let i = this.stackTop; i >= 0; i--) {
  331. const tn = this.treeAdapter.getTagName(this.items[i]);
  332. const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  333. if (ns !== NS.HTML) {
  334. continue;
  335. }
  336. if (tn === $.TBODY || tn === $.THEAD || tn === $.TFOOT) {
  337. return true;
  338. }
  339. if (tn === $.TABLE || tn === $.HTML) {
  340. return false;
  341. }
  342. }
  343. return true;
  344. }
  345. hasInSelectScope(tagName) {
  346. for (let i = this.stackTop; i >= 0; i--) {
  347. const tn = this.treeAdapter.getTagName(this.items[i]);
  348. const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  349. if (ns !== NS.HTML) {
  350. continue;
  351. }
  352. if (tn === tagName) {
  353. return true;
  354. }
  355. if (tn !== $.OPTION && tn !== $.OPTGROUP) {
  356. return false;
  357. }
  358. }
  359. return true;
  360. }
  361. //Implied end tags
  362. generateImpliedEndTags() {
  363. while (isImpliedEndTagRequired(this.currentTagName)) {
  364. this.pop();
  365. }
  366. }
  367. generateImpliedEndTagsThoroughly() {
  368. while (isImpliedEndTagRequiredThoroughly(this.currentTagName)) {
  369. this.pop();
  370. }
  371. }
  372. generateImpliedEndTagsWithExclusion(exclusionTagName) {
  373. while (isImpliedEndTagRequired(this.currentTagName) && this.currentTagName !== exclusionTagName) {
  374. this.pop();
  375. }
  376. }
  377. }
  378. module.exports = OpenElementStack;