loadCSS Tweaks

* skip loadcss initialization on empty link list
* add `selector` option to specify loadCSS before element
This commit is contained in:
Ben Zörb 2015-07-10 21:55:28 +02:00
parent c11e79bde8
commit e9c00fee13
7 changed files with 257 additions and 37 deletions

View File

@ -66,6 +66,7 @@ Run `inline-critical --help` to see the list of options.
- `extract` will remove the inlined styles from any stylesheets referenced in the HTML - `extract` will remove the inlined styles from any stylesheets referenced in the HTML
- `basePath` will be used when extracting styles to find the files references by `href` attributes - `basePath` will be used when extracting styles to find the files references by `href` attributes
- `ignore` ignore matching stylesheets when inlining. - `ignore` ignore matching stylesheets when inlining.
- `selector` defines the element used by loadCSS as a reference for inlining.
## License ## License

6
cli.js
View File

@ -19,7 +19,8 @@ var help = [
' -i, --ignore Skip matching stylesheets', ' -i, --ignore Skip matching stylesheets',
' -m, --minify Minify the styles before inlining', ' -m, --minify Minify the styles before inlining',
' -e, --extract Remove the inlined styles from any stylesheets referenced in the HTML', ' -e, --extract Remove the inlined styles from any stylesheets referenced in the HTML',
' -b, --base Is used when extracting styles to find the files references by `href` attributes' ' -b, --base Is used when extracting styles to find the files references by `href` attributes',
' -s, --selector Optionally defines the element used by loadCSS as a reference for inlining'
]; ];
var cli = meow({ var cli = meow({
@ -31,7 +32,8 @@ var cli = meow({
i: 'ignore', i: 'ignore',
m: 'minify', m: 'minify',
b: 'base', b: 'base',
e: 'extract' e: 'extract',
s: 'selector'
} }
}); });

View File

@ -55,8 +55,10 @@ module.exports = function (html, styles, options) {
var links = $('link[rel="stylesheet"]').filter(function () { var links = $('link[rel="stylesheet"]').filter(function () {
return !$(this).parents('noscript').length; return !$(this).parents('noscript').length;
}); });
var noscript = $('<noscript>\n</noscript>');
var o = options || {}; var o = options || {};
var target = o.selector || links.get(0) || $('script').get(0);
var $target = $(target);
if (_.isString(o.ignore)) { if (_.isString(o.ignore)) {
o.ignore = [o.ignore]; o.ignore = [o.ignore];
@ -76,48 +78,55 @@ module.exports = function (html, styles, options) {
styles = new CleanCSS().minify(styles).styles; styles = new CleanCSS().minify(styles).styles;
} }
// insert inline styles right before first <link rel="stylesheet" /> // insert inline styles right before first <link rel="stylesheet" />
links.eq(0).before('<style type="text/css">\n' + styles + '\n</style>\n'); $target.before('<style type="text/css">\n' + styles + '\n</style>\n');
// insert noscript block right after stylesheets
links.eq(0).first().after(noscript);
var hrefs = links.map(function (idx, el) { if (links.length) {
return $(this).attr('href'); var noscript = $('<noscript>\n</noscript>');
}).toArray();
// insert noscript block right after stylesheets
$target.after(noscript);
var hrefs = links.map(function (idx, el) {
return $(this).attr('href');
}).toArray();
// extract styles from stylesheets if extract option is set // extract styles from stylesheets if extract option is set
if (o.extract) { if (o.extract) {
if (!o.basePath) { if (!o.basePath) {
throw new Error('Option `basePath` is missing and required when using `extract`!'); throw new Error('Option `basePath` is missing and required when using `extract`!');
}
hrefs = hrefs.map(function (href) {
var file = path.resolve(path.join(o.basePath, href));
if (!fs.existsSync(file)) {
return href;
} }
var diff = normalizeNewline(cave(file, {css: styles})); hrefs = hrefs.map(function (href) {
fs.writeFileSync(reaver.rev(file, diff), diff); var file = path.resolve(path.join(o.basePath, href));
return normalizePath(reaver.rev(href, diff)); if (!fs.existsSync(file)) {
return href;
}
var diff = normalizeNewline(cave(file, {css: styles}));
fs.writeFileSync(reaver.rev(file, diff), diff);
return normalizePath(reaver.rev(href, diff));
});
}
// wrap links to stylesheets in noscript block so that they will evaluated when js is turned off
links.each(function (idx) {
var el = $(this);
el.attr('href', hrefs[idx]);
noscript.append(el);
noscript.append('\n');
}); });
// build js block to load blocking stylesheets and insert it right before
noscript.before('<script>\n' +
'(function(u){' +
loadCSS +
'for(var i in u){loadCSS(u[i]);}' +
'}([\'' + hrefs.join('\',\'') + '\']));\n' +
'</script>\n');
} }
// wrap links to stylesheets in noscript block so that they will evaluated when js is turned off
links.each(function (idx) {
var el = $(this);
el.attr('href', hrefs[idx]);
noscript.append(el);
noscript.append('\n');
});
// build js block to load blocking stylesheets and insert it right before
$(noscript).before('<script>\n' +
'(function(u){' +
loadCSS +
'for(var i in u){loadCSS(u[i]);}' +
'}([\'' + hrefs.join('\',\'') + '\']));\n' +
'</script>\n');
var dom = parse($.html()); var dom = parse($.html());
var markup = render(dom); var markup = render(dom);

View File

@ -0,0 +1,63 @@
<!doctype html>
<html class="no-js">
<head>
<meta charset="utf-8">
<style type="text/css">
.header,.jumbotron{border-bottom:1px solid #e5e5e5}.btn,.jumbotron{text-align:center}body{padding-top:20px;padding-bottom:20px}.header{padding-left:15px;padding-right:15px}.header h3{margin-top:0;margin-bottom:0;line-height:40px;padding-bottom:19px}.jumbotron .btn{font-size:21px;padding:14px 24px}@media screen and (min-width:768px){.container{max-width:730px}.header{padding-left:0;padding-right:0;margin-bottom:30px}.jumbotron{border-bottom:0}}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;font-size:62.5%;-webkit-tap-highlight-color:transparent}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}h1{margin:.67em 0;font-size:36px}@media print{*{color:#000!important;text-shadow:none!important;background:0 0!important;box-shadow:none!important}a{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}a[href^="#"]:after{content:""}h3,p{orphans:3;widows:3}h3{page-break-after:avoid}}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}a{background:0 0;color:#428bca;text-decoration:none}h1,h3{font-family:inherit;font-weight:500;line-height:1.1;color:inherit;margin-top:20px;margin-bottom:10px}h3{font-size:24px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}.text-muted{color:#999}ul{margin-top:0;margin-bottom:10px}.btn,.nav{margin-bottom:0}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.lead{font-size:21px}.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.btn{display:inline-block;padding:6px 12px;font-size:14px;font-weight:400;line-height:1.42857143;white-space:nowrap;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.nav>li,.nav>li>a{position:relative;display:block}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.nav{padding-left:0;list-style:none}.nav>li>a{padding:10px 15px}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a{color:#fff;background-color:#428bca}.jumbotron,.jumbotron h1{color:inherit}.jumbotron{padding:30px;margin-bottom:30px;background-color:#eee}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.container .jumbotron{border-radius:6px}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1{font-size:63px}}.container:after,.container:before,.nav:after,.nav:before{display:table;content:" "}.container:after,.nav:after{clear:both}.pull-right{float:right!important}
</style>
<title>critical css test</title>
<script>
(function(u){function loadCSS(e,n,o,t){"use strict";var d=window.document.createElement("link"),i=n||window.document.getElementsByTagName("script")[0],s=window.document.styleSheets;return d.rel="stylesheet",d.href=e,d.media="only x",t&&(d.onload=t),i.parentNode.insertBefore(d,i),d.onloadcssdefined=function(n){for(var o,t=0;t<s.length;t++)s[t].href&&s[t].href.indexOf(e)>-1&&(o=!0);o?n():setTimeout(function(){d.onloadcssdefined(n)})},d.onloadcssdefined(function(){d.media=o||"all"}),d}for(var i in u){loadCSS(u[i]);}}(['css/main.css','bower_components/bootstrap/dist/css/bootstrap.css']));
</script>
<noscript>
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
</noscript>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
<!-- build:css styles/main.css -->
<!-- endbuild -->
</head>
<body>
<!--[if lt IE 10]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<div class="container">
<div class="header">
<ul class="nav nav-pills pull-right">
<li class="active"><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
</ul>
<h3 class="text-muted">critical css test</h3>
</div>
<div class="jumbotron">
<h1>'Allo, 'Allo!</h1>
<p class="lead">Always a pleasure scaffolding your apps.</p>
<p><a class="btn btn-lg btn-success" href="#">Splendid!</a></p>
</div>
<div class="row marketing">
<div class="col-lg-6">
<h4>HTML5 Boilerplate</h4>
<p>HTML5 Boilerplate is a professional front-end template for building fast, robust, and adaptable web apps or sites.</p>
<h4>Bootstrap</h4>
<p>Sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development.</p>
</div>
</div>
<div class="footer">
<p>♥ from the Yeoman team</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,59 @@
<!doctype html>
<html class="no-js">
<head>
<meta charset="utf-8">
<title>critical css test</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
<!-- build:css styles/main.css -->
<style type="text/css">
.header,.jumbotron{border-bottom:1px solid #e5e5e5}.btn,.jumbotron{text-align:center}body{padding-top:20px;padding-bottom:20px}.header{padding-left:15px;padding-right:15px}.header h3{margin-top:0;margin-bottom:0;line-height:40px;padding-bottom:19px}.jumbotron .btn{font-size:21px;padding:14px 24px}@media screen and (min-width:768px){.container{max-width:730px}.header{padding-left:0;padding-right:0;margin-bottom:30px}.jumbotron{border-bottom:0}}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;font-size:62.5%;-webkit-tap-highlight-color:transparent}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}h1{margin:.67em 0;font-size:36px}@media print{*{color:#000!important;text-shadow:none!important;background:0 0!important;box-shadow:none!important}a{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}a[href^="#"]:after{content:""}h3,p{orphans:3;widows:3}h3{page-break-after:avoid}}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}a{background:0 0;color:#428bca;text-decoration:none}h1,h3{font-family:inherit;font-weight:500;line-height:1.1;color:inherit;margin-top:20px;margin-bottom:10px}h3{font-size:24px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}.text-muted{color:#999}ul{margin-top:0;margin-bottom:10px}.btn,.nav{margin-bottom:0}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.lead{font-size:21px}.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.btn{display:inline-block;padding:6px 12px;font-size:14px;font-weight:400;line-height:1.42857143;white-space:nowrap;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.nav>li,.nav>li>a{position:relative;display:block}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.nav{padding-left:0;list-style:none}.nav>li>a{padding:10px 15px}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a{color:#fff;background-color:#428bca}.jumbotron,.jumbotron h1{color:inherit}.jumbotron{padding:30px;margin-bottom:30px;background-color:#eee}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.container .jumbotron{border-radius:6px}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1{font-size:63px}}.container:after,.container:before,.nav:after,.nav:before{display:table;content:" "}.container:after,.nav:after{clear:both}.pull-right{float:right!important}
</style>
<script>
(function(u){function loadCSS(e,n,o,t){"use strict";var d=window.document.createElement("link"),i=n||window.document.getElementsByTagName("script")[0],s=window.document.styleSheets;return d.rel="stylesheet",d.href=e,d.media="only x",t&&(d.onload=t),i.parentNode.insertBefore(d,i),d.onloadcssdefined=function(n){for(var o,t=0;t<s.length;t++)s[t].href&&s[t].href.indexOf(e)>-1&&(o=!0);o?n():setTimeout(function(){d.onloadcssdefined(n)})},d.onloadcssdefined(function(){d.media=o||"all"}),d}for(var i in u){loadCSS(u[i]);}}(['css/main.css']));
</script>
<noscript>
<link rel="stylesheet" href="css/main.css">
</noscript>
<!-- endbuild -->
</head>
<body>
<!--[if lt IE 10]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<div class="container">
<div class="header">
<ul class="nav nav-pills pull-right">
<li class="active"><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
</ul>
<h3 class="text-muted">critical css test</h3>
</div>
<div class="jumbotron">
<h1>'Allo, 'Allo!</h1>
<p class="lead">Always a pleasure scaffolding your apps.</p>
<p><a class="btn btn-lg btn-success" href="#">Splendid!</a></p>
</div>
<div class="row marketing">
<div class="col-lg-6">
<h4>HTML5 Boilerplate</h4>
<p>HTML5 Boilerplate is a professional front-end template for building fast, robust, and adaptable web apps or sites.</p>
<h4>Bootstrap</h4>
<p>Sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development.</p>
</div>
</div>
<div class="footer">
<p>♥ from the Yeoman team</p>
</div>
</div>
</body>
</html>

57
test/fixtures/loadcss.html vendored Normal file
View File

@ -0,0 +1,57 @@
<!doctype html>
<html class="no-js">
<head>
<meta charset="utf-8">
<title>critical css test</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
<!-- build:css styles/main.css -->
<script>
(function(u){function loadCSS(e,n,o,t){"use strict";var d=window.document.createElement("link"),i=n||window.document.getElementsByTagName("script")[0],s=window.document.styleSheets;return d.rel="stylesheet",d.href=e,d.media="only x",t&&(d.onload=t),i.parentNode.insertBefore(d,i),d.onloadcssdefined=function(n){for(var o,t=0;t<s.length;t++)s[t].href&&s[t].href.indexOf(e)>-1&&(o=!0);o?n():setTimeout(function(){d.onloadcssdefined(n)})},d.onloadcssdefined(function(){d.media=o||"all"}),d}for(var i in u){loadCSS(u[i]);}}(['css/main.css']));
</script>
<noscript>
<link rel="stylesheet" href="css/main.css">
</noscript>
<!-- endbuild -->
</head>
<body>
<!--[if lt IE 10]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<div class="container">
<div class="header">
<ul class="nav nav-pills pull-right">
<li class="active"><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
</ul>
<h3 class="text-muted">critical css test</h3>
</div>
<div class="jumbotron">
<h1>'Allo, 'Allo!</h1>
<p class="lead">Always a pleasure scaffolding your apps.</p>
<p><a class="btn btn-lg btn-success" href="#">Splendid!</a></p>
</div>
<div class="row marketing">
<div class="col-lg-6">
<h4>HTML5 Boilerplate</h4>
<p>HTML5 Boilerplate is a professional front-end template for building fast, robust, and adaptable web apps or sites.</p>
<h4>Bootstrap</h4>
<p>Sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development.</p>
</div>
</div>
<div class="footer">
<p>♥ from the Yeoman team</p>
</div>
</div>
</body>
</html>

View File

@ -159,6 +159,18 @@ describe('Module: inline-critical', function() {
done(); done();
}); });
it('should respect selector option', function(done) {
var html = read('test/fixtures/index.html');
var css = read('test/fixtures/critical.css');
var expected = read('test/expected/index-before.html');
var out = inlineCritical(html, css, { minify: true, selector: 'title' });
expect(strip(out.toString('utf-8'))).to.be.equal(strip(expected));
done();
});
it('should ignore stylesheets wrapped in noscript', function(done) { it('should ignore stylesheets wrapped in noscript', function(done) {
var html = read('test/fixtures/index-noscript.html'); var html = read('test/fixtures/index-noscript.html');
var css = read('test/fixtures/critical.css'); var css = read('test/fixtures/critical.css');
@ -171,6 +183,19 @@ describe('Module: inline-critical', function() {
done(); done();
}); });
it('should skip loadcss if it\'s already present and used for all existing link tags', function(done) {
var html = read('test/fixtures/loadcss.html');
var css = read('test/fixtures/critical.css');
var expected = read('test/expected/index-loadcss.html');
var out = inlineCritical(html, css, { minify: true });
expect(strip(out.toString('utf-8'))).to.be.equal(strip(expected));
done();
});
}); });
@ -310,6 +335,7 @@ describe('CLI', function () {
'-i','ignore-me', '-i','ignore-me',
'-i','/regexp/', '-i','/regexp/',
'-b', 'basePath', '-b', 'basePath',
'-s', 'selector',
'-m', '-m',
'-e' '-e'
]; ];
@ -324,6 +350,7 @@ describe('CLI', function () {
process.stderr.write = this.stderr; process.stderr.write = this.stderr;
expect(this.mockOpts.css).to.equal(read('test/fixtures/critical.css')); expect(this.mockOpts.css).to.equal(read('test/fixtures/critical.css'));
expect(this.mockOpts.html).to.equal(read('test/fixtures/index.html')); expect(this.mockOpts.html).to.equal(read('test/fixtures/index.html'));
expect(this.mockOpts.selector).to.equal('selector');
expect(this.mockOpts.ignore).to.be.instanceof(Array); expect(this.mockOpts.ignore).to.be.instanceof(Array);
expect(this.mockOpts.ignore[0]).to.be.a('string'); expect(this.mockOpts.ignore[0]).to.be.a('string');
expect(this.mockOpts.ignore[1]).to.instanceof(RegExp); expect(this.mockOpts.ignore[1]).to.instanceof(RegExp);
@ -342,6 +369,7 @@ describe('CLI', function () {
'--ignore','ignore-me', '--ignore','ignore-me',
'--ignore','/regexp/', '--ignore','/regexp/',
'--base', 'basePath', '--base', 'basePath',
'--selector', 'selector',
'--minify', '--minify',
'--extract' '--extract'
]; ];
@ -356,6 +384,7 @@ describe('CLI', function () {
process.stderr.write = this.stderr; process.stderr.write = this.stderr;
expect(this.mockOpts.css).to.equal(read('test/fixtures/critical.css')); expect(this.mockOpts.css).to.equal(read('test/fixtures/critical.css'));
expect(this.mockOpts.html).to.equal(read('test/fixtures/index.html')); expect(this.mockOpts.html).to.equal(read('test/fixtures/index.html'));
expect(this.mockOpts.selector).to.equal('selector');
expect(this.mockOpts.ignore).to.be.instanceof(Array); expect(this.mockOpts.ignore).to.be.instanceof(Array);
expect(this.mockOpts.ignore[0]).to.be.a('string'); expect(this.mockOpts.ignore[0]).to.be.a('string');
expect(this.mockOpts.ignore[1]).to.instanceof(RegExp); expect(this.mockOpts.ignore[1]).to.instanceof(RegExp);