Skip to content
This repository has been archived by the owner on Mar 13, 2018. It is now read-only.

Commit

Permalink
Merge pull request #6 from Polymer/recursive-loader
Browse files Browse the repository at this point in the history
Recursive loader refactor
  • Loading branch information
Steve Orvell committed Feb 13, 2014
2 parents c79fbfd + 8e37b28 commit 427c8cb
Show file tree
Hide file tree
Showing 17 changed files with 334 additions and 75 deletions.
1 change: 1 addition & 0 deletions build.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"../CustomElements/build.json",
"src/patches-custom-elements.js",
"src/loader.js",
"src/styleloader.js",

"../PointerEvents/build.json",
"../PointerGestures/build.json",
Expand Down
1 change: 1 addition & 0 deletions platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ function processFlags(flags) {
'../CustomElements/custom-elements.js',
'src/patches-custom-elements.js',
'src/loader.js',
'src/styleloader.js',

// TODO(sjmiles): pointergestures.js loads pointerevents, but
// the build.json does not
Expand Down
152 changes: 77 additions & 75 deletions src/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,86 +4,88 @@
* license that can be found in the LICENSE file.
*/
(function(scope) {
var endOfMicrotask = scope.endOfMicrotask;

var STYLE_SELECTOR = 'style';

var urlResolver = scope.urlResolver;

var loader = {
cacheStyles: function(styles, callback) {
var css = [];
for (var i=0, l=styles.length, s; (i<l) && (s=styles[i]); i++) {
css.push(s.textContent);
}
cacheCssText(css.join('\n'), callback);
},
xhrStyles: function(styles, callback) {
var loaded=0, l = styles.length;
// called in the context of the style
function loadedStyle(style) {
//console.log(style.textContent);
loaded++;
if (loaded === l && callback) {
callback();
}
}
for (var i=0, s; (i<l) && (s=styles[i]); i++) {
xhrLoadStyle(s, loadedStyle);
}
// Generic url loader
function Loader(regex) {
this.regex = regex;
}
};

// use the platform to preload styles
var preloadElement = document.createElement('preloader');
preloadElement.style.display = 'none';
var preloadRoot = preloadElement.createShadowRoot();
document.head.appendChild(preloadElement);

function cacheCssText(cssText, callback) {
var style = createStyleElement(cssText);
if (callback) {
style.addEventListener('load', callback);
style.addEventListener('error', callback);
}
preloadRoot.appendChild(style);
}

function createStyleElement(cssText, scope) {
scope = scope || document;
scope = scope.createElement ? scope : scope.ownerDocument;
var style = scope.createElement('style');
style.textContent = cssText;
return style;
}

// TODO(sorvell): use a common loader shared with HTMLImports polyfill
// currently, this just loads the first @import per style element
// and does not recurse into loaded elements; we'll address this with a
// generalized loader that's built out of the one in the HTMLImports polyfill.
// polyfill the loading of a style element's @import via xhr
function xhrLoadStyle(style, callback) {
HTMLImports.xhr.load(atImportUrlFromStyle(style), function (err, resource,
url) {
replaceAtImportWithCssText(this, url, resource);
this.textContent = urlResolver.resolveCssText(this.textContent, url);
callback && callback(this);
}, style);
}
Loader.prototype = {
// TODO(dfreedm): there may be a better factoring here
// extract absolute urls from the text (full of relative urls)
extractUrls: function(text, base) {
var matches = [];
var matched, u;
while ((matched = this.regex.exec(text))) {
u = new URL(matched[1], base);
matches.push({matched: matched[0], url: u.href});
}
return matches;
},
// take a text blob, a root url, and a callback and load all the urls found within the text
// returns a map of absolute url to text
process: function(text, root, callback) {
var matches = this.extractUrls(text, root);
this.fetch(matches, {}, callback);
},
// build a mapping of url -> text from matches
fetch: function(matches, map, callback) {
var inflight = matches.length;

var atImportRe = /@import\s[(]?['"]?([^\s'";)]*)/;
// return early if there is no fetching to be done
if (!inflight) {
return callback(map);
}

// get the first @import rule from a style
function atImportUrlFromStyle(style) {
var matches = style.textContent.match(atImportRe);
return matches && matches[1];
}
var done = function() {
if (--inflight === 0) {
callback(map);
}
};

function replaceAtImportWithCssText(style, url, cssText) {
var re = new RegExp('@import[^;]*' + url + '[^;]*;', 'i');
style.textContent = style.textContent.replace(re, cssText);
}
// map url -> responseText
var handleXhr = function(err, request) {
var match = request.match;
var key = match.url;
// handle errors with an empty string
if (err) {
map[key] = '';
return done();
}
var response = request.response || request.responseText;
map[key] = response;
this.fetch(this.extractUrls(response, key), map, done);
};

// exports
scope.loader = loader;
var m, req, url;
for (var i = 0; i < inflight; i++) {
m = matches[i];
url = m.url;
// if this url has already been requested, skip requesting it again
if (map[url]) {
// Async call to done to simplify the inflight logic
endOfMicrotask(done);
continue;
}
req = this.xhr(url, handleXhr, this);
req.match = m;
// tag the map with an XHR request to deduplicate at the same level
map[url] = req;
}
},
xhr: function(url, callback, scope) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.send();
request.onload = function() {
callback.call(scope, null, request);
};
request.onerror = function() {
callback.call(scope, request, null);
};
return request;
}
};

scope.Loader = Loader;
})(window.Platform);
108 changes: 108 additions & 0 deletions src/styleloader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2014 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) {

var urlResolver = scope.urlResolver;
var Loader = scope.Loader;

function StyleResolver() {
this.loader = new Loader(this.regex);
}
StyleResolver.prototype = {
regex: /@import\s+(?:url)?["'\(]*([^'"\)]*)['"\)]*;/g,
// Recursively replace @imports with the text at that url
resolve: function(text, url, callback) {
var done = function(map) {
callback(this.flatten(text, url, map));
}.bind(this);
this.loader.process(text, url, done);
},
// resolve the textContent of a style node
resolveNode: function(style, callback) {
var text = style.textContent;
var url = style.ownerDocument.baseURI;
var done = function(text) {
style.textContent = text;
callback(style);
};
this.resolve(text, url, done);
},
// flatten all the @imports to text
flatten: function(text, base, map) {
var matches = this.loader.extractUrls(text, base);
var match, url, intermediate;
for (var i = 0; i < matches.length; i++) {
match = matches[i];
url = match.url;
// resolve any css text to be relative to the importer
intermediate = urlResolver.resolveCssText(map[url], url);
// flatten intermediate @imports
intermediate = this.flatten(intermediate, url, map);
text = text.replace(match.matched, intermediate);
}
return text;
}
};

var styleResolver = new StyleResolver();
var loader = {
cacheStyles: function(styles, callback) {
var css = [];
for (var i=0, l=styles.length, s; (i<l) && (s=styles[i]); i++) {
css.push(s.textContent);
}
cacheCssText(css.join('\n'), callback);
},
xhrStyles: function(styles, callback) {
var loaded=0, l = styles.length;
// called in the context of the style
function loadedStyle(style) {
//console.log(style.textContent);
loaded++;
if (loaded === l && callback) {
callback();
}
}
for (var i=0, s; (i<l) && (s=styles[i]); i++) {
styleResolver.resolveNode(s, loadedStyle);
}
},
};

// use the platform to preload styles
var preloadElement = document.createElement('preloader');
preloadElement.style.display = 'none';
var preloadRoot = preloadElement.createShadowRoot();
document.head.appendChild(preloadElement);

function cacheCssText(cssText, callback) {
var style = createStyleElement(cssText);
if (callback) {
style.addEventListener('load', callback);
style.addEventListener('error', callback);
}
preloadRoot.appendChild(style);
}

function createStyleElement(cssText, scope) {
scope = scope || document;
scope = scope.createElement ? scope : scope.ownerDocument;
var style = scope.createElement('style');
style.textContent = cssText;
return style;
}

function xhrLoadStyle(style, callback) {
styleResolver.resolveNode(style, function(){
callback(style);
});
}

// exports
scope.loader = loader;
scope.styleResolver = styleResolver;

})(window.Platform);
38 changes: 38 additions & 0 deletions test/html/loader-deduplicate.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<!--
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.
-->
<html>
<head>
<title>Deduplicating Loader</title>
<meta charset="UTF-8">
<script>
var origXHR = window.XMLHttpRequest;
var XHRCount = 0;
window.XMLHttpRequest = function() {
XHRCount++;
return new origXHR();
};
</script>
<script src="../../../tools/test/htmltest.js"></script>
<script src="../../../tools/test/chai/chai.js"></script>
<script src="../../platform.js"></script>
</head>
<body>
<x-thing id="test">
import: styling/rules/colors.css;
import: styling/rules/colors.css;
</x-thing>
<script>
var assert = chai.assert;
var loader = new Platform.Loader(/import: ([^;]*)/g);
var test = document.querySelector('#test');
loader.process(test.textContent, document.baseURI, function(map) {
assert.equal(XHRCount, 1);
done();
});
</script>
</body>
</html>
Loading

0 comments on commit 427c8cb

Please sign in to comment.