open_element_stack.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. 'use strict';
  2. var HTML = require('../common/html');
  3. //Aliases
  4. var $ = HTML.TAG_NAMES,
  5. 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 || tn === $.MENUITEM;
  21. }
  22. return false;
  23. }
  24. function isScopingElement(tn, ns) {
  25. switch (tn.length) {
  26. case 2:
  27. if (tn === $.TD || tn === $.TH)
  28. return ns === NS.HTML;
  29. else if (tn === $.MI || tn === $.MO || tn === $.MN || tn === $.MS)
  30. return ns === NS.MATHML;
  31. break;
  32. case 4:
  33. if (tn === $.HTML)
  34. return ns === NS.HTML;
  35. else if (tn === $.DESC)
  36. return ns === NS.SVG;
  37. break;
  38. case 5:
  39. if (tn === $.TABLE)
  40. return ns === NS.HTML;
  41. else if (tn === $.MTEXT)
  42. return ns === NS.MATHML;
  43. else if (tn === $.TITLE)
  44. return ns === NS.SVG;
  45. break;
  46. case 6:
  47. return (tn === $.APPLET || tn === $.OBJECT) && ns === NS.HTML;
  48. case 7:
  49. return (tn === $.CAPTION || tn === $.MARQUEE) && ns === NS.HTML;
  50. case 8:
  51. return tn === $.TEMPLATE && ns === NS.HTML;
  52. case 13:
  53. return tn === $.FOREIGN_OBJECT && ns === NS.SVG;
  54. case 14:
  55. return tn === $.ANNOTATION_XML && ns === NS.MATHML;
  56. }
  57. return false;
  58. }
  59. //Stack of open elements
  60. var OpenElementStack = module.exports = function (document, treeAdapter) {
  61. this.stackTop = -1;
  62. this.items = [];
  63. this.current = document;
  64. this.currentTagName = null;
  65. this.currentTmplContent = null;
  66. this.tmplCount = 0;
  67. this.treeAdapter = treeAdapter;
  68. };
  69. //Index of element
  70. OpenElementStack.prototype._indexOf = function (element) {
  71. var idx = -1;
  72. for (var i = this.stackTop; i >= 0; i--) {
  73. if (this.items[i] === element) {
  74. idx = i;
  75. break;
  76. }
  77. }
  78. return idx;
  79. };
  80. //Update current element
  81. OpenElementStack.prototype._isInTemplate = function () {
  82. return this.currentTagName === $.TEMPLATE && this.treeAdapter.getNamespaceURI(this.current) === NS.HTML;
  83. };
  84. OpenElementStack.prototype._updateCurrentElement = function () {
  85. this.current = this.items[this.stackTop];
  86. this.currentTagName = this.current && this.treeAdapter.getTagName(this.current);
  87. this.currentTmplContent = this._isInTemplate() ? this.treeAdapter.getTemplateContent(this.current) : null;
  88. };
  89. //Mutations
  90. OpenElementStack.prototype.push = function (element) {
  91. this.items[++this.stackTop] = element;
  92. this._updateCurrentElement();
  93. if (this._isInTemplate())
  94. this.tmplCount++;
  95. };
  96. OpenElementStack.prototype.pop = function () {
  97. this.stackTop--;
  98. if (this.tmplCount > 0 && this._isInTemplate())
  99. this.tmplCount--;
  100. this._updateCurrentElement();
  101. };
  102. OpenElementStack.prototype.replace = function (oldElement, newElement) {
  103. var idx = this._indexOf(oldElement);
  104. this.items[idx] = newElement;
  105. if (idx === this.stackTop)
  106. this._updateCurrentElement();
  107. };
  108. OpenElementStack.prototype.insertAfter = function (referenceElement, newElement) {
  109. var insertionIdx = this._indexOf(referenceElement) + 1;
  110. this.items.splice(insertionIdx, 0, newElement);
  111. if (insertionIdx === ++this.stackTop)
  112. this._updateCurrentElement();
  113. };
  114. OpenElementStack.prototype.popUntilTagNamePopped = function (tagName) {
  115. while (this.stackTop > -1) {
  116. var tn = this.currentTagName,
  117. ns = this.treeAdapter.getNamespaceURI(this.current);
  118. this.pop();
  119. if (tn === tagName && ns === NS.HTML)
  120. break;
  121. }
  122. };
  123. OpenElementStack.prototype.popUntilElementPopped = function (element) {
  124. while (this.stackTop > -1) {
  125. var poppedElement = this.current;
  126. this.pop();
  127. if (poppedElement === element)
  128. break;
  129. }
  130. };
  131. OpenElementStack.prototype.popUntilNumberedHeaderPopped = function () {
  132. while (this.stackTop > -1) {
  133. var tn = this.currentTagName,
  134. ns = this.treeAdapter.getNamespaceURI(this.current);
  135. this.pop();
  136. if (tn === $.H1 || tn === $.H2 || tn === $.H3 || tn === $.H4 || tn === $.H5 || tn === $.H6 && ns === NS.HTML)
  137. break;
  138. }
  139. };
  140. OpenElementStack.prototype.popUntilTableCellPopped = function () {
  141. while (this.stackTop > -1) {
  142. var tn = this.currentTagName,
  143. ns = this.treeAdapter.getNamespaceURI(this.current);
  144. this.pop();
  145. if (tn === $.TD || tn === $.TH && ns === NS.HTML)
  146. break;
  147. }
  148. };
  149. OpenElementStack.prototype.popAllUpToHtmlElement = function () {
  150. //NOTE: here we assume that root <html> element is always first in the open element stack, so
  151. //we perform this fast stack clean up.
  152. this.stackTop = 0;
  153. this._updateCurrentElement();
  154. };
  155. OpenElementStack.prototype.clearBackToTableContext = function () {
  156. while (this.currentTagName !== $.TABLE &&
  157. this.currentTagName !== $.TEMPLATE &&
  158. this.currentTagName !== $.HTML ||
  159. this.treeAdapter.getNamespaceURI(this.current) !== NS.HTML)
  160. this.pop();
  161. };
  162. OpenElementStack.prototype.clearBackToTableBodyContext = function () {
  163. while (this.currentTagName !== $.TBODY &&
  164. this.currentTagName !== $.TFOOT &&
  165. this.currentTagName !== $.THEAD &&
  166. this.currentTagName !== $.TEMPLATE &&
  167. this.currentTagName !== $.HTML ||
  168. this.treeAdapter.getNamespaceURI(this.current) !== NS.HTML)
  169. this.pop();
  170. };
  171. OpenElementStack.prototype.clearBackToTableRowContext = function () {
  172. while (this.currentTagName !== $.TR &&
  173. this.currentTagName !== $.TEMPLATE &&
  174. this.currentTagName !== $.HTML ||
  175. this.treeAdapter.getNamespaceURI(this.current) !== NS.HTML)
  176. this.pop();
  177. };
  178. OpenElementStack.prototype.remove = function (element) {
  179. for (var i = this.stackTop; i >= 0; i--) {
  180. if (this.items[i] === element) {
  181. this.items.splice(i, 1);
  182. this.stackTop--;
  183. this._updateCurrentElement();
  184. break;
  185. }
  186. }
  187. };
  188. //Search
  189. OpenElementStack.prototype.tryPeekProperlyNestedBodyElement = function () {
  190. //Properly nested <body> element (should be second element in stack).
  191. var element = this.items[1];
  192. return element && this.treeAdapter.getTagName(element) === $.BODY ? element : null;
  193. };
  194. OpenElementStack.prototype.contains = function (element) {
  195. return this._indexOf(element) > -1;
  196. };
  197. OpenElementStack.prototype.getCommonAncestor = function (element) {
  198. var elementIdx = this._indexOf(element);
  199. return --elementIdx >= 0 ? this.items[elementIdx] : null;
  200. };
  201. OpenElementStack.prototype.isRootHtmlElementCurrent = function () {
  202. return this.stackTop === 0 && this.currentTagName === $.HTML;
  203. };
  204. //Element in scope
  205. OpenElementStack.prototype.hasInScope = function (tagName) {
  206. for (var i = this.stackTop; i >= 0; i--) {
  207. var tn = this.treeAdapter.getTagName(this.items[i]),
  208. ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  209. if (tn === tagName && ns === NS.HTML)
  210. return true;
  211. if (isScopingElement(tn, ns))
  212. return false;
  213. }
  214. return true;
  215. };
  216. OpenElementStack.prototype.hasNumberedHeaderInScope = function () {
  217. for (var i = this.stackTop; i >= 0; i--) {
  218. var tn = this.treeAdapter.getTagName(this.items[i]),
  219. ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  220. if ((tn === $.H1 || tn === $.H2 || tn === $.H3 || tn === $.H4 || tn === $.H5 || tn === $.H6) && ns === NS.HTML)
  221. return true;
  222. if (isScopingElement(tn, ns))
  223. return false;
  224. }
  225. return true;
  226. };
  227. OpenElementStack.prototype.hasInListItemScope = function (tagName) {
  228. for (var i = this.stackTop; i >= 0; i--) {
  229. var tn = this.treeAdapter.getTagName(this.items[i]),
  230. ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  231. if (tn === tagName && ns === NS.HTML)
  232. return true;
  233. if ((tn === $.UL || tn === $.OL) && ns === NS.HTML || isScopingElement(tn, ns))
  234. return false;
  235. }
  236. return true;
  237. };
  238. OpenElementStack.prototype.hasInButtonScope = function (tagName) {
  239. for (var i = this.stackTop; i >= 0; i--) {
  240. var tn = this.treeAdapter.getTagName(this.items[i]),
  241. ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  242. if (tn === tagName && ns === NS.HTML)
  243. return true;
  244. if (tn === $.BUTTON && ns === NS.HTML || isScopingElement(tn, ns))
  245. return false;
  246. }
  247. return true;
  248. };
  249. OpenElementStack.prototype.hasInTableScope = function (tagName) {
  250. for (var i = this.stackTop; i >= 0; i--) {
  251. var tn = this.treeAdapter.getTagName(this.items[i]),
  252. ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  253. if (ns !== NS.HTML)
  254. continue;
  255. if (tn === tagName)
  256. return true;
  257. if (tn === $.TABLE || tn === $.TEMPLATE || tn === $.HTML)
  258. return false;
  259. }
  260. return true;
  261. };
  262. OpenElementStack.prototype.hasTableBodyContextInTableScope = function () {
  263. for (var i = this.stackTop; i >= 0; i--) {
  264. var tn = this.treeAdapter.getTagName(this.items[i]),
  265. ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  266. if (ns !== NS.HTML)
  267. continue;
  268. if (tn === $.TBODY || tn === $.THEAD || tn === $.TFOOT)
  269. return true;
  270. if (tn === $.TABLE || tn === $.HTML)
  271. return false;
  272. }
  273. return true;
  274. };
  275. OpenElementStack.prototype.hasInSelectScope = function (tagName) {
  276. for (var i = this.stackTop; i >= 0; i--) {
  277. var tn = this.treeAdapter.getTagName(this.items[i]),
  278. ns = this.treeAdapter.getNamespaceURI(this.items[i]);
  279. if (ns !== NS.HTML)
  280. continue;
  281. if (tn === tagName)
  282. return true;
  283. if (tn !== $.OPTION && tn !== $.OPTGROUP)
  284. return false;
  285. }
  286. return true;
  287. };
  288. //Implied end tags
  289. OpenElementStack.prototype.generateImpliedEndTags = function () {
  290. while (isImpliedEndTagRequired(this.currentTagName))
  291. this.pop();
  292. };
  293. OpenElementStack.prototype.generateImpliedEndTagsWithExclusion = function (exclusionTagName) {
  294. while (isImpliedEndTagRequired(this.currentTagName) && this.currentTagName !== exclusionTagName)
  295. this.pop();
  296. };