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

Commit

Permalink
Recursively resolve @import rules
Browse files Browse the repository at this point in the history
add tests for style loader

Use cssRules, wait for polymer to fully load

Fix bug after removing index as an argument

Add more @imports to test with

Make the XHR loader more generic, expose on Platform.loader.fetch

Split apart "Loading" from "Flattening"

Generic Loader takes urls, returns a map of url -> text.
StyleResolver takes maps of urls and flattens the @imports

Place recursive test in a non-conflicting location

Factor Loader to deduplicate XHRs for the same absolute url

Add a "process" to Loader that acts as an all in one.

Add tests for Loader deduplication
  • Loading branch information
dfreedm committed Feb 13, 2014
1 parent c79fbfd commit 8e37b28
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 8e37b28

Please sign in to comment.