hydrolysis
Advanced tools
Comparing version 1.22.0 to 1.23.3
30
index.js
@@ -10,5 +10,5 @@ /** | ||
*/ | ||
/*jslint node: true */ | ||
'use strict'; | ||
require("babel-polyfill"); | ||
/** | ||
@@ -18,14 +18,14 @@ * Static analysis for Polymer. | ||
*/ | ||
module.exports = { | ||
Analyzer: require('./lib/analyzer'), | ||
docs: require('./lib/ast-utils/docs'), | ||
FSResolver: require('./lib/loader/fs-resolver'), | ||
jsdoc: require('./lib/ast-utils/jsdoc'), | ||
Loader: require('./lib/loader/file-loader'), | ||
NoopResolver: require('./lib/loader/noop-resolver'), | ||
RedirectResolver: require('./lib/loader/redirect-resolver'), | ||
XHRResolver: require('./lib/loader/xhr-resolver'), | ||
StringResolver: require('./lib/loader/string-resolver'), | ||
_jsParse: require('./lib/ast-utils/js-parse'), | ||
_importParse: require('./lib/ast-utils/import-parse'), | ||
}; | ||
exports.Analyzer = require('./lib/analyzer').Analyzer; | ||
exports.FSResolver = require('./lib/loader/fs-resolver').FSResolver; | ||
exports.Loader = require('./lib/loader/file-loader').FileLoader; | ||
exports.NoopResolver = require('./lib/loader/noop-resolver').NoopResolver; | ||
exports.RedirectResolver = | ||
require('./lib/loader/redirect-resolver').RedirectResolver; | ||
exports.XHRResolver = require('./lib/loader/xhr-resolver').XHRResolver; | ||
exports.StringResolver = require('./lib/loader/string-resolver').StringResolver; | ||
exports._jsParse = require('./lib/ast-utils/js-parse').jsParse; | ||
exports._importParse = require('./lib/ast-utils/import-parse').importParse; | ||
exports.docs = require('./lib/ast-utils/docs'); | ||
exports.jsdoc = require('./lib/ast-utils/jsdoc'); |
1497
lib/analyzer.js
@@ -10,765 +10,878 @@ /** | ||
*/ | ||
// jshint node: true | ||
'use strict'; | ||
// jshint -W079 | ||
var Promise = global.Promise || require('es6-promise').Promise; | ||
// jshint +W079 | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
var __awaiter = undefined && undefined.__awaiter || function (thisArg, _arguments, P, generator) { | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { | ||
try { | ||
step(generator.next(value)); | ||
} catch (e) { | ||
reject(e); | ||
} | ||
} | ||
function rejected(value) { | ||
try { | ||
step(generator.throw(value)); | ||
} catch (e) { | ||
reject(e); | ||
} | ||
} | ||
function step(result) { | ||
result.done ? resolve(result.value) : new P(function (resolve) { | ||
resolve(result.value); | ||
}).then(fulfilled, rejected); | ||
} | ||
step((generator = generator.apply(thisArg, _arguments)).next()); | ||
}); | ||
}; | ||
var dom5 = require('dom5'); | ||
var url = require('url'); | ||
var docs = require('./ast-utils/docs'); | ||
var FileLoader = require('./loader/file-loader'); | ||
var importParse = require('./ast-utils/import-parse'); | ||
var jsParse = require('./ast-utils/js-parse'); | ||
var NoopResolver = require('./loader/noop-resolver'); | ||
var StringResolver = require('./loader/string-resolver'); | ||
var file_loader_1 = require('./loader/file-loader'); | ||
var import_parse_1 = require('./ast-utils/import-parse'); | ||
var js_parse_1 = require('./ast-utils/js-parse'); | ||
var noop_resolver_1 = require('./loader/noop-resolver'); | ||
var string_resolver_1 = require('./loader/string-resolver'); | ||
var fs_resolver_1 = require('./loader/fs-resolver'); | ||
var xhr_resolver_1 = require('./loader/xhr-resolver'); | ||
var error_swallowing_fs_resolver_1 = require('./loader/error-swallowing-fs-resolver'); | ||
function reduceMetadata(m1, m2) { | ||
return { | ||
elements: m1.elements.concat(m2.elements), | ||
features: m1.features.concat(m2.features), | ||
behaviors: m1.behaviors.concat(m2.behaviors), | ||
}; | ||
return { | ||
elements: m1.elements.concat(m2.elements), | ||
features: m1.features.concat(m2.features), | ||
behaviors: m1.behaviors.concat(m2.behaviors) | ||
}; | ||
} | ||
var EMPTY_METADATA = {elements: [], features: [], behaviors: []}; | ||
var EMPTY_METADATA = { elements: [], features: [], behaviors: [] }; | ||
/** | ||
* Parse5's representation of a parsed html document | ||
* @typedef {Object} DocumentAST | ||
* @memberof hydrolysis | ||
* A database of Polymer metadata defined in HTML | ||
*/ | ||
/** | ||
* espree's representation of a parsed html document | ||
* @typedef {Object} JSAST | ||
* @memberof hydrolysis | ||
*/ | ||
var Analyzer = function () { | ||
/** | ||
* @param {boolean} attachAST If true, attach a parse5 compliant AST | ||
* @param {FileLoader=} loader An optional `FileLoader` used to load external | ||
* resources | ||
*/ | ||
/** | ||
* Package of a parsed JS script | ||
* @typedef {Object} ParsedJS | ||
* @property {JSAST} ast The script's AST | ||
* @property {DocumentAST} scriptElement If inline, the script's containing tag. | ||
* @memberof hydrolysis | ||
*/ | ||
function Analyzer(attachAST, loader) { | ||
_classCallCheck(this, Analyzer); | ||
/** | ||
* The metadata for a single polymer element | ||
* @typedef {Object} ElementDescriptor | ||
* @memberof hydrolysis | ||
*/ | ||
/** | ||
* A list of all elements the `Analyzer` has metadata for. | ||
*/ | ||
this.elements = []; | ||
/** | ||
* A view into `elements`, keyed by tag name. | ||
*/ | ||
this.elementsByTagName = {}; | ||
/** | ||
* A list of API features added to `Polymer.Base` encountered by the | ||
* analyzer. | ||
*/ | ||
this.features = []; | ||
/** | ||
* The behaviors collected by the analysis pass. | ||
*/ | ||
this.behaviors = []; | ||
/** | ||
* The behaviors collected by the analysis pass by name. | ||
*/ | ||
this.behaviorsByName = {}; | ||
/** | ||
* A map, keyed by absolute path, of Document metadata. | ||
*/ | ||
this.html = {}; | ||
/** | ||
* A map, keyed by path, of HTML document ASTs. | ||
*/ | ||
this.parsedDocuments = {}; | ||
/** | ||
* A map, keyed by path, of JS script ASTs. | ||
* | ||
* If the path is an HTML file with multiple scripts, | ||
* the entry will be an array of scripts. | ||
*/ | ||
this.parsedScripts = {}; | ||
/** | ||
* A map, keyed by path, of document content. | ||
*/ | ||
this._content = {}; | ||
this.loader = loader; | ||
} | ||
/** | ||
* The metadata for a Polymer feature. | ||
* @typedef {Object} FeatureDescriptor | ||
* @memberof hydrolysis | ||
*/ | ||
_createClass(Analyzer, [{ | ||
key: 'load', | ||
value: function load(href) { | ||
var _this = this; | ||
/** | ||
* The metadata for a Polymer behavior mixin. | ||
* @typedef {Object} BehaviorDescriptor | ||
* @memberof hydrolysis | ||
*/ | ||
return this.loader.request(href).then(function (content) { | ||
return new Promise(function (resolve, reject) { | ||
setTimeout(function () { | ||
_this._content[href] = content; | ||
resolve(_this._parseHTML(content, href)); | ||
}, 0); | ||
}).catch(function (err) { | ||
console.error("Error processing document at " + href); | ||
throw err; | ||
}); | ||
}); | ||
} | ||
}, { | ||
key: '_parseHTML', | ||
/** | ||
* The metadata for all features and elements defined in one document | ||
* @typedef {Object} DocumentDescriptor | ||
* @memberof hydrolysis | ||
* @property {Array<ElementDescriptor>} elements The elements from the document | ||
* @property {Array<FeatureDescriptor>} features The features from the document | ||
* @property {Array<FeatureDescriptor>} behaviors The behaviors from the document | ||
*/ | ||
/** | ||
* Returns an `AnalyzedDocument` representing the provided document | ||
* @private | ||
* @param {string} htmlImport Raw text of an HTML document | ||
* @param {string} href The document's URL. | ||
* @return {AnalyzedDocument} An `AnalyzedDocument` | ||
*/ | ||
value: function _parseHTML(htmlImport, href) { | ||
var _this2 = this; | ||
/** | ||
* The metadata of an entire HTML document, in promises. | ||
* @typedef {Object} AnalyzedDocument | ||
* @memberof hydrolysis | ||
* @property {string} href The url of the document. | ||
* @property {Promise<ParsedImport>} htmlLoaded The parsed representation of | ||
* the doc. Use the `ast` property to get the full `parse5` ast | ||
* | ||
* @property {Promise<Array<string>>} depsLoaded Resolves to the list of this | ||
* Document's transitive import dependencies | ||
* | ||
* @property {Array<string>} depHrefs The direct dependencies of the document. | ||
* | ||
* @property {Promise<DocumentDescriptor>} metadataLoaded Resolves to the list of | ||
* this Document's import dependencies | ||
*/ | ||
if (href in this.html) { | ||
return this.html[href]; | ||
} | ||
var depsLoaded = []; | ||
var depHrefs = []; | ||
var metadataLoaded = Promise.resolve(EMPTY_METADATA); | ||
var parsed; | ||
try { | ||
parsed = import_parse_1.importParse(htmlImport, href); | ||
} catch (err) { | ||
console.error('Error parsing!'); | ||
throw err; | ||
} | ||
var htmlLoaded = Promise.resolve(parsed); | ||
if (parsed.script) { | ||
metadataLoaded = this._processScripts(parsed.script, href); | ||
} | ||
var commentText = parsed.comment.map(function (comment) { | ||
return dom5.getTextContent(comment); | ||
}); | ||
var pseudoElements = docs.parsePseudoElements(commentText); | ||
var _iteratorNormalCompletion = true; | ||
var _didIteratorError = false; | ||
var _iteratorError = undefined; | ||
/** | ||
* A database of Polymer metadata defined in HTML | ||
* | ||
* @constructor | ||
* @memberOf hydrolysis | ||
* @param {boolean} attachAST If true, attach a parse5 compliant AST | ||
* @param {FileLoader=} loader An optional `FileLoader` used to load external | ||
* resources | ||
*/ | ||
var Analyzer = function Analyzer(attachAST, | ||
loader) { | ||
this.loader = loader; | ||
try { | ||
for (var _iterator = pseudoElements[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { | ||
var element = _step.value; | ||
/** | ||
* A list of all elements the `Analyzer` has metadata for. | ||
* @member {Array.<ElementDescriptor>} | ||
*/ | ||
this.elements = []; | ||
element.contentHref = href; | ||
this.elements.push(element); | ||
this.elementsByTagName[element.is] = element; | ||
} | ||
} catch (err) { | ||
_didIteratorError = true; | ||
_iteratorError = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion && _iterator.return) { | ||
_iterator.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError) { | ||
throw _iteratorError; | ||
} | ||
} | ||
} | ||
/** | ||
* A view into `elements`, keyed by tag name. | ||
* @member {Object.<string,ElementDescriptor>} | ||
*/ | ||
this.elementsByTagName = {}; | ||
metadataLoaded = metadataLoaded.then(function (metadata) { | ||
var metadataEntry = { | ||
elements: pseudoElements, | ||
features: [], | ||
behaviors: [] | ||
}; | ||
return [metadata, metadataEntry].reduce(reduceMetadata); | ||
}); | ||
depsLoaded.push(metadataLoaded); | ||
if (this.loader) { | ||
var baseUri = href; | ||
if (parsed.base.length > 1) { | ||
console.error("Only one base tag per document!"); | ||
throw "Multiple base tags in " + href; | ||
} else if (parsed.base.length == 1) { | ||
var baseHref = dom5.getAttribute(parsed.base[0], "href"); | ||
if (baseHref) { | ||
baseHref = baseHref + "/"; | ||
baseUri = url.resolve(baseUri, baseHref); | ||
} | ||
} | ||
var _iteratorNormalCompletion2 = true; | ||
var _didIteratorError2 = false; | ||
var _iteratorError2 = undefined; | ||
/** | ||
* A list of API features added to `Polymer.Base` encountered by the | ||
* analyzer. | ||
* @member {Array<FeatureDescriptor>} | ||
*/ | ||
this.features = []; | ||
try { | ||
for (var _iterator2 = parsed.import[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { | ||
var link = _step2.value; | ||
/** | ||
* The behaviors collected by the analysis pass. | ||
* | ||
* @member {Array<BehaviorDescriptor>} | ||
*/ | ||
this.behaviors = []; | ||
var linkurl = dom5.getAttribute(link, 'href'); | ||
if (linkurl) { | ||
var resolvedUrl = url.resolve(baseUri, linkurl); | ||
depHrefs.push(resolvedUrl); | ||
depsLoaded.push(this._dependenciesLoadedFor(resolvedUrl, href)); | ||
} | ||
} | ||
} catch (err) { | ||
_didIteratorError2 = true; | ||
_iteratorError2 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion2 && _iterator2.return) { | ||
_iterator2.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError2) { | ||
throw _iteratorError2; | ||
} | ||
} | ||
} | ||
/** | ||
* The behaviors collected by the analysis pass by name. | ||
* | ||
* @member {Object<string,BehaviorDescriptor>} | ||
*/ | ||
this.behaviorsByName = {}; | ||
var _iteratorNormalCompletion3 = true; | ||
var _didIteratorError3 = false; | ||
var _iteratorError3 = undefined; | ||
/** | ||
* A map, keyed by absolute path, of Document metadata. | ||
* @member {Object<string,AnalyzedDocument>} | ||
*/ | ||
this.html = {}; | ||
try { | ||
for (var _iterator3 = parsed.style[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { | ||
var styleElement = _step3.value; | ||
/** | ||
* A map, keyed by path, of HTML document ASTs. | ||
* @type {Object} | ||
*/ | ||
this.parsedDocuments = {}; | ||
if (polymerExternalStyle(styleElement)) { | ||
var styleHref = dom5.getAttribute(styleElement, 'href'); | ||
if (href) { | ||
styleHref = url.resolve(baseUri, styleHref); | ||
depsLoaded.push(this.loader.request(styleHref).then(function (content) { | ||
_this2._content[styleHref] = content; | ||
return {}; | ||
})); | ||
} | ||
} | ||
} | ||
} catch (err) { | ||
_didIteratorError3 = true; | ||
_iteratorError3 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion3 && _iterator3.return) { | ||
_iterator3.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError3) { | ||
throw _iteratorError3; | ||
} | ||
} | ||
} | ||
} | ||
var depsStrLoaded = Promise.all(depsLoaded).then(function () { | ||
return depHrefs; | ||
}).catch(function (err) { | ||
throw err; | ||
}); | ||
this.parsedDocuments[href] = parsed.ast; | ||
this.html[href] = { | ||
href: href, | ||
htmlLoaded: htmlLoaded, | ||
metadataLoaded: metadataLoaded, | ||
depHrefs: depHrefs, | ||
depsLoaded: depsStrLoaded | ||
}; | ||
return this.html[href]; | ||
} | ||
}, { | ||
key: '_processScripts', | ||
value: function _processScripts(scripts, href) { | ||
var _this3 = this; | ||
/** | ||
* A map, keyed by path, of JS script ASTs. | ||
* | ||
* If the path is an HTML file with multiple scripts, the entry will be an array of scripts. | ||
* | ||
* @type {Object<string,Array<ParsedJS>>} | ||
*/ | ||
this.parsedScripts = {}; | ||
var scriptPromises = []; | ||
scripts.forEach(function (script) { | ||
scriptPromises.push(_this3._processScript(script, href)); | ||
}); | ||
return Promise.all(scriptPromises).then(function (metadataList) { | ||
// TODO(ajo) remove this cast. | ||
var list = metadataList; | ||
return list.reduce(reduceMetadata, EMPTY_METADATA); | ||
}); | ||
} | ||
}, { | ||
key: '_processScript', | ||
value: function _processScript(script, href) { | ||
var _this4 = this; | ||
var src = dom5.getAttribute(script, 'src'); | ||
var parsedJs; | ||
if (!src) { | ||
try { | ||
parsedJs = js_parse_1.jsParse(script.childNodes.length ? script.childNodes[0].value : ''); | ||
} catch (err) { | ||
// Figure out the correct line number for the error. | ||
var line = 0; | ||
var col = 0; | ||
if (script.__ownerDocument && script.__ownerDocument == href) { | ||
line = script.__locationDetail.line - 1; | ||
col = script.__locationDetail.column - 1; | ||
} | ||
line += err.lineNumber; | ||
col += err.column; | ||
var message = "Error parsing script in " + href + " at " + line + ":" + col; | ||
message += "\n" + err.stack; | ||
var fixedErr = new Error(message); | ||
fixedErr.location = { line: line, column: col }; | ||
fixedErr.ownerDocument = script.__ownerDocument; | ||
return Promise.reject(fixedErr); | ||
} | ||
if (parsedJs.elements) { | ||
parsedJs.elements.forEach(function (element) { | ||
element.scriptElement = script; | ||
element.contentHref = href; | ||
_this4.elements.push(element); | ||
if (element.is in _this4.elementsByTagName) { | ||
console.warn('Ignoring duplicate element definition: ' + element.is); | ||
} else { | ||
_this4.elementsByTagName[element.is] = element; | ||
} | ||
}); | ||
} | ||
if (parsedJs.features) { | ||
parsedJs.features.forEach(function (feature) { | ||
feature.contentHref = href; | ||
feature.scriptElement = script; | ||
}); | ||
this.features = this.features.concat(parsedJs.features); | ||
} | ||
if (parsedJs.behaviors) { | ||
parsedJs.behaviors.forEach(function (behavior) { | ||
behavior.contentHref = href; | ||
_this4.behaviorsByName[behavior.is] = behavior; | ||
_this4.behaviorsByName[behavior.symbol] = behavior; | ||
}); | ||
this.behaviors = this.behaviors.concat(parsedJs.behaviors); | ||
} | ||
if (!Object.hasOwnProperty.call(this.parsedScripts, href)) { | ||
this.parsedScripts[href] = []; | ||
} | ||
var scriptElement; | ||
if (script.__ownerDocument && script.__ownerDocument == href) { | ||
scriptElement = script; | ||
} | ||
this.parsedScripts[href].push({ | ||
ast: parsedJs.parsedScript, | ||
scriptElement: scriptElement | ||
}); | ||
return Promise.resolve(parsedJs); | ||
} | ||
if (this.loader) { | ||
var resolvedSrc = url.resolve(href, src); | ||
return this.loader.request(resolvedSrc).then(function (content) { | ||
_this4._content[resolvedSrc] = content; | ||
var scriptText = dom5.constructors.text(content); | ||
dom5.append(script, scriptText); | ||
dom5.removeAttribute(script, 'src'); | ||
script.__hydrolysisInlined = src; | ||
return _this4._processScript(script, resolvedSrc); | ||
}).catch(function (err) { | ||
throw err; | ||
}); | ||
} else { | ||
return Promise.resolve(EMPTY_METADATA); | ||
} | ||
} | ||
}, { | ||
key: '_dependenciesLoadedFor', | ||
value: function _dependenciesLoadedFor(href, root) { | ||
var _this5 = this; | ||
var found = {}; | ||
if (root !== undefined) { | ||
found[root] = true; | ||
} | ||
return this._getDependencies(href, found).then(function (deps) { | ||
var depPromises = deps.map(function (depHref) { | ||
return _this5.load(depHref).then(function (htmlMonomer) { | ||
return htmlMonomer.metadataLoaded; | ||
}); | ||
}); | ||
return Promise.all(depPromises); | ||
}); | ||
} | ||
}, { | ||
key: '_getDependencies', | ||
/** | ||
* A map, keyed by path, of document content. | ||
* @type {Object} | ||
*/ | ||
this._content = {}; | ||
}; | ||
/** | ||
* List all the html dependencies for the document at `href`. | ||
* @param {string} href The href to get dependencies for. | ||
* @param {Object.<string,boolean>=} found An object keyed by URL of the | ||
* already resolved dependencies. | ||
* @param {boolean=} transitive Whether to load transitive | ||
* dependencies. Defaults to true. | ||
* @return {Array.<string>} A list of all the html dependencies. | ||
*/ | ||
value: function _getDependencies(href, found, transitive) { | ||
var _this6 = this; | ||
/** | ||
* Options for `Analyzer.analzye` | ||
* @typedef {Object} LoadOptions | ||
* @memberof hydrolysis | ||
* @property {boolean} noAnnotations Whether `annotate()` should be skipped. | ||
* @property {String=} content Content to resolve `href` to instead of loading | ||
* from the file system. | ||
* @property {boolean} clean Whether the generated descriptors should be cleaned | ||
* of redundant data. | ||
* @property {string=} resolver. | ||
* `xhr` to use XMLHttpRequest | ||
* `fs` to use the local filesystem. | ||
* `permissive` to use the local filesystem and return empty files when a | ||
* path can't be found. | ||
* Default is `fs` in node and `xhr` in the browser. | ||
* @property {function(string): boolean} filter A predicate function that | ||
* indicates which files should be ignored by the loader. By default all | ||
* files not located under the dirname of `href` will be ignored. | ||
*/ | ||
if (found === undefined) { | ||
found = {}; | ||
found[href] = true; | ||
} | ||
if (transitive === undefined) { | ||
transitive = true; | ||
} | ||
var deps = []; | ||
return this.load(href).then(function (htmlMonomer) { | ||
var transitiveDeps = []; | ||
htmlMonomer.depHrefs.forEach(function (depHref) { | ||
if (found[depHref]) { | ||
return; | ||
} | ||
deps.push(depHref); | ||
found[depHref] = true; | ||
if (transitive) { | ||
transitiveDeps.push(_this6._getDependencies(depHref, found)); | ||
} | ||
}); | ||
return Promise.all(transitiveDeps); | ||
}).then(function (transitiveDeps) { | ||
var alldeps = transitiveDeps.reduce(function (a, b) { | ||
return a.concat(b); | ||
}, []).concat(deps); | ||
return alldeps; | ||
}); | ||
} | ||
}, { | ||
key: 'elementsForFolder', | ||
/** | ||
* Shorthand for transitively loading and processing all imports beginning at | ||
* `href`. | ||
* | ||
* In order to properly filter paths, `href` _must_ be an absolute URI. | ||
* | ||
* @param {string} href The root import to begin loading from. | ||
* @param {LoadOptions=} options Any additional options for the load. | ||
* @return {Promise<Analyzer>} A promise that will resolve once `href` and its | ||
* dependencies have been loaded and analyzed. | ||
*/ | ||
Analyzer.analyze = function analyze(href, options) { | ||
options = options || {}; | ||
options.filter = options.filter || _defaultFilter(href); | ||
/** | ||
* Returns the elements defined in the folder containing `href`. | ||
* @param {string} href path to search. | ||
*/ | ||
value: function elementsForFolder(href) { | ||
return this.elements.filter(function (element) { | ||
return matchesDocumentFolder(element, href); | ||
}); | ||
} | ||
}, { | ||
key: 'behaviorsForFolder', | ||
var loader = new FileLoader(); | ||
/** | ||
* Returns the behaviors defined in the folder containing `href`. | ||
* @param {string} href path to search. | ||
* @return {Array.<BehaviorDescriptor>} | ||
*/ | ||
value: function behaviorsForFolder(href) { | ||
return this.behaviors.filter(function (behavior) { | ||
return matchesDocumentFolder(behavior, href); | ||
}); | ||
} | ||
}, { | ||
key: 'metadataTree', | ||
var resolver = options.resolver; | ||
if (resolver === undefined) { | ||
if (typeof window === 'undefined') { | ||
resolver = 'fs'; | ||
} else { | ||
resolver = 'xhr'; | ||
} | ||
} | ||
var PrimaryResolver; | ||
if (resolver === 'fs') { | ||
PrimaryResolver = require('./loader/fs-resolver'); | ||
} else if (resolver === 'xhr') { | ||
PrimaryResolver = require('./loader/xhr-resolver'); | ||
} else if (resolver === 'permissive') { | ||
PrimaryResolver = require('./loader/error-swallowing-fs-resolver'); | ||
} else { | ||
throw new Error("Resolver must be one of 'fs' or 'xhr'"); | ||
} | ||
/** | ||
* Returns a promise that resolves to a POJO representation of the import | ||
* tree, in a format that maintains the ordering of the HTML imports spec. | ||
* @param {string} href the import to get metadata for. | ||
* @return {Promise} | ||
*/ | ||
value: function metadataTree(href) { | ||
var _this7 = this; | ||
loader.addResolver(new PrimaryResolver(options)); | ||
if (options.content) { | ||
loader.addResolver(new StringResolver({url: href, content: options.content})); | ||
} | ||
loader.addResolver(new NoopResolver({test: options.filter})); | ||
return this.load(href).then(function (monomer) { | ||
var loadedHrefs = {}; | ||
loadedHrefs[href] = true; | ||
return _this7._metadataTree(monomer, loadedHrefs); | ||
}); | ||
} | ||
}, { | ||
key: '_metadataTree', | ||
value: function _metadataTree(htmlMonomer, loadedHrefs) { | ||
return __awaiter(this, void 0, void 0, regeneratorRuntime.mark(function _callee() { | ||
var metadata, hrefs, depMetadata, _iteratorNormalCompletion4, _didIteratorError4, _iteratorError4, _iterator4, _step4, href, metadataPromise; | ||
var analyzer = new this(null, loader); | ||
return analyzer.metadataTree(href).then(function(root) { | ||
if (!options.noAnnotations) { | ||
analyzer.annotate(); | ||
} | ||
if (options.clean) { | ||
analyzer.clean(); | ||
} | ||
return Promise.resolve(analyzer); | ||
}); | ||
}; | ||
return regeneratorRuntime.wrap(function _callee$(_context) { | ||
while (1) { | ||
switch (_context.prev = _context.next) { | ||
case 0: | ||
if (loadedHrefs === undefined) { | ||
loadedHrefs = {}; | ||
} | ||
_context.next = 3; | ||
return htmlMonomer.metadataLoaded; | ||
/** | ||
* @private | ||
* @param {string} href | ||
* @return {function(string): boolean} | ||
*/ | ||
function _defaultFilter(href) { | ||
// Everything up to the last `/` or `\`. | ||
var base = href.match(/^(.*?)[^\/\\]*$/)[1]; | ||
return function(uri) { | ||
return uri.indexOf(base) !== 0; | ||
}; | ||
} | ||
case 3: | ||
metadata = _context.sent; | ||
Analyzer.prototype.load = function load(href) { | ||
return this.loader.request(href).then(function(content) { | ||
return new Promise(function(resolve, reject) { | ||
setTimeout(function() { | ||
this._content[href] = content; | ||
resolve(this._parseHTML(content, href)); | ||
}.bind(this), 0); | ||
}.bind(this)).catch(function(err){ | ||
console.error("Error processing document at " + href); | ||
throw err; | ||
}); | ||
}.bind(this)); | ||
}; | ||
metadata = { | ||
elements: metadata.elements, | ||
features: metadata.features, | ||
behaviors: [], | ||
href: htmlMonomer.href | ||
}; | ||
_context.next = 7; | ||
return htmlMonomer.depsLoaded; | ||
/** | ||
* Returns an `AnalyzedDocument` representing the provided document | ||
* @private | ||
* @param {string} htmlImport Raw text of an HTML document | ||
* @param {string} href The document's URL. | ||
* @return {AnalyzedDocument} An `AnalyzedDocument` | ||
*/ | ||
Analyzer.prototype._parseHTML = function _parseHTML(htmlImport, | ||
href) { | ||
if (href in this.html) { | ||
return this.html[href]; | ||
} | ||
var depsLoaded = []; | ||
var depHrefs = []; | ||
var metadataLoaded = Promise.resolve(EMPTY_METADATA); | ||
var parsed; | ||
try { | ||
parsed = importParse(htmlImport, href); | ||
} catch (err) { | ||
console.error('Error parsing!'); | ||
throw err; | ||
} | ||
var htmlLoaded = Promise.resolve(parsed); | ||
if (parsed.script) { | ||
metadataLoaded = this._processScripts(parsed.script, href); | ||
} | ||
var commentText = parsed.comment.map(function(comment){ | ||
return dom5.getTextContent(comment); | ||
}); | ||
var pseudoElements = docs.parsePseudoElements(commentText); | ||
pseudoElements.forEach(function(element){ | ||
element.contentHref = href; | ||
this.elements.push(element); | ||
this.elementsByTagName[element.is] = element; | ||
}.bind(this)); | ||
metadataLoaded = metadataLoaded.then(function(metadata){ | ||
var metadataEntry = { | ||
elements: pseudoElements, | ||
features: [], | ||
behaviors: [] | ||
}; | ||
return [metadata, metadataEntry].reduce(reduceMetadata); | ||
}); | ||
depsLoaded.push(metadataLoaded); | ||
case 7: | ||
hrefs = _context.sent; | ||
depMetadata = []; | ||
_iteratorNormalCompletion4 = true; | ||
_didIteratorError4 = false; | ||
_iteratorError4 = undefined; | ||
_context.prev = 12; | ||
_iterator4 = hrefs[Symbol.iterator](); | ||
case 14: | ||
if (_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done) { | ||
_context.next = 29; | ||
break; | ||
} | ||
if (this.loader) { | ||
var baseUri = href; | ||
if (parsed.base.length > 1) { | ||
console.error("Only one base tag per document!"); | ||
throw "Multiple base tags in " + href; | ||
} else if (parsed.base.length == 1) { | ||
var baseHref = dom5.getAttribute(parsed.base[0], "href"); | ||
if (baseHref) { | ||
baseHref = baseHref + "/"; | ||
baseUri = url.resolve(baseUri, baseHref); | ||
} | ||
} | ||
parsed.import.forEach(function(link) { | ||
var linkurl = dom5.getAttribute(link, 'href'); | ||
if (linkurl) { | ||
var resolvedUrl = url.resolve(baseUri, linkurl); | ||
depHrefs.push(resolvedUrl); | ||
depsLoaded.push(this._dependenciesLoadedFor(resolvedUrl, href)); | ||
} | ||
}.bind(this)); | ||
parsed.style.forEach(function(styleElement) { | ||
if (polymerExternalStyle(styleElement)) { | ||
var styleHref = dom5.getAttribute(styleElement, 'href'); | ||
if (href) { | ||
styleHref = url.resolve(baseUri, styleHref); | ||
depsLoaded.push(this.loader.request(styleHref).then(function(content){ | ||
this._content[styleHref] = content; | ||
}.bind(this))); | ||
} | ||
} | ||
}.bind(this)); | ||
} | ||
depsLoaded = Promise.all(depsLoaded) | ||
.then(function() {return depHrefs;}) | ||
.catch(function(err) {throw err;}); | ||
this.parsedDocuments[href] = parsed.ast; | ||
this.html[href] = { | ||
href: href, | ||
htmlLoaded: htmlLoaded, | ||
metadataLoaded: metadataLoaded, | ||
depHrefs: depHrefs, | ||
depsLoaded: depsLoaded | ||
}; | ||
return this.html[href]; | ||
}; | ||
href = _step4.value; | ||
metadataPromise = undefined; | ||
Analyzer.prototype._processScripts = function _processScripts(scripts, href) { | ||
var scriptPromises = []; | ||
scripts.forEach(function(script) { | ||
scriptPromises.push(this._processScript(script, href)); | ||
}.bind(this)); | ||
return Promise.all(scriptPromises).then(function(metadataList) { | ||
return metadataList.reduce(reduceMetadata, EMPTY_METADATA); | ||
}); | ||
}; | ||
if (loadedHrefs[href]) { | ||
_context.next = 24; | ||
break; | ||
} | ||
Analyzer.prototype._processScript = function _processScript(script, href) { | ||
var src = dom5.getAttribute(script, 'src'); | ||
var parsedJs; | ||
if (!src) { | ||
try { | ||
parsedJs = jsParse((script.childNodes.length) ? script.childNodes[0].value : ''); | ||
} catch (err) { | ||
// Figure out the correct line number for the error. | ||
var line = 0; | ||
var col = 0; | ||
if (script.__ownerDocument && script.__ownerDocument == href) { | ||
line = script.__locationDetail.line - 1; | ||
col = script.__locationDetail.column - 1; | ||
} | ||
line += err.lineNumber; | ||
col += err.column; | ||
var message = "Error parsing script in " + href + " at " + line + ":" + col; | ||
message += "\n" + err.stack; | ||
var fixedErr = new Error(message); | ||
fixedErr.location = {line: line, column: col}; | ||
fixedErr.ownerDocument = script.__ownerDocument; | ||
return Promise.reject(fixedErr); | ||
} | ||
if (parsedJs.elements) { | ||
parsedJs.elements.forEach(function(element) { | ||
element.scriptElement = script; | ||
element.contentHref = href; | ||
this.elements.push(element); | ||
if (element.is in this.elementsByTagName) { | ||
console.warn('Ignoring duplicate element definition: ' + element.is); | ||
} else { | ||
this.elementsByTagName[element.is] = element; | ||
} | ||
}.bind(this)); | ||
} | ||
if (parsedJs.features) { | ||
parsedJs.features.forEach(function(feature){ | ||
feature.contentHref = href; | ||
feature.scriptElement = script; | ||
}); | ||
this.features = this.features.concat(parsedJs.features); | ||
} | ||
if (parsedJs.behaviors) { | ||
parsedJs.behaviors.forEach(function(behavior){ | ||
behavior.contentHref = href; | ||
this.behaviorsByName[behavior.is] = behavior; | ||
this.behaviorsByName[behavior.symbol] = behavior; | ||
}.bind(this)); | ||
this.behaviors = this.behaviors.concat(parsedJs.behaviors); | ||
} | ||
if (!Object.hasOwnProperty.call(this.parsedScripts, href)) { | ||
this.parsedScripts[href] = []; | ||
} | ||
var scriptElement; | ||
if (script.__ownerDocument && script.__ownerDocument == href) { | ||
scriptElement = script; | ||
} | ||
this.parsedScripts[href].push({ | ||
ast: parsedJs.parsedScript, | ||
scriptElement: scriptElement | ||
}); | ||
return parsedJs; | ||
} | ||
if (this.loader) { | ||
var resolvedSrc = url.resolve(href, src); | ||
return this.loader.request(resolvedSrc).then(function(content) { | ||
this._content[resolvedSrc] = content; | ||
var resolvedScript = Object.create(script); | ||
resolvedScript.childNodes = [{value: content}]; | ||
resolvedScript.attrs = resolvedScript.attrs.slice(); | ||
dom5.removeAttribute(resolvedScript, 'src'); | ||
return this._processScript(resolvedScript, resolvedSrc); | ||
}.bind(this)).catch(function(err) {throw err;}); | ||
} else { | ||
return Promise.resolve(EMPTY_METADATA); | ||
} | ||
}; | ||
loadedHrefs[href] = true; | ||
metadataPromise = this._metadataTree(this.html[href], loadedHrefs); | ||
_context.next = 22; | ||
return metadataPromise; | ||
Analyzer.prototype._dependenciesLoadedFor = function _dependenciesLoadedFor(href, root) { | ||
var found = {}; | ||
if (root !== undefined) { | ||
found[root] = true; | ||
} | ||
return this._getDependencies(href, found).then(function(deps) { | ||
var depMetadataLoaded = []; | ||
var depPromises = deps.map(function(depHref){ | ||
return this.load(depHref).then(function(htmlMonomer) { | ||
return htmlMonomer.metadataLoaded; | ||
}); | ||
}.bind(this)); | ||
return Promise.all(depPromises); | ||
}.bind(this)); | ||
}; | ||
case 22: | ||
_context.next = 25; | ||
break; | ||
/** | ||
* List all the html dependencies for the document at `href`. | ||
* @param {string} href The href to get dependencies for. | ||
* @param {Object.<string,boolean>=} found An object keyed by URL of the | ||
* already resolved dependencies. | ||
* @param {boolean=} transitive Whether to load transitive | ||
* dependencies. Defaults to true. | ||
* @return {Array.<string>} A list of all the html dependencies. | ||
*/ | ||
Analyzer.prototype._getDependencies = function _getDependencies(href, found, transitive) { | ||
if (found === undefined) { | ||
found = {}; | ||
found[href] = true; | ||
} | ||
if (transitive === undefined) { | ||
transitive = true; | ||
} | ||
var deps = []; | ||
return this.load(href).then(function(htmlMonomer) { | ||
var transitiveDeps = []; | ||
htmlMonomer.depHrefs.forEach(function(depHref){ | ||
if (found[depHref]) { | ||
return; | ||
} | ||
deps.push(depHref); | ||
found[depHref] = true; | ||
if (transitive) { | ||
transitiveDeps.push(this._getDependencies(depHref, found)); | ||
} | ||
}.bind(this)); | ||
return Promise.all(transitiveDeps); | ||
}.bind(this)).then(function(transitiveDeps) { | ||
var alldeps = transitiveDeps.reduce(function(a, b) { | ||
return a.concat(b); | ||
}, []).concat(deps); | ||
return alldeps; | ||
}); | ||
}; | ||
case 24: | ||
metadataPromise = Promise.resolve({}); | ||
function matchesDocumentFolder(descriptor, href) { | ||
if (!descriptor.contentHref) { | ||
return false; | ||
} | ||
var descriptorDoc = url.parse(descriptor.contentHref); | ||
if (!descriptorDoc || !descriptorDoc.pathname) { | ||
return false; | ||
} | ||
var searchDoc = url.parse(href); | ||
if (!searchDoc || !searchDoc.pathname) { | ||
return false; | ||
} | ||
var searchPath = searchDoc.pathname; | ||
var lastSlash = searchPath.lastIndexOf("/"); | ||
if (lastSlash > 0) { | ||
searchPath = searchPath.slice(0, lastSlash); | ||
} | ||
return descriptorDoc.pathname.indexOf(searchPath) === 0; | ||
} | ||
case 25: | ||
depMetadata.push(metadataPromise); | ||
/** | ||
* Returns the elements defined in the folder containing `href`. | ||
* @param {string} href path to search. | ||
* @return {Array.<ElementDescriptor>} | ||
*/ | ||
Analyzer.prototype.elementsForFolder = function elementsForFolder(href) { | ||
return this.elements.filter(function(element){ | ||
return matchesDocumentFolder(element, href); | ||
}); | ||
}; | ||
case 26: | ||
_iteratorNormalCompletion4 = true; | ||
_context.next = 14; | ||
break; | ||
/** | ||
* Returns the behaviors defined in the folder containing `href`. | ||
* @param {string} href path to search. | ||
* @return {Array.<BehaviorDescriptor>} | ||
*/ | ||
Analyzer.prototype.behaviorsForFolder = function behaviorsForFolder(href) { | ||
return this.behaviors.filter(function(behavior){ | ||
return matchesDocumentFolder(behavior, href); | ||
}); | ||
}; | ||
case 29: | ||
_context.next = 35; | ||
break; | ||
/** | ||
* Returns a promise that resolves to a POJO representation of the import | ||
* tree, in a format that maintains the ordering of the HTML imports spec. | ||
* @param {string} href the import to get metadata for. | ||
* @return {Promise} | ||
*/ | ||
Analyzer.prototype.metadataTree = function metadataTree(href) { | ||
return this.load(href).then(function(monomer){ | ||
var loadedHrefs = {}; | ||
loadedHrefs[href] = true; | ||
return this._metadataTree(monomer, loadedHrefs); | ||
}.bind(this)); | ||
}; | ||
case 31: | ||
_context.prev = 31; | ||
_context.t0 = _context['catch'](12); | ||
_didIteratorError4 = true; | ||
_iteratorError4 = _context.t0; | ||
Analyzer.prototype._metadataTree = function _metadataTree(htmlMonomer, | ||
loadedHrefs) { | ||
if (loadedHrefs === undefined) { | ||
loadedHrefs = {}; | ||
} | ||
return htmlMonomer.metadataLoaded.then(function(metadata) { | ||
metadata = { | ||
elements: metadata.elements, | ||
features: metadata.features, | ||
href: htmlMonomer.href | ||
}; | ||
return htmlMonomer.depsLoaded.then(function(hrefs) { | ||
var depMetadata = []; | ||
hrefs.forEach(function(href) { | ||
var metadataPromise = Promise.resolve(true); | ||
if (depMetadata.length > 0) { | ||
metadataPromise = depMetadata[depMetadata.length - 1]; | ||
case 35: | ||
_context.prev = 35; | ||
_context.prev = 36; | ||
if (!_iteratorNormalCompletion4 && _iterator4.return) { | ||
_iterator4.return(); | ||
} | ||
case 38: | ||
_context.prev = 38; | ||
if (!_didIteratorError4) { | ||
_context.next = 41; | ||
break; | ||
} | ||
throw _iteratorError4; | ||
case 41: | ||
return _context.finish(38); | ||
case 42: | ||
return _context.finish(35); | ||
case 43: | ||
return _context.abrupt('return', Promise.all(depMetadata).then(function (importMetadata) { | ||
// TODO(ajo): remove this when tsc stops having issues. | ||
metadata.imports = importMetadata; | ||
return htmlMonomer.htmlLoaded.then(function (parsedHtml) { | ||
metadata.html = parsedHtml; | ||
if (metadata.elements) { | ||
metadata.elements.forEach(function (element) { | ||
attachDomModule(parsedHtml, element); | ||
}); | ||
} | ||
return metadata; | ||
}); | ||
})); | ||
case 44: | ||
case 'end': | ||
return _context.stop(); | ||
} | ||
} | ||
}, _callee, this, [[12, 31, 35, 43], [36,, 38, 42]]); | ||
})); | ||
} | ||
metadataPromise = metadataPromise.then(function() { | ||
if (!loadedHrefs[href]) { | ||
loadedHrefs[href] = true; | ||
return this._metadataTree(this.html[href], loadedHrefs); | ||
} else { | ||
return Promise.resolve({}); | ||
} | ||
}.bind(this)); | ||
depMetadata.push(metadataPromise); | ||
}.bind(this)); | ||
return Promise.all(depMetadata).then(function(importMetadata) { | ||
metadata.imports = importMetadata; | ||
return htmlMonomer.htmlLoaded.then(function(parsedHtml) { | ||
metadata.html = parsedHtml; | ||
if (metadata.elements) { | ||
metadata.elements.forEach(function(element) { | ||
attachDomModule(parsedHtml, element); | ||
}, { | ||
key: '_inlineStyles', | ||
value: function _inlineStyles(ast, href) { | ||
var _this8 = this; | ||
var cssLinks = dom5.queryAll(ast, polymerExternalStyle); | ||
cssLinks.forEach(function (link) { | ||
var linkHref = dom5.getAttribute(link, 'href'); | ||
var uri = url.resolve(href, linkHref); | ||
var content = _this8._content[uri]; | ||
var style = dom5.constructors.element('style'); | ||
dom5.setTextContent(style, '\n' + content + '\n'); | ||
dom5.replace(link, style); | ||
}); | ||
} | ||
return metadata; | ||
}); | ||
}); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}; | ||
return cssLinks.length > 0; | ||
} | ||
}, { | ||
key: '_inlineScripts', | ||
value: function _inlineScripts(ast, href) { | ||
var _this9 = this; | ||
function matchingImport(importElement) { | ||
var matchesTag = dom5.predicates.hasTagName(importElement.tagName); | ||
var matchesHref = dom5.predicates.hasAttrValue('href', dom5.getAttribute(importElement, 'href')); | ||
var matchesRel = dom5.predicates.hasAttrValue('rel', dom5.getAttribute(importElement, 'rel')); | ||
return dom5.predicates.AND(matchesTag, matchesHref, matchesRel); | ||
} | ||
var scripts = dom5.queryAll(ast, externalScript); | ||
scripts.forEach(function (script) { | ||
var scriptHref = dom5.getAttribute(script, 'src'); | ||
var uri = url.resolve(href, scriptHref); | ||
var content = _this9._content[uri]; | ||
var inlined = dom5.constructors.element('script'); | ||
dom5.setTextContent(inlined, '\n' + content + '\n'); | ||
dom5.replace(script, inlined); | ||
}); | ||
return scripts.length > 0; | ||
} | ||
}, { | ||
key: '_inlineImports', | ||
value: function _inlineImports(ast, href, loaded) { | ||
var _this10 = this; | ||
// TODO(ajo): Refactor out of vulcanize into dom5. | ||
var polymerExternalStyle = dom5.predicates.AND( | ||
dom5.predicates.hasTagName('link'), | ||
dom5.predicates.hasAttrValue('rel', 'import'), | ||
dom5.predicates.hasAttrValue('type', 'css') | ||
); | ||
var imports = dom5.queryAll(ast, isHtmlImportNode); | ||
imports.forEach(function (htmlImport) { | ||
var importHref = dom5.getAttribute(htmlImport, 'href'); | ||
var uri = url.resolve(href, importHref); | ||
if (loaded[uri]) { | ||
dom5.remove(htmlImport); | ||
return; | ||
} | ||
var content = _this10.getLoadedAst(uri, loaded); | ||
dom5.replace(htmlImport, content); | ||
}); | ||
return imports.length > 0; | ||
} | ||
}, { | ||
key: 'getLoadedAst', | ||
var externalScript = dom5.predicates.AND( | ||
dom5.predicates.hasTagName('script'), | ||
dom5.predicates.hasAttr('src') | ||
); | ||
/** | ||
* Returns a promise resolving to a form of the AST with all links replaced | ||
* with the document they link to. .css and .script files become <style> and | ||
* <script>, respectively. | ||
* | ||
* The elements in the loaded document are unmodified from their original | ||
* documents. | ||
* | ||
* @param {string} href The document to load. | ||
* @param {Object.<string,boolean>=} loaded An object keyed by already loaded documents. | ||
* @return {Promise.<DocumentAST>} | ||
*/ | ||
value: function getLoadedAst(href, loaded) { | ||
if (!loaded) { | ||
loaded = {}; | ||
} | ||
loaded[href] = true; | ||
var parsedDocument = this.parsedDocuments[href]; | ||
var analyzedDocument = this.html[href]; | ||
var astCopy = dom5.parse(dom5.serialize(parsedDocument)); | ||
// Whenever we inline something, reset inlined to true to know that anoather | ||
// inlining pass is needed; | ||
this._inlineStyles(astCopy, href); | ||
this._inlineScripts(astCopy, href); | ||
this._inlineImports(astCopy, href, loaded); | ||
return astCopy; | ||
} | ||
}, { | ||
key: 'nodeWalkDocuments', | ||
var isHtmlImportNode = dom5.predicates.AND( | ||
dom5.predicates.hasTagName('link'), | ||
dom5.predicates.hasAttrValue('rel', 'import'), | ||
dom5.predicates.NOT( | ||
dom5.predicates.hasAttrValue('type', 'css') | ||
) | ||
); | ||
/** | ||
* Calls `dom5.nodeWalkAll` on each document that `Anayzler` has laoded. | ||
*/ | ||
value: function nodeWalkDocuments(predicate) { | ||
var results = []; | ||
for (var href in this.parsedDocuments) { | ||
var newNodes = dom5.nodeWalkAll(this.parsedDocuments[href], predicate); | ||
results = results.concat(newNodes); | ||
} | ||
return results; | ||
} | ||
}, { | ||
key: 'nodeWalkAllDocuments', | ||
Analyzer.prototype._inlineStyles = function _inlineStyles(ast, href) { | ||
var cssLinks = dom5.queryAll(ast, polymerExternalStyle); | ||
cssLinks.forEach(function(link) { | ||
var linkHref = dom5.getAttribute(link, 'href'); | ||
var uri = url.resolve(href, linkHref); | ||
var content = this._content[uri]; | ||
var style = dom5.constructors.element('style'); | ||
dom5.setTextContent(style, '\n' + content + '\n'); | ||
dom5.replace(link, style); | ||
}.bind(this)); | ||
return cssLinks.length > 0; | ||
}; | ||
/** | ||
* Calls `dom5.nodeWalkAll` on each document that `Anayzler` has laoded. | ||
* | ||
* TODO: make nodeWalkAll & nodeWalkAllDocuments distict, or delete one. | ||
*/ | ||
value: function nodeWalkAllDocuments(predicate) { | ||
var results = []; | ||
for (var href in this.parsedDocuments) { | ||
var newNodes = dom5.nodeWalkAll(this.parsedDocuments[href], predicate); | ||
results = results.concat(newNodes); | ||
} | ||
return results; | ||
} | ||
}, { | ||
key: 'annotate', | ||
Analyzer.prototype._inlineScripts = function _inlineScripts(ast, href) { | ||
var scripts = dom5.queryAll(ast, externalScript); | ||
scripts.forEach(function(script) { | ||
var scriptHref = dom5.getAttribute(script, 'src'); | ||
var uri = url.resolve(href, scriptHref); | ||
var content = this._content[uri]; | ||
var inlined = dom5.constructors.element('script'); | ||
dom5.setTextContent(inlined, '\n' + content + '\n'); | ||
dom5.replace(script, inlined); | ||
}.bind(this)); | ||
return scripts.length > 0; | ||
}; | ||
/** Annotates all loaded metadata with its documentation. */ | ||
value: function annotate() { | ||
var _this11 = this; | ||
Analyzer.prototype._inlineImports = function _inlineImports(ast, href, loaded) { | ||
var imports = dom5.queryAll(ast, isHtmlImportNode); | ||
imports.forEach(function(htmlImport) { | ||
var importHref = dom5.getAttribute(htmlImport, 'href'); | ||
var uri = url.resolve(href, importHref); | ||
if (loaded[uri]) { | ||
dom5.remove(htmlImport); | ||
return; | ||
} | ||
var content = this.getLoadedAst(uri, loaded); | ||
dom5.replace(htmlImport, content); | ||
}.bind(this)); | ||
return imports.length > 0; | ||
}; | ||
if (this.features.length > 0) { | ||
var featureEl = docs.featureElement(this.features); | ||
this.elements.unshift(featureEl); | ||
this.elementsByTagName[featureEl.is] = featureEl; | ||
} | ||
var behaviorsByName = this.behaviorsByName; | ||
var elementHelper = function elementHelper(descriptor) { | ||
docs.annotateElement(descriptor, behaviorsByName); | ||
}; | ||
this.elements.forEach(elementHelper); | ||
this.behaviors.forEach(elementHelper); // Same shape. | ||
this.behaviors.forEach(function (behavior) { | ||
if (behavior.is !== behavior.symbol && behavior.symbol) { | ||
_this11.behaviorsByName[behavior.symbol] = undefined; | ||
} | ||
}); | ||
} | ||
}, { | ||
key: 'clean', | ||
/** Removes redundant properties from the collected descriptors. */ | ||
value: function clean() { | ||
this.elements.forEach(docs.cleanElement); | ||
} | ||
}]); | ||
return Analyzer; | ||
}(); | ||
/** | ||
* Returns a promise resolving to a form of the AST with all links replaced | ||
* with the document they link to. .css and .script files become <style> and | ||
* <script>, respectively. | ||
* Shorthand for transitively loading and processing all imports beginning at | ||
* `href`. | ||
* | ||
* The elements in the loaded document are unmodified from their original | ||
* documents. | ||
* In order to properly filter paths, `href` _must_ be an absolute URI. | ||
* | ||
* @param {string} href The document to load. | ||
* @param {Object.<string,boolean>=} loaded An object keyed by already loaded documents. | ||
* @return {Promise.<DocumentAST>} | ||
* @param {string} href The root import to begin loading from. | ||
* @param {LoadOptions=} options Any additional options for the load. | ||
* @return {Promise<Analyzer>} A promise that will resolve once `href` and its | ||
* dependencies have been loaded and analyzed. | ||
*/ | ||
Analyzer.prototype.getLoadedAst = function getLoadedAst(href, loaded) { | ||
if (!loaded) { | ||
loaded = {}; | ||
} | ||
loaded[href] = true; | ||
var parsedDocument = this.parsedDocuments[href]; | ||
var analyzedDocument = this.html[href]; | ||
var astCopy = dom5.parse(dom5.serialize(parsedDocument)); | ||
// Whenever we inline something, reset inlined to true to know that anoather | ||
// inlining pass is needed; | ||
this._inlineStyles(astCopy, href); | ||
this._inlineScripts(astCopy, href); | ||
this._inlineImports(astCopy, href, loaded); | ||
return astCopy; | ||
}; | ||
/** | ||
* Calls `dom5.nodeWalkAll` on each document that `Anayzler` has laoded. | ||
* @param {Object} predicate A dom5 predicate. | ||
* @return {Object} | ||
*/ | ||
Analyzer.prototype.nodeWalkDocuments = function nodeWalkDocuments(predicate) { | ||
for (var href in this.parsedDocuments) { | ||
var match = dom5.nodeWalk(this.parsedDocuments[href], predicate); | ||
if (match) { | ||
return match; | ||
Analyzer.analyze = function analyze(href, options) { | ||
options = options || {}; | ||
options.filter = options.filter || _defaultFilter(href); | ||
var loader = new file_loader_1.FileLoader(); | ||
var resolver = options.resolver; | ||
if (resolver === undefined) { | ||
if (typeof window === 'undefined') { | ||
resolver = 'fs'; | ||
} else { | ||
resolver = 'xhr'; | ||
} | ||
} | ||
} | ||
return null; | ||
var primaryResolver = undefined; | ||
if (resolver === 'fs') { | ||
primaryResolver = new fs_resolver_1.FSResolver(options); | ||
} else if (resolver === 'xhr') { | ||
primaryResolver = new xhr_resolver_1.XHRResolver(options); | ||
} else if (resolver === 'permissive') { | ||
primaryResolver = new error_swallowing_fs_resolver_1.ErrorSwallowingFSResolver(options); | ||
} else { | ||
throw new Error("Resolver must be one of 'fs', 'xhr', or 'permissive'"); | ||
} | ||
loader.addResolver(primaryResolver); | ||
if (options.content) { | ||
loader.addResolver(new string_resolver_1.StringResolver({ url: href, content: options.content })); | ||
} | ||
loader.addResolver(new noop_resolver_1.NoopResolver({ test: options.filter })); | ||
var analyzer = new Analyzer(false, loader); | ||
return analyzer.metadataTree(href).then(function (root) { | ||
if (!options.noAnnotations) { | ||
analyzer.annotate(); | ||
} | ||
if (options.clean) { | ||
analyzer.clean(); | ||
} | ||
return Promise.resolve(analyzer); | ||
}); | ||
}; | ||
exports.Analyzer = Analyzer; | ||
; | ||
/** | ||
* Calls `dom5.nodeWalkAll` on each document that `Anayzler` has laoded. | ||
* @param {Object} predicate A dom5 predicate. | ||
* @return {Object} | ||
* @private | ||
* @param {string} href | ||
* @return {function(string): boolean} | ||
*/ | ||
Analyzer.prototype.nodeWalkAllDocuments = function nodeWalkDocuments(predicate) { | ||
var results = []; | ||
for (var href in this.parsedDocuments) { | ||
var newNodes = dom5.nodeWalkAll(this.parsedDocuments[href], predicate); | ||
results = results.concat(newNodes); | ||
} | ||
return results; | ||
}; | ||
/** Annotates all loaded metadata with its documentation. */ | ||
Analyzer.prototype.annotate = function annotate() { | ||
if (this.features.length > 0) { | ||
var featureEl = docs.featureElement(this.features); | ||
this.elements.unshift(featureEl); | ||
this.elementsByTagName[featureEl.is] = featureEl; | ||
} | ||
var behaviorsByName = this.behaviorsByName; | ||
var elementHelper = function(descriptor){ | ||
docs.annotateElement(descriptor, behaviorsByName); | ||
}; | ||
this.elements.forEach(elementHelper); | ||
this.behaviors.forEach(elementHelper); // Same shape. | ||
this.behaviors.forEach(function(behavior){ | ||
if (behavior.is !== behavior.symbol && behavior.symbol) { | ||
this.behaviorsByName[behavior.symbol] = undefined; | ||
function _defaultFilter(href) { | ||
// Everything up to the last `/` or `\`. | ||
var base = href.match(/^(.*?)[^\/\\]*$/)[1]; | ||
return function (uri) { | ||
return uri.indexOf(base) !== 0; | ||
}; | ||
} | ||
function matchesDocumentFolder(descriptor, href) { | ||
if (!descriptor.contentHref) { | ||
return false; | ||
} | ||
}.bind(this)); | ||
}; | ||
function attachDomModule(parsedImport, element) { | ||
var domModules = parsedImport['dom-module']; | ||
for (var i = 0, domModule; i < domModules.length; i++) { | ||
domModule = domModules[i]; | ||
if (dom5.getAttribute(domModule, 'id') === element.is) { | ||
element.domModule = domModule; | ||
return; | ||
var descriptorDoc = url.parse(descriptor.contentHref); | ||
if (!descriptorDoc || !descriptorDoc.pathname) { | ||
return false; | ||
} | ||
} | ||
var searchDoc = url.parse(href); | ||
if (!searchDoc || !searchDoc.pathname) { | ||
return false; | ||
} | ||
var searchPath = searchDoc.pathname; | ||
var lastSlash = searchPath.lastIndexOf("/"); | ||
if (lastSlash > 0) { | ||
searchPath = searchPath.slice(0, lastSlash); | ||
} | ||
return descriptorDoc.pathname.indexOf(searchPath) === 0; | ||
} | ||
// TODO(ajo): Refactor out of vulcanize into dom5. | ||
var polymerExternalStyle = dom5.predicates.AND(dom5.predicates.hasTagName('link'), dom5.predicates.hasAttrValue('rel', 'import'), dom5.predicates.hasAttrValue('type', 'css')); | ||
var externalScript = dom5.predicates.AND(dom5.predicates.hasTagName('script'), dom5.predicates.hasAttr('src')); | ||
var isHtmlImportNode = dom5.predicates.AND(dom5.predicates.hasTagName('link'), dom5.predicates.hasAttrValue('rel', 'import'), dom5.predicates.NOT(dom5.predicates.hasAttrValue('type', 'css'))); | ||
function attachDomModule(parsedImport, element) { | ||
var domModules = parsedImport['dom-module']; | ||
var _iteratorNormalCompletion5 = true; | ||
var _didIteratorError5 = false; | ||
var _iteratorError5 = undefined; | ||
/** Removes redundant properties from the collected descriptors. */ | ||
Analyzer.prototype.clean = function clean() { | ||
this.elements.forEach(docs.cleanElement); | ||
}; | ||
try { | ||
for (var _iterator5 = domModules[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { | ||
var domModule = _step5.value; | ||
module.exports = Analyzer; | ||
if (dom5.getAttribute(domModule, 'id') === element.is) { | ||
element.domModule = domModule; | ||
return; | ||
} | ||
} | ||
} catch (err) { | ||
_didIteratorError5 = true; | ||
_iteratorError5 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion5 && _iterator5.return) { | ||
_iterator5.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError5) { | ||
throw _iteratorError5; | ||
} | ||
} | ||
} | ||
} |
@@ -10,99 +10,92 @@ /** | ||
*/ | ||
// jshint node: true | ||
'use strict'; | ||
var esutil = require('./esutil'); | ||
var esutil = require('./esutil'); | ||
var astValue = require('./ast-value'); | ||
var analyzeProperties = function(node) { | ||
var analyzedProps = []; | ||
if (node.type != 'ObjectExpression') { | ||
return analyzedProps; | ||
} | ||
for (var i = 0; i < node.properties.length; i++) { | ||
var property = node.properties[i]; | ||
var prop = esutil.toPropertyDescriptor(property); | ||
prop.published = true; | ||
if (property.value.type == 'ObjectExpression') { | ||
/** | ||
* Parse the expression inside a property object block. | ||
* property: { | ||
* key: { | ||
* type: String, | ||
* notify: true, | ||
* value: -1, | ||
* readOnly: true, | ||
* reflectToAttribute: true | ||
* } | ||
* } | ||
*/ | ||
for (var j = 0; j < property.value.properties.length; j++) { | ||
var propertyArg = property.value.properties[j]; | ||
var propertyKey = esutil.objectKeyToString(propertyArg.key); | ||
switch(propertyKey) { | ||
case 'type': { | ||
prop.type = esutil.objectKeyToString(propertyArg.value); | ||
if (prop.type === undefined) { | ||
throw { | ||
message: 'Invalid type in property object.', | ||
location: propertyArg.loc.start | ||
}; | ||
function analyzeProperties(node) { | ||
var analyzedProps = []; | ||
if (node.type != 'ObjectExpression') { | ||
return analyzedProps; | ||
} | ||
var obEx = node; | ||
for (var i = 0; i < obEx.properties.length; i++) { | ||
var property = obEx.properties[i]; | ||
var prop = esutil.toPropertyDescriptor(property); | ||
prop.published = true; | ||
if (property.value.type == 'ObjectExpression') { | ||
/** | ||
* Parse the expression inside a property object block. e.g. | ||
* property: { | ||
* key: { | ||
* type: String, | ||
* notify: true, | ||
* value: -1, | ||
* readOnly: true, | ||
* reflectToAttribute: true | ||
* } | ||
* } | ||
*/ | ||
var propDescExpr = property.value; | ||
for (var j = 0; j < propDescExpr.properties.length; j++) { | ||
var propertyArg = propDescExpr.properties[j]; | ||
var propertyKey = esutil.objectKeyToString(propertyArg.key); | ||
switch (propertyKey) { | ||
case 'type': | ||
{ | ||
prop.type = esutil.objectKeyToString(propertyArg.value); | ||
if (prop.type === undefined) { | ||
throw { | ||
message: 'Invalid type in property object.', | ||
location: propertyArg.loc.start | ||
}; | ||
} | ||
} | ||
break; | ||
case 'notify': | ||
{ | ||
prop.notify = astValue.expressionToValue(propertyArg.value); | ||
if (prop.notify === undefined) prop.notify = astValue.CANT_CONVERT; | ||
} | ||
break; | ||
case 'observer': | ||
{ | ||
prop.observer = astValue.expressionToValue(propertyArg.value); | ||
prop.observerNode = propertyArg.value; | ||
if (prop.observer === undefined) prop.observer = astValue.CANT_CONVERT; | ||
} | ||
break; | ||
case 'readOnly': | ||
{ | ||
prop.readOnly = astValue.expressionToValue(propertyArg.value); | ||
if (prop.readOnly === undefined) prop.readOnly = astValue.CANT_CONVERT; | ||
} | ||
break; | ||
case 'reflectToAttribute': | ||
{ | ||
prop.reflectToAttribute = astValue.expressionToValue(propertyArg); | ||
if (prop.reflectToAttribute === undefined) prop.reflectToAttribute = astValue.CANT_CONVERT; | ||
} | ||
break; | ||
case 'value': | ||
{ | ||
prop.default = astValue.expressionToValue(propertyArg.value); | ||
if (prop.default === undefined) prop.default = astValue.CANT_CONVERT; | ||
} | ||
break; | ||
default: | ||
break; | ||
} | ||
} | ||
} | ||
break; | ||
case 'notify': { | ||
prop.notify = astValue.expressionToValue(propertyArg.value); | ||
if (prop.notify === undefined) | ||
prop.notify = astValue.CANT_CONVERT; | ||
} | ||
break; | ||
case 'observer': { | ||
prop.observer = astValue.expressionToValue(propertyArg.value); | ||
prop.observerNode = propertyArg.value; | ||
if (prop.observer === undefined) | ||
prop.observer = astValue.CANT_CONVERT; | ||
} | ||
break; | ||
case 'readOnly': { | ||
prop.readOnly = astValue.expressionToValue(propertyArg.value); | ||
if (prop.readOnly === undefined) | ||
prop.readOnly = astValue.CANT_CONVERT; | ||
} | ||
break; | ||
case 'reflectToAttribute': { | ||
prop.reflectToAttribute = astValue.expressionToValue(propertyArg); | ||
if (prop.reflectToAttribute === undefined) | ||
prop.reflectToAttribute = astValue.CANT_CONVERT; | ||
} | ||
break; | ||
case 'value': { | ||
prop.default = astValue.expressionToValue(propertyArg.value); | ||
if (prop.default === undefined) | ||
prop.default = astValue.CANT_CONVERT; | ||
} | ||
break; | ||
default: | ||
break; | ||
} | ||
} | ||
if (!prop.type) { | ||
throw { | ||
message: 'Unable to determine name for property key.', | ||
location: node.loc.start | ||
}; | ||
} | ||
analyzedProps.push(prop); | ||
} | ||
if (!prop.type) { | ||
throw { | ||
message: 'Unable to determine name for property key.', | ||
location: node.loc.start | ||
}; | ||
} | ||
analyzedProps.push(prop); | ||
} | ||
return analyzedProps; | ||
}; | ||
module.exports = analyzeProperties; | ||
return analyzedProps; | ||
} | ||
exports.analyzeProperties = analyzeProperties; | ||
; |
@@ -10,7 +10,4 @@ /** | ||
*/ | ||
// jshint node: true | ||
'use strict'; | ||
// useful tool to visualize AST: http://esprima.org/demo/parse.html | ||
/** | ||
@@ -20,17 +17,14 @@ * converts literal: {"type": "Literal", "value": 5, "raw": "5" } | ||
*/ | ||
function literalToValue(literal) { | ||
return literal.value; | ||
return literal.value; | ||
} | ||
/** | ||
* converts unary to string | ||
* unary: { type: 'UnaryExpression', operator: '-', argument: { ... } } | ||
*/ | ||
function unaryToValue(unary) { | ||
var argValue = expressionToValue(unary.argument); | ||
if (argValue === undefined) | ||
return; | ||
return unary.operator + argValue; | ||
var argValue = expressionToValue(unary.argument); | ||
if (argValue === undefined) return; | ||
return unary.operator + argValue; | ||
} | ||
/** | ||
@@ -41,5 +35,4 @@ * converts identifier to its value | ||
function identifierToValue(identifier) { | ||
return identifier.name; | ||
return identifier.name; | ||
} | ||
/** | ||
@@ -49,9 +42,6 @@ * Function is a block statement. | ||
function functionDeclarationToValue(fn) { | ||
if (fn.body.type == "BlockStatement") | ||
return blockStatementToValue(fn.body); | ||
if (fn.body.type == "BlockStatement") return blockStatementToValue(fn.body); | ||
} | ||
function functionExpressionToValue(fn) { | ||
if (fn.body.type == "BlockStatement") | ||
return blockStatementToValue(fn.body); | ||
if (fn.body.type == "BlockStatement") return blockStatementToValue(fn.body); | ||
} | ||
@@ -62,8 +52,6 @@ /** | ||
function blockStatementToValue(block) { | ||
for (var i=block.body.length - 1; i>= 0; i--) { | ||
if (block.body[i].type === "ReturnStatement") | ||
return returnStatementToValue(block.body[i]); | ||
} | ||
for (var i = block.body.length - 1; i >= 0; i--) { | ||
if (block.body[i].type === "ReturnStatement") return returnStatementToValue(block.body[i]); | ||
} | ||
} | ||
/** | ||
@@ -73,5 +61,4 @@ * Evaluates return's argument | ||
function returnStatementToValue(ret) { | ||
return expressionToValue(ret.argument); | ||
return expressionToValue(ret.argument); | ||
} | ||
/** | ||
@@ -81,15 +68,12 @@ * Enclose containing values in [] | ||
function arrayExpressionToValue(arry) { | ||
var value = '['; | ||
for (var i=0; i<arry.elements.length; i++) { | ||
var v = expressionToValue(arry.elements[i]); | ||
if (v === undefined) | ||
continue; | ||
if (i !== 0) | ||
value += ', '; | ||
value += v; | ||
} | ||
value += ']'; | ||
return value; | ||
var value = '['; | ||
for (var i = 0; i < arry.elements.length; i++) { | ||
var v = expressionToValue(arry.elements[i]); | ||
if (v === undefined) continue; | ||
if (i !== 0) value += ', '; | ||
value += v; | ||
} | ||
value += ']'; | ||
return value; | ||
} | ||
/** | ||
@@ -99,16 +83,13 @@ * Make it look like an object | ||
function objectExpressionToValue(obj) { | ||
var value = '{'; | ||
for (var i=0; i<obj.properties.length; i++) { | ||
var k = expressionToValue(obj.properties[i].key); | ||
var v = expressionToValue(obj.properties[i].value); | ||
if (v === undefined) | ||
continue; | ||
if (i !== 0) | ||
value += ', '; | ||
value += '"' + k + '": ' + v; | ||
} | ||
value += '}'; | ||
return value; | ||
var value = '{'; | ||
for (var i = 0; i < obj.properties.length; i++) { | ||
var k = expressionToValue(obj.properties[i].key); | ||
var v = expressionToValue(obj.properties[i].value); | ||
if (v === undefined) continue; | ||
if (i !== 0) value += ', '; | ||
value += '"' + k + '": ' + v; | ||
} | ||
value += '}'; | ||
return value; | ||
} | ||
/** | ||
@@ -118,8 +99,10 @@ * BinaryExpressions are of the form "literal" + "literal" | ||
function binaryExpressionToValue(member) { | ||
if (member.operator == "+") { | ||
return expressionToValue(member.left) + expressionToValue(member.right); | ||
} | ||
return | ||
if (member.operator == "+") { | ||
// We need to cast to `any` here because, while it's usually not the right | ||
// thing to do to use '+' on two values of a mix of types because it's | ||
// unpredictable, that is what the original code we're evaluating does. | ||
return expressionToValue(member.left) + expressionToValue(member.right); | ||
} | ||
return; | ||
} | ||
/** | ||
@@ -129,42 +112,34 @@ * MemberExpression references a variable with name | ||
function memberExpressionToValue(member) { | ||
return expressionToValue(member.object) + "." + expressionToValue(member.property); | ||
return expressionToValue(member.object) + "." + expressionToValue(member.property); | ||
} | ||
/** | ||
* Tries to get a value from expression. Handles Literal, UnaryExpression | ||
* returns undefined on failure | ||
* valueExpression example: | ||
* { type: "Literal", | ||
* Tries to get the value of an expression. Returns undefined on failure. | ||
*/ | ||
function expressionToValue(valueExpression) { | ||
switch(valueExpression.type) { | ||
case 'Literal': | ||
return literalToValue(valueExpression); | ||
case 'UnaryExpression': | ||
return unaryToValue(valueExpression); | ||
case 'Identifier': | ||
return identifierToValue(valueExpression); | ||
case 'FunctionDeclaration': | ||
return functionDeclarationToValue(valueExpression); | ||
case 'FunctionExpression': | ||
return functionExpressionToValue(valueExpression); | ||
case 'ArrayExpression': | ||
return arrayExpressionToValue(valueExpression); | ||
case 'ObjectExpression': | ||
return objectExpressionToValue(valueExpression); | ||
case 'Identifier': | ||
return identifierToValue(valueExpression); | ||
case 'MemberExpression': | ||
return memberExpressionToValue(valueExpression); | ||
case 'BinaryExpression': | ||
return binaryExpressionToValue(valueExpression); | ||
default: | ||
return; | ||
} | ||
switch (valueExpression.type) { | ||
case 'Literal': | ||
return literalToValue(valueExpression); | ||
case 'UnaryExpression': | ||
return unaryToValue(valueExpression); | ||
case 'Identifier': | ||
return identifierToValue(valueExpression); | ||
case 'FunctionDeclaration': | ||
return functionDeclarationToValue(valueExpression); | ||
case 'FunctionExpression': | ||
return functionExpressionToValue(valueExpression); | ||
case 'ArrayExpression': | ||
return arrayExpressionToValue(valueExpression); | ||
case 'ObjectExpression': | ||
return objectExpressionToValue(valueExpression); | ||
case 'Identifier': | ||
return identifierToValue(valueExpression); | ||
case 'MemberExpression': | ||
return memberExpressionToValue(valueExpression); | ||
case 'BinaryExpression': | ||
return binaryExpressionToValue(valueExpression); | ||
default: | ||
return; | ||
} | ||
} | ||
var CANT_CONVERT = 'UNKNOWN'; | ||
module.exports = { | ||
CANT_CONVERT: CANT_CONVERT, | ||
expressionToValue: expressionToValue | ||
}; | ||
exports.expressionToValue = expressionToValue; | ||
exports.CANT_CONVERT = 'UNKNOWN'; |
@@ -10,217 +10,208 @@ /** | ||
*/ | ||
// jshint node: true | ||
'use strict'; | ||
var estraverse = require('estraverse'); | ||
var docs = require('./docs'); | ||
var docs = require('./docs'); | ||
var esutil = require('./esutil'); | ||
var jsdoc = require('./jsdoc'); | ||
var analyzeProperties = require('./analyze-properties'); | ||
var astValue = require('./ast-value.js'); | ||
var declarationPropertyHandlers = require('./declaration-property-handlers'); | ||
module.exports = function behaviorFinder() { | ||
/** @type {!Array<BehaviorDescriptor>} The behaviors we've found. */ | ||
var behaviors = []; | ||
var currentBehavior = null; | ||
var propertyHandlers = null; | ||
/** | ||
* merges behavior with preexisting behavior with the same name. | ||
* here to support multiple @polymerBehavior tags referring | ||
* to same behavior. See iron-multi-selectable for example. | ||
*/ | ||
function mergeBehavior(newBehavior) { | ||
var isBehaviorImpl = function(b) { // filter out BehaviorImpl | ||
return b.indexOf(newBehavior.is) === -1; | ||
}; | ||
for (var i=0; i<behaviors.length; i++) { | ||
if (newBehavior.is !== behaviors[i].is) | ||
continue; | ||
// merge desc, longest desc wins | ||
if (newBehavior.desc) { | ||
if (behaviors[i].desc) { | ||
if (newBehavior.desc.length > behaviors[i].desc.length) | ||
behaviors[i].desc = newBehavior.desc; | ||
var jsdoc = require('./jsdoc'); | ||
var astValue = require('./ast-value'); | ||
var declaration_property_handlers_1 = require('./declaration-property-handlers'); | ||
function dedupe(array, keyFunc) { | ||
var bucket = {}; | ||
array.forEach(function (el) { | ||
var key = keyFunc(el); | ||
if (key in bucket) { | ||
return; | ||
} | ||
else { | ||
behaviors[i].desc = newBehavior.desc; | ||
bucket[key] = el; | ||
}); | ||
var returned = []; | ||
Object.keys(bucket).forEach(function (k) { | ||
returned.push(bucket[k]); | ||
}); | ||
return returned; | ||
} | ||
// TODO(rictic): turn this into a class. | ||
function behaviorFinder() { | ||
/** The behaviors we've found. */ | ||
var behaviors = []; | ||
var currentBehavior = null; | ||
var propertyHandlers = null; | ||
/** | ||
* merges behavior with preexisting behavior with the same name. | ||
* here to support multiple @polymerBehavior tags referring | ||
* to same behavior. See iron-multi-selectable for example. | ||
*/ | ||
function mergeBehavior(newBehavior) { | ||
var isBehaviorImpl = function isBehaviorImpl(b) { | ||
// filter out BehaviorImpl | ||
return b.indexOf(newBehavior.is) === -1; | ||
}; | ||
for (var i = 0; i < behaviors.length; i++) { | ||
if (newBehavior.is !== behaviors[i].is) continue; | ||
// merge desc, longest desc wins | ||
if (newBehavior.desc) { | ||
if (behaviors[i].desc) { | ||
if (newBehavior.desc.length > behaviors[i].desc.length) behaviors[i].desc = newBehavior.desc; | ||
} else { | ||
behaviors[i].desc = newBehavior.desc; | ||
} | ||
} | ||
// merge demos | ||
behaviors[i].demos = (behaviors[i].demos || []).concat(newBehavior.demos || []); | ||
// merge events, | ||
behaviors[i].events = (behaviors[i].events || []).concat(newBehavior.events || []); | ||
behaviors[i].events = dedupe(behaviors[i].events, function (e) { | ||
return e.name; | ||
}); | ||
// merge properties | ||
behaviors[i].properties = (behaviors[i].properties || []).concat(newBehavior.properties || []); | ||
// merge observers | ||
behaviors[i].observers = (behaviors[i].observers || []).concat(newBehavior.observers || []); | ||
// merge behaviors | ||
behaviors[i].behaviors = (behaviors[i].behaviors || []).concat(newBehavior.behaviors || []).filter(isBehaviorImpl); | ||
return behaviors[i]; | ||
} | ||
} | ||
// merge demos | ||
behaviors[i].demos = (behaviors[i].demos || []).concat(newBehavior.demos || []); | ||
// merge events, | ||
behaviors[i].events = (behaviors[i].events || []).concat(newBehavior.events || []); | ||
// merge properties | ||
behaviors[i].properties = (behaviors[i].properties || []).concat(newBehavior.properties || []); | ||
// merge observers | ||
behaviors[i].observers = (behaviors[i].observers || []).concat(newBehavior.observers || []); | ||
// merge behaviors | ||
behaviors[i].behaviors = | ||
(behaviors[i].behaviors || []).concat(newBehavior.behaviors || []) | ||
.filter(isBehaviorImpl); | ||
return behaviors[i]; | ||
return newBehavior; | ||
} | ||
return newBehavior; | ||
} | ||
/** | ||
* gets the expression representing a behavior from a node. | ||
*/ | ||
function behaviorExpression(node) { | ||
switch(node.type) { | ||
case 'ExpressionStatement': | ||
return node.expression.right; | ||
case 'VariableDeclaration': | ||
return node.declarations.length > 0 ? node.declarations[0].init : null; | ||
} | ||
} | ||
/** | ||
* checks whether an expression is a simple array containing only member | ||
* expressions or identifiers. | ||
*/ | ||
function isSimpleBehaviorArray(expression) { | ||
if (!expression || expression.type !== 'ArrayExpression') return false; | ||
for (var i=0; i < expression.elements.length; i++) { | ||
if (expression.elements[i].type !== 'MemberExpression' && | ||
expression.elements[i].type !== 'Identifier') { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
var templatizer = "Polymer.Templatizer"; | ||
var visitors = { | ||
/** | ||
* Look for object declarations with @behavior in the docs. | ||
* gets the expression representing a behavior from a node. | ||
*/ | ||
enterVariableDeclaration: function(node, parent) { | ||
if (node.declarations.length !== 1) return; // Ambiguous. | ||
this._initBehavior(node, function () { | ||
return esutil.objectKeyToString(node.declarations[0].id); | ||
}); | ||
}, | ||
function behaviorExpression(node) { | ||
switch (node.type) { | ||
case 'ExpressionStatement': | ||
// need to cast to `any` here because ExpressionStatement is super | ||
// super general. this code is suspicious. | ||
return node.expression.right; | ||
case 'VariableDeclaration': | ||
var n = node; | ||
return n.declarations.length > 0 ? n.declarations[0].init : null; | ||
} | ||
} | ||
/** | ||
* Look for object assignments with @polymerBehavior in the docs. | ||
* checks whether an expression is a simple array containing only member | ||
* expressions or identifiers. | ||
*/ | ||
enterAssignmentExpression: function(node, parent) { | ||
this._initBehavior(parent, function () { | ||
return esutil.objectKeyToString(node.left); | ||
}); | ||
}, | ||
_parseChainedBehaviors: function(node) { | ||
// if current behavior is part of an array, it gets extended by other behaviors | ||
// inside the array. Ex: | ||
// Polymer.IronMultiSelectableBehavior = [ {....}, Polymer.IronSelectableBehavior] | ||
// We add these to behaviors array | ||
var expression = behaviorExpression(node); | ||
var chained = []; | ||
if (expression && expression.type === 'ArrayExpression') { | ||
for (var i=0; i < expression.elements.length; i++) { | ||
if (expression.elements[i].type === 'MemberExpression' || | ||
expression.elements[i].type === 'Identifier') { | ||
chained.push(astValue.expressionToValue(expression.elements[i])); | ||
} | ||
function isSimpleBehaviorArray(expression) { | ||
if (!expression || expression.type !== 'ArrayExpression') return false; | ||
var arrayExpr = expression; | ||
for (var i = 0; i < arrayExpr.elements.length; i++) { | ||
if (arrayExpr.elements[i].type !== 'MemberExpression' && arrayExpr.elements[i].type !== 'Identifier') { | ||
return false; | ||
} | ||
} | ||
if (chained.length > 0) | ||
currentBehavior.behaviors = chained; | ||
} | ||
}, | ||
_initBehavior: function(node, getName) { | ||
var comment = esutil.getAttachedComment(node); | ||
var symbol = getName(); | ||
// Quickly filter down to potential candidates. | ||
if (!comment || comment.indexOf('@polymerBehavior') === -1) { | ||
if (symbol !== templatizer) { | ||
return; | ||
return true; | ||
} | ||
var templatizer = "Polymer.Templatizer"; | ||
function _parseChainedBehaviors(node) { | ||
// if current behavior is part of an array, it gets extended by other behaviors | ||
// inside the array. Ex: | ||
// Polymer.IronMultiSelectableBehavior = [ {....}, Polymer.IronSelectableBehavior] | ||
// We add these to behaviors array | ||
var expression = behaviorExpression(node); | ||
var chained = []; | ||
if (expression && expression.type === 'ArrayExpression') { | ||
var arrExpr = expression; | ||
for (var i = 0; i < arrExpr.elements.length; i++) { | ||
if (arrExpr.elements[i].type === 'MemberExpression' || arrExpr.elements[i].type === 'Identifier') { | ||
chained.push(astValue.expressionToValue(arrExpr.elements[i])); | ||
} | ||
} | ||
if (chained.length > 0) currentBehavior.behaviors = chained; | ||
} | ||
} | ||
currentBehavior = { | ||
type: 'behavior', | ||
desc: comment, | ||
events: esutil.getEventComments(node).map( function(event) { | ||
return { desc: event}; | ||
}) | ||
}; | ||
propertyHandlers = declarationPropertyHandlers(currentBehavior); | ||
docs.annotateBehavior(currentBehavior); | ||
// Make sure that we actually parsed a behavior tag! | ||
if (!jsdoc.hasTag(currentBehavior.jsdoc, 'polymerBehavior') && | ||
symbol !== templatizer) { | ||
currentBehavior = null; | ||
propertyHandlers = null; | ||
return; | ||
} | ||
var name = jsdoc.getTag(currentBehavior.jsdoc, 'polymerBehavior', 'name'); | ||
currentBehavior.symbol = symbol; | ||
if (!name) { | ||
name = currentBehavior.symbol; | ||
} | ||
if (!name) { | ||
console.warn('Unable to determine name for @polymerBehavior:', comment); | ||
} | ||
currentBehavior.is = name; | ||
this._parseChainedBehaviors(node); | ||
currentBehavior = mergeBehavior(currentBehavior); | ||
propertyHandlers = declarationPropertyHandlers(currentBehavior); | ||
// Some behaviors are just lists of other behaviors. If this is one then | ||
// add it to behaviors right away. | ||
if (isSimpleBehaviorArray(behaviorExpression(node))) { | ||
// TODO(ajo): Add a test to confirm the presence of `properties` | ||
if (!currentBehavior.observers) currentBehavior.observers = []; | ||
if (!currentBehavior.properties) currentBehavior.properties = []; | ||
if (behaviors.indexOf(currentBehavior) === -1) | ||
behaviors.push(currentBehavior); | ||
currentBehavior = null; | ||
propertyHandlers = null; | ||
} | ||
}, | ||
/** | ||
* We assume that the object expression after such an assignment is the | ||
* behavior's declaration. Seems to be a decent assumption for now. | ||
*/ | ||
enterObjectExpression: function(node, parent) { | ||
if (!currentBehavior || currentBehavior.properties) return; | ||
currentBehavior.properties = currentBehavior.properties || []; | ||
currentBehavior.observers = currentBehavior.observers || []; | ||
for (var i = 0; i < node.properties.length; i++) { | ||
var prop = node.properties[i]; | ||
var name = esutil.objectKeyToString(prop.key); | ||
} | ||
function _initBehavior(node, getName) { | ||
var comment = esutil.getAttachedComment(node); | ||
var symbol = getName(); | ||
// Quickly filter down to potential candidates. | ||
if (!comment || comment.indexOf('@polymerBehavior') === -1) { | ||
if (symbol !== templatizer) { | ||
return; | ||
} | ||
} | ||
currentBehavior = { | ||
type: 'behavior', | ||
desc: comment, | ||
events: esutil.getEventComments(node).map(function (event) { | ||
return { desc: event }; | ||
}) | ||
}; | ||
propertyHandlers = declaration_property_handlers_1.declarationPropertyHandlers(currentBehavior); | ||
docs.annotateBehavior(currentBehavior); | ||
// Make sure that we actually parsed a behavior tag! | ||
if (!jsdoc.hasTag(currentBehavior.jsdoc, 'polymerBehavior') && symbol !== templatizer) { | ||
currentBehavior = null; | ||
propertyHandlers = null; | ||
return; | ||
} | ||
var name = jsdoc.getTag(currentBehavior.jsdoc, 'polymerBehavior', 'name'); | ||
currentBehavior.symbol = symbol; | ||
if (!name) { | ||
throw { | ||
message: 'Cant determine name for property key.', | ||
location: node.loc.start | ||
}; | ||
name = currentBehavior.symbol; | ||
} | ||
if (name in propertyHandlers) { | ||
propertyHandlers[name](prop.value); | ||
if (!name) { | ||
console.warn('Unable to determine name for @polymerBehavior:', comment); | ||
} | ||
else { | ||
currentBehavior.properties.push(esutil.toPropertyDescriptor(prop)); | ||
currentBehavior.is = name; | ||
_parseChainedBehaviors(node); | ||
currentBehavior = mergeBehavior(currentBehavior); | ||
propertyHandlers = declaration_property_handlers_1.declarationPropertyHandlers(currentBehavior); | ||
// Some behaviors are just lists of other behaviors. If this is one then | ||
// add it to behaviors right away. | ||
if (isSimpleBehaviorArray(behaviorExpression(node))) { | ||
// TODO(ajo): Add a test to confirm the presence of `properties` | ||
if (!currentBehavior.observers) currentBehavior.observers = []; | ||
if (!currentBehavior.properties) currentBehavior.properties = []; | ||
if (behaviors.indexOf(currentBehavior) === -1) behaviors.push(currentBehavior); | ||
currentBehavior = null; | ||
propertyHandlers = null; | ||
} | ||
} | ||
behaviors.push(currentBehavior); | ||
currentBehavior = null; | ||
}, | ||
}; | ||
return {visitors: visitors, behaviors: behaviors}; | ||
}; | ||
} | ||
var visitors = { | ||
/** | ||
* Look for object declarations with @behavior in the docs. | ||
*/ | ||
enterVariableDeclaration: function enterVariableDeclaration(node, parent) { | ||
if (node.declarations.length !== 1) return; // Ambiguous. | ||
_initBehavior(node, function () { | ||
return esutil.objectKeyToString(node.declarations[0].id); | ||
}); | ||
}, | ||
/** | ||
* Look for object assignments with @polymerBehavior in the docs. | ||
*/ | ||
enterAssignmentExpression: function enterAssignmentExpression(node, parent) { | ||
_initBehavior(parent, function () { | ||
return esutil.objectKeyToString(node.left); | ||
}); | ||
}, | ||
/** | ||
* We assume that the object expression after such an assignment is the | ||
* behavior's declaration. Seems to be a decent assumption for now. | ||
*/ | ||
enterObjectExpression: function enterObjectExpression(node, parent) { | ||
if (!currentBehavior || currentBehavior.properties) return; | ||
currentBehavior.properties = currentBehavior.properties || []; | ||
currentBehavior.observers = currentBehavior.observers || []; | ||
for (var i = 0; i < node.properties.length; i++) { | ||
var prop = node.properties[i]; | ||
var name = esutil.objectKeyToString(prop.key); | ||
if (!name) { | ||
throw { | ||
message: 'Cant determine name for property key.', | ||
location: node.loc.start | ||
}; | ||
} | ||
if (name in propertyHandlers) { | ||
propertyHandlers[name](prop.value); | ||
} else { | ||
currentBehavior.properties.push(esutil.toPropertyDescriptor(prop)); | ||
} | ||
} | ||
behaviors.push(currentBehavior); | ||
currentBehavior = null; | ||
} | ||
}; | ||
return { visitors: visitors, behaviors: behaviors }; | ||
} | ||
exports.behaviorFinder = behaviorFinder; | ||
; |
@@ -10,8 +10,6 @@ /** | ||
*/ | ||
// jshint node: true | ||
'use strict'; | ||
var astValue = require('./ast-value'); | ||
var analyzeProperties = require('./analyze-properties'); | ||
var analyze_properties_1 = require('./analyze-properties'); | ||
/** | ||
@@ -26,45 +24,85 @@ * Returns an object containing functions that will annotate `declaration` with | ||
function declarationPropertyHandlers(declaration) { | ||
return { | ||
is: function(node) { | ||
if (node.type == 'Literal') { | ||
declaration.is = node.value; | ||
} | ||
}, | ||
properties: function(node) { | ||
return { | ||
is: function is(node) { | ||
if (node.type == 'Literal') { | ||
declaration.is = node.value.toString(); | ||
} | ||
}, | ||
properties: function properties(node) { | ||
var props = analyze_properties_1.analyzeProperties(node); | ||
for (var i = 0; i < props.length; i++) { | ||
declaration.properties.push(props[i]); | ||
} | ||
}, | ||
behaviors: function behaviors(node) { | ||
if (node.type != 'ArrayExpression') { | ||
return; | ||
} | ||
var arrNode = node; | ||
var _iteratorNormalCompletion = true; | ||
var _didIteratorError = false; | ||
var _iteratorError = undefined; | ||
var props = analyzeProperties(node); | ||
try { | ||
for (var _iterator = arrNode.elements[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { | ||
var element = _step.value; | ||
for (var i=0; i<props.length; i++) { | ||
declaration.properties.push(props[i]); | ||
} | ||
}, | ||
behaviors: function(node) { | ||
if (node.type != 'ArrayExpression') { | ||
return; | ||
} | ||
var v = astValue.expressionToValue(element); | ||
if (v === undefined) { | ||
v = astValue.CANT_CONVERT; | ||
} | ||
declaration.behaviors.push(v); | ||
} | ||
} catch (err) { | ||
_didIteratorError = true; | ||
_iteratorError = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion && _iterator.return) { | ||
_iterator.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError) { | ||
throw _iteratorError; | ||
} | ||
} | ||
} | ||
}, | ||
observers: function observers(node) { | ||
if (node.type != 'ArrayExpression') { | ||
return; | ||
} | ||
var arrNode = node; | ||
var _iteratorNormalCompletion2 = true; | ||
var _didIteratorError2 = false; | ||
var _iteratorError2 = undefined; | ||
for (var i=0; i<node.elements.length; i++) { | ||
var v = astValue.expressionToValue(node.elements[i]); | ||
if (v === undefined) | ||
v = astValue.CANT_CONVERT; | ||
declaration.behaviors.push(v); | ||
} | ||
}, | ||
observers: function(node) { | ||
if (node.type != 'ArrayExpression') { | ||
return; | ||
} | ||
for (var i=0; i<node.elements.length; i++) { | ||
var v = astValue.expressionToValue(node.elements[i]); | ||
if (v === undefined) | ||
v = astValue.CANT_CONVERT; | ||
declaration.observers.push({ | ||
javascriptNode: node.elements[i], | ||
expression: v | ||
}); | ||
} | ||
} | ||
}; | ||
try { | ||
for (var _iterator2 = arrNode.elements[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { | ||
var element = _step2.value; | ||
var v = astValue.expressionToValue(element); | ||
if (v === undefined) v = astValue.CANT_CONVERT; | ||
declaration.observers.push({ | ||
javascriptNode: element, | ||
expression: v | ||
}); | ||
} | ||
} catch (err) { | ||
_didIteratorError2 = true; | ||
_iteratorError2 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion2 && _iterator2.return) { | ||
_iterator2.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError2) { | ||
throw _iteratorError2; | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
} | ||
module.exports = declarationPropertyHandlers; | ||
exports.declarationPropertyHandlers = declarationPropertyHandlers; |
@@ -12,34 +12,8 @@ /** | ||
// jshint node:true | ||
var jsdoc = require('./jsdoc'); | ||
var dom5 = require('dom5'); | ||
/** Properties on element prototypes that are purely configuration. */ | ||
var ELEMENT_CONFIGURATION = [ | ||
'attached', | ||
'attributeChanged', | ||
'configure', | ||
'constructor', | ||
'created', | ||
'detached', | ||
'enableCustomStyleProperties', | ||
'extends', | ||
'hostAttributes', | ||
'is', | ||
'listeners', | ||
'mixins', | ||
'properties', | ||
'ready', | ||
'registered' | ||
]; | ||
var ELEMENT_CONFIGURATION = ['attached', 'attributeChanged', 'beforeRegister', 'configure', 'constructor', 'created', 'detached', 'enableCustomStyleProperties', 'extends', 'hostAttributes', 'is', 'listeners', 'mixins', 'properties', 'ready', 'registered']; | ||
/** Tags understood by the annotation process, to be removed during `clean`. */ | ||
var HANDLED_TAGS = [ | ||
'param', | ||
'return', | ||
'type', | ||
]; | ||
var HANDLED_TAGS = ['param', 'return', 'type']; | ||
/** | ||
@@ -58,14 +32,12 @@ * Annotates Hydrolysis descriptors, processing any `desc` properties as JSDoc. | ||
function annotate(descriptor) { | ||
if (!descriptor || descriptor.jsdoc) return descriptor; | ||
if (typeof descriptor.desc === 'string') { | ||
descriptor.jsdoc = jsdoc.parseJsdoc(descriptor.desc); | ||
// We want to present the normalized form of a descriptor. | ||
descriptor.jsdoc.orig = descriptor.desc; | ||
descriptor.desc = descriptor.jsdoc.description; | ||
} | ||
return descriptor; | ||
if (!descriptor || descriptor.jsdoc) return descriptor; | ||
if (typeof descriptor.desc === 'string') { | ||
descriptor.jsdoc = jsdoc.parseJsdoc(descriptor.desc); | ||
// We want to present the normalized form of a descriptor. | ||
descriptor.jsdoc.orig = descriptor.desc; | ||
descriptor.desc = descriptor.jsdoc.description; | ||
} | ||
return descriptor; | ||
} | ||
exports.annotate = annotate; | ||
/** | ||
@@ -75,94 +47,91 @@ * Annotates @event, @hero, & @demo tags | ||
function annotateElementHeader(descriptor) { | ||
if (descriptor.events) { | ||
descriptor.events.forEach(function(event) { | ||
_annotateEvent(event); | ||
}); | ||
descriptor.events.sort( function(a,b) { | ||
return a.name.localeCompare(b.name); | ||
}); | ||
} | ||
descriptor.demos = []; | ||
if (descriptor.jsdoc && descriptor.jsdoc.tags) { | ||
descriptor.jsdoc.tags.forEach( function(tag) { | ||
switch(tag.tag) { | ||
case 'hero': | ||
descriptor.hero = tag.name || 'hero.png'; | ||
break; | ||
case 'demo': | ||
descriptor.demos.push({ | ||
desc: tag.description || 'demo', | ||
path: tag.name || 'demo/index.html' | ||
}); | ||
} | ||
}); | ||
} | ||
if (descriptor.events) { | ||
descriptor.events.forEach(function (event) { | ||
_annotateEvent(event); | ||
}); | ||
descriptor.events.sort(function (a, b) { | ||
return a.name.localeCompare(b.name); | ||
}); | ||
} | ||
descriptor.demos = []; | ||
if (descriptor.jsdoc && descriptor.jsdoc.tags) { | ||
descriptor.jsdoc.tags.forEach(function (tag) { | ||
switch (tag.tag) { | ||
case 'hero': | ||
descriptor.hero = tag.name || 'hero.png'; | ||
break; | ||
case 'demo': | ||
descriptor.demos.push({ | ||
desc: tag.description || 'demo', | ||
path: tag.name || 'demo/index.html' | ||
}); | ||
break; | ||
} | ||
}); | ||
} | ||
} | ||
function matchByName(propa, propb) { | ||
return propa.name == propb.name; | ||
} | ||
exports.annotateElementHeader = annotateElementHeader; | ||
function copyProperties(from, to, behaviorsByName) { | ||
if (from.properties) { | ||
from.properties.forEach(function(fromProp){ | ||
for (var toProp, i = 0; i < to.properties.length; i++) { | ||
toProp = to.properties[i]; | ||
if (fromProp.name === toProp.name) { | ||
return; | ||
} | ||
} | ||
var newProp = {__fromBehavior: from.is}; | ||
if (fromProp.__fromBehavior) { | ||
if (from.properties) { | ||
from.properties.forEach(function (fromProp) { | ||
for (var toProp, i = 0; i < to.properties.length; i++) { | ||
toProp = to.properties[i]; | ||
if (fromProp.name === toProp.name) { | ||
return; | ||
} | ||
} | ||
var newProp = { __fromBehavior: from.is }; | ||
if (fromProp.__fromBehavior) { | ||
return; | ||
} | ||
Object.keys(fromProp).forEach(function (propertyField) { | ||
newProp[propertyField] = fromProp[propertyField]; | ||
}); | ||
to.properties.push(newProp); | ||
}); | ||
from.events.forEach(function (fromEvent) { | ||
for (var toEvent, i = 0; i < to.events.length; i++) { | ||
toEvent = to.events[i]; | ||
if (fromEvent.name === toEvent.name) { | ||
return; | ||
} | ||
} | ||
if (fromEvent.__fromBehavior) { | ||
return; | ||
} | ||
var newEvent = { __fromBehavior: from.is }; | ||
Object.keys(fromEvent).forEach(function (eventField) { | ||
newEvent[eventField] = fromEvent[eventField]; | ||
}); | ||
to.events.push(newEvent); | ||
}); | ||
} | ||
if (!from.behaviors) { | ||
return; | ||
} | ||
Object.keys(fromProp).forEach(function(propertyField){ | ||
newProp[propertyField] = fromProp[propertyField]; | ||
}); | ||
to.properties.push(newProp); | ||
}); | ||
from.events.forEach(function(fromEvent){ | ||
for (var toEvent, i = 0; i < to.events.length; i++) { | ||
toEvent = to.events[i]; | ||
if (fromEvent.name === toEvent.name) { | ||
return; | ||
} | ||
for (var i = from.behaviors.length - 1; i >= 0; i--) { | ||
// TODO: what's up with behaviors sometimes being a literal, and sometimes | ||
// being a descriptor object? | ||
var localBehavior = from.behaviors[i]; | ||
var definedBehavior = behaviorsByName[localBehavior] || behaviorsByName[localBehavior.symbol]; | ||
if (!definedBehavior) { | ||
console.warn("Behavior " + localBehavior + " not found when mixing " + "properties into " + to.is + "!"); | ||
return; | ||
} | ||
} | ||
if (fromEvent.__fromBehavior) { | ||
return; | ||
} | ||
var newEvent = Object.create(fromEvent); | ||
newEvent.__fromBehavior = from.is; | ||
to.events.push(newEvent); | ||
}); | ||
} | ||
if (!from.behaviors) { | ||
return; | ||
} | ||
for (var i = from.behaviors.length - 1; i >= 0; i--) { | ||
var behavior = from.behaviors[i]; | ||
var definedBehavior = behaviorsByName[behavior] || behaviorsByName[behavior.symbol]; | ||
if (!definedBehavior) { | ||
console.warn("Behavior " + behavior + " not found when mixing " + | ||
"properties into " + to.is + "!"); | ||
return; | ||
copyProperties(definedBehavior, to, behaviorsByName); | ||
} | ||
copyProperties(definedBehavior, to, behaviorsByName); | ||
} | ||
} | ||
function mixinBehaviors(descriptor, behaviorsByName) { | ||
if (descriptor.behaviors) { | ||
for (var i = descriptor.behaviors.length - 1; i >= 0; i--) { | ||
var behavior = descriptor.behaviors[i]; | ||
if (!behaviorsByName[behavior]) { | ||
console.warn("Behavior " + behavior + " not found when mixing " + | ||
"properties into " + descriptor.is + "!"); | ||
break; | ||
} | ||
var definedBehavior = behaviorsByName[behavior]; | ||
copyProperties(definedBehavior, descriptor, behaviorsByName); | ||
if (descriptor.behaviors) { | ||
for (var i = descriptor.behaviors.length - 1; i >= 0; i--) { | ||
var behavior = descriptor.behaviors[i]; | ||
if (!behaviorsByName[behavior]) { | ||
console.warn("Behavior " + behavior + " not found when mixing " + "properties into " + descriptor.is + "!"); | ||
break; | ||
} | ||
var definedBehavior = behaviorsByName[behavior]; | ||
copyProperties(definedBehavior, descriptor, behaviorsByName); | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
@@ -179,44 +148,35 @@ * Annotates documentation found within a Hydrolysis element descriptor. Also | ||
function annotateElement(descriptor, behaviorsByName) { | ||
if (!descriptor.desc && descriptor.type === 'element') { | ||
descriptor.desc = _findElementDocs(descriptor.is, | ||
descriptor.domModule, | ||
descriptor.scriptElement); | ||
} | ||
annotate(descriptor); | ||
// The `<dom-module>` is too low level for most needs, and it is _not_ | ||
// serializable. So we drop it now that we've extracted all the useful bits | ||
// from it. | ||
// TODO: Don't worry about serializability here, provide an API to get JSON. | ||
delete descriptor.domModule; | ||
mixinBehaviors(descriptor, behaviorsByName); | ||
// Descriptors that should have their `desc` properties parsed as JSDoc. | ||
descriptor.properties.forEach(function(property) { | ||
// Feature properties are special, configuration is really just a matter of | ||
// inheritance... | ||
annotateProperty(property, descriptor.abstract); | ||
}); | ||
// It may seem like overkill to always sort, but we have an assumption that | ||
// these properties are typically being consumed by user-visible tooling. | ||
// As such, it's good to have consistent output/ordering to aid the user. | ||
descriptor.properties.sort(function(a, b) { | ||
// Private properties are always last. | ||
if (a.private && !b.private) { | ||
return 1; | ||
} else if (!a.private && b.private) { | ||
return -1; | ||
// Otherwise, we're just sorting alphabetically. | ||
} else { | ||
return a.name.localeCompare(b.name); | ||
if (!descriptor.desc && descriptor.type === 'element') { | ||
descriptor.desc = _findElementDocs(descriptor.is, descriptor.domModule, descriptor.scriptElement); | ||
} | ||
}); | ||
annotateElementHeader(descriptor); | ||
return descriptor; | ||
annotate(descriptor); | ||
// The `<dom-module>` is too low level for most needs, and it is _not_ | ||
// serializable. So we drop it now that we've extracted all the useful bits | ||
// from it. | ||
// TODO: Don't worry about serializability here, provide an API to get JSON. | ||
delete descriptor.domModule; | ||
mixinBehaviors(descriptor, behaviorsByName); | ||
// Descriptors that should have their `desc` properties parsed as JSDoc. | ||
descriptor.properties.forEach(function (property) { | ||
// Feature properties are special, configuration is really just a matter of | ||
// inheritance... | ||
annotateProperty(property, descriptor.abstract); | ||
}); | ||
// It may seem like overkill to always sort, but we have an assumption that | ||
// these properties are typically being consumed by user-visible tooling. | ||
// As such, it's good to have consistent output/ordering to aid the user. | ||
descriptor.properties.sort(function (a, b) { | ||
// Private properties are always last. | ||
if (a.private && !b.private) { | ||
return 1; | ||
} else if (!a.private && b.private) { | ||
return -1; | ||
} else { | ||
return a.name.localeCompare(b.name); | ||
} | ||
}); | ||
annotateElementHeader(descriptor); | ||
return descriptor; | ||
} | ||
exports.annotateElement = annotateElement; | ||
/** | ||
@@ -227,9 +187,8 @@ * Annotates behavior descriptor. | ||
*/ | ||
function annotateBehavior(descriptor, behaviorsByName) { | ||
annotate(descriptor); | ||
annotateElementHeader(descriptor); | ||
return descriptor; | ||
function annotateBehavior(descriptor) { | ||
annotate(descriptor); | ||
annotateElementHeader(descriptor); | ||
return descriptor; | ||
} | ||
exports.annotateBehavior = annotateBehavior; | ||
/** | ||
@@ -239,23 +198,19 @@ * Annotates event documentation | ||
function _annotateEvent(descriptor) { | ||
annotate(descriptor); | ||
// process @event | ||
var eventTag = jsdoc.getTag(descriptor.jsdoc, 'event'); | ||
descriptor.name = eventTag ? eventTag.description : "N/A"; | ||
// process @params | ||
descriptor.params = (descriptor.jsdoc.tags || []) | ||
.filter( function(tag) { | ||
return tag.tag === 'param'; | ||
}) | ||
.map( function(tag) { | ||
return { | ||
type: tag.type || "N/A", | ||
desc: tag.description, | ||
name: tag.name || "N/A" | ||
}; | ||
annotate(descriptor); | ||
// process @event | ||
var eventTag = jsdoc.getTag(descriptor.jsdoc, 'event'); | ||
descriptor.name = eventTag ? eventTag.description : "N/A"; | ||
// process @params | ||
descriptor.params = (descriptor.jsdoc.tags || []).filter(function (tag) { | ||
return tag.tag === 'param'; | ||
}).map(function (tag) { | ||
return { | ||
type: tag.type || "N/A", | ||
desc: tag.description, | ||
name: tag.name || "N/A" | ||
}; | ||
}); | ||
// process @params | ||
return descriptor; | ||
// process @params | ||
return descriptor; | ||
} | ||
/** | ||
@@ -269,59 +224,48 @@ * Annotates documentation found about a Hydrolysis property descriptor. | ||
function annotateProperty(descriptor, ignoreConfiguration) { | ||
annotate(descriptor); | ||
if (descriptor.name[0] === '_' || jsdoc.hasTag(descriptor.jsdoc, 'private')) { | ||
descriptor.private = true; | ||
} | ||
if (!ignoreConfiguration && ELEMENT_CONFIGURATION.indexOf(descriptor.name) !== -1) { | ||
descriptor.private = true; | ||
descriptor.configuration = true; | ||
} | ||
// @type JSDoc wins | ||
descriptor.type = jsdoc.getTag(descriptor.jsdoc, 'type', 'type') || descriptor.type; | ||
if (descriptor.type.match(/^function/i)) { | ||
_annotateFunctionProperty(descriptor); | ||
} | ||
// @default JSDoc wins | ||
var defaultTag = jsdoc.getTag(descriptor.jsdoc, 'default'); | ||
if (defaultTag !== null) { | ||
var newDefault = (defaultTag.name || '') + (defaultTag.description || ''); | ||
if (newDefault !== '') { | ||
descriptor.default = newDefault; | ||
annotate(descriptor); | ||
if (descriptor.name[0] === '_' || jsdoc.hasTag(descriptor.jsdoc, 'private')) { | ||
descriptor.private = true; | ||
} | ||
} | ||
return descriptor; | ||
if (!ignoreConfiguration && ELEMENT_CONFIGURATION.indexOf(descriptor.name) !== -1) { | ||
descriptor.private = true; | ||
descriptor.configuration = true; | ||
} | ||
// @type JSDoc wins | ||
descriptor.type = jsdoc.getTag(descriptor.jsdoc, 'type', 'type') || descriptor.type; | ||
if (descriptor.type.match(/^function/i)) { | ||
_annotateFunctionProperty(descriptor); | ||
} | ||
// @default JSDoc wins | ||
var defaultTag = jsdoc.getTag(descriptor.jsdoc, 'default'); | ||
if (defaultTag !== null) { | ||
var newDefault = (defaultTag.name || '') + (defaultTag.description || ''); | ||
if (newDefault !== '') { | ||
descriptor.default = newDefault; | ||
} | ||
} | ||
return descriptor; | ||
} | ||
/** @param {Object} descriptor */ | ||
function _annotateFunctionProperty(descriptor) { | ||
descriptor.function = true; | ||
var returnTag = jsdoc.getTag(descriptor.jsdoc, 'return'); | ||
if (returnTag) { | ||
descriptor.return = { | ||
type: returnTag.type, | ||
desc: returnTag.description, | ||
}; | ||
} | ||
var paramsByName = {}; | ||
(descriptor.params || []).forEach(function(param) { | ||
paramsByName[param.name] = param; | ||
}); | ||
(descriptor.jsdoc && descriptor.jsdoc.tags || []).forEach(function(tag) { | ||
if (tag.tag !== 'param') return; | ||
var param = paramsByName[tag.name]; | ||
if (!param) { | ||
return; | ||
descriptor.function = true; | ||
var returnTag = jsdoc.getTag(descriptor.jsdoc, 'return'); | ||
if (returnTag) { | ||
descriptor.return = { | ||
type: returnTag.type, | ||
desc: returnTag.description | ||
}; | ||
} | ||
param.type = tag.type || param.type; | ||
param.desc = tag.description; | ||
}); | ||
var paramsByName = {}; | ||
(descriptor.params || []).forEach(function (param) { | ||
paramsByName[param.name] = param; | ||
}); | ||
(descriptor.jsdoc && descriptor.jsdoc.tags || []).forEach(function (tag) { | ||
if (tag.tag !== 'param') return; | ||
var param = paramsByName[tag.name]; | ||
if (!param) { | ||
return; | ||
} | ||
param.type = tag.type || param.type; | ||
param.desc = tag.description; | ||
}); | ||
} | ||
/** | ||
@@ -337,21 +281,14 @@ * Converts raw features into an abstract `Polymer.Base` element. | ||
function featureElement(features) { | ||
var properties = features.reduce(function(result, feature) { | ||
return result.concat(feature.properties); | ||
}, []); | ||
return { | ||
type: 'element', | ||
is: 'Polymer.Base', | ||
abstract: true, | ||
properties: properties, | ||
desc: '`Polymer.Base` acts as a base prototype for all Polymer ' + | ||
'elements. It is composed via various calls to ' + | ||
'`Polymer.Base._addFeature()`.\n' + | ||
'\n' + | ||
'The properties reflected here are the combined view of all ' + | ||
'features found in this library. There may be more properties ' + | ||
'added via other libraries, as well.', | ||
}; | ||
var properties = features.reduce(function (result, feature) { | ||
return result.concat(feature.properties); | ||
}, []); | ||
return { | ||
type: 'element', | ||
is: 'Polymer.Base', | ||
abstract: true, | ||
properties: properties, | ||
desc: '`Polymer.Base` acts as a base prototype for all Polymer ' + 'elements. It is composed via various calls to ' + '`Polymer.Base._addFeature()`.\n' + '\n' + 'The properties reflected here are the combined view of all ' + 'features found in this library. There may be more properties ' + 'added via other libraries, as well.' | ||
}; | ||
} | ||
exports.featureElement = featureElement; | ||
/** | ||
@@ -364,22 +301,20 @@ * Cleans redundant properties from a descriptor, assuming that you have already | ||
function clean(descriptor) { | ||
if (!descriptor.jsdoc) return; | ||
// The doctext was written to `descriptor.desc` | ||
delete descriptor.jsdoc.description; | ||
delete descriptor.jsdoc.orig; | ||
var cleanTags = []; | ||
(descriptor.jsdoc.tags || []).forEach(function(tag) { | ||
// Drop any tags we've consumed. | ||
if (HANDLED_TAGS.indexOf(tag.tag) !== -1) return; | ||
cleanTags.push(tag); | ||
}); | ||
if (cleanTags.length === 0) { | ||
// No tags? no docs left! | ||
delete descriptor.jsdoc; | ||
} else { | ||
descriptor.jsdoc.tags = cleanTags; | ||
} | ||
if (!descriptor.jsdoc) return; | ||
// The doctext was written to `descriptor.desc` | ||
delete descriptor.jsdoc.description; | ||
delete descriptor.jsdoc.orig; | ||
var cleanTags = []; | ||
(descriptor.jsdoc.tags || []).forEach(function (tag) { | ||
// Drop any tags we've consumed. | ||
if (HANDLED_TAGS.indexOf(tag.tag) !== -1) return; | ||
cleanTags.push(tag); | ||
}); | ||
if (cleanTags.length === 0) { | ||
// No tags? no docs left! | ||
delete descriptor.jsdoc; | ||
} else { | ||
descriptor.jsdoc.tags = cleanTags; | ||
} | ||
} | ||
exports.clean = clean; | ||
/** | ||
@@ -392,6 +327,6 @@ * Cleans redundant properties from an element, assuming that you have already | ||
function cleanElement(element) { | ||
clean(element); | ||
element.properties.forEach(cleanProperty); | ||
clean(element); | ||
element.properties.forEach(cleanProperty); | ||
} | ||
exports.cleanElement = cleanElement; | ||
/** | ||
@@ -404,5 +339,4 @@ * Cleans redundant properties from a property, assuming that you have already | ||
function cleanProperty(property) { | ||
clean(property); | ||
clean(property); | ||
} | ||
/** | ||
@@ -414,84 +348,70 @@ * Parse elements defined only in comments. | ||
function parsePseudoElements(comments) { | ||
var elements = []; | ||
comments.forEach(function(comment) { | ||
var parsed = jsdoc.parseJsdoc(comment); | ||
var pseudoTag = jsdoc.getTag(parsed, 'pseudoElement', 'name'); | ||
if (pseudoTag) { | ||
parsed.is = pseudoTag; | ||
parsed.jsdoc = {description: parsed.description, tags: parsed.tags}; | ||
parsed.properties = []; | ||
parsed.desc = parsed.description; | ||
parsed.description = undefined; | ||
parsed.tags = undefined; | ||
annotateElementHeader(parsed); | ||
elements.push(parsed); | ||
} | ||
}); | ||
return elements; | ||
var elements = []; | ||
comments.forEach(function (comment) { | ||
var parsedJsdoc = jsdoc.parseJsdoc(comment); | ||
var pseudoTag = jsdoc.getTag(parsedJsdoc, 'pseudoElement', 'name'); | ||
if (pseudoTag) { | ||
var element = { | ||
is: pseudoTag, | ||
type: 'element', | ||
jsdoc: { description: parsedJsdoc.description, tags: parsedJsdoc.tags }, | ||
properties: [], | ||
desc: parsedJsdoc.description | ||
}; | ||
annotateElementHeader(element); | ||
elements.push(element); | ||
} | ||
}); | ||
return elements; | ||
} | ||
exports.parsePseudoElements = parsePseudoElements; | ||
/** | ||
* @param {string} elementId | ||
* @param {DocumentAST} domModule | ||
* @param {DocumentAST} scriptElement The script that the element was defined in. | ||
* @param {DocumentAST} scriptElement The script that the element was | ||
* defined in. | ||
*/ | ||
function _findElementDocs(elementId, domModule, scriptElement) { | ||
// Note that we concatenate docs from all sources if we find them. | ||
// element can be defined in: | ||
// html comment right before dom-module | ||
// html commnet right before script defining the module, if dom-module is empty | ||
var found = []; | ||
// Do we have a HTML comment on the `<dom-module>` or `<script>`? | ||
// | ||
// Confusingly, with our current style, the comment will be attached to | ||
// `<head>`, rather than being a sibling to the `<dom-module>` | ||
var searchRoot = domModule || scriptElement; | ||
var parents = dom5.nodeWalkAllPrior(searchRoot, dom5.isCommentNode); | ||
var comment = parents.length > 0 ? parents[0] : null; | ||
if (comment && comment.data) { | ||
found.push(comment.data); | ||
} | ||
if (found.length === 0) return null; | ||
return found | ||
.filter(function(comment) { | ||
// skip @license comments | ||
if (comment && comment.indexOf('@license' === -1)) { | ||
return true; | ||
} | ||
else { | ||
return false; | ||
} | ||
}) | ||
.map(jsdoc.unindent).join('\n'); | ||
// Note that we concatenate docs from all sources if we find them. | ||
// element can be defined in: | ||
// html comment right before dom-module | ||
// html commnet right before script defining the module, | ||
// if dom-module is empty | ||
var found = []; | ||
// Do we have a HTML comment on the `<dom-module>` or `<script>`? | ||
// | ||
// Confusingly, with our current style, the comment will be attached to | ||
// `<head>`, rather than being a sibling to the `<dom-module>` | ||
var searchRoot = domModule || scriptElement; | ||
var parents = dom5.nodeWalkAllPrior(searchRoot, dom5.isCommentNode); | ||
var comment = parents.length > 0 ? parents[0] : null; | ||
if (comment && comment.data) { | ||
found.push(comment.data); | ||
} | ||
if (found.length === 0) return null; | ||
return found.filter(function (comment) { | ||
// skip @license comments | ||
if (comment && comment.indexOf('@license') === -1) { | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
}).map(jsdoc.unindent).join('\n'); | ||
} | ||
function _findLastChildNamed(name, parent) { | ||
var children = parent.childNodes; | ||
for (var i = children.length - 1, child; i >= 0; i--) { | ||
child = children[i]; | ||
if (child.nodeName === name) return child; | ||
} | ||
return null; | ||
var children = parent.childNodes; | ||
for (var i = children.length - 1; i >= 0; i--) { | ||
var child = children[i]; | ||
if (child.nodeName === name) return child; | ||
} | ||
return null; | ||
} | ||
// TODO(nevir): parse5-utils! | ||
function _getNodeAttribute(node, name) { | ||
for (var i = 0, attr; i < node.attrs.length; i++) { | ||
attr = node.attrs[i]; | ||
if (attr.name === name) { | ||
return attr.value; | ||
for (var i = 0; i < node.attrs.length; i++) { | ||
var attr = node.attrs[i]; | ||
if (attr.name === name) { | ||
return attr.value; | ||
} | ||
} | ||
} | ||
} | ||
module.exports = { | ||
annotate: annotate, | ||
annotateElement: annotateElement, | ||
annotateBehavior: annotateBehavior, | ||
clean: clean, | ||
cleanElement: cleanElement, | ||
featureElement: featureElement, | ||
parsePseudoElements: parsePseudoElements | ||
}; | ||
} |
@@ -10,107 +10,182 @@ /** | ||
*/ | ||
// jshint node: true | ||
'use strict'; | ||
var estraverse = require('estraverse'); | ||
var esutil = require('./esutil'); | ||
var findAlias = require('./find-alias'); | ||
var analyzeProperties = require('./analyze-properties'); | ||
var astValue = require('./ast-value'); | ||
var declarationPropertyHandlers = require('./declaration-property-handlers'); | ||
var elementFinder = function elementFinder() { | ||
/** | ||
* The list of elements exported by each traversed script. | ||
*/ | ||
var elements = []; | ||
/** | ||
* The element being built during a traversal; | ||
*/ | ||
var element = null; | ||
var propertyHandlers = null; | ||
var visitors = { | ||
enterCallExpression: function enterCallExpression(node, parent) { | ||
var callee = node.callee; | ||
if (callee.type == 'Identifier') { | ||
if (callee.name == 'Polymer') { | ||
element = { | ||
type: 'element', | ||
desc: esutil.getAttachedComment(parent), | ||
events: esutil.getEventComments(parent).map( function(event) { | ||
return {desc: event}; | ||
}) | ||
}; | ||
propertyHandlers = declarationPropertyHandlers(element); | ||
} | ||
} | ||
}, | ||
leaveCallExpression: function leaveCallExpression(node, parent) { | ||
var callee = node.callee; | ||
if (callee.type == 'Identifier') { | ||
if (callee.name == 'Polymer') { | ||
if (element) { | ||
elements.push(element); | ||
element = null; | ||
propertyHandlers = null; | ||
} | ||
} | ||
} | ||
}, | ||
enterObjectExpression: function enterObjectExpression(node, parent) { | ||
if (element && !element.properties) { | ||
element.properties = []; | ||
element.behaviors = []; | ||
element.observers = []; | ||
var getters = {}; | ||
var setters = {}; | ||
var definedProperties = {}; | ||
for (var i = 0; i < node.properties.length; i++) { | ||
var prop = node.properties[i]; | ||
var name = esutil.objectKeyToString(prop.key); | ||
if (!name) { | ||
throw { | ||
message: 'Cant determine name for property key.', | ||
location: node.loc.start | ||
var esutil = require('./esutil'); | ||
var declaration_property_handlers_1 = require('./declaration-property-handlers'); | ||
var docs = require('./docs'); | ||
function elementFinder() { | ||
/** | ||
* The list of elements exported by each traversed script. | ||
*/ | ||
var elements = []; | ||
/** | ||
* The element being built during a traversal; | ||
*/ | ||
var element = null; | ||
var propertyHandlers = null; | ||
var visitors = { | ||
classDetected: false, | ||
enterClassDeclaration: function enterClassDeclaration(node, parent) { | ||
this.classDetected = true; | ||
element = { | ||
type: 'element', | ||
desc: esutil.getAttachedComment(node), | ||
events: esutil.getEventComments(node).map(function (event) { | ||
return { desc: event }; | ||
}), | ||
properties: [], | ||
behaviors: [], | ||
observers: [] | ||
}; | ||
} | ||
if (name in propertyHandlers) { | ||
propertyHandlers[name](prop.value); | ||
continue; | ||
} | ||
var descriptor = esutil.toPropertyDescriptor(prop); | ||
if (descriptor.getter) { | ||
getters[descriptor.name] = descriptor; | ||
} else if (descriptor.setter) { | ||
setters[descriptor.name] = descriptor; | ||
} else { | ||
element.properties.push(esutil.toPropertyDescriptor(prop)); | ||
} | ||
propertyHandlers = declaration_property_handlers_1.declarationPropertyHandlers(element); | ||
}, | ||
leaveClassDeclaration: function leaveClassDeclaration(node, parent) { | ||
element.properties.map(function (property) { | ||
return docs.annotate(property); | ||
}); | ||
if (element) { | ||
elements.push(element); | ||
element = null; | ||
propertyHandlers = null; | ||
} | ||
this.classDetected = false; | ||
}, | ||
enterAssignmentExpression: function enterAssignmentExpression(node, parent) { | ||
if (!element) { | ||
return; | ||
} | ||
var left = node.left; | ||
if (left && left.object && left.object.type !== 'ThisExpression') { | ||
return; | ||
} | ||
var prop = left.property; | ||
if (prop && prop.name) { | ||
var name = prop.name; | ||
if (name in propertyHandlers) { | ||
propertyHandlers[name](node.right); | ||
} | ||
} | ||
}, | ||
enterMethodDefinition: function enterMethodDefinition(node, parent) { | ||
if (!element) { | ||
return; | ||
} | ||
var prop = { | ||
key: node.key, | ||
value: node.value, | ||
kind: node.kind, | ||
method: true, | ||
leadingComments: node.leadingComments, | ||
shorthand: false, | ||
computed: false, | ||
type: 'Property' | ||
}; | ||
var propDesc = docs.annotate(esutil.toPropertyDescriptor(prop)); | ||
if (prop && prop.kind === 'get' && (propDesc.name === 'behaviors' || propDesc.name === 'observers')) { | ||
var returnStatement = node.value.body.body[0]; | ||
var argument = returnStatement.argument; | ||
if (propDesc.name === 'behaviors') { | ||
argument.elements.forEach(function (elementObject) { | ||
element.behaviors.push(elementObject.name); | ||
}); | ||
} else { | ||
argument.elements.forEach(function (elementObject) { | ||
element.observers.push({ javascriptNode: elementObject, expression: elementObject.raw }); | ||
}); | ||
} | ||
} else { | ||
element.properties.push(propDesc); | ||
} | ||
}, | ||
enterCallExpression: function enterCallExpression(node, parent) { | ||
// When dealing with a class, enterCallExpression is called after the parsing actually starts | ||
if (this.classDetected) { | ||
return estraverse.VisitorOption.Skip; | ||
} | ||
var callee = node.callee; | ||
if (callee.type == 'Identifier') { | ||
var ident = callee; | ||
if (ident.name == 'Polymer') { | ||
element = { | ||
type: 'element', | ||
desc: esutil.getAttachedComment(parent), | ||
events: esutil.getEventComments(parent).map(function (event) { | ||
return { desc: event }; | ||
}) | ||
}; | ||
propertyHandlers = declaration_property_handlers_1.declarationPropertyHandlers(element); | ||
} | ||
} | ||
}, | ||
leaveCallExpression: function leaveCallExpression(node, parent) { | ||
var callee = node.callee; | ||
if (callee.type == 'Identifier') { | ||
var ident = callee; | ||
if (ident.name == 'Polymer') { | ||
if (element) { | ||
elements.push(element); | ||
element = null; | ||
propertyHandlers = null; | ||
} | ||
} | ||
} | ||
}, | ||
enterObjectExpression: function enterObjectExpression(node, parent) { | ||
// When dealing with a class, there is no single object that we can parse to retrieve all properties | ||
if (this.classDetected) { | ||
return estraverse.VisitorOption.Skip; | ||
} | ||
if (element && !element.properties) { | ||
element.properties = []; | ||
element.behaviors = []; | ||
element.observers = []; | ||
var getters = {}; | ||
var setters = {}; | ||
var definedProperties = {}; | ||
for (var i = 0; i < node.properties.length; i++) { | ||
var prop = node.properties[i]; | ||
var name = esutil.objectKeyToString(prop.key); | ||
if (!name) { | ||
throw { | ||
message: 'Cant determine name for property key.', | ||
location: node.loc.start | ||
}; | ||
} | ||
if (name in propertyHandlers) { | ||
propertyHandlers[name](prop.value); | ||
continue; | ||
} | ||
var descriptor = esutil.toPropertyDescriptor(prop); | ||
if (descriptor.getter) { | ||
getters[descriptor.name] = descriptor; | ||
} else if (descriptor.setter) { | ||
setters[descriptor.name] = descriptor; | ||
} else { | ||
element.properties.push(esutil.toPropertyDescriptor(prop)); | ||
} | ||
} | ||
Object.keys(getters).forEach(function (getter) { | ||
var get = getters[getter]; | ||
definedProperties[get.name] = get; | ||
}); | ||
Object.keys(setters).forEach(function (setter) { | ||
var set = setters[setter]; | ||
if (!(set.name in definedProperties)) { | ||
definedProperties[set.name] = set; | ||
} else { | ||
definedProperties[set.name].setter = true; | ||
} | ||
}); | ||
Object.keys(definedProperties).forEach(function (p) { | ||
var prop = definedProperties[p]; | ||
element.properties.push(prop); | ||
}); | ||
return estraverse.VisitorOption.Skip; | ||
} | ||
} | ||
Object.keys(getters).forEach(function(getter) { | ||
var get = getters[getter]; | ||
definedProperties[get.name] = get; | ||
}); | ||
Object.keys(setters).forEach(function(setter) { | ||
var set = setters[setter]; | ||
if (!(set.name in definedProperties)) { | ||
definedProperties[set.name] = set; | ||
} else { | ||
definedProperties[set.name].setter = true; | ||
} | ||
}); | ||
Object.keys(definedProperties).forEach(function(p){ | ||
var prop = definedProperties[p]; | ||
element.properties.push(p); | ||
}); | ||
return estraverse.VisitorOption.Skip; | ||
} | ||
} | ||
}; | ||
return {visitors: visitors, elements: elements}; | ||
}; | ||
module.exports = elementFinder; | ||
}; | ||
return { visitors: visitors, elements: elements }; | ||
} | ||
exports.elementFinder = elementFinder; | ||
; |
@@ -10,6 +10,8 @@ /** | ||
*/ | ||
// jshint node: true | ||
'use strict'; | ||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; | ||
var estraverse = require("estraverse"); | ||
var escodegen = require('escodegen'); | ||
/** | ||
@@ -23,23 +25,25 @@ * Returns whether an Espree node matches a particular object path. | ||
* | ||
* @param {Node} expression The Espree node to match against. | ||
* @param {ESTree.Node} expression The Espree node to match against. | ||
* @param {Array<string>} path The path to look for. | ||
*/ | ||
function matchesCallExpression(expression, path) { | ||
if (!expression.property || !expression.object) return; | ||
console.assert(path.length >= 2); | ||
// Unravel backwards, make sure properties match each step of the way. | ||
if (expression.property.name !== path[path.length - 1]) return false; | ||
// We've got ourselves a final member expression. | ||
if (path.length == 2 && expression.object.type === 'Identifier') { | ||
return expression.object.name === path[0]; | ||
} | ||
// Nested expressions. | ||
if (path.length > 2 && expression.object.type == 'MemberExpression') { | ||
return matchesCallExpression(expression.object, path.slice(0, path.length - 1)); | ||
} | ||
return false; | ||
if (!expression.property || !expression.object) return; | ||
console.assert(path.length >= 2); | ||
if (expression.property.type !== 'Identifier') { | ||
return; | ||
} | ||
var property = expression.property; | ||
// Unravel backwards, make sure properties match each step of the way. | ||
if (property.name !== path[path.length - 1]) return false; | ||
// We've got ourselves a final member expression. | ||
if (path.length == 2 && expression.object.type === 'Identifier') { | ||
return expression.object.name === path[0]; | ||
} | ||
// Nested expressions. | ||
if (path.length > 2 && expression.object.type == 'MemberExpression') { | ||
return matchesCallExpression(expression.object, path.slice(0, path.length - 1)); | ||
} | ||
return false; | ||
} | ||
exports.matchesCallExpression = matchesCallExpression; | ||
/** | ||
@@ -50,19 +54,19 @@ * @param {Node} key The node representing an object key or expression. | ||
function objectKeyToString(key) { | ||
if (key.type == 'Identifier') { | ||
return key.name; | ||
} | ||
if (key.type == 'Literal') { | ||
return key.value; | ||
} | ||
if (key.type == 'MemberExpression') { | ||
return objectKeyToString(key.object) + '.' + objectKeyToString(key.property); | ||
} | ||
if (key.type == 'Identifier') { | ||
return key.name; | ||
} | ||
if (key.type == 'Literal') { | ||
return key.value.toString(); | ||
} | ||
if (key.type == 'MemberExpression') { | ||
var mEx = key; | ||
return objectKeyToString(mEx.object) + '.' + objectKeyToString(mEx.property); | ||
} | ||
} | ||
exports.objectKeyToString = objectKeyToString; | ||
var CLOSURE_CONSTRUCTOR_MAP = { | ||
'Boolean': 'boolean', | ||
'Number': 'number', | ||
'String': 'string', | ||
'Boolean': 'boolean', | ||
'Number': 'number', | ||
'String': 'string' | ||
}; | ||
/** | ||
@@ -77,106 +81,86 @@ * AST expression -> Closure type. | ||
function closureType(node) { | ||
if (node.type.match(/Expression$/)) { | ||
return node.type.substr(0, node.type.length - 10); | ||
} else if (node.type === 'Literal') { | ||
return typeof node.value; | ||
} else if (node.type === 'Identifier') { | ||
return CLOSURE_CONSTRUCTOR_MAP[node.name] || node.name; | ||
} else { | ||
throw { | ||
message: 'Unknown Closure type for node: ' + node.type, | ||
location: node.loc.start, | ||
}; | ||
} | ||
if (node.type.match(/Expression$/)) { | ||
return node.type.substr(0, node.type.length - 10); | ||
} else if (node.type === 'Literal') { | ||
return _typeof(node.value); | ||
} else if (node.type === 'Identifier') { | ||
var ident = node; | ||
return CLOSURE_CONSTRUCTOR_MAP[ident.name] || ident.name; | ||
} else { | ||
throw { | ||
message: 'Unknown Closure type for node: ' + node.type, | ||
location: node.loc.start | ||
}; | ||
} | ||
} | ||
/** | ||
* @param {Node} node | ||
* @return {?string} | ||
*/ | ||
exports.closureType = closureType; | ||
function getAttachedComment(node) { | ||
var comments = getLeadingComments(node) || getLeadingComments(node.key); | ||
if (!comments) { | ||
return; | ||
} | ||
return comments[comments.length - 1]; | ||
var comments = getLeadingComments(node) || getLeadingComments(node['key']); | ||
if (!comments) { | ||
return; | ||
} | ||
return comments[comments.length - 1]; | ||
} | ||
exports.getAttachedComment = getAttachedComment; | ||
/** | ||
* Returns all comments from a tree defined with @event. | ||
* @param {Node} node [description] | ||
* @return {[type]} [description] | ||
*/ | ||
function getEventComments(node) { | ||
var eventComments = []; | ||
estraverse.traverse(node, { | ||
enter: function (node) { | ||
var comments = (node.leadingComments || []).concat(node.trailingComments || []) | ||
.map( function(commentAST) { | ||
return commentAST.value; | ||
}) | ||
.filter( function(comment) { | ||
return comment.indexOf("@event") != -1; | ||
}); | ||
eventComments = eventComments.concat(comments); | ||
} | ||
}); | ||
// dedup | ||
return eventComments.filter( function(el, index, array) { | ||
return array.indexOf(el) === index; | ||
}); | ||
var eventComments = []; | ||
estraverse.traverse(node, { | ||
enter: function enter(node) { | ||
var comments = (node.leadingComments || []).concat(node.trailingComments || []).map(function (commentAST) { | ||
return commentAST.value; | ||
}).filter(function (comment) { | ||
return comment.indexOf("@event") != -1; | ||
}); | ||
eventComments = eventComments.concat(comments); | ||
}, | ||
keys: { | ||
Super: [] | ||
} | ||
}); | ||
// dedup | ||
return eventComments.filter(function (el, index, array) { | ||
return array.indexOf(el) === index; | ||
}); | ||
} | ||
/** | ||
* @param {Node} node | ||
* @param | ||
* @return {Array.<string>} | ||
*/ | ||
exports.getEventComments = getEventComments; | ||
function getLeadingComments(node) { | ||
if (!node) { | ||
return; | ||
} | ||
var comments = node.leadingComments; | ||
if (!comments || comments.length === 0) return; | ||
return comments.map(function(comment) { | ||
return comment.value; | ||
}); | ||
if (!node) { | ||
return; | ||
} | ||
var comments = node.leadingComments; | ||
if (!comments || comments.length === 0) return; | ||
return comments.map(function (comment) { | ||
return comment.value; | ||
}); | ||
} | ||
/** | ||
* Converts a parse5 Property AST node into its Hydrolysis representation. | ||
* | ||
* @param {Node} node | ||
* @return {PropertyDescriptor} | ||
* Converts a estree Property AST node into its Hydrolysis representation. | ||
*/ | ||
function toPropertyDescriptor(node) { | ||
var type = closureType(node.value); | ||
if (type == "Function") { | ||
if (node.kind === "get" || node.kind === "set") { | ||
type = ''; | ||
node[node.kind+"ter"] = true; | ||
var type = closureType(node.value); | ||
if (type == "Function") { | ||
if (node.kind === "get" || node.kind === "set") { | ||
type = ''; | ||
node[node.kind + "ter"] = true; | ||
} | ||
} | ||
} | ||
var result = { | ||
name: objectKeyToString(node.key), | ||
type: type, | ||
desc: getAttachedComment(node), | ||
javascriptNode: node | ||
}; | ||
if (type === 'Function') { | ||
result.params = (node.value.params || []).map(function(param) { | ||
return {name: param.name}; | ||
}); | ||
} | ||
return result; | ||
var result = { | ||
name: objectKeyToString(node.key), | ||
type: type, | ||
desc: getAttachedComment(node), | ||
javascriptNode: node | ||
}; | ||
if (type === 'Function') { | ||
var value = node.value; | ||
result.params = (value.params || []).map(function (param) { | ||
// With ES6 we can have a variety of param patterns. Best to leave the | ||
// formatting to escodegen. | ||
return { name: escodegen.generate(param) }; | ||
}); | ||
} | ||
return result; | ||
} | ||
module.exports = { | ||
closureType: closureType, | ||
getAttachedComment: getAttachedComment, | ||
getEventComments: getEventComments, | ||
matchesCallExpression: matchesCallExpression, | ||
objectKeyToString: objectKeyToString, | ||
toPropertyDescriptor: toPropertyDescriptor, | ||
}; | ||
exports.toPropertyDescriptor = toPropertyDescriptor; |
@@ -10,48 +10,38 @@ /** | ||
*/ | ||
// jshint node: true | ||
'use strict'; | ||
var estraverse = require('estraverse'); | ||
var esutil = require('./esutil'); | ||
var numFeatures = 0; | ||
module.exports = function featureFinder() { | ||
/** @type {!Array<FeatureDescriptor>} The features we've found. */ | ||
var features = []; | ||
var visitors = { | ||
enterCallExpression: function enterCallExpression(node, parent) { | ||
if (!esutil.matchesCallExpression(node.callee, ['Polymer', 'Base', '_addFeature'])) { | ||
return; | ||
} | ||
/** @type {!FeatureDescriptor} */ | ||
var feature = {}; | ||
this._extractDesc(feature, node, parent); | ||
this._extractProperties(feature, node, parent); | ||
features.push(feature); | ||
}, | ||
_extractDesc: function _extractDesc(feature, node, parent) { | ||
feature.desc = esutil.getAttachedComment(parent); | ||
}, | ||
_extractProperties: function _extractProperties(feature, node, parent) { | ||
var featureNode = node.arguments[0]; | ||
if (featureNode.type !== 'ObjectExpression') { | ||
console.warn( | ||
'Expected first argument to Polymer.Base._addFeature to be an object.', | ||
'Got', featureNode.type, 'instead.'); | ||
return; | ||
} | ||
if (!featureNode.properties) return; | ||
feature.properties = featureNode.properties.map(esutil.toPropertyDescriptor); | ||
}, | ||
}; | ||
return {visitors: visitors, features: features}; | ||
}; | ||
function featureFinder() { | ||
/** The features we've found. */ | ||
var features = []; | ||
function _extractDesc(feature, node, parent) { | ||
feature.desc = esutil.getAttachedComment(parent); | ||
} | ||
function _extractProperties(feature, node, parent) { | ||
var featureNode = node.arguments[0]; | ||
if (featureNode.type !== 'ObjectExpression') { | ||
console.warn('Expected first argument to Polymer.Base._addFeature to be an object.', 'Got', featureNode.type, 'instead.'); | ||
return; | ||
} | ||
var objExpr = featureNode; | ||
if (!objExpr.properties) return; | ||
feature.properties = objExpr.properties.map(esutil.toPropertyDescriptor); | ||
} | ||
var visitors = { | ||
enterCallExpression: function enterCallExpression(node, parent) { | ||
var isAddFeatureCall = esutil.matchesCallExpression(node.callee, ['Polymer', 'Base', '_addFeature']); | ||
if (!isAddFeatureCall) { | ||
return; | ||
} | ||
/** @type {!FeatureDescriptor} */ | ||
var feature = {}; | ||
_extractDesc(feature, node, parent); | ||
_extractProperties(feature, node, parent); | ||
features.push(feature); | ||
} | ||
}; | ||
return { visitors: visitors, features: features }; | ||
} | ||
exports.featureFinder = featureFinder; | ||
; |
@@ -10,133 +10,90 @@ /** | ||
*/ | ||
// jshint node: true | ||
'use strict'; | ||
var dom5 = require('dom5'); | ||
var p = dom5.predicates; | ||
var isHtmlImportNode = p.AND( | ||
p.hasTagName('link'), | ||
p.hasAttrValue('rel', 'import'), | ||
p.NOT( | ||
p.hasAttrValue('type', 'css') | ||
) | ||
); | ||
var isHtmlImportNode = p.AND(p.hasTagName('link'), p.hasAttrValue('rel', 'import'), p.NOT(p.hasAttrValue('type', 'css'))); | ||
var isStyleNode = p.OR( | ||
// inline style | ||
p.hasTagName('style'), | ||
// external stylesheet | ||
p.AND( | ||
p.hasTagName('link'), | ||
p.hasAttrValue('rel', 'stylesheet') | ||
), | ||
// polymer specific external stylesheet | ||
p.AND( | ||
p.hasTagName('link'), | ||
p.hasAttrValue('rel', 'import'), | ||
p.hasAttrValue('type', 'css') | ||
) | ||
); | ||
var isJSScriptNode = p.AND( | ||
p.hasTagName('script'), | ||
p.OR( | ||
p.NOT(p.hasAttr('type')), | ||
p.hasAttrValue('type', 'text/javascript'), | ||
p.hasAttrValue('type', 'application/javascript') | ||
) | ||
); | ||
// inline style | ||
p.hasTagName('style'), | ||
// external stylesheet | ||
p.AND(p.hasTagName('link'), p.hasAttrValue('rel', 'stylesheet')), | ||
// polymer specific external stylesheet | ||
p.AND(p.hasTagName('link'), p.hasAttrValue('rel', 'import'), p.hasAttrValue('type', 'css'))); | ||
var isJSScriptNode = p.AND(p.hasTagName('script'), p.OR(p.NOT(p.hasAttr('type')), p.hasAttrValue('type', 'text/javascript'), p.hasAttrValue('type', 'application/javascript'))); | ||
function addNode(node, registry) { | ||
if (isHtmlImportNode(node)) { | ||
registry.import.push(node); | ||
} else if (isStyleNode(node)) { | ||
registry.style.push(node); | ||
} else if (isJSScriptNode(node)) { | ||
registry.script.push(node); | ||
} else if (node.tagName === 'base') { | ||
registry.base.push(node); | ||
} else if (node.tagName === 'template') { | ||
registry.template.push(node); | ||
} else if (node.tagName === 'dom-module') { | ||
registry['dom-module'].push(node); | ||
} else if (dom5.isCommentNode(node)) { | ||
registry.comment.push(node); | ||
} | ||
if (isHtmlImportNode(node)) { | ||
registry.import.push(node); | ||
} else if (isStyleNode(node)) { | ||
registry.style.push(node); | ||
} else if (isJSScriptNode(node)) { | ||
registry.script.push(node); | ||
} else if (node.tagName === 'base') { | ||
registry.base.push(node); | ||
} else if (node.tagName === 'template') { | ||
registry.template.push(node); | ||
} else if (node.tagName === 'dom-module') { | ||
registry['dom-module'].push(node); | ||
} else if (dom5.isCommentNode(node)) { | ||
registry.comment.push(node); | ||
} | ||
} | ||
function getLineAndColumn(string, charNumber) { | ||
if (charNumber > string.length) { | ||
return undefined; | ||
} | ||
// TODO(ajo): Caching the line lengths of each document could be much faster. | ||
var sliced = string.slice(0,charNumber+1); | ||
var split = sliced.split('\n'); | ||
var line = split.length; | ||
var column = split[split.length - 1].length; | ||
return {line: line, column: column}; | ||
if (charNumber > string.length) { | ||
return undefined; | ||
} | ||
// TODO(ajo): Caching the line lengths of each document could be much faster. | ||
var sliced = string.slice(0, charNumber + 1); | ||
var split = sliced.split('\n'); | ||
var line = split.length; | ||
var column = split[split.length - 1].length; | ||
return { line: line, column: column }; | ||
} | ||
/** | ||
* Parse5's representation of a parsed html document. | ||
* @typedef {Object} DocumentAST | ||
*/ | ||
/** | ||
* The ASTs of the HTML elements needed to represent Polymer elements. | ||
* @typedef {Object} ParsedImport | ||
* @property {Array<DocumentAST>} template The entry points to the AST at each outermost template tag. | ||
* @property {Array<DocumentAST>} script The entry points to the AST at each script tag not inside a template. | ||
* @property {Array<DocumentAST>} style The entry points to the AST at style tag outside a template. | ||
* @property {Array<DocumentAST>} dom-module The entry points to the AST at each outermost dom-module element. | ||
* @property {DocumentAST} ast The full parse5 ast for the document. | ||
*/ | ||
/** | ||
* Parse html into ASTs. | ||
* @param {string} htmlString A utf8, html5 document containing polymer element or module definitons. | ||
* @param {string} href The path of the document. | ||
* @return {ParsedImport} | ||
* | ||
* htmlString is a utf8, html5 document containing polymer elements | ||
* or module definitons. | ||
* | ||
* href is the path of the document. | ||
*/ | ||
var importParse = function importParse(htmlString, href) { | ||
var doc; | ||
try { | ||
doc = dom5.parse(htmlString, {locationInfo: true}); | ||
} catch (err) { | ||
console.log(err); | ||
return null; | ||
} | ||
// Add line/column information | ||
dom5.treeMap(doc, function(node) { | ||
if (node.__location && node.__location.start >= 0) { | ||
node.__locationDetail = getLineAndColumn(htmlString, node.__location.start); | ||
if (href) { | ||
node.__ownerDocument = href; | ||
} | ||
function importParse(htmlString, href) { | ||
var doc; | ||
try { | ||
doc = dom5.parse(htmlString, { locationInfo: true }); | ||
} catch (err) { | ||
console.log(err); | ||
return null; | ||
} | ||
}); | ||
var registry = { | ||
base: [], | ||
template: [], | ||
script: [], | ||
style: [], | ||
import: [], | ||
'dom-module': [], | ||
comment: []}; | ||
var queue = [].concat(doc.childNodes); | ||
var nextNode; | ||
while (queue.length > 0) { | ||
nextNode = queue.shift(); | ||
if (nextNode) { | ||
queue = queue.concat(nextNode.childNodes); | ||
addNode(nextNode, registry); | ||
// Add line/column information | ||
dom5.treeMap(doc, function (node) { | ||
if (node.__location && node.__location.start >= 0) { | ||
node.__locationDetail = getLineAndColumn(htmlString, node.__location.start); | ||
if (href) { | ||
node.__ownerDocument = href; | ||
} | ||
} | ||
}); | ||
var registry = { | ||
base: [], | ||
template: [], | ||
script: [], | ||
style: [], | ||
import: [], | ||
'dom-module': [], | ||
comment: [], | ||
ast: doc }; | ||
var queue = [].concat(doc.childNodes); | ||
var nextNode; | ||
while (queue.length > 0) { | ||
nextNode = queue.shift(); | ||
if (nextNode) { | ||
queue = queue.concat(nextNode.childNodes); | ||
addNode(nextNode, registry); | ||
} | ||
} | ||
} | ||
registry.ast = doc; | ||
return registry; | ||
}; | ||
module.exports = importParse; | ||
; | ||
return registry; | ||
} | ||
exports.importParse = importParse; | ||
; |
@@ -13,84 +13,73 @@ /** | ||
*/ | ||
// jshint node: true | ||
'use strict'; | ||
var espree = require('espree'); | ||
var estraverse = require('estraverse'); | ||
var behavior_finder_1 = require('./behavior-finder'); | ||
var element_finder_1 = require('./element-finder'); | ||
var feature_finder_1 = require('./feature-finder'); | ||
function traverse(visitorRegistries) { | ||
function applyVisitors(name, node, parent) { | ||
var _iteratorNormalCompletion = true; | ||
var _didIteratorError = false; | ||
var _iteratorError = undefined; | ||
var behaviorFinder = require('./behavior-finder'); | ||
var elementFinder = require('./element-finder'); | ||
var featureFinder = require('./feature-finder'); | ||
try { | ||
for (var _iterator = visitorRegistries[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { | ||
var registry = _step.value; | ||
function traverse(visitorRegistries) { | ||
var visitor; | ||
function applyVisitors(name, node, parent) { | ||
var returnVal; | ||
for (var i = 0; i < visitorRegistries.length; i++) { | ||
if (name in visitorRegistries[i]) { | ||
returnVal = visitorRegistries[i][name](node, parent); | ||
if (returnVal) { | ||
return returnVal; | ||
if (name in registry) { | ||
var returnVal = registry[name](node, parent); | ||
if (returnVal) { | ||
return returnVal; | ||
} | ||
} | ||
} | ||
} catch (err) { | ||
_didIteratorError = true; | ||
_iteratorError = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion && _iterator.return) { | ||
_iterator.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError) { | ||
throw _iteratorError; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return { | ||
enter: function(node, parent) { | ||
visitor = 'enter' + node.type; | ||
return applyVisitors(visitor, node, parent); | ||
}, | ||
leave: function(node, parent) { | ||
visitor = 'leave' + node.type; | ||
return applyVisitors(visitor, node, parent); | ||
}, | ||
fallback: 'iteration', | ||
}; | ||
return { | ||
enter: function enter(node, parent) { | ||
return applyVisitors('enter' + node.type, node, parent); | ||
}, | ||
leave: function leave(node, parent) { | ||
return applyVisitors('leave' + node.type, node, parent); | ||
}, | ||
fallback: 'iteration' | ||
}; | ||
} | ||
var jsParse = function jsParse(jsString) { | ||
var script = espree.parse(jsString, { | ||
attachComment: true, | ||
comment: true, | ||
loc: true, | ||
ecmaFeatures: { | ||
arrowFunctions: true, | ||
blockBindings: true, | ||
destructuring: true, | ||
regexYFlag: true, | ||
regexUFlag: true, | ||
templateStrings: true, | ||
binaryLiterals: true, | ||
unicodeCodePointEscapes: true, | ||
defaultParams: true, | ||
restParams: true, | ||
forOf: true, | ||
objectLiteralComputedProperties: true, | ||
objectLiteralShorthandMethods: true, | ||
objectLiteralShorthandProperties: true, | ||
objectLiteralDuplicateProperties: true, | ||
generators: true, | ||
spread: true, | ||
classes: true, | ||
modules: true, | ||
jsx: true, | ||
globalReturn: true, | ||
} | ||
}); | ||
var featureInfo = featureFinder(); | ||
var behaviorInfo = behaviorFinder(); | ||
var elementInfo = elementFinder(); | ||
var visitors = [featureInfo, behaviorInfo, elementInfo].map(function(info) { | ||
return info.visitors; | ||
}); | ||
estraverse.traverse(script, traverse(visitors)); | ||
return { | ||
behaviors: behaviorInfo.behaviors, | ||
elements: elementInfo.elements, | ||
features: featureInfo.features, | ||
parsedScript: script | ||
}; | ||
}; | ||
module.exports = jsParse; | ||
function jsParse(jsString) { | ||
var script = espree.parse(jsString, { | ||
attachComment: true, | ||
comment: true, | ||
loc: true, | ||
ecmaVersion: 6 | ||
}); | ||
var featureInfo = feature_finder_1.featureFinder(); | ||
var behaviorInfo = behavior_finder_1.behaviorFinder(); | ||
var elementInfo = element_finder_1.elementFinder(); | ||
var visitors = [featureInfo, behaviorInfo, elementInfo].map(function (info) { | ||
return info.visitors; | ||
}); | ||
estraverse.traverse(script, traverse(visitors)); | ||
return { | ||
behaviors: behaviorInfo.behaviors, | ||
elements: elementInfo.elements, | ||
features: featureInfo.features, | ||
parsedScript: script | ||
}; | ||
} | ||
exports.jsParse = jsParse; | ||
; |
@@ -10,38 +10,6 @@ /** | ||
*/ | ||
// jshint node: true | ||
'use strict'; | ||
var doctrine = require('doctrine'); | ||
/** | ||
* An annotated JSDoc block tag, all fields are optionally processed except for | ||
* the tag: | ||
* | ||
* @TAG {TYPE} NAME DESC | ||
* | ||
* `line` and `col` indicate the position of the first character of text that | ||
* the tag was extracted from - relative to the first character of the comment | ||
* contents (e.g. the value of `desc` on a descriptor node). Lines are | ||
* 1-indexed. | ||
* | ||
* @typedef {{ | ||
* tag: string, | ||
* type: ?string, | ||
* name: ?string, | ||
* description: ?string, | ||
* }} | ||
*/ | ||
var JsdocTag; | ||
/** | ||
* The parsed representation of a JSDoc comment. | ||
* | ||
* @typedef {{ | ||
* description: ?string, | ||
* tags: Array<JsdocTag>, | ||
* }} | ||
*/ | ||
var JsdocAnnotation; | ||
/** | ||
* doctrine configuration, | ||
@@ -51,9 +19,6 @@ * CURRENTLY UNUSED BECAUSE PRIVATE | ||
// function configureDoctrine() { | ||
// // @hero [path/to/image] | ||
// doctrine.Rules['hero'] = ['parseNamePathOptional', 'ensureEnd']; | ||
// // // @demo [path/to/demo] [Demo title] | ||
// doctrine.Rules['demo'] = ['parseNamePathOptional', 'parseDescription', 'ensureEnd']; | ||
// // // @polymerBehavior [Polymer.BehaviorName] | ||
@@ -63,90 +28,74 @@ // doctrine.Rules['polymerBehavior'] = ['parseNamePathOptional', 'ensureEnd']; | ||
// configureDoctrine(); | ||
// @demo [path] [title] | ||
function parseDemo(tag) { | ||
var match = (tag.description || "").match(/^\s*(\S*)\s*(.*)$/); | ||
return { | ||
tag: 'demo', | ||
type: null, | ||
name: match ? match[1] : null, | ||
description: match ? match[2] : null | ||
}; | ||
var match = (tag.description || "").match(/^\s*(\S*)\s*(.*)$/); | ||
return { | ||
tag: 'demo', | ||
type: null, | ||
name: match ? match[1] : null, | ||
description: match ? match[2] : null | ||
}; | ||
} | ||
// @hero [path] | ||
function parseHero(tag) { | ||
return { | ||
tag: tag.title, | ||
type: null, | ||
name: tag.description, | ||
description: null | ||
}; | ||
return { | ||
tag: tag.title, | ||
type: null, | ||
name: tag.description, | ||
description: null | ||
}; | ||
} | ||
// @polymerBehavior [name] | ||
function parsePolymerBehavior(tag) { | ||
return { | ||
tag: tag.title, | ||
type: null, | ||
name: tag.description, | ||
description: null | ||
}; | ||
return { | ||
tag: tag.title, | ||
type: null, | ||
name: tag.description, | ||
description: null | ||
}; | ||
} | ||
// @pseudoElement name | ||
function parsePseudoElement(tag) { | ||
return { | ||
tag: tag.title, | ||
type: null, | ||
name: tag.description, | ||
description: null | ||
}; | ||
return { | ||
tag: tag.title, | ||
type: null, | ||
name: tag.description, | ||
description: null | ||
}; | ||
} | ||
var CUSTOM_TAGS = { | ||
demo: parseDemo, | ||
hero: parseHero, | ||
polymerBehavior: parsePolymerBehavior, | ||
pseudoElement: parsePseudoElement | ||
demo: parseDemo, | ||
hero: parseHero, | ||
polymerBehavior: parsePolymerBehavior, | ||
pseudoElement: parsePseudoElement | ||
}; | ||
/** | ||
* Convert doctrine tags to hydrolysis tag format | ||
* Convert doctrine tags to our tag format | ||
*/ | ||
function _tagsToHydroTags(tags) { | ||
if (!tags) | ||
return null; | ||
return tags.map( function(tag) { | ||
if (tag.title in CUSTOM_TAGS) { | ||
return CUSTOM_TAGS[tag.title](tag); | ||
} | ||
else { | ||
return { | ||
tag: tag.title, | ||
type: tag.type ? doctrine.type.stringify(tag.type) : null, | ||
name: tag.name, | ||
description: tag.description, | ||
}; | ||
} | ||
}); | ||
if (!tags) return null; | ||
return tags.map(function (tag) { | ||
if (tag.title in CUSTOM_TAGS) { | ||
return CUSTOM_TAGS[tag.title](tag); | ||
} else { | ||
return { | ||
tag: tag.title, | ||
type: tag.type ? doctrine.type.stringify(tag.type) : null, | ||
name: tag.name, | ||
description: tag.description | ||
}; | ||
} | ||
}); | ||
} | ||
/** | ||
* removes leading *, and any space before it | ||
* @param {string} description -- js doc description | ||
*/ | ||
function _removeLeadingAsterisks(description) { | ||
if ((typeof description) !== 'string') | ||
return description; | ||
return description | ||
.split('\n') | ||
.map( function(line) { | ||
// remove leading '\s*' from each line | ||
var match = line.match(/^[\s]*\*\s?(.*)$/); | ||
return match ? match[1] : line; | ||
}) | ||
.join('\n'); | ||
if (typeof description !== 'string') return description; | ||
return description.split('\n').map(function (line) { | ||
// remove leading '\s*' from each line | ||
var match = line.match(/^[\s]*\*\s?(.*)$/); | ||
return match ? match[1] : line; | ||
}).join('\n'); | ||
} | ||
/** | ||
@@ -157,71 +106,49 @@ * Given a JSDoc string (minus opening/closing comment delimiters), extract its | ||
* @param {string} docs | ||
* @return {?JsdocAnnotation} | ||
* @return {?Annotation} | ||
*/ | ||
function parseJsdoc(docs) { | ||
docs = _removeLeadingAsterisks(docs); | ||
var d = doctrine.parse(docs, { | ||
unwrap: false, | ||
lineNumber: true, | ||
preserveWhitespace: true | ||
}); | ||
return { | ||
description: d.description, | ||
tags: _tagsToHydroTags(d.tags) | ||
}; | ||
docs = _removeLeadingAsterisks(docs); | ||
var d = doctrine.parse(docs, { | ||
unwrap: false, | ||
lineNumber: true, | ||
preserveWhitespace: true | ||
}); | ||
return { | ||
description: d.description, | ||
tags: _tagsToHydroTags(d.tags) | ||
}; | ||
} | ||
exports.parseJsdoc = parseJsdoc; | ||
// Utility | ||
/** | ||
* @param {JsdocAnnotation} jsdoc | ||
* @param {string} tagName | ||
* @return {boolean} | ||
*/ | ||
function hasTag(jsdoc, tagName) { | ||
if (!jsdoc || !jsdoc.tags) return false; | ||
return jsdoc.tags.some(function(tag) { return tag.tag === tagName; }); | ||
if (!jsdoc || !jsdoc.tags) return false; | ||
return jsdoc.tags.some(function (tag) { | ||
return tag.tag === tagName; | ||
}); | ||
} | ||
/** | ||
* Finds the first JSDoc tag matching `name` and returns its value at `key`. | ||
* | ||
* @param {JsdocAnnotation} jsdoc | ||
* @param {string} tagName | ||
* @param {string=} key If omitted, the entire tag object is returned. | ||
* @return {?string|Object} | ||
*/ | ||
exports.hasTag = hasTag; | ||
function getTag(jsdoc, tagName, key) { | ||
if (!jsdoc || !jsdoc.tags) return false; | ||
for (var i = 0; i < jsdoc.tags.length; i++) { | ||
var tag = jsdoc.tags[i]; | ||
if (tag.tag === tagName) { | ||
return key ? tag[key] : tag; | ||
if (!jsdoc || !jsdoc.tags) return null; | ||
for (var i = 0; i < jsdoc.tags.length; i++) { | ||
var tag = jsdoc.tags[i]; | ||
if (tag.tag === tagName) { | ||
return key ? tag[key] : tag; | ||
} | ||
} | ||
} | ||
return null; | ||
return null; | ||
} | ||
/** | ||
* @param {?string} text | ||
* @return {?string} | ||
*/ | ||
exports.getTag = getTag; | ||
function unindent(text) { | ||
if (!text) return text; | ||
var lines = text.replace(/\t/g, ' ').split('\n'); | ||
var indent = lines.reduce(function(prev, line) { | ||
if (/^\s*$/.test(line)) return prev; // Completely ignore blank lines. | ||
var lineIndent = line.match(/^(\s*)/)[0].length; | ||
if (prev === null) return lineIndent; | ||
return lineIndent < prev ? lineIndent : prev; | ||
}, null); | ||
return lines.map(function(l) { return l.substr(indent); }).join('\n'); | ||
if (!text) return text; | ||
var lines = text.replace(/\t/g, ' ').split('\n'); | ||
var indent = lines.reduce(function (prev, line) { | ||
if (/^\s*$/.test(line)) return prev; // Completely ignore blank lines. | ||
var lineIndent = line.match(/^(\s*)/)[0].length; | ||
if (prev === null) return lineIndent; | ||
return lineIndent < prev ? lineIndent : prev; | ||
}, null); | ||
return lines.map(function (l) { | ||
return l.substr(indent); | ||
}).join('\n'); | ||
} | ||
module.exports = { | ||
getTag: getTag, | ||
hasTag: hasTag, | ||
parseJsdoc: parseJsdoc, | ||
unindent: unindent | ||
}; | ||
exports.unindent = unindent; |
@@ -10,22 +10,37 @@ /** | ||
*/ | ||
// jshint node:true | ||
'use strict'; | ||
var FSResolver = require('./fs-resolver'); | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
function ErrorSwallowingFSResolver(config) { | ||
FSResolver.call(this, config); | ||
} | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
ErrorSwallowingFSResolver.prototype = Object.create(FSResolver.prototype); | ||
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } | ||
ErrorSwallowingFSResolver.prototype.accept = function(uri, deferred) { | ||
var reject = deferred.reject; | ||
deferred.reject = function(arg) { | ||
deferred.resolve(""); | ||
}; | ||
return FSResolver.prototype.accept.call(this, uri, deferred); | ||
}; | ||
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } | ||
module.exports = ErrorSwallowingFSResolver; | ||
var fs_resolver_1 = require('./fs-resolver'); | ||
var ErrorSwallowingFSResolver = function (_fs_resolver_1$FSReso) { | ||
_inherits(ErrorSwallowingFSResolver, _fs_resolver_1$FSReso); | ||
function ErrorSwallowingFSResolver(config) { | ||
_classCallCheck(this, ErrorSwallowingFSResolver); | ||
return _possibleConstructorReturn(this, Object.getPrototypeOf(ErrorSwallowingFSResolver).call(this, config)); | ||
} | ||
_createClass(ErrorSwallowingFSResolver, [{ | ||
key: 'accept', | ||
value: function accept(uri, deferred) { | ||
var reject = deferred.reject; | ||
deferred.reject = function (arg) { | ||
deferred.resolve(""); | ||
}; | ||
return fs_resolver_1.FSResolver.prototype.accept.call(this, uri, deferred); | ||
} | ||
}]); | ||
return ErrorSwallowingFSResolver; | ||
}(fs_resolver_1.FSResolver); | ||
exports.ErrorSwallowingFSResolver = ErrorSwallowingFSResolver; |
@@ -10,91 +10,76 @@ /** | ||
*/ | ||
// jshint node:true | ||
'use strict'; | ||
// jshint -W079 | ||
// Promise polyfill | ||
var Promise = global.Promise || require('es6-promise').Promise; | ||
// jshint +W079 | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
function Deferred() { | ||
var self = this; | ||
this.promise = new Promise(function(resolve, reject) { | ||
self.resolve = resolve; | ||
self.reject = reject; | ||
}); | ||
} | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
var resolver_1 = require('./resolver'); | ||
/** | ||
* An object that knows how to resolve resources. | ||
* @typedef {Object} Resolver | ||
* @memberof hydrolysis | ||
* @property {function(string, Deferred): boolean} accept Attempt to resolve | ||
* `deferred` with the contents the specified URL. Returns false if the | ||
* Resolver is unable to resolve the URL. | ||
*/ | ||
/** | ||
* A FileLoader lets you resolve URLs with a set of potential resolvers. | ||
* @constructor | ||
* @memberof hydrolysis | ||
*/ | ||
function FileLoader() { | ||
this.resolvers = []; | ||
// map url -> Deferred | ||
this.requests = {}; | ||
} | ||
FileLoader.prototype = { | ||
/** | ||
* Add an instance of a Resolver class to the list of url resolvers | ||
* | ||
* Ordering of resolvers is most to least recently added | ||
* The first resolver to "accept" the url wins. | ||
* @param {Resolver} resolver The resolver to add. | ||
*/ | ||
addResolver: function(resolver) { | ||
this.resolvers.push(resolver); | ||
}, | ||
var FileLoader = function () { | ||
function FileLoader() { | ||
_classCallCheck(this, FileLoader); | ||
/** | ||
* Return a promise for an absolute url | ||
* | ||
* Url requests are deduplicated by the loader, returning the same Promise for | ||
* identical urls | ||
* | ||
* @param {string} url The absolute url to request. | ||
* @return {Promise.<string>} A promise that resolves to the contents of the URL. | ||
*/ | ||
request: function(uri) { | ||
var promise; | ||
this.resolvers = []; | ||
// map url -> Deferred | ||
this.requests = {}; | ||
} | ||
/** | ||
* Add an instance of a Resolver class to the list of url resolvers | ||
* | ||
* Ordering of resolvers is most to least recently added | ||
* The first resolver to "accept" the url wins. | ||
* @param {Resolver} resolver The resolver to add. | ||
*/ | ||
if (!(uri in this.requests)) { | ||
var handled = false; | ||
var deferred = new Deferred(); | ||
this.requests[uri] = deferred; | ||
// loop backwards through resolvers until one "accepts" the request | ||
for (var i = this.resolvers.length - 1, r; i >= 0; i--) { | ||
r = this.resolvers[i]; | ||
if (r.accept(uri, deferred)) { | ||
handled = true; | ||
break; | ||
_createClass(FileLoader, [{ | ||
key: 'addResolver', | ||
value: function addResolver(resolver) { | ||
this.resolvers.push(resolver); | ||
} | ||
} | ||
}, { | ||
key: 'request', | ||
if (!handled) { | ||
deferred.reject(new Error('no resolver found for ' + uri)); | ||
} | ||
/** | ||
* Return a promise for an absolute url | ||
* | ||
* Url requests are deduplicated by the loader, returning the same Promise for | ||
* identical urls | ||
* | ||
* @param {string} url The absolute url to request. | ||
* @return {Promise.<string>} A promise that resolves to the contents of the URL. | ||
*/ | ||
value: function request(uri) { | ||
var promise; | ||
if (!(uri in this.requests)) { | ||
var handled = false; | ||
var deferred = new resolver_1.Deferred(); | ||
this.requests[uri] = deferred; | ||
// loop backwards through resolvers until one "accepts" the request | ||
for (var i = this.resolvers.length - 1; i >= 0; i--) { | ||
var r = this.resolvers[i]; | ||
if (r.accept(uri, deferred)) { | ||
handled = true; | ||
break; | ||
} | ||
} | ||
if (!handled) { | ||
deferred.reject(new Error('no resolver found for ' + uri)); | ||
} | ||
promise = deferred.promise; | ||
} else { | ||
promise = this.requests[uri].promise; | ||
} | ||
return promise; | ||
} | ||
}]); | ||
promise = deferred.promise; | ||
} else { | ||
promise = this.requests[uri].promise; | ||
} | ||
return FileLoader; | ||
}(); | ||
return promise; | ||
} | ||
}; | ||
module.exports = FileLoader; | ||
exports.FileLoader = FileLoader; | ||
; |
@@ -10,102 +10,91 @@ /** | ||
*/ | ||
// jshint node:true | ||
'use strict'; | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
var fs = require('fs'); | ||
var path = require('path'); | ||
var pathIsAbsolute = require('path-is-absolute'); | ||
var url = require('url'); | ||
function getFile(filePath, deferred, secondPath) { | ||
fs.readFile(filePath, 'utf-8', function(err, content) { | ||
if (err) { | ||
if (secondPath) { | ||
getFile(secondPath, deferred); | ||
} else { | ||
console.log("ERROR finding " + filePath); | ||
deferred.reject(err); | ||
} | ||
} else { | ||
deferred.resolve(content); | ||
} | ||
}); | ||
fs.readFile(filePath, 'utf-8', function (err, content) { | ||
if (err) { | ||
if (secondPath) { | ||
getFile(secondPath, deferred); | ||
} else { | ||
console.log("ERROR finding " + filePath); | ||
deferred.reject(err); | ||
} | ||
} else { | ||
deferred.resolve(content); | ||
} | ||
}); | ||
} | ||
/** | ||
* Returns true if `patha` is a sibling or aunt of `pathb`. | ||
* @return {boolean} | ||
*/ | ||
function isSiblingOrAunt(patha, pathb) { | ||
var parent = path.dirname(patha); | ||
if (pathb.indexOf(patha) === -1 && pathb.indexOf(parent) === 0) { | ||
return true; | ||
} | ||
return false; | ||
var parent = path.dirname(patha); | ||
if (pathb.indexOf(patha) === -1 && pathb.indexOf(parent) === 0) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
/** | ||
* Change `localPath` from a sibling of `basePath` to be a child of | ||
* `basePath` joined with `redirect`. | ||
* @return {string} | ||
*/ | ||
function redirectSibling(basePath, localPath, redirect) { | ||
var parent = path.dirname(basePath); | ||
var redirected = path.join(basePath, redirect, localPath.slice(parent.length)); | ||
return redirected; | ||
var parent = path.dirname(basePath); | ||
var redirected = path.join(basePath, redirect, localPath.slice(parent.length)); | ||
return redirected; | ||
} | ||
/** | ||
* Resolves requests via the file system. | ||
* @constructor | ||
* @memberof hydrolysis | ||
* @param {Object} config configuration options. | ||
* @param {string} config.host Hostname to match for absolute urls. | ||
* Matches "/" by default | ||
* @param {string} config.basePath Prefix directory for components in url. | ||
* Defaults to "/". | ||
* @param {string} config.root Filesystem root to search. Defaults to the | ||
* current working directory. | ||
* @param {string} config.redirect Where to redirect lookups to siblings. | ||
*/ | ||
function FSResolver(config) { | ||
this.config = config || {}; | ||
} | ||
FSResolver.prototype = { | ||
accept: function(uri, deferred) { | ||
var parsed = url.parse(uri); | ||
var host = this.config.host; | ||
var base = this.config.basePath && decodeURIComponent(this.config.basePath); | ||
var root = this.config.root && path.normalize(this.config.root); | ||
var redirect = this.config.redirect; | ||
var local; | ||
var FSResolver = function () { | ||
function FSResolver(config) { | ||
_classCallCheck(this, FSResolver); | ||
if (!parsed.hostname || parsed.hostname === host) { | ||
local = parsed.pathname; | ||
this.config = config || {}; | ||
} | ||
if (local) { | ||
// un-escape HTML escapes | ||
local = decodeURIComponent(local); | ||
if (base) { | ||
local = path.relative(base, local); | ||
} | ||
if (root) { | ||
local = path.join(root, local); | ||
} | ||
_createClass(FSResolver, [{ | ||
key: 'accept', | ||
value: function accept(uri, deferred) { | ||
var parsed = url.parse(uri); | ||
var host = this.config.host; | ||
var base = this.config.basePath && decodeURIComponent(this.config.basePath); | ||
var root = this.config.root && path.normalize(this.config.root); | ||
var redirect = this.config.redirect; | ||
var local; | ||
if (!parsed.hostname || parsed.hostname === host) { | ||
local = parsed.pathname; | ||
} | ||
if (local) { | ||
// un-escape HTML escapes | ||
local = decodeURIComponent(local); | ||
if (base) { | ||
local = path.relative(base, local); | ||
} | ||
if (root) { | ||
local = path.join(root, local); | ||
} | ||
var backup; | ||
if (redirect && isSiblingOrAunt(root, local)) { | ||
backup = redirectSibling(root, local, redirect); | ||
} | ||
getFile(local, deferred, backup); | ||
return true; | ||
} | ||
return false; | ||
} | ||
}]); | ||
var backup; | ||
if (redirect && isSiblingOrAunt(root, local)) { | ||
backup = redirectSibling(root, local, redirect); | ||
} | ||
return FSResolver; | ||
}(); | ||
getFile(local, deferred, backup); | ||
return true; | ||
} | ||
return false; | ||
} | ||
}; | ||
module.exports = FSResolver; | ||
exports.FSResolver = FSResolver; | ||
; |
@@ -10,36 +10,46 @@ /** | ||
*/ | ||
// jshint node:true | ||
'use strict'; | ||
/** | ||
* A resolver that resolves to null any uri matching config. | ||
* @constructor | ||
* @memberof hydrolysis | ||
* @param {string} config The url to `accept`. | ||
* A resolver that resolves to empty string any uri that matches config. | ||
*/ | ||
function NoopResolver(config) { | ||
this.config = config; | ||
} | ||
NoopResolver.prototype = { | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
/** | ||
* @param {string} uri The absolute URI being requested. | ||
* @param {!Deferred} deferred The deferred promise that should be resolved if | ||
* this resolver handles the URI. | ||
* @return {boolean} Whether the URI is handled by this resolver. | ||
*/ | ||
accept: function(uri, deferred) { | ||
if (!this.config.test) { | ||
if (uri.search(this.config) == -1) { | ||
return false; | ||
} | ||
} else if (!this.config.test(uri)) return false; | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
deferred.resolve(''); | ||
return true; | ||
} | ||
}; | ||
var NoopResolver = function () { | ||
function NoopResolver(config) { | ||
_classCallCheck(this, NoopResolver); | ||
module.exports = NoopResolver; | ||
this.config = config; | ||
} | ||
/** | ||
* @param {string} uri The absolute URI being requested. | ||
* @param {!Deferred} deferred The deferred promise that should be resolved if | ||
* this resolver will handle the URI. | ||
* @return {boolean} Whether the URI is handled by this resolver. | ||
*/ | ||
_createClass(NoopResolver, [{ | ||
key: 'accept', | ||
value: function accept(uri, deferred) { | ||
var config = this.config; | ||
if (typeof config === 'string') { | ||
if (uri.search(config) == -1) { | ||
return false; | ||
} | ||
} else { | ||
if (!config.test(uri)) { | ||
return false; | ||
} | ||
} | ||
deferred.resolve(''); | ||
return true; | ||
} | ||
}]); | ||
return NoopResolver; | ||
}(); | ||
exports.NoopResolver = NoopResolver; |
@@ -10,12 +10,15 @@ /** | ||
*/ | ||
// jshint node:true | ||
'use strict'; | ||
var fs = require('fs'); | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } | ||
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
var path = require('path'); | ||
var url = require('url'); | ||
var FSResolver = require('./fs-resolver'); | ||
var fs_resolver_1 = require('./fs-resolver'); | ||
/** | ||
@@ -31,47 +34,32 @@ * A single redirect configuration | ||
*/ | ||
function ProtocolRedirect(config){ | ||
this.protocol = config.protocol; | ||
this.hostname = config.hostname; | ||
this.path = config.path; | ||
this.redirectPath = config.redirectPath; | ||
} | ||
ProtocolRedirect.prototype = { | ||
/** | ||
* The protocol this redirect matches. | ||
* @type {string} | ||
*/ | ||
protocol: null, | ||
/** | ||
* The host name this redirect matches. | ||
* @type {string} | ||
*/ | ||
hostname: null, | ||
var ProtocolRedirect = function () { | ||
function ProtocolRedirect(config) { | ||
_classCallCheck(this, ProtocolRedirect); | ||
/** | ||
* The part of the path to match and replace with 'redirectPath' | ||
* @type {string} | ||
*/ | ||
path: null, | ||
this.protocol = config.protocol; | ||
this.hostname = config.hostname; | ||
this.path = config.path; | ||
this.redirectPath = config.redirectPath; | ||
} | ||
/** | ||
* The local filesystem path that should replace "protocol://hosname/path/" | ||
* @type {string} | ||
*/ | ||
redirectPath: null, | ||
_createClass(ProtocolRedirect, [{ | ||
key: 'redirect', | ||
value: function redirect(uri) { | ||
var parsed = url.parse(uri); | ||
if (this.protocol !== parsed.protocol) { | ||
return null; | ||
} else if (this.hostname !== parsed.hostname) { | ||
return null; | ||
} else if (parsed.pathname.indexOf(this.path) !== 0) { | ||
return null; | ||
} | ||
return path.join(this.redirectPath, parsed.pathname.slice(this.path.length)); | ||
} | ||
}]); | ||
redirect: function redirect(uri) { | ||
var parsed = url.parse(uri); | ||
if (this.protocol !== parsed.protocol) { | ||
return null; | ||
} else if (this.hostname !== parsed.hostname) { | ||
return null; | ||
} else if (parsed.pathname.indexOf(this.path) !== 0) { | ||
return null; | ||
} | ||
return path.join(this.redirectPath, | ||
parsed.pathname.slice(this.path.length)); | ||
} | ||
}; | ||
return ProtocolRedirect; | ||
}(); | ||
; | ||
/** | ||
@@ -87,23 +75,32 @@ * Resolves protocol://hostname/path to the local filesystem. | ||
*/ | ||
function RedirectResolver(config) { | ||
FSResolver.call(this, config); | ||
this.redirects = config.redirects || []; | ||
} | ||
RedirectResolver.prototype = Object.create(FSResolver.prototype); | ||
var RedirectResolver = function (_fs_resolver_1$FSReso) { | ||
_inherits(RedirectResolver, _fs_resolver_1$FSReso); | ||
RedirectResolver.prototype.accept = function(uri, deferred) { | ||
for (var i = 0; i < this.redirects.length; i++) { | ||
var redirected = this.redirects[i].redirect(uri); | ||
if (redirected) { | ||
return FSResolver.prototype.accept.call(this, redirected, deferred); | ||
function RedirectResolver(config) { | ||
_classCallCheck(this, RedirectResolver); | ||
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(RedirectResolver).call(this, config)); | ||
_this.redirects = config.redirects || []; | ||
return _this; | ||
} | ||
} | ||
return false; | ||
}; | ||
RedirectResolver.prototype.constructor = RedirectResolver; | ||
RedirectResolver.ProtocolRedirect = ProtocolRedirect; | ||
_createClass(RedirectResolver, [{ | ||
key: 'accept', | ||
value: function accept(uri, deferred) { | ||
for (var i = 0; i < this.redirects.length; i++) { | ||
var redirected = this.redirects[i].redirect(uri); | ||
if (redirected) { | ||
return fs_resolver_1.FSResolver.prototype.accept.call(this, redirected, deferred); | ||
} | ||
} | ||
return false; | ||
} | ||
}]); | ||
return RedirectResolver; | ||
}(fs_resolver_1.FSResolver); | ||
module.exports = RedirectResolver; | ||
RedirectResolver.ProtocolRedirect = ProtocolRedirect; | ||
exports.RedirectResolver = RedirectResolver; |
@@ -10,46 +10,51 @@ /** | ||
*/ | ||
// jshint node:true | ||
'use strict'; | ||
/** | ||
* A resolver that resolves to `config.content` any uri matching config. | ||
* @constructor | ||
* @memberof hydrolysis | ||
* @param {string|RegExp} config.url The url or rejex to accept. | ||
* @param {string} config.content The content to serve for `url`. | ||
*/ | ||
function StringResolver(config) { | ||
this.url = config.url; | ||
this.content = config.content; | ||
if (!this.url || !this.content) { | ||
throw new Error("Must provide a url and content to the string resolver."); | ||
} | ||
} | ||
StringResolver.prototype = { | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
/** | ||
* @param {string} uri The absolute URI being requested. | ||
* @param {!Deferred} deferred The deferred promise that should be resolved if | ||
* this resolver handles the URI. | ||
* @return {boolean} Whether the URI is handled by this resolver. | ||
*/ | ||
accept: function(uri, deferred) { | ||
if (this.url.test) { | ||
// this.url is a regex | ||
if (!this.url.test(uri)) { | ||
return false; | ||
} | ||
} else { | ||
// this.url is a string | ||
if (uri.search(this.url) == -1) { | ||
return false; | ||
} | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
var StringResolver = function () { | ||
function StringResolver(config) { | ||
_classCallCheck(this, StringResolver); | ||
this.url = config.url; | ||
this.content = config.content; | ||
if (!this.url || !this.content) { | ||
throw new Error("Must provide a url and content to the string resolver."); | ||
} | ||
} | ||
deferred.resolve(this.content); | ||
return true; | ||
} | ||
}; | ||
/** | ||
* @param {string} uri The absolute URI being requested. | ||
* @param {!Deferred} deferred The deferred promise that should be resolved if | ||
* this resolver handles the URI. | ||
* @return {boolean} Whether the URI is handled by this resolver. | ||
*/ | ||
module.exports = StringResolver; | ||
_createClass(StringResolver, [{ | ||
key: "accept", | ||
value: function accept(uri, deferred) { | ||
var url = this.url; | ||
if (url instanceof RegExp) { | ||
if (!url.test(uri)) { | ||
return false; | ||
} | ||
} else { | ||
if (uri.indexOf(url) == -1) { | ||
return false; | ||
} | ||
} | ||
deferred.resolve(this.content); | ||
return true; | ||
} | ||
}]); | ||
return StringResolver; | ||
}(); | ||
exports.StringResolver = StringResolver; | ||
; |
@@ -10,45 +10,51 @@ /** | ||
*/ | ||
// jshint node:true | ||
'use strict'; | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
function getFile(url, deferred, config) { | ||
/* global XMLHttpRequest:false */ | ||
var x = new XMLHttpRequest(); | ||
x.onload = function() { | ||
var status = x.status || 0; | ||
if (status >= 200 && status < 300) { | ||
deferred.resolve(x.response); | ||
} else { | ||
deferred.reject('xhr status: ' + status); | ||
/* global XMLHttpRequest:false */ | ||
var x = new XMLHttpRequest(); | ||
x.onload = function () { | ||
var status = x.status || 0; | ||
if (status >= 200 && status < 300) { | ||
deferred.resolve(x.response); | ||
} else { | ||
deferred.reject('xhr status: ' + status); | ||
} | ||
}; | ||
x.onerror = function (e) { | ||
deferred.reject(e); | ||
}; | ||
x.open('GET', url, true); | ||
if (config && config.responseType) { | ||
x.responseType = config.responseType; | ||
} | ||
}; | ||
x.onerror = function(e) { | ||
deferred.reject(e); | ||
}; | ||
x.open('GET', url, true); | ||
if (config && config.responseType) { | ||
x.responseType = config.responseType; | ||
} | ||
x.send(); | ||
x.send(); | ||
} | ||
/** | ||
* Construct a resolver that requests resources over XHR. | ||
* @constructor | ||
* @memberof hydrolysis | ||
* @param {Object} config configuration arguments. | ||
* @param {string} config.responseType Type of object to be returned by the | ||
* XHR. Defaults to 'text', accepts 'document', 'arraybuffer', and 'json'. | ||
*/ | ||
function XHRResolver(config) { | ||
this.config = config; | ||
} | ||
XHRResolver.prototype = { | ||
accept: function(uri, deferred) { | ||
getFile(uri, deferred, this.config); | ||
return true; | ||
} | ||
}; | ||
module.exports = XHRResolver; | ||
var XHRResolver = function () { | ||
function XHRResolver(config) { | ||
_classCallCheck(this, XHRResolver); | ||
this.config = config; | ||
} | ||
_createClass(XHRResolver, [{ | ||
key: 'accept', | ||
value: function accept(uri, deferred) { | ||
getFile(uri, deferred, this.config); | ||
return true; | ||
} | ||
}]); | ||
return XHRResolver; | ||
}(); | ||
exports.XHRResolver = XHRResolver; | ||
; |
{ | ||
"name": "hydrolysis", | ||
"version": "1.22.0", | ||
"version": "1.23.3", | ||
"description": "Breaks polymers into monomers", | ||
@@ -14,2 +14,5 @@ "homepage": "https://github.com/Polymer/hydrolysis", | ||
"files": [ | ||
"hydrolysis-analyzer.html", | ||
"hydrolysis.html", | ||
"hydrolysis.js", | ||
"index.js", | ||
@@ -20,22 +23,31 @@ "LICENSE", | ||
"scripts": { | ||
"build": "browserify -d -r ./index.js:hydrolysis -o hydrolysis.js", | ||
"build:watch": "watch 'npm run build' ./lib", | ||
"release": "browserify -r ./index.js:hydrolysis -o hydrolysis.js", | ||
"init": "typings install", | ||
"clean": "rm -rf typings/ lib/ && find src | grep \\.js$ | xargs rm", | ||
"compile": "if [ ! -f typings/main.d.ts ]; then npm run init; fi; tsc && babel --presets es2015 -q -d lib src", | ||
"build": "npm run compile && browserify -d -r ./index.js:hydrolysis -o hydrolysis.js", | ||
"release": "npm run compile && browserify -r ./index.js:hydrolysis -o hydrolysis.js", | ||
"build:watch": "watch 'npm run build' ./src", | ||
"apidocs": "node_modules/jsdoc-to-markdown/bin/cli.js {index.js,lib/{analyzer,loader/*}.js} > API.md", | ||
"test": "jshint index.js lib/ && npm run build && wct && mocha test/test.js" | ||
"test": "npm run build && wct && mocha test/test.js", | ||
"testNode": "npm run build && mocha test/test.js" | ||
}, | ||
"devDependencies": { | ||
"babel-cli": "^6.4.5", | ||
"babel-preset-es2015": "^6.1.18", | ||
"babelify": "^7.2.0", | ||
"browserify": "^9.0.8", | ||
"chai": "^2.2.0", | ||
"jsdoc-to-markdown": "^1.0.3", | ||
"jshint": "^2.7.0", | ||
"mocha": "^2.2.4", | ||
"typescript": "^1.8.0", | ||
"typings": "^0.6.3", | ||
"watch": "latest", | ||
"web-component-tester": "^3.3.21" | ||
"web-component-tester": "^4.2.2" | ||
}, | ||
"dependencies": { | ||
"babel-polyfill": "^6.2.0", | ||
"doctrine": "^0.7.0", | ||
"dom5": "^1.1.0", | ||
"es6-promise": "^2.1.0", | ||
"espree": "^2.0.1", | ||
"escodegen": "^1.7.0", | ||
"espree": "^3.1.3", | ||
"estraverse": "^3.1.0", | ||
@@ -42,0 +54,0 @@ "path-is-absolute": "^1.0.0" |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
4856736
29
3968
1
7
11
1
+ Addedbabel-polyfill@^6.2.0
+ Addedescodegen@^1.7.0
+ Addedacorn@3.3.05.7.4(transitive)
+ Addedacorn-jsx@3.0.1(transitive)
+ Addedbabel-polyfill@6.26.0(transitive)
+ Addedbabel-runtime@6.26.0(transitive)
+ Addedcore-js@2.6.12(transitive)
+ Addeddeep-is@0.1.4(transitive)
+ Addedescodegen@1.14.3(transitive)
+ Addedespree@3.5.4(transitive)
+ Addedesprima@4.0.1(transitive)
+ Addedestraverse@4.3.0(transitive)
+ Addedesutils@2.0.3(transitive)
+ Addedfast-levenshtein@2.0.6(transitive)
+ Addedlevn@0.3.0(transitive)
+ Addedoptionator@0.8.3(transitive)
+ Addedprelude-ls@1.1.2(transitive)
+ Addedregenerator-runtime@0.10.50.11.1(transitive)
+ Addedsource-map@0.6.1(transitive)
+ Addedtype-check@0.3.2(transitive)
+ Addedword-wrap@1.2.5(transitive)
- Removedes6-promise@^2.1.0
- Removedes6-promise@2.3.0(transitive)
- Removedespree@2.2.5(transitive)
Updatedespree@^3.1.3