diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6a804c2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[package.json] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a815a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.swp +.DS_Store +node_modules/ +npm-debug.log diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..fcb5338 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,3 @@ +{ + "laxcomma": true +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6e5919d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - "0.10" diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..ab63cfd --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,51 @@ +// Generated on 2014-08-03 using generator-nodejs 2.0.1 +module.exports = function (grunt) { + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + complexity: { + generic: { + src: ['app/**/*.js'], + options: { + errorsOnly: false, + cyclometric: 6, // default is 3 + halstead: 16, // default is 8 + maintainability: 100 // default is 100 + } + } + }, + jshint: { + all: [ + 'Gruntfile.js', + 'app/**/*.js', + 'test/**/*.js' + ], + options: { + jshintrc: '.jshintrc' + } + }, + mochacli: { + all: ['test/**/*.js'], + options: { + reporter: 'spec', + ui: 'bdd' + } + }, + watch: { + js: { + files: ['**/*.js', '!node_modules/**/*.js'], + tasks: ['default'], + options: { + nospawn: true + } + } + } + }); + + grunt.loadNpmTasks('grunt-complexity'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-mocha-cli'); + grunt.registerTask('test', ['complexity', 'jshint', 'mochacli', 'watch']); + grunt.registerTask('ci', ['complexity', 'jshint', 'mochacli']); + grunt.registerTask('default', ['test']); +}; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6062d08 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2014, Ben Zörb +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of Ben Zörb nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY BEN ZÖRB ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BEN ZÖRB BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc8de7f --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# inline-critical + +Inline critical-path css and load the existing stylesheets asynchronously + +[![build status](https://secure.travis-ci.org/bezoerb/inline-critical.png)](http://travis-ci.org/bezoerb/inline-critical) + +## Installation + +This module is installed via npm: + +``` bash +$ npm install inline-critical +``` + +## Example Usage + +``` js +var inlineCritical = require('inline-critical'); +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..d9f32db --- /dev/null +++ b/index.js @@ -0,0 +1,53 @@ +/** + * Module to inline styles while loading the existing stylesheets async + * + * @author Ben Zörb @bezoerb https://github.com/bezoerb + * @copyright Copyright (c) 2014 Ben Zörb + * + * Licensed under the MIT license. + * http://bezoerb.mit-license.org/ + * All rights reserved. + */ +'use strict'; +var cheerio = require('cheerio'); +var CleanCSS = require('clean-css'); + +module.exports = function(html, styles, minify) { + + var $ = cheerio.load(String(html)); + var links = $('link[rel="stylesheet"]'); + var noscript = $(''); + + // minify if minify option is set + if (minify) { + styles = new CleanCSS().minify(styles); + } + + // insert inline styles right before first + links.eq(0).before('\n'); + // insert noscript block right after stylesheets + links.eq(0).first().after(noscript); + + // wrap links to stylesheets in noscript block so that they will evaluated when js is turned off + var hrefs = links.map(function(idx, el) { + el = $(el); + noscript.append(el); + noscript.append('\n'); + return el.attr('href'); + }).toArray(); + + // build js block to load blocking stylesheets + $('body').append('\n'); + + return new Buffer($.html()); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..da44cef --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "inline-critical", + "version": "0.0.1", + "description": "Inline critical-path css and load the existing stylesheets asynchronously", + "main": "index.js", + "scripts": { + "test": "node_modules/.bin/grunt ci" + }, + "repository": { + "type": "git", + "url": "https://github.com/bezoerb/inline-critical" + }, + "keywords": [ + "css critical-path" + ], + "author": "Ben Zörb ", + "license": "MIT", + "bugs": { + "url": "https://github.com/bezoerb/inline-critical/issues" + }, + "dependencies": { + "cheerio": "^0.17.0", + "clean-css": "^2.2.12" + }, + "devDependencies": { + "chai": "~1.8.1", + "grunt-contrib-jshint": "~0.6.4", + "grunt-contrib-watch": "~0.5.3", + "grunt": "~0.4.1", + "grunt-mocha-cli": "~1.1.0", + "grunt-complexity": "~0.1.3", + "grunt-cli": "~0.1.9" + } +} diff --git a/test/fixtures/critical.css b/test/fixtures/critical.css new file mode 100644 index 0000000..2b9e968 --- /dev/null +++ b/test/fixtures/critical.css @@ -0,0 +1,290 @@ +body { + padding-top: 20px; + padding-bottom: 20px; +} + + +.header{ + padding-left: 15px; + padding-right: 15px; +} + + +.header { + border-bottom: 1px solid #e5e5e5; +} + + +.header h3 { + margin-top: 0; + margin-bottom: 0; + line-height: 40px; + padding-bottom: 19px; +} + + +.jumbotron { + text-align: center; + border-bottom: 1px solid #e5e5e5; +} + +.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; + } + + + .header { + margin-bottom: 30px; + } + + + .jumbotron { + border-bottom: 0; + } +} + + + + +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +body { + margin: 0; +} +a { + background: transparent; +} +h1 { + margin: .67em 0; + font-size: 2em; +} +@media print { + * { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + box-shadow: none !important; + } + a{ + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + a[href^="#"]:after { + content: ""; + } + p, + h3 { + orphans: 3; + widows: 3; + } + h3 { + page-break-after: avoid; + } +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +html { + font-size: 62.5%; + + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #333; + background-color: #fff; +} +a { + color: #428bca; + text-decoration: none; +} +h1, +h3{ + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +h1, +h3{ + margin-top: 20px; + margin-bottom: 10px; +} +h1{ + font-size: 36px; +} +h3{ + font-size: 24px; +} +p { + margin: 0 0 10px; +} +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 200; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +.text-muted { + color: #999; +} +ul{ + margin-top: 0; + margin-bottom: 10px; +} +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + 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; +} +.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; + margin-bottom: 0; + list-style: none; +} +.nav > li { + position: relative; + display: block; +} +.nav > li > a { + position: relative; + display: block; + 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 { + padding: 30px; + margin-bottom: 30px; + color: inherit; + background-color: #eee; +} +.jumbotron h1{ + color: inherit; +} +.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:before, +.container:after, +.nav:before, +.nav:after{ + display: table; + content: " "; +} +.container:after, +.nav:after{ + clear: both; +} +.pull-right { + float: right !important; +} \ No newline at end of file diff --git a/test/fixtures/index-inlined-async-final.html b/test/fixtures/index-inlined-async-final.html new file mode 100644 index 0000000..3d034f9 --- /dev/null +++ b/test/fixtures/index-inlined-async-final.html @@ -0,0 +1,387 @@ + + + + +critical css test + + + + + + + + + + + + + + +
+
+ +

critical css test

+
+ +
+

'Allo, 'Allo!

+

Always a pleasure scaffolding your apps.

+

Splendid!

+
+ +
+
+

HTML5 Boilerplate

+

HTML5 Boilerplate is a professional front-end template for building fast, robust, and adaptable web apps or sites.

+ +

Bootstrap

+

Sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development.

+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/index-inlined-async-minified-final.html b/test/fixtures/index-inlined-async-minified-final.html new file mode 100644 index 0000000..a246c41 --- /dev/null +++ b/test/fixtures/index-inlined-async-minified-final.html @@ -0,0 +1,97 @@ + + + + + critical css test + + + + + + + + + + + + + + +
+
+ +

critical css test

+
+ +
+

'Allo, 'Allo!

+

Always a pleasure scaffolding your apps.

+

Splendid!

+
+ +
+
+

HTML5 Boilerplate

+

HTML5 Boilerplate is a professional front-end template for building fast, robust, and adaptable web apps or sites.

+ +

Bootstrap

+

Sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development.

+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/index.html b/test/fixtures/index.html new file mode 100644 index 0000000..2106f05 --- /dev/null +++ b/test/fixtures/index.html @@ -0,0 +1,88 @@ + + + + + critical css test + + + + + + + + + + + + + +
+
+ +

critical css test

+
+ +
+

'Allo, 'Allo!

+

Always a pleasure scaffolding your apps.

+

Splendid!

+
+ +
+
+

HTML5 Boilerplate

+

HTML5 Boilerplate is a professional front-end template for building fast, robust, and adaptable web apps or sites.

+ +

Bootstrap

+

Sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development.

+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..deb9d3f --- /dev/null +++ b/test/index.js @@ -0,0 +1,39 @@ +var expect = require('chai').expect, + fs = require('fs'), + inlineCritical = require('..'); + +/** + * Strip whitespaces, tabs and newlines and replace with one space. + * Usefull when comparing string contents. + * @param string + */ +function stripWhitespace(string) { + return string.replace(/[\r\n]+/mg,' ').replace(/\s+/gm,''); +} + +describe('inline-critical', function() { + it('should inline css', function(done) { + var html = fs.readFileSync('test/fixtures/index.html', 'utf8'); + var css = fs.readFileSync('test/fixtures/critical.css', 'utf8'); + + var expected = fs.readFileSync('test/fixtures/index-inlined-async-final.html', 'utf8'); + var out = inlineCritical(html, css); + + expect(stripWhitespace(out.toString('utf-8'))).to.be.equal(stripWhitespace(expected)); + + done(); + }); + + + it('should inline css', function(done) { + var html = fs.readFileSync('test/fixtures/index.html', 'utf8'); + var css = fs.readFileSync('test/fixtures/critical.css', 'utf8'); + + var expected = fs.readFileSync('test/fixtures/index-inlined-async-minified-final.html', 'utf8'); + var out = inlineCritical(html, css, true); + + expect(stripWhitespace(out.toString('utf-8'))).to.be.equal(stripWhitespace(expected)); + + done(); + }); +});