blob: a77e7165e762204c44d3b7fa8cd4b8af8e325c26 [file] [log] [blame]
/*
* Copyright 2013 The Polymer Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
(function(scope) {
if (!scope) {
scope = window.HTMLImports = {flags:{}};
}
// imports
var xhr = scope.xhr;
// importer
var IMPORT_LINK_TYPE = 'import';
var STYLE_LINK_TYPE = 'stylesheet';
// highlander object represents a primary document (the argument to 'load')
// at the root of a tree of documents
// for any document, importer:
// - loads any linked documents (with deduping), modifies paths and feeds them back into importer
// - loads text of external script tags
// - loads text of external style tags inside of <element>, modifies paths
// when importer 'modifies paths' in a document, this includes
// - href/src/action in node attributes
// - paths in inline stylesheets
// - all content inside templates
// linked style sheets in an import have their own path fixed up when their containing import modifies paths
// linked style sheets in an <element> are loaded, and the content gets path fixups
// inline style sheets get path fixups when their containing import modifies paths
var loader;
var importer = {
documents: {},
cache: {},
preloadSelectors: [
'link[rel=' + IMPORT_LINK_TYPE + ']',
'element link[rel=' + STYLE_LINK_TYPE + ']',
'template',
'script[src]:not([type])',
'script[src][type="text/javascript"]'
].join(','),
loader: function(inNext) {
// construct a loader instance
loader = new Loader(importer.loaded, inNext);
// alias the loader cache (for debugging)
loader.cache = importer.cache;
return loader;
},
load: function(inDocument, inNext) {
// construct a loader instance
loader = importer.loader(inNext);
// add nodes from document into loader queue
importer.preload(inDocument);
},
preload: function(inDocument) {
// all preloadable nodes in inDocument
var nodes = inDocument.querySelectorAll(importer.preloadSelectors);
// from the main document, only load imports
// TODO(sjmiles): do this by altering the selector list instead
nodes = this.filterMainDocumentNodes(inDocument, nodes);
// extra link nodes from templates, filter templates out of the nodes list
nodes = this.extractTemplateNodes(nodes);
// add these nodes to loader's queue
loader.addNodes(nodes);
},
filterMainDocumentNodes: function(inDocument, nodes) {
if (inDocument === document) {
nodes = Array.prototype.filter.call(nodes, function(n) {
return !isScript(n);
});
}
return nodes;
},
extractTemplateNodes: function(nodes) {
var extra = [];
nodes = Array.prototype.filter.call(nodes, function(n) {
if (n.localName === 'template') {
if (n.content) {
var l$ = n.content.querySelectorAll('link[rel=' + STYLE_LINK_TYPE +
']');
if (l$.length) {
extra = extra.concat(Array.prototype.slice.call(l$, 0));
}
}
return false;
}
return true;
});
if (extra.length) {
nodes = nodes.concat(extra);
}
return nodes;
},
loaded: function(url, elt, resource) {
if (isDocumentLink(elt)) {
var document = importer.documents[url];
// if we've never seen a document at this url
if (!document) {
// generate an HTMLDocument from data
document = makeDocument(resource, url);
// resolve resource paths relative to host document
path.resolvePathsInHTML(document);
// cache document
importer.documents[url] = document;
// add nodes from this document to the loader queue
importer.preload(document);
}
// store import record
elt.import = {
href: url,
ownerNode: elt,
content: document
};
// store document resource
elt.content = resource = document;
}
// store generic resource
// TODO(sorvell): fails for nodes inside <template>.content
// see https://code.google.com/p/chromium/issues/detail?id=249381.
elt.__resource = resource;
// css path fixups
if (isStylesheetLink(elt)) {
path.resolvePathsInStylesheet(elt);
}
}
};
function isDocumentLink(elt) {
return isLinkRel(elt, IMPORT_LINK_TYPE);
}
function isStylesheetLink(elt) {
return isLinkRel(elt, STYLE_LINK_TYPE);
}
function isLinkRel(elt, rel) {
return elt.localName === 'link' && elt.getAttribute('rel') === rel;
}
function isScript(elt) {
return elt.localName === 'script';
}
function makeDocument(resource, url) {
// create a new HTML document
var doc = resource;
if (!(doc instanceof Document)) {
doc = document.implementation.createHTMLDocument(IMPORT_LINK_TYPE);
// install html
doc.body.innerHTML = resource;
}
// cache the new document's source url
doc._URL = url;
// establish a relative path via <base>
var base = doc.createElement('base');
base.setAttribute('href', document.baseURI);
doc.head.appendChild(base);
// TODO(sorvell): MDV Polyfill intrusion: boostrap template polyfill
if (window.HTMLTemplateElement && HTMLTemplateElement.bootstrap) {
HTMLTemplateElement.bootstrap(doc);
}
return doc;
}
var Loader = function(inOnLoad, inOnComplete) {
this.onload = inOnLoad;
this.oncomplete = inOnComplete;
this.inflight = 0;
this.pending = {};
this.cache = {};
};
Loader.prototype = {
addNodes: function(inNodes) {
// number of transactions to complete
this.inflight += inNodes.length;
// commence transactions
forEach(inNodes, this.require, this);
// anything to do?
this.checkDone();
},
require: function(inElt) {
var url = path.nodeUrl(inElt);
// TODO(sjmiles): ad-hoc
inElt.__nodeUrl = url;
// deduplication
if (!this.dedupe(url, inElt)) {
// fetch this resource
this.fetch(url, inElt);
}
},
dedupe: function(inUrl, inElt) {
if (this.pending[inUrl]) {
// add to list of nodes waiting for inUrl
this.pending[inUrl].push(inElt);
// don't need fetch
return true;
}
if (this.cache[inUrl]) {
// complete load using cache data
this.onload(inUrl, inElt, loader.cache[inUrl]);
// finished this transaction
this.tail();
// don't need fetch
return true;
}
// first node waiting for inUrl
this.pending[inUrl] = [inElt];
// need fetch (not a dupe)
return false;
},
fetch: function(url, elt) {
var receiveXhr = function(err, resource) {
this.receive(url, elt, err, resource);
}.bind(this);
xhr.load(url, receiveXhr);
// TODO(sorvell): blocked on
// https://code.google.com/p/chromium/issues/detail?id=257221
// xhr'ing for a document makes scripts in imports runnable; otherwise
// they are not; however, it requires that we have doctype=html in
// the import which is unacceptable. This is only needed on Chrome
// to avoid the bug above.
/*
if (isDocumentLink(elt)) {
xhr.loadDocument(url, receiveXhr);
} else {
xhr.load(url, receiveXhr);
}
*/
},
receive: function(inUrl, inElt, inErr, inResource) {
if (!inErr) {
loader.cache[inUrl] = inResource;
}
loader.pending[inUrl].forEach(function(e) {
if (!inErr) {
this.onload(inUrl, e, inResource);
}
this.tail();
}, this);
loader.pending[inUrl] = null;
},
tail: function() {
--this.inflight;
this.checkDone();
},
checkDone: function() {
if (!this.inflight) {
this.oncomplete();
}
}
};
var URL_ATTRS = ['href', 'src', 'action'];
var URL_ATTRS_SELECTOR = '[' + URL_ATTRS.join('],[') + ']';
var URL_TEMPLATE_SEARCH = '{{.*}}';
var path = {
nodeUrl: function(inNode) {
return path.resolveUrl(path.getDocumentUrl(document), path.hrefOrSrc(inNode));
},
hrefOrSrc: function(inNode) {
return inNode.getAttribute("href") || inNode.getAttribute("src");
},
documentUrlFromNode: function(inNode) {
return path.getDocumentUrl(inNode.ownerDocument || inNode);
},
getDocumentUrl: function(inDocument) {
var url = inDocument &&
// TODO(sjmiles): ShadowDOMPolyfill intrusion
(inDocument._URL || (inDocument.impl && inDocument.impl._URL)
|| inDocument.baseURI || inDocument.URL)
|| '';
// take only the left side if there is a #
return url.split('#')[0];
},
resolveUrl: function(inBaseUrl, inUrl, inRelativeToDocument) {
if (this.isAbsUrl(inUrl)) {
return inUrl;
}
var url = this.compressUrl(this.urlToPath(inBaseUrl) + inUrl);
if (inRelativeToDocument) {
url = path.makeRelPath(path.getDocumentUrl(document), url);
}
return url;
},
isAbsUrl: function(inUrl) {
return /(^data:)|(^http[s]?:)|(^\/)/.test(inUrl);
},
urlToPath: function(inBaseUrl) {
var parts = inBaseUrl.split("/");
parts.pop();
parts.push('');
return parts.join("/");
},
compressUrl: function(inUrl) {
var parts = inUrl.split("/");
for (var i=0, p; i<parts.length; i++) {
p = parts[i];
if (p === "..") {
parts.splice(i-1, 2);
i -= 2;
}
}
return parts.join("/");
},
// make a relative path from source to target
makeRelPath: function(inSource, inTarget) {
var s, t;
s = this.compressUrl(inSource).split("/");
t = this.compressUrl(inTarget).split("/");
while (s.length && s[0] === t[0]){
s.shift();
t.shift();
}
for(var i = 0, l = s.length-1; i < l; i++) {
t.unshift("..");
}
var r = t.join("/");
return r;
},
resolvePathsInHTML: function(root, url) {
url = url || path.documentUrlFromNode(root)
path.resolveAttributes(root, url);
path.resolveStyleElts(root, url);
// handle template.content
var templates = root.querySelectorAll('template');
if (templates) {
forEach(templates, function(t) {
if (t.content) {
path.resolvePathsInHTML(t.content, url);
}
});
}
},
resolvePathsInStylesheet: function(inSheet) {
var docUrl = path.nodeUrl(inSheet);
inSheet.__resource = path.resolveCssText(inSheet.__resource, docUrl);
},
resolveStyleElts: function(inRoot, inUrl) {
var styles = inRoot.querySelectorAll('style');
if (styles) {
forEach(styles, function(style) {
style.textContent = path.resolveCssText(style.textContent, inUrl);
});
}
},
resolveCssText: function(inCssText, inBaseUrl) {
return inCssText.replace(/url\([^)]*\)/g, function(inMatch) {
// find the url path, ignore quotes in url string
var urlPath = inMatch.replace(/["']/g, "").slice(4, -1);
urlPath = path.resolveUrl(inBaseUrl, urlPath, true);
return "url(" + urlPath + ")";
});
},
resolveAttributes: function(inRoot, inUrl) {
// search for attributes that host urls
var nodes = inRoot && inRoot.querySelectorAll(URL_ATTRS_SELECTOR);
if (nodes) {
forEach(nodes, function(n) {
this.resolveNodeAttributes(n, inUrl);
}, this);
}
},
resolveNodeAttributes: function(inNode, inUrl) {
URL_ATTRS.forEach(function(v) {
var attr = inNode.attributes[v];
if (attr && attr.value &&
(attr.value.search(URL_TEMPLATE_SEARCH) < 0)) {
var urlPath = path.resolveUrl(inUrl, attr.value, true);
attr.value = urlPath;
}
});
}
};
xhr = xhr || {
async: true,
ok: function(inRequest) {
return (inRequest.status >= 200 && inRequest.status < 300)
|| (inRequest.status === 304)
|| (inRequest.status === 0);
},
load: function(url, next, nextContext) {
var request = new XMLHttpRequest();
if (scope.flags.debug || scope.flags.bust) {
url += '?' + Math.random();
}
request.open('GET', url, xhr.async);
request.addEventListener('readystatechange', function(e) {
if (request.readyState === 4) {
next.call(nextContext, !xhr.ok(request) && request,
request.response, url);
}
});
request.send();
return request;
},
loadDocument: function(url, next, nextContext) {
this.load(url, next, nextContext).responseType = 'document';
}
};
var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach);
// exports
scope.path = path;
scope.xhr = xhr;
scope.importer = importer;
scope.getDocumentUrl = path.getDocumentUrl;
scope.IMPORT_LINK_TYPE = IMPORT_LINK_TYPE;
})(window.HTMLImports);