UNPKG

polymer-analyzer

Version:
254 lines (252 loc) 9.31 kB
"use strict"; /** * @license * Copyright (c) 2016 The Polymer Project Authors. All rights reserved. * This code may only be used under the BSD style license found at * http://polymer.github.io/LICENSE.txt * The complete set of authors may be found at * http://polymer.github.io/AUTHORS.txt * The complete set of contributors may be found at * http://polymer.github.io/CONTRIBUTORS.txt * Code distributed by Google as part of the polymer project is also * subject to an additional IP rights grant found at * http://polymer.github.io/PATENTS.txt */ Object.defineProperty(exports, "__esModule", { value: true }); const clone = require("clone"); const dom5 = require("dom5"); const parse5 = require("parse5"); const document_1 = require("../parser/document"); class ParsedHtmlDocument extends document_1.ParsedDocument { constructor(from) { super(from); this.type = 'html'; } visit(visitors) { dom5.nodeWalk(this.ast, (node) => { visitors.forEach((visitor) => visitor(node)); return false; }); } forEachNode(callback) { dom5.nodeWalk(this.ast, (node) => { callback(node); return false; }); } // An element node with end tag information will produce a source range that // includes the closing tag. It is assumed for offset calculation that the // closing tag is always of the expected `</${tagName}>` form. _sourceRangeForElementWithEndTag(node) { const location = node.__location; if (isElementLocationInfo(location)) { return { file: this.url, start: { line: location.startTag.line - 1, column: location.startTag.col - 1 }, end: { line: location.endTag.line - 1, column: location.endTag.col + (node.tagName || '').length + 2 } }; } } // parse5 locations are 1 based but ours are 0 based. _sourceRangeForNode(node) { const location = node.__location; if (!node.__location) { return; } if (isElementLocationInfo(location)) { if (voidTagNames.has(node.tagName || '')) { return this.sourceRangeForStartTag(node); } return this._sourceRangeForElementWithEndTag(node); } return this._getSourceRangeForLocation(location); } sourceRangeForAttribute(node, attrName) { return this._getSourceRangeForLocation(getAttributeLocation(node, attrName)); } sourceRangeForAttributeName(node, attrName) { const range = this.sourceRangeForAttribute(node, attrName); if (!range) { return; } // The attribute name can't have any spaces, newlines, or other funny // business in it, so this is pretty simple. return { file: range.file, start: range.start, end: { line: range.start.line, column: range.start.column + attrName.length } }; } sourceRangeForAttributeValue(node, attrName, excludeQuotes) { const attributeRange = this.sourceRangeForAttribute(node, attrName); if (!attributeRange) { return; } // This is an attribute without a value. if ((attributeRange.start.line === attributeRange.end.line) && (attributeRange.end.column - attributeRange.start.column === attrName.length)) { return undefined; } const location = getAttributeLocation(node, attrName); // This is complex because there may be whitespace around the = sign. const fullAttribute = this.contents.substring(location.startOffset, location.endOffset); const equalsIndex = fullAttribute.indexOf('='); if (equalsIndex === -1) { // This is super weird and shouldn't happen, but it's probably better to // just return the most reasonable thing we have here rather than // throwing. return undefined; } const whitespaceAfterEquals = fullAttribute.substring(equalsIndex + 1).match(/[\s\n]*/)[0]; let endOfTextToSkip = // the beginning of the attribute key value pair location.startOffset + // everything up to the equals sign equalsIndex + // plus one for the equals sign 1 + // plus all the whitespace after the equals sign whitespaceAfterEquals.length; if (excludeQuotes) { const maybeQuote = this.contents.charAt(endOfTextToSkip); if (maybeQuote === '\'' || maybeQuote === '"') { endOfTextToSkip += 1; } } return this.offsetsToSourceRange(endOfTextToSkip, location.endOffset); } sourceRangeForStartTag(node) { return this._getSourceRangeForLocation(getStartTagLocation(node)); } sourceRangeForEndTag(node) { return this._getSourceRangeForLocation(getEndTagLocation(node)); } _getSourceRangeForLocation(location) { if (!location) { return; } return this.offsetsToSourceRange(location.startOffset, location.endOffset); } stringify(options) { options = options || {}; /** * We want to mutate this.ast with the results of stringifying our inline * documents. This will mutate this.ast even if no one else has mutated it * yet, because our inline documents' stringifiers may not perfectly * reproduce their input. However, we don't want to mutate any analyzer * object after they've been produced and cached, ParsedHtmlDocuments * included. So we want to clone first. * * Because our inline documents contain references into this.ast, we need to * make of copy of `this` and the inline documents such the * inlineDoc.astNode references into this.ast are maintained. Fortunately, * clone() does this! So we'll clone them all together in a single call by * putting them all into an array. */ const immutableDocuments = options.inlineDocuments || []; immutableDocuments.unshift(this); // We can modify these, as they don't escape this method. const mutableDocuments = clone(immutableDocuments); const selfClone = mutableDocuments.shift(); for (const doc of mutableDocuments) { // TODO(rictic): infer this from doc.astNode's indentation. const expectedIndentation = 2; dom5.setTextContent(doc.astNode, '\n' + doc.stringify({ indent: expectedIndentation }) + ' '.repeat(expectedIndentation - 1)); } removeFakeNodes(selfClone.ast); return parse5.serialize(selfClone.ast); } } exports.ParsedHtmlDocument = ParsedHtmlDocument; const injectedTagNames = new Set(['html', 'head', 'body']); function removeFakeNodes(ast) { const children = (ast.childNodes || []).slice(); if (ast.parentNode && !ast.__location && injectedTagNames.has(ast.nodeName)) { for (const child of children) { dom5.insertBefore(ast.parentNode, ast, child); } dom5.remove(ast); } for (const child of children) { removeFakeNodes(child); } } function isElementLocationInfo(location) { const loc = location; return (loc.startTag && loc.endTag) != null; } function getStartTagLocation(node) { if (voidTagNames.has(node.tagName || '')) { return node.__location; } if ('startTag' in node.__location) { return node.__location.startTag; } // Sometimes parse5 throws an attrs attribute on a location info that seems // to correspond to an unclosed tag with attributes but no children. // In that case, the node's location corresponds to the start tag. In other // cases though, node.__location will include children. if ('attrs' in node.__location) { return node.__location; } } function getEndTagLocation(node) { if ('endTag' in node.__location) { return node.__location.endTag; } } function getAttributeLocation(node, attrName) { if (!node || !node.__location) { return; } let attrs = undefined; const location = node.__location; const elemLocation = location; const elemStartLocation = location; if (elemLocation.startTag !== undefined && elemLocation.startTag.attrs) { attrs = elemLocation.startTag.attrs; } else if (elemStartLocation.attrs !== undefined) { attrs = elemStartLocation.attrs; } if (!attrs) { return; } return attrs[attrName]; } /** * HTML5 treats these tags as *always* self-closing. This is relevant for * getting start tag information. * * Source: https://www.w3.org/TR/html5/syntax.html#void-elements */ const voidTagNames = new Set([ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' ]); //# sourceMappingURL=html-document.js.map