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