All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.polymer.public.bower_components.hydrolysis.hydrolysis.js Maven / Gradle / Ivy

There is a newer version: 1.9.3.1
Show newest version
require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o} elements The elements from the document
 * @property {Array}  features The features from the document
 * @property {Array}  behaviors The behaviors from the document
 */

/**
 * The metadata of an entire HTML document, in promises.
 * @typedef {Object} AnalyzedDocument
 * @memberof hydrolysis
 * @property {string} href The url of the document.
 * @property {Promise}  htmlLoaded The parsed representation of
 *     the doc. Use the `ast` property to get the full `parse5` ast
 *
 * @property {Promise>} depsLoaded Resolves to the list of this
 *     Document's transitive import dependencies
 *
 * @property {Array} depHrefs The direct dependencies of the document.
 *
 * @property {Promise} metadataLoaded Resolves to the list of
 *     this Document's import dependencies
 */

/**
 * 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;

  /**
   * A list of all elements the `Analyzer` has metadata for.
   * @member {Array.}
   */
  this.elements = [];

  /**
   * A view into `elements`, keyed by tag name.
   * @member {Object.}
   */
  this.elementsByTagName = {};

  /**
   * A list of API features added to `Polymer.Base` encountered by the
   * analyzer.
   * @member {Array}
   */
  this.features = [];

  /**
   * The behaviors collected by the analysis pass.
   *
   * @member {Array}
   */
  this.behaviors = [];

  /**
   * A map, keyed by absolute path, of Document metadata.
   * @member {Object}
   */
  this.html = {};

  this._parsedDocuments = {};
  
  /**
   * A map, keyed by path, of HTML document ASTs.
   * @type {Object}
   */
  this.parsedDocuments = {};

  /**
   * A map, keyed by path, of document content.
   * @type {Object}
   */
  this._content = {};
};

/**
 * Options for `Analyzer.analzye`
 * @typedef {Object} LoadOptions
 * @memberof hydrolysis
 * @property {boolean} noAnnotations Whether `annotate()` should be skipped.
 * @property {boolean} clean Whether the generated descriptors should be cleaned
 *     of redundant data.
 * @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.
 */

/**
 * 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} 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);

  var loader = new FileLoader();
  var PrimaryResolver = typeof window === 'undefined' ?
                        require('./loader/fs-resolver') :
                        require('./loader/xhr-resolver');
  loader.addResolver(new PrimaryResolver(options));
  loader.addResolver(new NoopResolver({test: options.filter}));

  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);
  });
};

/**
 * @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;
  };
}

Analyzer.prototype.load = function load(href) {
  return this.loader.request(href).then(function(content) {
    return new Promise(function(resolve, reject) {
      setImmediate(function() {
        this._content[href] = content;
        resolve(this._parseHTML(content, href));
      }.bind(this));
    }.bind(this));
  }.bind(this));
};

/**
 * 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);
    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);
      }
    }
    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;
  this.parsedDocuments[href] = parsed.ast;
  this.html[href] = {
      href: href,
      htmlLoaded: htmlLoaded,
      metadataLoaded: metadataLoaded,
      depHrefs: depHrefs,
      depsLoaded: depsLoaded
  };
  return this.html[href];
};

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);
  });
};

Analyzer.prototype._processScript = function _processScript(script, href) {
  var src = dom5.getAttribute(script, 'src');
  var parsedJs;
  if (!src) {
    try {
      parsedJs = jsParse(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.line - 1;
      }
      line += err.lineNumber;
      col += err.column;
      var message = "Error parsing script in " + href + " at " + line + ":" + col;
      message += "\n" + err.description;
      throw new Error(message);
    }
    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.behaviors = this.behaviors.concat(parsedJs.behaviors);
    }
    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);
  }
};

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));
};

/**
 * List all the html dependencies for the document at `href`.
 * @param  {string}                   href      The href to get dependencies for.
 * @param  {Object.=} found     An object keyed by URL of the
 *     already resolved dependencies.
 * @param  {boolean=}                transitive Whether to load transitive
 *     dependencies. Defaults to true.
 * @return {Array.}  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;
  });
};

/**
 * 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));
};

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];
        }
        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);
            });
          }
          return metadata;
        });
      });
    }.bind(this));
  }.bind(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);
}

// 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')
  )
);

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;
};

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;
};

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;
};

/**
 * 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