OLD | NEW |
(Empty) | |
| 1 /* |
| 2 * Copyright 2013 The Polymer Authors. All rights reserved. |
| 3 * Use of this source code is governed by a BSD-style |
| 4 * license that can be found in the LICENSE file. |
| 5 */ |
| 6 |
| 7 (function(scope) { |
| 8 |
| 9 if (!scope) { |
| 10 scope = window.HTMLImports = {flags:{}}; |
| 11 } |
| 12 |
| 13 // imports |
| 14 |
| 15 var xhr = scope.xhr; |
| 16 |
| 17 // importer |
| 18 |
| 19 var IMPORT_LINK_TYPE = 'import'; |
| 20 var STYLE_LINK_TYPE = 'stylesheet'; |
| 21 |
| 22 // highlander object represents a primary document (the argument to 'load') |
| 23 // at the root of a tree of documents |
| 24 |
| 25 // for any document, importer: |
| 26 // - loads any linked documents (with deduping), modifies paths and feeds them b
ack into importer |
| 27 // - loads text of external script tags |
| 28 // - loads text of external style tags inside of <element>, modifies paths |
| 29 |
| 30 // when importer 'modifies paths' in a document, this includes |
| 31 // - href/src/action in node attributes |
| 32 // - paths in inline stylesheets |
| 33 // - all content inside templates |
| 34 |
| 35 // linked style sheets in an import have their own path fixed up when their cont
aining import modifies paths |
| 36 // linked style sheets in an <element> are loaded, and the content gets path fix
ups |
| 37 // inline style sheets get path fixups when their containing import modifies pat
hs |
| 38 |
| 39 var loader; |
| 40 |
| 41 var importer = { |
| 42 documents: {}, |
| 43 cache: {}, |
| 44 preloadSelectors: [ |
| 45 'link[rel=' + IMPORT_LINK_TYPE + ']', |
| 46 'element link[rel=' + STYLE_LINK_TYPE + ']', |
| 47 'template', |
| 48 'script[src]:not([type])', |
| 49 'script[src][type="text/javascript"]' |
| 50 ].join(','), |
| 51 loader: function(inNext) { |
| 52 // construct a loader instance |
| 53 loader = new Loader(importer.loaded, inNext); |
| 54 // alias the loader cache (for debugging) |
| 55 loader.cache = importer.cache; |
| 56 return loader; |
| 57 }, |
| 58 load: function(inDocument, inNext) { |
| 59 // construct a loader instance |
| 60 loader = importer.loader(inNext); |
| 61 // add nodes from document into loader queue |
| 62 importer.preload(inDocument); |
| 63 }, |
| 64 preload: function(inDocument) { |
| 65 // all preloadable nodes in inDocument |
| 66 var nodes = inDocument.querySelectorAll(importer.preloadSelectors); |
| 67 // from the main document, only load imports |
| 68 // TODO(sjmiles): do this by altering the selector list instead |
| 69 nodes = this.filterMainDocumentNodes(inDocument, nodes); |
| 70 // extra link nodes from templates, filter templates out of the nodes list |
| 71 nodes = this.extractTemplateNodes(nodes); |
| 72 // add these nodes to loader's queue |
| 73 loader.addNodes(nodes); |
| 74 }, |
| 75 filterMainDocumentNodes: function(inDocument, nodes) { |
| 76 if (inDocument === document) { |
| 77 nodes = Array.prototype.filter.call(nodes, function(n) { |
| 78 return !isScript(n); |
| 79 }); |
| 80 } |
| 81 return nodes; |
| 82 }, |
| 83 extractTemplateNodes: function(nodes) { |
| 84 var extra = []; |
| 85 nodes = Array.prototype.filter.call(nodes, function(n) { |
| 86 if (n.localName === 'template') { |
| 87 if (n.content) { |
| 88 var l$ = n.content.querySelectorAll('link[rel=' + STYLE_LINK_TYPE + |
| 89 ']'); |
| 90 if (l$.length) { |
| 91 extra = extra.concat(Array.prototype.slice.call(l$, 0)); |
| 92 } |
| 93 } |
| 94 return false; |
| 95 } |
| 96 return true; |
| 97 }); |
| 98 if (extra.length) { |
| 99 nodes = nodes.concat(extra); |
| 100 } |
| 101 return nodes; |
| 102 }, |
| 103 loaded: function(url, elt, resource) { |
| 104 if (isDocumentLink(elt)) { |
| 105 var document = importer.documents[url]; |
| 106 // if we've never seen a document at this url |
| 107 if (!document) { |
| 108 // generate an HTMLDocument from data |
| 109 document = makeDocument(resource, url); |
| 110 // resolve resource paths relative to host document |
| 111 path.resolvePathsInHTML(document); |
| 112 // cache document |
| 113 importer.documents[url] = document; |
| 114 // add nodes from this document to the loader queue |
| 115 importer.preload(document); |
| 116 } |
| 117 // store import record |
| 118 elt.import = { |
| 119 href: url, |
| 120 ownerNode: elt, |
| 121 content: document |
| 122 }; |
| 123 // store document resource |
| 124 elt.content = resource = document; |
| 125 } |
| 126 // store generic resource |
| 127 // TODO(sorvell): fails for nodes inside <template>.content |
| 128 // see https://code.google.com/p/chromium/issues/detail?id=249381. |
| 129 elt.__resource = resource; |
| 130 // css path fixups |
| 131 if (isStylesheetLink(elt)) { |
| 132 path.resolvePathsInStylesheet(elt); |
| 133 } |
| 134 } |
| 135 }; |
| 136 |
| 137 function isDocumentLink(elt) { |
| 138 return isLinkRel(elt, IMPORT_LINK_TYPE); |
| 139 } |
| 140 |
| 141 function isStylesheetLink(elt) { |
| 142 return isLinkRel(elt, STYLE_LINK_TYPE); |
| 143 } |
| 144 |
| 145 function isLinkRel(elt, rel) { |
| 146 return elt.localName === 'link' && elt.getAttribute('rel') === rel; |
| 147 } |
| 148 |
| 149 function isScript(elt) { |
| 150 return elt.localName === 'script'; |
| 151 } |
| 152 |
| 153 function makeDocument(resource, url) { |
| 154 // create a new HTML document |
| 155 var doc = resource; |
| 156 if (!(doc instanceof Document)) { |
| 157 doc = document.implementation.createHTMLDocument(IMPORT_LINK_TYPE); |
| 158 // install html |
| 159 doc.body.innerHTML = resource; |
| 160 } |
| 161 // cache the new document's source url |
| 162 doc._URL = url; |
| 163 // establish a relative path via <base> |
| 164 var base = doc.createElement('base'); |
| 165 base.setAttribute('href', document.baseURI); |
| 166 doc.head.appendChild(base); |
| 167 // TODO(sorvell): MDV Polyfill intrusion: boostrap template polyfill |
| 168 if (window.HTMLTemplateElement && HTMLTemplateElement.bootstrap) { |
| 169 HTMLTemplateElement.bootstrap(doc); |
| 170 } |
| 171 return doc; |
| 172 } |
| 173 |
| 174 var Loader = function(inOnLoad, inOnComplete) { |
| 175 this.onload = inOnLoad; |
| 176 this.oncomplete = inOnComplete; |
| 177 this.inflight = 0; |
| 178 this.pending = {}; |
| 179 this.cache = {}; |
| 180 }; |
| 181 |
| 182 Loader.prototype = { |
| 183 addNodes: function(inNodes) { |
| 184 // number of transactions to complete |
| 185 this.inflight += inNodes.length; |
| 186 // commence transactions |
| 187 forEach(inNodes, this.require, this); |
| 188 // anything to do? |
| 189 this.checkDone(); |
| 190 }, |
| 191 require: function(inElt) { |
| 192 var url = path.nodeUrl(inElt); |
| 193 // TODO(sjmiles): ad-hoc |
| 194 inElt.__nodeUrl = url; |
| 195 // deduplication |
| 196 if (!this.dedupe(url, inElt)) { |
| 197 // fetch this resource |
| 198 this.fetch(url, inElt); |
| 199 } |
| 200 }, |
| 201 dedupe: function(inUrl, inElt) { |
| 202 if (this.pending[inUrl]) { |
| 203 // add to list of nodes waiting for inUrl |
| 204 this.pending[inUrl].push(inElt); |
| 205 // don't need fetch |
| 206 return true; |
| 207 } |
| 208 if (this.cache[inUrl]) { |
| 209 // complete load using cache data |
| 210 this.onload(inUrl, inElt, loader.cache[inUrl]); |
| 211 // finished this transaction |
| 212 this.tail(); |
| 213 // don't need fetch |
| 214 return true; |
| 215 } |
| 216 // first node waiting for inUrl |
| 217 this.pending[inUrl] = [inElt]; |
| 218 // need fetch (not a dupe) |
| 219 return false; |
| 220 }, |
| 221 fetch: function(url, elt) { |
| 222 var receiveXhr = function(err, resource) { |
| 223 this.receive(url, elt, err, resource); |
| 224 }.bind(this); |
| 225 xhr.load(url, receiveXhr); |
| 226 // TODO(sorvell): blocked on |
| 227 // https://code.google.com/p/chromium/issues/detail?id=257221 |
| 228 // xhr'ing for a document makes scripts in imports runnable; otherwise |
| 229 // they are not; however, it requires that we have doctype=html in |
| 230 // the import which is unacceptable. This is only needed on Chrome |
| 231 // to avoid the bug above. |
| 232 /* |
| 233 if (isDocumentLink(elt)) { |
| 234 xhr.loadDocument(url, receiveXhr); |
| 235 } else { |
| 236 xhr.load(url, receiveXhr); |
| 237 } |
| 238 */ |
| 239 }, |
| 240 receive: function(inUrl, inElt, inErr, inResource) { |
| 241 if (!inErr) { |
| 242 loader.cache[inUrl] = inResource; |
| 243 } |
| 244 loader.pending[inUrl].forEach(function(e) { |
| 245 if (!inErr) { |
| 246 this.onload(inUrl, e, inResource); |
| 247 } |
| 248 this.tail(); |
| 249 }, this); |
| 250 loader.pending[inUrl] = null; |
| 251 }, |
| 252 tail: function() { |
| 253 --this.inflight; |
| 254 this.checkDone(); |
| 255 }, |
| 256 checkDone: function() { |
| 257 if (!this.inflight) { |
| 258 this.oncomplete(); |
| 259 } |
| 260 } |
| 261 }; |
| 262 |
| 263 var URL_ATTRS = ['href', 'src', 'action']; |
| 264 var URL_ATTRS_SELECTOR = '[' + URL_ATTRS.join('],[') + ']'; |
| 265 var URL_TEMPLATE_SEARCH = '{{.*}}'; |
| 266 |
| 267 var path = { |
| 268 nodeUrl: function(inNode) { |
| 269 return path.resolveUrl(path.getDocumentUrl(document), path.hrefOrSrc(inNode)
); |
| 270 }, |
| 271 hrefOrSrc: function(inNode) { |
| 272 return inNode.getAttribute("href") || inNode.getAttribute("src"); |
| 273 }, |
| 274 documentUrlFromNode: function(inNode) { |
| 275 return path.getDocumentUrl(inNode.ownerDocument || inNode); |
| 276 }, |
| 277 getDocumentUrl: function(inDocument) { |
| 278 var url = inDocument && |
| 279 // TODO(sjmiles): ShadowDOMPolyfill intrusion |
| 280 (inDocument._URL || (inDocument.impl && inDocument.impl._URL) |
| 281 || inDocument.baseURI || inDocument.URL) |
| 282 || ''; |
| 283 // take only the left side if there is a # |
| 284 return url.split('#')[0]; |
| 285 }, |
| 286 resolveUrl: function(inBaseUrl, inUrl, inRelativeToDocument) { |
| 287 if (this.isAbsUrl(inUrl)) { |
| 288 return inUrl; |
| 289 } |
| 290 var url = this.compressUrl(this.urlToPath(inBaseUrl) + inUrl); |
| 291 if (inRelativeToDocument) { |
| 292 url = path.makeRelPath(path.getDocumentUrl(document), url); |
| 293 } |
| 294 return url; |
| 295 }, |
| 296 isAbsUrl: function(inUrl) { |
| 297 return /(^data:)|(^http[s]?:)|(^\/)/.test(inUrl); |
| 298 }, |
| 299 urlToPath: function(inBaseUrl) { |
| 300 var parts = inBaseUrl.split("/"); |
| 301 parts.pop(); |
| 302 parts.push(''); |
| 303 return parts.join("/"); |
| 304 }, |
| 305 compressUrl: function(inUrl) { |
| 306 var parts = inUrl.split("/"); |
| 307 for (var i=0, p; i<parts.length; i++) { |
| 308 p = parts[i]; |
| 309 if (p === "..") { |
| 310 parts.splice(i-1, 2); |
| 311 i -= 2; |
| 312 } |
| 313 } |
| 314 return parts.join("/"); |
| 315 }, |
| 316 // make a relative path from source to target |
| 317 makeRelPath: function(inSource, inTarget) { |
| 318 var s, t; |
| 319 s = this.compressUrl(inSource).split("/"); |
| 320 t = this.compressUrl(inTarget).split("/"); |
| 321 while (s.length && s[0] === t[0]){ |
| 322 s.shift(); |
| 323 t.shift(); |
| 324 } |
| 325 for(var i = 0, l = s.length-1; i < l; i++) { |
| 326 t.unshift(".."); |
| 327 } |
| 328 var r = t.join("/"); |
| 329 return r; |
| 330 }, |
| 331 resolvePathsInHTML: function(root, url) { |
| 332 url = url || path.documentUrlFromNode(root) |
| 333 path.resolveAttributes(root, url); |
| 334 path.resolveStyleElts(root, url); |
| 335 // handle template.content |
| 336 var templates = root.querySelectorAll('template'); |
| 337 if (templates) { |
| 338 forEach(templates, function(t) { |
| 339 if (t.content) { |
| 340 path.resolvePathsInHTML(t.content, url); |
| 341 } |
| 342 }); |
| 343 } |
| 344 }, |
| 345 resolvePathsInStylesheet: function(inSheet) { |
| 346 var docUrl = path.nodeUrl(inSheet); |
| 347 inSheet.__resource = path.resolveCssText(inSheet.__resource, docUrl); |
| 348 }, |
| 349 resolveStyleElts: function(inRoot, inUrl) { |
| 350 var styles = inRoot.querySelectorAll('style'); |
| 351 if (styles) { |
| 352 forEach(styles, function(style) { |
| 353 style.textContent = path.resolveCssText(style.textContent, inUrl); |
| 354 }); |
| 355 } |
| 356 }, |
| 357 resolveCssText: function(inCssText, inBaseUrl) { |
| 358 return inCssText.replace(/url\([^)]*\)/g, function(inMatch) { |
| 359 // find the url path, ignore quotes in url string |
| 360 var urlPath = inMatch.replace(/["']/g, "").slice(4, -1); |
| 361 urlPath = path.resolveUrl(inBaseUrl, urlPath, true); |
| 362 return "url(" + urlPath + ")"; |
| 363 }); |
| 364 }, |
| 365 resolveAttributes: function(inRoot, inUrl) { |
| 366 // search for attributes that host urls |
| 367 var nodes = inRoot && inRoot.querySelectorAll(URL_ATTRS_SELECTOR); |
| 368 if (nodes) { |
| 369 forEach(nodes, function(n) { |
| 370 this.resolveNodeAttributes(n, inUrl); |
| 371 }, this); |
| 372 } |
| 373 }, |
| 374 resolveNodeAttributes: function(inNode, inUrl) { |
| 375 URL_ATTRS.forEach(function(v) { |
| 376 var attr = inNode.attributes[v]; |
| 377 if (attr && attr.value && |
| 378 (attr.value.search(URL_TEMPLATE_SEARCH) < 0)) { |
| 379 var urlPath = path.resolveUrl(inUrl, attr.value, true); |
| 380 attr.value = urlPath; |
| 381 } |
| 382 }); |
| 383 } |
| 384 }; |
| 385 |
| 386 xhr = xhr || { |
| 387 async: true, |
| 388 ok: function(inRequest) { |
| 389 return (inRequest.status >= 200 && inRequest.status < 300) |
| 390 || (inRequest.status === 304) |
| 391 || (inRequest.status === 0); |
| 392 }, |
| 393 load: function(url, next, nextContext) { |
| 394 var request = new XMLHttpRequest(); |
| 395 if (scope.flags.debug || scope.flags.bust) { |
| 396 url += '?' + Math.random(); |
| 397 } |
| 398 request.open('GET', url, xhr.async); |
| 399 request.addEventListener('readystatechange', function(e) { |
| 400 if (request.readyState === 4) { |
| 401 next.call(nextContext, !xhr.ok(request) && request, |
| 402 request.response, url); |
| 403 } |
| 404 }); |
| 405 request.send(); |
| 406 return request; |
| 407 }, |
| 408 loadDocument: function(url, next, nextContext) { |
| 409 this.load(url, next, nextContext).responseType = 'document'; |
| 410 } |
| 411 }; |
| 412 |
| 413 var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach); |
| 414 |
| 415 // exports |
| 416 |
| 417 scope.path = path; |
| 418 scope.xhr = xhr; |
| 419 scope.importer = importer; |
| 420 scope.getDocumentUrl = path.getDocumentUrl; |
| 421 scope.IMPORT_LINK_TYPE = IMPORT_LINK_TYPE; |
| 422 |
| 423 })(window.HTMLImports); |
OLD | NEW |