polymer-analyzer
Version:
Static analysis for Web Components
254 lines (252 loc) • 9.31 kB
JavaScript
"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