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 |