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.

222 lines
8.3 KiB

  1. 'use strict';
  2. const Mixin = require('../../utils/mixin');
  3. const Tokenizer = require('../../tokenizer');
  4. const LocationInfoTokenizerMixin = require('./tokenizer-mixin');
  5. const LocationInfoOpenElementStackMixin = require('./open-element-stack-mixin');
  6. const HTML = require('../../common/html');
  7. //Aliases
  8. const $ = HTML.TAG_NAMES;
  9. class LocationInfoParserMixin extends Mixin {
  10. constructor(parser) {
  11. super(parser);
  12. this.parser = parser;
  13. this.treeAdapter = this.parser.treeAdapter;
  14. this.posTracker = null;
  15. this.lastStartTagToken = null;
  16. this.lastFosterParentingLocation = null;
  17. this.currentToken = null;
  18. }
  19. _setStartLocation(element) {
  20. let loc = null;
  21. if (this.lastStartTagToken) {
  22. loc = Object.assign({}, this.lastStartTagToken.location);
  23. loc.startTag = this.lastStartTagToken.location;
  24. }
  25. this.treeAdapter.setNodeSourceCodeLocation(element, loc);
  26. }
  27. _setEndLocation(element, closingToken) {
  28. const loc = this.treeAdapter.getNodeSourceCodeLocation(element);
  29. if (loc) {
  30. if (closingToken.location) {
  31. const ctLoc = closingToken.location;
  32. const tn = this.treeAdapter.getTagName(element);
  33. // NOTE: For cases like <p> <p> </p> - First 'p' closes without a closing
  34. // tag and for cases like <td> <p> </td> - 'p' closes without a closing tag.
  35. const isClosingEndTag = closingToken.type === Tokenizer.END_TAG_TOKEN && tn === closingToken.tagName;
  36. if (isClosingEndTag) {
  37. loc.endTag = Object.assign({}, ctLoc);
  38. loc.endLine = ctLoc.endLine;
  39. loc.endCol = ctLoc.endCol;
  40. loc.endOffset = ctLoc.endOffset;
  41. } else {
  42. loc.endLine = ctLoc.startLine;
  43. loc.endCol = ctLoc.startCol;
  44. loc.endOffset = ctLoc.startOffset;
  45. }
  46. }
  47. }
  48. }
  49. _getOverriddenMethods(mxn, orig) {
  50. return {
  51. _bootstrap(document, fragmentContext) {
  52. orig._bootstrap.call(this, document, fragmentContext);
  53. mxn.lastStartTagToken = null;
  54. mxn.lastFosterParentingLocation = null;
  55. mxn.currentToken = null;
  56. const tokenizerMixin = Mixin.install(this.tokenizer, LocationInfoTokenizerMixin);
  57. mxn.posTracker = tokenizerMixin.posTracker;
  58. Mixin.install(this.openElements, LocationInfoOpenElementStackMixin, {
  59. onItemPop: function(element) {
  60. mxn._setEndLocation(element, mxn.currentToken);
  61. }
  62. });
  63. },
  64. _runParsingLoop(scriptHandler) {
  65. orig._runParsingLoop.call(this, scriptHandler);
  66. // NOTE: generate location info for elements
  67. // that remains on open element stack
  68. for (let i = this.openElements.stackTop; i >= 0; i--) {
  69. mxn._setEndLocation(this.openElements.items[i], mxn.currentToken);
  70. }
  71. },
  72. //Token processing
  73. _processTokenInForeignContent(token) {
  74. mxn.currentToken = token;
  75. orig._processTokenInForeignContent.call(this, token);
  76. },
  77. _processToken(token) {
  78. mxn.currentToken = token;
  79. orig._processToken.call(this, token);
  80. //NOTE: <body> and <html> are never popped from the stack, so we need to updated
  81. //their end location explicitly.
  82. const requireExplicitUpdate =
  83. token.type === Tokenizer.END_TAG_TOKEN &&
  84. (token.tagName === $.HTML || (token.tagName === $.BODY && this.openElements.hasInScope($.BODY)));
  85. if (requireExplicitUpdate) {
  86. for (let i = this.openElements.stackTop; i >= 0; i--) {
  87. const element = this.openElements.items[i];
  88. if (this.treeAdapter.getTagName(element) === token.tagName) {
  89. mxn._setEndLocation(element, token);
  90. break;
  91. }
  92. }
  93. }
  94. },
  95. //Doctype
  96. _setDocumentType(token) {
  97. orig._setDocumentType.call(this, token);
  98. const documentChildren = this.treeAdapter.getChildNodes(this.document);
  99. const cnLength = documentChildren.length;
  100. for (let i = 0; i < cnLength; i++) {
  101. const node = documentChildren[i];
  102. if (this.treeAdapter.isDocumentTypeNode(node)) {
  103. this.treeAdapter.setNodeSourceCodeLocation(node, token.location);
  104. break;
  105. }
  106. }
  107. },
  108. //Elements
  109. _attachElementToTree(element) {
  110. //NOTE: _attachElementToTree is called from _appendElement, _insertElement and _insertTemplate methods.
  111. //So we will use token location stored in this methods for the element.
  112. mxn._setStartLocation(element);
  113. mxn.lastStartTagToken = null;
  114. orig._attachElementToTree.call(this, element);
  115. },
  116. _appendElement(token, namespaceURI) {
  117. mxn.lastStartTagToken = token;
  118. orig._appendElement.call(this, token, namespaceURI);
  119. },
  120. _insertElement(token, namespaceURI) {
  121. mxn.lastStartTagToken = token;
  122. orig._insertElement.call(this, token, namespaceURI);
  123. },
  124. _insertTemplate(token) {
  125. mxn.lastStartTagToken = token;
  126. orig._insertTemplate.call(this, token);
  127. const tmplContent = this.treeAdapter.getTemplateContent(this.openElements.current);
  128. this.treeAdapter.setNodeSourceCodeLocation(tmplContent, null);
  129. },
  130. _insertFakeRootElement() {
  131. orig._insertFakeRootElement.call(this);
  132. this.treeAdapter.setNodeSourceCodeLocation(this.openElements.current, null);
  133. },
  134. //Comments
  135. _appendCommentNode(token, parent) {
  136. orig._appendCommentNode.call(this, token, parent);
  137. const children = this.treeAdapter.getChildNodes(parent);
  138. const commentNode = children[children.length - 1];
  139. this.treeAdapter.setNodeSourceCodeLocation(commentNode, token.location);
  140. },
  141. //Text
  142. _findFosterParentingLocation() {
  143. //NOTE: store last foster parenting location, so we will be able to find inserted text
  144. //in case of foster parenting
  145. mxn.lastFosterParentingLocation = orig._findFosterParentingLocation.call(this);
  146. return mxn.lastFosterParentingLocation;
  147. },
  148. _insertCharacters(token) {
  149. orig._insertCharacters.call(this, token);
  150. const hasFosterParent = this._shouldFosterParentOnInsertion();
  151. const parent =
  152. (hasFosterParent && mxn.lastFosterParentingLocation.parent) ||
  153. this.openElements.currentTmplContent ||
  154. this.openElements.current;
  155. const siblings = this.treeAdapter.getChildNodes(parent);
  156. const textNodeIdx =
  157. hasFosterParent && mxn.lastFosterParentingLocation.beforeElement
  158. ? siblings.indexOf(mxn.lastFosterParentingLocation.beforeElement) - 1
  159. : siblings.length - 1;
  160. const textNode = siblings[textNodeIdx];
  161. //NOTE: if we have location assigned by another token, then just update end position
  162. const tnLoc = this.treeAdapter.getNodeSourceCodeLocation(textNode);
  163. if (tnLoc) {
  164. tnLoc.endLine = token.location.endLine;
  165. tnLoc.endCol = token.location.endCol;
  166. tnLoc.endOffset = token.location.endOffset;
  167. } else {
  168. this.treeAdapter.setNodeSourceCodeLocation(textNode, token.location);
  169. }
  170. }
  171. };
  172. }
  173. }
  174. module.exports = LocationInfoParserMixin;