├── .jshintignore
├── index.js
├── .gitignore
├── test
├── data
│ ├── gradient.gif
│ ├── gradient.jpg
│ ├── gradient.png
│ ├── gradient.webp
│ ├── gradient.special.png
│ ├── gradient.svg
│ └── large.css
├── module-test.js
├── binary-test.js
└── embed-test.js
├── .travis.yml
├── .jshintrc
├── LICENSE
├── package.json
├── History.md
├── bin
└── enhancecss
├── Readme.md
└── lib
└── enhance.js
/.jshintignore:
--------------------------------------------------------------------------------
1 | .git
2 | node_modules
3 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/enhance');
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/test/data/gradient.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jakubpawlowicz/enhance-css/HEAD/test/data/gradient.gif
--------------------------------------------------------------------------------
/test/data/gradient.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jakubpawlowicz/enhance-css/HEAD/test/data/gradient.jpg
--------------------------------------------------------------------------------
/test/data/gradient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jakubpawlowicz/enhance-css/HEAD/test/data/gradient.png
--------------------------------------------------------------------------------
/test/data/gradient.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jakubpawlowicz/enhance-css/HEAD/test/data/gradient.webp
--------------------------------------------------------------------------------
/test/data/gradient.special.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jakubpawlowicz/enhance-css/HEAD/test/data/gradient.special.png
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '0.8'
4 | - '0.10'
5 | - '0.11'
6 | matrix:
7 | allow_failures:
8 | - node_js: '0.11'
9 | install:
10 | - npm update npm -g
11 | - npm install
12 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "camelcase": true,
3 | "curly": false,
4 | "eqeqeq": false,
5 | "eqnull": true,
6 | "immed": true,
7 | "indent": 2,
8 | "latedef": true,
9 | "noarg": true,
10 | "node" : true,
11 | "plusplus": false,
12 | "quotmark": "single",
13 | "strict": false,
14 | "undef": true,
15 | "unused": true
16 | }
17 |
--------------------------------------------------------------------------------
/test/data/gradient.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2014 JakubPawlowicz.com
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "enhance-css",
3 | "version": "1.1.0",
4 | "author": "Jakub Pawlowicz (http://twitter.com/jakubpawlowicz)",
5 | "description": "A well-tested CSS enhancer (Base64, assets hosts, cache boosters, etc)",
6 | "license": "MIT",
7 | "keywords": [
8 | "css",
9 | "enhance",
10 | "base64",
11 | "assets",
12 | "asset hosts"
13 | ],
14 | "homepage": "https://github.com/jakubpawlowicz/enhance-css",
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/jakubpawlowicz/enhance-css.git"
18 | },
19 | "bugs": {
20 | "url": "https://github.com/jakubpawlowicz/enhance-css/issues"
21 | },
22 | "bin": {
23 | "enhancecss": "./bin/enhancecss"
24 | },
25 | "main": "index.js",
26 | "files": [
27 | "bin",
28 | "lib",
29 | "History.md",
30 | "index.js",
31 | "LICENSE"
32 | ],
33 | "scripts": {
34 | "check": "jshint ./bin/enhancecss .",
35 | "prepublish": "npm run check",
36 | "test": "vows"
37 | },
38 | "dependencies": {
39 | "commander": "2.3.x"
40 | },
41 | "devDependencies": {
42 | "jshint": "2.5.x",
43 | "vows": "0.7.x"
44 | },
45 | "engines": {
46 | "node": ">=0.8.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/module-test.js:
--------------------------------------------------------------------------------
1 | var vows = require('vows');
2 | var assert = require('assert');
3 | var EnhanceCSS = require('../lib/enhance.js');
4 |
5 | vows.describe('module').addBatch({
6 | 'imported as a function': {
7 | topic: function() {
8 | var instance = new EnhanceCSS();
9 | return instance.process.bind(instance);
10 | },
11 | 'should not throw an error': function(process) {
12 | assert.doesNotThrow(function() {
13 | process('a{color:red}');
14 | });
15 | }
16 | },
17 | 'initialization without new (back-compat)': {
18 | topic: function() {
19 | return EnhanceCSS();
20 | },
21 | 'should be an EnhanceCSS instance': function(instance) {
22 | assert.isObject(instance);
23 | assert.equal(instance instanceof EnhanceCSS, true);
24 | assert.isFunction(instance.process);
25 | },
26 | 'should process CSS correctly': function(instance) {
27 | assert.equal(instance.process('a{color:red}').embedded.plain, 'a{color:red}');
28 | }
29 | },
30 | 'extended via prototype': {
31 | topic: function() {
32 | EnhanceCSS.prototype.foo = function(data, callback) {
33 | callback(null, this.process(data));
34 | };
35 | new EnhanceCSS().foo('a{color:red}', this.callback);
36 | },
37 | 'should output correct CSS': function(error, processed) {
38 | assert.equal(processed.embedded.plain, 'a{color:red}');
39 | },
40 | teardown: function() {
41 | delete EnhanceCSS.prototype.foo;
42 | }
43 | },
44 | 'initialization without options': {
45 | topic: function() {
46 | return new EnhanceCSS();
47 | },
48 | 'should process CSS correctly': function(instance) {
49 | assert.equal(instance.process('a{color:red}').embedded.plain, 'a{color:red}');
50 | }
51 | }
52 | }).export(module);
53 |
--------------------------------------------------------------------------------
/History.md:
--------------------------------------------------------------------------------
1 | [1.1.0 / 2014-03-16](https://github.com/jakubpawlowicz/enhance-css/compare/v1.0.0...v1.1.0)
2 | ==================
3 |
4 | * Fixed issue [#19](https://github.com/jakubpawlowicz/enhance-css/issues/19) - adds option forcing embed on all assets.
5 | * Fixed issue [#21](https://github.com/jakubpawlowicz/enhance-css/issues/21) - adds warnings to binary and library.
6 |
7 | [1.0.0 / 2014-02-23](https://github.com/jakubpawlowicz/enhance-css/compare/v0.6.0...v1.0.0)
8 | ==================
9 |
10 | * Adds a slightly different CLI options (because of #10).
11 | * Adds JSHint and makes sure code is valid. Patch by [@XhmikosR](https://github.com/XhmikosR).
12 | * Drops node 0.6 support.
13 | * Fixes #10 - use commander for CLI options parsing.
14 | * Fixes #16 - use prototypal inheritance.
15 | * Fixes #17 - makes the options argument to `new EnhanceCSS()` optional.
16 |
17 | 0.6.0 / 2012-11-30
18 | ==================
19 |
20 | * Added `stamp` option (defaults to true) which controls adding timestamps. Patch by [@borbit](https://github.com/borbit).
21 | * Added `--nostamp` option to binary.
22 |
23 | 0.5.2 / 2012-09-05
24 | ==================
25 |
26 | * Added relative protocol to asset hosts if protocol part is not provided. Patch by [@borbit](https://github.com/borbit).
27 |
28 | 0.5.1 / 2012-08-14
29 | ==================
30 |
31 | * Fixed parsing relative URLs (kudos to [@borbit](https://github.com/borbit) for the patch!)
32 |
33 | 0.5.0 / 2012-08-05
34 | ==================
35 |
36 | * Added Windows support with tests.
37 |
38 | 0.4.1 / 2012-08-02
39 | ==================
40 |
41 | * Fixed vows dev dependency.
42 | * Added `fs.existsSync` fallback to get rid of node.js's v0.8 warnings.
43 |
44 | 0.4.0 / 2012-07-09
45 | ==================
46 |
47 | * Requires node.js 0.6+.
48 | * Replaced gzip with node.js's native zlib.
49 | * Fixed asynchronous mode for binaries (creating gzip data).
50 | * Added testing for `noembed` and `pregzip`.
51 |
52 | 0.3.3 / 2012-07-04
53 | ==================
54 |
55 | * Fix for script failing for missing embedded files when using crypted stamps.
56 |
57 | 0.3.2 / 2012-07-03
58 | ==================
59 |
60 | * Leaves missing files as is.
61 |
62 | 0.3.1 / 2012-07-03
63 | ==================
64 |
65 | * Fixed assembling MD5 hash file name.
66 |
67 | 0.3.0 / 2012-07-03
68 | ==================
69 |
70 | * Added node.js 0.4.x requirement.
71 | * Added `cryptedStamp` option for renaming image files with MD5 hash attached (hard cache boosters).
72 |
73 | 0.2.2 / 2011-09-25
74 | ==================
75 |
76 | * Fixed dependencies - missing 'gzip'. Thanks to [@fairwinds](https://github.com/fairwinds) for reporting it.
77 |
78 | 0.2.1 / 2011-04-07
79 | ==================
80 |
81 | * Fixed bug in assembling compressed output (for large files only).
82 |
83 | 0.2.0 / 2011-04-03
84 | ==================
85 |
86 | * Added `--pregzip` option for automatic gzipping of enhanced files (not available when output is set to STDOUT).
87 | * Added binary file tests.
88 |
89 | 0.1.0 / 2011-03-20
90 | ==================
91 |
92 | * First version of enhance-css library.
93 | * Implemented GIF, JPG, PNG, and SVG images embedding (performed if the `?embed` parameter is present).
94 | * Implemented cache booster (via timestamp).
95 | * Implemented randomized asset hosts picker.
96 |
--------------------------------------------------------------------------------
/bin/enhancecss:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /* jshint latedef: false */
4 |
5 | var commands = require('commander');
6 | var EnhanceCSS = require('../index');
7 | var fs = require('fs');
8 | var path = require('path');
9 |
10 | var packageConfig = fs.readFileSync(path.join(path.dirname(fs.realpathSync(process.argv[1])), '../package.json'));
11 | var buildVersion = JSON.parse(packageConfig).version;
12 |
13 | // Specify commander options to parse command line params correctly
14 | commands
15 | .version(buildVersion, '-v, --version')
16 | .usage('[options] [source-file]')
17 | .option('-r, --root [root-path]', 'Set a root path to which resolve absolute @import rules')
18 | .option('-o, --output [output-file]', 'Use [output-file] as output instead of STDOUT')
19 | .option('--crypted-stamp', 'Rename image files with MD5 hash attached (hard cache boosters)')
20 | .option('--no-stamp', 'Disable adding stamp to URLs')
21 | .option('--no-embed-version', 'Output both embedded and non embedded version')
22 | .option('--force-embed', 'Forces embed on all supported assets')
23 | .option('--asset-hosts [host-pattern]', 'Use one or more asset hosts, e.g assets[0,1,2].example.com')
24 | .option('--pregzip', 'Automatically gzip the enhanced files (not available when no output file given)')
25 | .parse(process.argv);
26 |
27 | var fromStdin = !process.env.__DIRECT__ && !process.stdin.isTTY;
28 |
29 | // If no sensible data passed in just print help and exit
30 | if (!fromStdin && commands.args.length === 0) {
31 | commands.outputHelp();
32 | return 0;
33 | }
34 |
35 | var options = {
36 | source: commands.args[0],
37 | target: commands.output,
38 | rootPath: commands.root || process.cwd(),
39 | assetHosts: !!commands.assetHosts,
40 | pregzip: !!commands.pregzip,
41 | noEmbedVersion: !commands.embedVersion,
42 | cryptedStamp: !!commands.cryptedStamp,
43 | stamp: !!commands.stamp,
44 | forceEmbed: !!commands.forceEmbed
45 | };
46 |
47 | if (options.source) {
48 | fs.readFile(options.source, 'utf8', function(error, text) {
49 | if (error)
50 | throw error;
51 | enhance(text, output);
52 | });
53 | } else {
54 | var stdin = process.openStdin();
55 | stdin.setEncoding('utf-8');
56 | var text = '';
57 | stdin.on('data', function(chunk) { text += chunk; });
58 | stdin.on('end', function() { enhance(text, output); });
59 | }
60 |
61 | function enhance(source, callback) {
62 | return new EnhanceCSS(options).process(source, function(error, data) {
63 | if (error)
64 | throw error;
65 |
66 | callback(data);
67 | });
68 | }
69 |
70 | function write(target, content) {
71 | if (typeof target == 'string') {
72 | fs.writeFileSync(target, content.plain);
73 |
74 | if (options.pregzip)
75 | fs.writeFileSync(target + '.gz', content.compressed);
76 | } else {
77 | target.write(content);
78 | }
79 | }
80 |
81 | function reportWarnings(list) {
82 | list.forEach(function(warning) {
83 | console.warn('WARNING: ' + warning);
84 | });
85 | }
86 |
87 | function output(enhanced) {
88 | if (options.target) {
89 | write(options.target, enhanced.embedded);
90 | if (options.noEmbedVersion)
91 | write(options.target.replace(/\.(\w+)$/, '-noembed.$1'), enhanced.notEmbedded);
92 | } else {
93 | write(process.stdout, enhanced.embedded);
94 | }
95 |
96 | reportWarnings(enhanced.warnings);
97 | }
98 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | [](https://badge.fury.io/js/enhance-css)
2 | [](https://travis-ci.org/jakubpawlowicz/enhance-css)
3 | [](https://david-dm.org/jakubpawlowicz/enhance-css)
4 | [](https://david-dm.org/jakubpawlowicz/enhance-css#info=devDependencies)
5 |
6 | ## What is enhance-css?
7 |
8 | Enhance-css is a [node.js](http://nodejs.org/) tool which can tweak your CSS files to:
9 |
10 | * improve caching - by rewriting URLs and renaming files to include either timestamps or MD5 hashes;
11 | * parellelize requests - by rewriting URLs with one or more asset hosts;
12 | * reduce number of requests - by embedding images as [Base64](http://en.wikipedia.org/wiki/Base64) data.
13 |
14 | There is also an option to create non-embedded version suited well
15 | for older browsers (IE 7 and below).
16 |
17 |
18 | ## Usage
19 |
20 | ### What are the requirements?
21 |
22 | ```
23 | node.js 0.8.0+ (fully tested on OS X 10.6+, CentOS, and Windows 7)
24 | ```
25 |
26 | ### How to install enhance-css?
27 |
28 | ```
29 | npm install enhance-css
30 | ```
31 |
32 | ### How to use enhance-css CLI?
33 |
34 | ```
35 | enhancecss [options] [source-file]
36 |
37 | -h, --help output usage information
38 | -v, --version output the version number
39 | -r, --root [root-path] Set a root path to which resolve absolute @import rules
40 | -o, --output [output-file] Use [output-file] as output instead of STDOUT
41 | --crypted-stamp Rename image files with MD5 hash attached (hard cache boosters)
42 | --no-stamp Disable adding stamp to URLs
43 | --no-embed-version Output both embedded and non embedded version
44 | --force-embed Forces embed on all supported assets
45 | --asset-hosts [host-pattern] Use one or more asset hosts, e.g assets[0,1,2].example.com
46 | --pregzip Automatically gzip the enhanced files (not available when no output file given)
47 | ```
48 |
49 | #### Examples:
50 |
51 | Most likely you are going to pass multiple CSS files into it
52 | and specify root directory and output file, e.g.
53 |
54 | ```bash
55 | cat path/to/first.css path/to/second.css path/to/third.css | enhancecss -o bundled.css --root ./public/
56 | ```
57 |
58 | The `--root` parameter is required to properly locate images referenced in the css files.
59 |
60 | To **embed images** in Base64 just add the *embed* argument to the image url, e.g.
61 |
62 | ```css
63 | a { background: url(/images/arrow.png?embed) 0 0 no-repeat; }
64 | ```
65 |
66 | ### Non-embedded version
67 |
68 | In case you also need to support older browser, just add `--noembedversion` parameter, e.g.
69 |
70 | ```bash
71 | cat path/to/first.css path/to/second.css path/to/third.css | enhancecss -o bundled.css --root ./public/ --noembedversion
72 | ```
73 |
74 | which will result in two output files: *bundled.css* and *bundled-noembed.css*.
75 |
76 | ### Asset hosts
77 |
78 | To use one or more asset hosts, just specify `--assetshosts` parameter, e.g.
79 |
80 | ```bash
81 | cat path/to/first.css path/to/second.css path/to/third.css | enhancecss -o bundled.css --root ./public/ --assethosts assets[0,1].example.com
82 | ```
83 |
84 | which will result in all non-embedded image URLs bound to either assets0.example.com or assets1.example.com.
85 |
86 | ### What are the enhance-css' dev commands?
87 |
88 | First clone the source, then run:
89 |
90 | * `npm run check` to check JS sources with [JSHint](https://github.com/jshint/jshint/)
91 | * `npm test` for the test suite
92 |
93 |
94 | ## License
95 |
96 | Enhance-css is released under the [MIT License](/LICENSE).
97 |
--------------------------------------------------------------------------------
/test/binary-test.js:
--------------------------------------------------------------------------------
1 | var vows = require('vows');
2 | var assert = require('assert');
3 | var fs = require('fs');
4 | var exec = require('child_process').exec;
5 | var zlib = require('zlib');
6 |
7 | var isWindows = process.platform == 'win32';
8 |
9 | var source = 'a{background:url(/test/data/gradient.png?embed)}';
10 |
11 | var checkFiles = function(fileName, options) {
12 | var pathToFile = function(noEmbed, pregzip) {
13 | return '/tmp/' + fileName + (noEmbed ? '-noembed' : '') + '.css' + (pregzip ? '.gz' : '');
14 | };
15 |
16 | assert.equal(fs.existsSync(pathToFile()), true);
17 | assert.equal(fs.existsSync(pathToFile(true)), !!options.noEmbed);
18 | assert.equal(fs.existsSync(pathToFile(false, true)), !!options.pregzip);
19 | assert.equal(fs.existsSync(pathToFile(true, true)), !!(options.pregzip && options.noEmbed));
20 |
21 | // verify content
22 | assert.include(fs.readFileSync(pathToFile()).toString('utf8'), 'a{background:url(data:image/png;base64');
23 | if (options.noEmbed) {
24 | assert.include(fs.readFileSync(pathToFile(true)).toString('utf8'), 'a{background:url(/test/data/gradient');
25 |
26 | if (options.stamp === false) {
27 | assert.match(fs.readFileSync(pathToFile(true)).toString('utf8'), /gradient\.\w+\)/);
28 | }
29 | }
30 |
31 | if (options.pregzip) {
32 | zlib.gunzip(fs.readFileSync(pathToFile(false, true)), function(error, result) {
33 | assert.include(result.toString('utf8'), 'a{background:url(data:image/png;base64');
34 | });
35 |
36 | if (options.noEmbed) {
37 | zlib.gunzip(fs.readFileSync(pathToFile(true, true)), function(error, result) {
38 | assert.include(result.toString('utf8'), 'a{background:url(/test/data/gradient');
39 | });
40 | }
41 | }
42 | };
43 |
44 | var cleanup = function(no, callback) {
45 | var swallowErrors = function() {};
46 |
47 | fs.unlink('/tmp/test' + no + '.css', swallowErrors);
48 | fs.unlink('/tmp/test' + no + '-noembed.css', swallowErrors);
49 | fs.unlink('/tmp/test' + no + '.css.gz', swallowErrors);
50 | fs.unlink('/tmp/test' + no + '-noembed.css.gz', swallowErrors);
51 |
52 | if (callback)
53 | callback();
54 | };
55 |
56 | var binaryContext = function(options, context) {
57 | if (isWindows)
58 | return {};
59 |
60 | context.topic = function() {
61 | exec('__DIRECT__=1 ./bin/enhancecss ' + options, this.callback);
62 | };
63 | return context;
64 | };
65 |
66 | var pipelinedContext = function(options, context) {
67 | if (isWindows)
68 | return {};
69 |
70 | var cssSource = source;
71 | if ('source' in context) {
72 | cssSource = context.source;
73 | delete context.source;
74 | }
75 |
76 | context.topic = function() {
77 | exec('echo "' + cssSource + '" | ./bin/enhancecss ' + options, this.callback);
78 | };
79 | return context;
80 | };
81 |
82 | vows.describe('enhance css binary').addBatch({
83 | 'no option': binaryContext('', {
84 | 'should give usage info': function(error, stdout) {
85 | assert.notEqual(-1, stdout.indexOf('Usage:'));
86 | }
87 | }),
88 | 'help option': binaryContext('-h', {
89 | 'should give usage info': function(error, stdout) {
90 | assert.notEqual(-1, stdout.indexOf('Usage:'));
91 | }
92 | }),
93 | 'version option': binaryContext('-v', {
94 | 'should give usage info': function(error, stdout) {
95 | var version = JSON.parse(fs.readFileSync('./package.json')).version;
96 | assert.equal(stdout, version + '\n');
97 | }
98 | }),
99 | 'simple embed': pipelinedContext('-o /tmp/test.css', {
100 | 'should give empty output': function(error, stdout) {
101 | assert.isEmpty(stdout);
102 | },
103 | 'should create valid files': function() {
104 | checkFiles('test', {
105 | noEmbed: false,
106 | pregzip: false
107 | });
108 | },
109 | teardown: cleanup(1)
110 | }),
111 | 'simple embed with no stamps': pipelinedContext('--no-embed-version --no-stamp -o /tmp/test1.css', {
112 | 'should give empty output': function(error, stdout) {
113 | assert.isEmpty(stdout);
114 | },
115 | 'should create valid files': function() {
116 | checkFiles('test1', {
117 | stamp: false,
118 | noEmbed: true,
119 | pregzip: false
120 | });
121 | },
122 | teardown: cleanup(1)
123 | }),
124 | 'embed with --no-embed-version option': pipelinedContext('--no-embed-version -o /tmp/test2.css', {
125 | 'should give empty output': function(error, stdout) {
126 | assert.isEmpty(stdout);
127 | },
128 | 'should create valid files': function() {
129 | checkFiles('test2', {
130 | noEmbed: true,
131 | pregzip: false
132 | });
133 | },
134 | teardown: cleanup(2)
135 | }),
136 | 'embed with noembed and gzip': pipelinedContext('--no-embed-version --pregzip -o /tmp/test3.css', {
137 | 'should give empty output': function(error, stdout) {
138 | assert.isEmpty(stdout);
139 | },
140 | 'should create valid files': function() {
141 | checkFiles('test3', {
142 | noEmbed: true,
143 | pregzip: true
144 | });
145 | },
146 | teardown: cleanup(3)
147 | }),
148 | 'noembed and crypted stamp options': pipelinedContext('--crypted-stamp --no-embed-version -o /tmp/test4.css', {
149 | 'should give empty output': function(error, stdout) {
150 | assert.isEmpty(stdout);
151 | },
152 | 'should create valid files': function() {
153 | checkFiles('test4', { noEmbed: true });
154 | },
155 | 'should create crypted file': function() {
156 | var data = fs.readFileSync(process.cwd() + '/test/data/gradient.png');
157 | var stamp = require('crypto').createHash('md5');
158 | stamp.update(data.toString('utf8'));
159 | var cryptedStamp = stamp.digest('hex');
160 |
161 | assert.equal(fs.existsSync(process.cwd() + '/test/data/gradient-' + cryptedStamp + '.png'), true);
162 | },
163 | teardown: cleanup(4, function() {
164 | exec('rm -rf ' + process.cwd() + '/test/data/gradient-*.png');
165 | })
166 | }),
167 | 'forced embed': pipelinedContext('--force-embed -o /tmp/test5.css', {
168 | 'should give empty output': function(error, stdout) {
169 | assert.isEmpty(stdout);
170 | },
171 | 'should create valid files': function() {
172 | checkFiles('test5', { noEmbed: false });
173 | }
174 | }),
175 | 'warnings': pipelinedContext('-o /tmp/test6', {
176 | 'source': 'a{background:url(/test/data/gradient.webp?embed)}',
177 | 'should give empty output': function(error, stdout) {
178 | assert.isEmpty(stdout);
179 | },
180 | 'should output warnings in stderr': function(error, stdout, stderr) {
181 | assert.equal(stderr, 'WARNING: File \'/test/data/gradient.webp\' skipped because of unknown content type.\n');
182 | },
183 | teardown: cleanup(6)
184 | })
185 | }).export(module);
186 |
--------------------------------------------------------------------------------
/lib/enhance.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Enhance-css - https://github.com/jakubpawlowicz/enhance-css
3 | * Released under the terms of MIT license
4 | *
5 | * Copyright (C) 2014 JakubPawlowicz.com
6 | */
7 |
8 | /* jshint latedef: false */
9 |
10 | var fs = require('fs');
11 | var path = require('path');
12 | var zlib = require('zlib');
13 | var crypto = require('crypto');
14 | var querystring = require('querystring');
15 |
16 | var isWindows = process.platform == 'win32';
17 |
18 | var EnhanceCSS = module.exports = function EnhanceCSS(options) {
19 | options = options || {};
20 |
21 | if (!(this instanceof EnhanceCSS))
22 | return new EnhanceCSS(options);
23 |
24 | options.stamp = 'stamp' in options ?
25 | options.stamp :
26 | true;
27 |
28 | this.options = options;
29 | this.urlPattern = /url\(([^\)]+)\)/g;
30 | this.hostsCycle = null;
31 | };
32 |
33 | EnhanceCSS.prototype.process = function(css, callback) {
34 | var self = this;
35 | var options = this.options;
36 | var missing = {};
37 | var embedUrls = {};
38 | var allUrls = [];
39 | var data = {
40 | original: css,
41 | warnings: []
42 | };
43 |
44 | // Only to find duplicates
45 | (css.match(this.urlPattern) || []).forEach(function(url) {
46 | var pathInfo = self.parseImageUrl(url.substring(4, url.length - 1));
47 |
48 | if (pathInfo.query.embed !== undefined) {
49 | if (embedUrls[pathInfo.relative])
50 | embedUrls[pathInfo.relative]++;
51 | else
52 | embedUrls[pathInfo.relative] = 1;
53 | }
54 |
55 | allUrls.push(pathInfo);
56 | });
57 |
58 | // Get embedded version
59 | data.embedded = {};
60 | data.embedded.plain = css.replace(this.urlPattern, function(match, url) {
61 | var pathInfo = self.parseImageUrl(url);
62 |
63 | if (pathInfo.relative.indexOf('data:image') === 0)
64 | return match;
65 |
66 | if (pathInfo.remote) {
67 | addWarning(data.warnings, pathInfo, 'skipped because is not local');
68 | return match;
69 | }
70 |
71 | // Break early if file does not exist
72 | if (!pathInfo.exists) {
73 | addWarning(data.warnings, pathInfo, 'does not exist');
74 | missing[pathInfo.relative] = 1;
75 | return match;
76 | }
77 |
78 | // Break unless ?embed param or there's more than one such image
79 | var moreThanOnce = embedUrls[pathInfo.relative] > 1;
80 | if (!options.forceEmbed && (pathInfo.query.embed === undefined || moreThanOnce)) {
81 | if (moreThanOnce)
82 | addWarning(data.warnings, pathInfo, 'set for embedding more than once');
83 | if (options.stamp)
84 | addFileStamp(pathInfo, options);
85 | return ['url(', (self.nextAssetHost() || ''), pathInfo.relative, ')'].join('');
86 | }
87 |
88 | var type = path.extname(pathInfo.relative).substring(1);
89 | if (type == 'jpg')
90 | type = 'jpeg';
91 | if (type == 'svg')
92 | type = 'svg+xml';
93 |
94 | // Break unless unsupported type
95 | if (!/(jpeg|gif|png|svg\+xml)/.test(type)) {
96 | addWarning(data.warnings, pathInfo, 'skipped because of unknown content type');
97 | return match;
98 | }
99 |
100 | var base64 = fs.readFileSync(pathInfo.absolute).toString('base64');
101 |
102 | return 'url(data:image/' + type + ';base64,' + base64 + ')';
103 | });
104 |
105 | // Get not embedded version (aka <= IE7)
106 | if (options.noEmbedVersion) {
107 | data.notEmbedded = {};
108 | data.notEmbedded.plain = css.replace(this.urlPattern, function(match, url) {
109 | var pathInfo = self.parseImageUrl(url);
110 |
111 | // Break early if file does not exist
112 | if (!pathInfo.exists)
113 | return match;
114 |
115 | if (options.stamp)
116 | addFileStamp(pathInfo, options);
117 |
118 | return ['url(', (self.nextAssetHost() || ''), pathInfo.relative, ')'].join('');
119 | });
120 | }
121 |
122 | if (options.cryptedStamp) {
123 | allUrls.forEach(function(url) {
124 | addFileStamp(url, options);
125 | });
126 | }
127 |
128 | // Update missing & duplicates lists
129 | data.missing = Object.keys(missing);
130 | data.duplicates = [];
131 | for (var key in embedUrls) {
132 | if (hasOwnProperty.call(embedUrls, key)) {
133 | if (embedUrls[key] > 1)
134 | data.duplicates.push(key);
135 | }
136 | }
137 |
138 | // Create gzipped version too if requested
139 | if (options.pregzip) {
140 | var count = options.noEmbedVersion ? 2 : 1;
141 | var compress = function(type) {
142 | zlib.gzip(data[type].plain, function(error, result) {
143 | data[type].compressed = result;
144 | if (--count === 0)
145 | callback(null, data);
146 | });
147 | };
148 |
149 | compress('embedded');
150 | if (options.noEmbedVersion)
151 | compress('notEmbedded');
152 |
153 | return;
154 | }
155 |
156 | if (callback)
157 | callback(null, data);
158 | else
159 | return data;
160 | };
161 |
162 | EnhanceCSS.prototype.parseImageUrl = function(url) {
163 | var remote = /^(http:\/\/|https:\/\/|\/\/)/.test(url);
164 | var tokens = url.replace(/['"]/g, '').split('?');
165 | var query = tokens[1] ? querystring.parse(tokens[1]) : {};
166 | var imagePath = remote ?
167 | tokens[0] :
168 | path.normalize(tokens[0]);
169 | var absolutePath = remote ?
170 | imagePath :
171 | path.join(this.options.rootPath, imagePath);
172 |
173 | if (isWindows && imagePath.indexOf('data:image') < 0)
174 | imagePath = imagePath.replace(/\\/g, '/');
175 |
176 | return {
177 | remote: remote,
178 | relative: imagePath,
179 | absolute: absolutePath,
180 | query: query,
181 | exists: remote ? false : fs.existsSync(absolutePath)
182 | };
183 | };
184 |
185 | EnhanceCSS.prototype.nextAssetHost = function() {
186 | var hosts = this.options.assetHosts;
187 | if (!hosts)
188 | return null;
189 | if (hosts.indexOf('[') == -1)
190 | return fixAssetHost(hosts);
191 |
192 | if (!this.hostsCycle) {
193 | this.hostsCycle = {
194 | next: function() {
195 | if (!this.cycleList) {
196 | var cycleList = [];
197 | var start = hosts.indexOf('[');
198 | var end = hosts.indexOf(']');
199 | var pattern = hosts.substring(start + 1, end);
200 |
201 | pattern.split(',').forEach(function(version) {
202 | cycleList.push(hosts.replace(/\[([^\]])+\]/, version));
203 | });
204 |
205 | this.cycleList = cycleList;
206 | this.index = 0;
207 | }
208 |
209 | if (this.index == this.cycleList.length)
210 | this.index = 0;
211 | return this.cycleList[this.index++];
212 | }
213 | };
214 | }
215 |
216 | return fixAssetHost(this.hostsCycle.next());
217 | };
218 |
219 | function fixAssetHost(host) {
220 | if (/^http:\/\//.test(host) || /^https:\/\//.test(host) || /^\/\//.test(host))
221 | return host;
222 |
223 | return '//' + host;
224 | }
225 |
226 | function addFileStamp(pathInfo, options) {
227 | if (!fs.existsSync(pathInfo.absolute))
228 | return;
229 |
230 | if (options.cryptedStamp && (pathInfo.query.embed === undefined || options.noEmbedVersion)) {
231 | var source = fs.readFileSync(pathInfo.absolute);
232 | var encrypted = crypto.createHash('md5');
233 | var toStampedPath = function(path) {
234 | var extensionDotIndex = path.lastIndexOf('.');
235 | return path.substring(0, extensionDotIndex) + '-' + stamp + '.' + path.substring(extensionDotIndex + 1);
236 | };
237 |
238 | encrypted.update(source.toString('utf8'));
239 | var stamp = encrypted.digest('hex');
240 | var targetPath = toStampedPath(pathInfo.absolute);
241 |
242 | if (!fs.existsSync(targetPath))
243 | fs.writeFileSync(targetPath, source);
244 |
245 | pathInfo.relative = toStampedPath(pathInfo.relative);
246 | } else {
247 | pathInfo.relative += '?' + Date.parse(fs.statSync(pathInfo.absolute).mtime) / 1000;
248 | }
249 | }
250 |
251 | function addWarning(warnings, pathInfo, reason) {
252 | var message = 'File \'' + pathInfo.relative + '\' ' + reason + '.';
253 | if (warnings.indexOf(message) === -1)
254 | warnings.push(message);
255 | }
256 |
--------------------------------------------------------------------------------
/test/data/large.css:
--------------------------------------------------------------------------------
1 | /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; }
2 | /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; }
3 | /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; }
--------------------------------------------------------------------------------
/test/embed-test.js:
--------------------------------------------------------------------------------
1 | var vows = require('vows');
2 | var assert = require('assert');
3 | var fs = require('fs');
4 | var zlib = require('zlib');
5 | var path = require('path');
6 | var exec = require('child_process').exec;
7 | var crypto = require('crypto');
8 | var EnhanceCSS = require('../lib/enhance.js');
9 |
10 | var runOn = function(css, options) {
11 | options = options || {};
12 | options.rootPath = options.rootPath || process.cwd();
13 |
14 | return function() {
15 | return new EnhanceCSS(options).process(css, this.callback);
16 | };
17 | };
18 |
19 | var base64 = function(imageName) {
20 | return fs.readFileSync(process.cwd() + '/test/data/' + imageName).toString('base64');
21 | };
22 |
23 | var mtime = function(imageName) {
24 | return Date.parse(fs.statSync(process.cwd() + '/test/data/' + imageName).mtime) / 1000;
25 | };
26 |
27 | var cryptedStamp = function(imageName) {
28 | var data = fs.readFileSync(process.cwd() + '/test/data/' + imageName);
29 | var stamp = crypto.createHash('md5');
30 | stamp.update(data.toString('utf8'));
31 | return stamp.digest('hex');
32 | };
33 |
34 | vows.describe('embedding images').addBatch({
35 | 'plain content': {
36 | topic: runOn('div{width:100px;height:50px}'),
37 | 'should be left intact': function(data) {
38 | assert.equal(data.embedded.plain, data.original);
39 | },
40 | 'should yield no warnings': function(data) {
41 | assert.deepEqual(data.warnings, []);
42 | }
43 | },
44 | 'no embed': {
45 | topic: runOn('a{background:url(/test/data/gradient.jpg);}'),
46 | 'should add a timestamp': function(data) {
47 | assert.equal(data.embedded.plain, 'a{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ');}');
48 | }
49 | },
50 | 'unsupported': {
51 | topic: runOn('a{background:url(/test/data/gradient.webp?embed);}', { stamp: false }),
52 | 'should be left intact': function(data) {
53 | assert.equal(data.embedded.plain, data.original);
54 | },
55 | 'should yield a warning': function(data) {
56 | assert.deepEqual(data.warnings, ['File \'/test/data/gradient.webp\' skipped because of unknown content type.']);
57 | }
58 | },
59 | 'urls with special characters #1': {
60 | topic: runOn('a{background:url("/test/data/gradient.jpg");}'),
61 | 'should be processed': function(data) {
62 | assert.equal(data.embedded.plain, 'a{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ');}');
63 | }
64 | },
65 | 'urls with special characters #2': {
66 | topic: runOn('a{background:url("/test/data/gradient.jpg");}'),
67 | 'should be processed': function(data) {
68 | assert.equal(data.embedded.plain, 'a{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ');}');
69 | }
70 | },
71 | 'already embedded': {
72 | topic: runOn('a{background:url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAKAAoDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD0D/h7BrH/AEG7P/vmH/Gj/h7BrH/Qbs/++Yf8a/kX/tbVf+gnqH/gbc//AByj+1tV/wCgnqH/AIG3P/xyv9DFmeH0/wCEnL+n/LqP9z+75/gux/qgsJlWn/CRl/T/AJcR/wCnfkf/2Q==)}'),
73 | 'should not be changed': function(data) {
74 | assert.equal(data.embedded.plain, data.original);
75 | },
76 | 'should yield no warnings': function(data) {
77 | assert.deepEqual(data.warnings, []);
78 | }
79 | },
80 | 'same urls with mixed characters': {
81 | topic: runOn('a{background:url("/test/data/gradient.jpg?embed");} div{background:url(/test/data/gradient.jpg?embed);}'),
82 | 'should not be embedded': function(data) {
83 | assert.equal(data.embedded.plain,
84 | 'a{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ');} ' +
85 | 'div{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ');}');
86 | }
87 | },
88 | 'url with relative parts': {
89 | topic: runOn('a{background:url(/test/data/../data/gradient.png)}'),
90 | 'should be normalized': function(data) {
91 | assert.equal(data.embedded.plain, 'a{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')}');
92 | }
93 | },
94 | 'remote url': {
95 | 'via http': {
96 | topic: runOn('a{background:url(http://pro.goalsmashers.com/test.png)}'),
97 | 'should not be transformed': function(data) {
98 | assert.equal(data.embedded.plain, data.original);
99 | },
100 | 'should yield a warning': function(data) {
101 | assert.deepEqual(data.warnings, ['File \'http://pro.goalsmashers.com/test.png\' skipped because is not local.']);
102 | }
103 | },
104 | 'via https': {
105 | topic: runOn('a{background:url(https://pro.goalsmashers.com/test.png)}'),
106 | 'should not be transformed': function(data) {
107 | assert.equal(data.embedded.plain, data.original);
108 | },
109 | 'should yield a warning': function(data) {
110 | assert.deepEqual(data.warnings, ['File \'https://pro.goalsmashers.com/test.png\' skipped because is not local.']);
111 | }
112 | },
113 | 'same protocol': {
114 | topic: runOn('a{background:url(//pro.goalsmashers.com/test.png)}'),
115 | 'should not be transformed': function(data) {
116 | assert.equal(data.embedded.plain, data.original);
117 | },
118 | 'should yield a warning': function(data) {
119 | assert.deepEqual(data.warnings, ['File \'//pro.goalsmashers.com/test.png\' skipped because is not local.']);
120 | }
121 | }
122 | },
123 | 'one file to be embedded': {
124 | topic: function() {
125 | return function(type) {
126 | return 'a{background:url(/test/data/gradient.' + type + '?embed)}';
127 | };
128 | },
129 | 'should give Base64 embedded jpg': function(css) {
130 | assert.equal(runOn(css('jpg'))().embedded.plain, 'a{background:url(data:image/jpeg;base64,' + base64('gradient.jpg') + ')}');
131 | },
132 | 'should give Base64 embedded png': function(css) {
133 | assert.equal(runOn(css('png'))().embedded.plain, 'a{background:url(data:image/png;base64,' + base64('gradient.png') + ')}');
134 | },
135 | 'should give Base64 embedded gif': function(css) {
136 | assert.equal(runOn(css('gif'))().embedded.plain, 'a{background:url(data:image/gif;base64,' + base64('gradient.gif') + ')}');
137 | },
138 | 'should give Base64 embedded svg': function(css) {
139 | assert.equal(runOn(css('svg'))().embedded.plain, 'a{background:url(data:image/svg+xml;base64,' + base64('gradient.svg') + ')}');
140 | }
141 | },
142 | 'same file marked with ?embed twice': {
143 | topic: runOn('a{background:url(/test/data/gradient.jpg?embed)} div{background:url(/test/data/gradient.jpg?embed)}'),
144 | 'should not embed to Base64': function(data) {
145 | assert.equal(data.embedded.plain,
146 | 'a{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ')} div{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ')}'
147 | );
148 | },
149 | 'should yield a warning': function(data) {
150 | assert.deepEqual(data.warnings, ['File \'/test/data/gradient.jpg\' set for embedding more than once.']);
151 | }
152 | },
153 | 'more than one file and only one marked with ?embed': {
154 | topic: runOn('a{background:url(/test/data/gradient.png)} div{background:url(/test/data/gradient.png?embed)} p{border-image:url(/test/data/gradient.png)}'),
155 | 'should embed one file to Base64': function(data) {
156 | assert.equal(data.embedded.plain, [
157 | 'a{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')}',
158 | 'div{background:url(data:image/png;base64,' + base64('gradient.png') + ')}',
159 | 'p{border-image:url(/test/data/gradient.png?' + mtime('gradient.png') + ')}'
160 | ].join(' '));
161 | }
162 | },
163 | 'not embedded files': {
164 | topic: runOn('a{background:url(/test/data/gradient.png)} div{background:url(/test/data/gradient.jpg)}'),
165 | 'should get mtime timestamp': function(data) {
166 | assert.equal(data.embedded.plain,
167 | 'a{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')} div{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ')}');
168 | }
169 | },
170 | 'not found files': {
171 | topic: runOn('a{background:url(/test/data/gradient2.png)}'),
172 | 'should be left intact': function(data) {
173 | assert.equal(data.embedded.plain, data.original);
174 | },
175 | 'should yield a warning': function(data) {
176 | assert.deepEqual(data.warnings, ['File \'/test/data/gradient2.png\' does not exist.']);
177 | }
178 | },
179 | 'forced embedding': {
180 | 'for same assets': {
181 | topic: runOn('a{background:url(/test/data/gradient.png)} div{background:url(/test/data/gradient.png)}', { forceEmbed: true }),
182 | 'should embed all resources': function(data) {
183 | assert.equal(data.embedded.plain, [
184 | 'a{background:url(data:image/png;base64,' + base64('gradient.png') + ')}',
185 | 'div{background:url(data:image/png;base64,' + base64('gradient.png') + ')}',
186 | ].join(' '));
187 | }
188 | },
189 | 'for different assets': {
190 | topic: runOn('a{background:url(/test/data/gradient.png)} div{background:url(/test/data/gradient.jpg)}', { forceEmbed: true }),
191 | 'should embed all resources': function(data) {
192 | assert.equal(data.embedded.plain, [
193 | 'a{background:url(data:image/png;base64,' + base64('gradient.png') + ')}',
194 | 'div{background:url(data:image/jpeg;base64,' + base64('gradient.jpg') + ')}',
195 | ].join(' '));
196 | }
197 | }
198 | },
199 | 'adding assets hosts': {
200 | topic: 'a{background:url(/test/data/gradient.png)} p{background:url(/test/data/gradient.jpg)} div{background:url(/test/data/gradient.gif)}',
201 | 'single': function(css) {
202 | assert.equal(runOn(css, { assetHosts: 'assets.example.com' })().embedded.plain, [
203 | 'a{background:url(//assets.example.com/test/data/gradient.png?' + mtime('gradient.png') + ')}',
204 | 'p{background:url(//assets.example.com/test/data/gradient.jpg?' + mtime('gradient.jpg') + ')}',
205 | 'div{background:url(//assets.example.com/test/data/gradient.gif?' + mtime('gradient.gif') + ')}'
206 | ].join(' '));
207 | },
208 | 'multiple': function(css) {
209 | assert.equal(runOn(css, { assetHosts: 'assets[0,1,2].example.com' })().embedded.plain, [
210 | 'a{background:url(//assets0.example.com/test/data/gradient.png?' + mtime('gradient.png') + ')}',
211 | 'p{background:url(//assets1.example.com/test/data/gradient.jpg?' + mtime('gradient.jpg') + ')}',
212 | 'div{background:url(//assets2.example.com/test/data/gradient.gif?' + mtime('gradient.gif') + ')}'
213 | ].join(' '));
214 | }
215 | }
216 | })
217 | .addBatch({
218 | 'getting non-embedded version (IE7)': {
219 | topic: 'a{background:url(/test/data/gradient.png)} p{background:url(/test/data/gradient.jpg)}',
220 | 'not by default': function(css) {
221 | assert.isUndefined(runOn(css)().notEmbedded);
222 | },
223 | 'if requested': function(css) {
224 | assert.equal(runOn(css, { noEmbedVersion: true })().notEmbedded.plain,
225 | 'a{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')} p{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ')}');
226 | }
227 | },
228 | 'getting non-embedded version (IE7) with embed': {
229 | topic: 'a{background:url(/test/data/gradient.png?embed)}',
230 | 'if requested': function(css) {
231 | assert.equal(runOn(css, { noEmbedVersion: true })().notEmbedded.plain,
232 | 'a{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')}');
233 | }
234 | },
235 | 'getting non-embedded version (IE7) with duplicates and embed': {
236 | topic: 'a{background:url(/test/data/gradient.png?embed)} p{background:url(/test/data/gradient.png?embed)}',
237 | 'if requested': function(css) {
238 | assert.equal(runOn(css, { noEmbedVersion: true })().notEmbedded.plain,
239 | 'a{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')} p{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')}');
240 | }
241 | }
242 | }).addBatch({
243 | 'should not add crypted stamp instead of timestamp': {
244 | 'on CSS without images': {
245 | topic: runOn('a{background:#fff}', { cryptedStamp: true }),
246 | 'should act as identity transformation': function(css) {
247 | assert.equal(css.embedded.plain, css.original);
248 | }
249 | },
250 | 'on CSS with embedded images': {
251 | topic: runOn('a{background:url(/test/data/gradient.jpg?embed)}', { cryptedStamp: true }),
252 | 'should not create new file': function() {
253 | var stamp = cryptedStamp('gradient.jpg');
254 | assert.equal(fs.existsSync(process.cwd() + '/test/data/gradient-' + stamp + '.jpg'), false);
255 | }
256 | }
257 | }
258 | }).addBatch({
259 | 'should add crypted stamp instead of timestamp on CSS with normal images': {
260 | topic: runOn('a{background:url(/test/data/gradient.png)}', { cryptedStamp: true }),
261 | 'should create new file': function() {
262 | var stamp = cryptedStamp('gradient.png');
263 | assert.equal(fs.existsSync(process.cwd() + '/test/data/gradient-' + stamp + '.png'), true);
264 | },
265 | 'should include stamped file in embed source': function(css) {
266 | var stamp = cryptedStamp('gradient.png');
267 | assert.equal('a{background:url(/test/data/gradient-' + stamp + '.png)}', css.embedded.plain);
268 | },
269 | teardown: function() {
270 | exec('rm -rf test/data/gradient-*');
271 | }
272 | }
273 | }).addBatch({
274 | 'should add crypted stamp instead of timestamp on non-embedded source': {
275 | topic: runOn('a{background:url(/test/data/gradient.png)}', {
276 | cryptedStamp: true,
277 | noEmbedVersion: true
278 | }),
279 | 'once file exists': {
280 | topic: function(css) {
281 | var self = this;
282 | var stamp = cryptedStamp('gradient.png');
283 |
284 | fs.exists(process.cwd() + '/test/data/gradient-' + stamp + '.png', function() {
285 | self.callback(css, stamp);
286 | });
287 | },
288 | 'should include stamped file in embed source': function(css, stamp) {
289 | assert.equal('a{background:url(/test/data/gradient-' + stamp + '.png)}', css.embedded.plain);
290 | },
291 | 'should include stamped file in non-embedded source': function(css, stamp) {
292 | assert.equal('a{background:url(/test/data/gradient-' + stamp + '.png)}', css.notEmbedded.plain);
293 | }
294 | },
295 | teardown: function() {
296 | exec('rm -rf test/data/gradient-*');
297 | }
298 | }
299 | }).addBatch({
300 | 'should add crypted stamp instead of timestamp on non-embedded source for embedded image': {
301 | topic: runOn('a{background:url(/test/data/gradient.png?embed)}', {
302 | cryptedStamp: true,
303 | noEmbedVersion: true
304 | }),
305 | 'once file exists': {
306 | topic: function(css) {
307 | var self = this;
308 | var stamp = cryptedStamp('gradient.png');
309 |
310 | fs.exists(process.cwd() + '/test/data/gradient-' + stamp + '.png', function() {
311 | self.callback(css, stamp);
312 | });
313 | },
314 | 'should not include stamped file in embed source': function(css, stamp) {
315 | assert.notEqual('a{background:url(/test/data/gradient-' + stamp + '.png)}', css.embedded.plain);
316 | },
317 | 'should include stamped file in non-embedded source': function(css, stamp) {
318 | assert.equal('a{background:url(/test/data/gradient-' + stamp + '.png)}', css.notEmbedded.plain);
319 | }
320 | },
321 | teardown: function() {
322 | exec('rm -rf test/data/gradient-*');
323 | }
324 | }
325 | }).addBatch({
326 | 'should correctly process files with dots': {
327 | topic: runOn('a{background:url(/test/data/gradient.special.png)}', { cryptedStamp: true }),
328 | 'should create new file': function() {
329 | var stamp = cryptedStamp('gradient.png');
330 | assert.equal(fs.existsSync(process.cwd() + '/test/data/gradient.special-' + stamp + '.png'), true);
331 | },
332 | 'should include stamped file in embed source': function(css) {
333 | var stamp = cryptedStamp('gradient.png');
334 | assert.equal('a{background:url(/test/data/gradient.special-' + stamp + '.png)}', css.embedded.plain);
335 | },
336 | teardown: function() {
337 | exec('rm -rf test/data/gradient.special-*');
338 | }
339 | }
340 | }).addBatch({
341 | 'should correctly process missing files with embed': {
342 | topic: runOn('a{background:url(/test/data/gradient2.png?embed)}', { cryptedStamp: true }),
343 | 'should keep path as is': function(css) {
344 | assert.equal('a{background:url(/test/data/gradient2.png?embed)}', css.embedded.plain);
345 | }
346 | },
347 | 'should correctly process missing files for crypted stamps': {
348 | topic: runOn('a{background:url(/test/data/gradient2.png)}', { cryptedStamp: true }),
349 | 'should keep path as is': function(css) {
350 | assert.equal('a{background:url(/test/data/gradient2.png)}', css.embedded.plain);
351 | }
352 | }
353 | }).addBatch({
354 | 'compressed content': {
355 | topic: runOn('a{background:#fff}'),
356 | 'not by default': function(data) {
357 | assert.isUndefined(data.embedded.compressed);
358 | }
359 | },
360 | 'compressed embedded content': {
361 | topic: runOn('a{background:#fff}', { pregzip: true }),
362 | 'should be buffer': function(data) {
363 | assert.ok(Buffer.isBuffer(data.embedded.compressed));
364 | },
365 | 'should be different from uncompressed': function(data) {
366 | assert.notEqual(data.embedded.compressed.toString(), data.embedded.plain);
367 | },
368 | 'should be different from original': function(data) {
369 | assert.notEqual(data.embedded.compressed.toString(), data.original);
370 | },
371 | 'uncompressing': {
372 | topic: function(data) {
373 | zlib.unzip(data.embedded.compressed, this.callback);
374 | },
375 | 'should be equal to embedded': function(error, uncompressed) {
376 | assert.equal('a{background:#fff}', uncompressed);
377 | }
378 | }
379 | },
380 | 'compressed non-embedded content': {
381 | topic: runOn('a{background:#fff}', {
382 | pregzip: true,
383 | noEmbedVersion: true
384 | }),
385 | 'should be buffer': function(data) {
386 | assert.ok(Buffer.isBuffer(data.notEmbedded.compressed));
387 | },
388 | 'should be different from uncompressed': function(data) {
389 | assert.notEqual(data.notEmbedded.compressed.toString(), data.notEmbedded.plain);
390 | },
391 | 'should be different from original': function(data) {
392 | assert.notEqual(data.notEmbedded.compressed.toString(), data.original);
393 | },
394 | 'uncompressing': {
395 | topic: function(data) {
396 | zlib.unzip(data.notEmbedded.compressed, this.callback);
397 | },
398 | 'should be equal to embedded': function(error, uncompressed) {
399 | assert.equal('a{background:#fff}', uncompressed);
400 | }
401 | }
402 | },
403 | 'long content': {
404 | topic: runOn(fs.readFileSync('./test/data/large.css', 'utf-8'), { pregzip: true }),
405 | 'uncompressing': {
406 | topic: function(data) {
407 | zlib.unzip(data.embedded.compressed, this.callback);
408 | },
409 | 'should be equal to embedded': function(error, uncompressed) {
410 | assert.equal(fs.readFileSync('./test/data/large.css', 'utf-8'), uncompressed);
411 | }
412 | }
413 | }
414 | }).addBatch({
415 | 'list of missing files': {
416 | topic: runOn('a{background:url(/test/data/gradient2.png)} p{background:url(/test/data/gradient2.jpg)}')().missing,
417 | 'should have both files': function(missing) {
418 | assert.equal(missing.length, 2);
419 | },
420 | 'should have files in right order': function(missing) {
421 | assert.equal(missing[0], '/test/data/gradient2.png');
422 | assert.equal(missing[1], '/test/data/gradient2.jpg');
423 | }
424 | },
425 | 'list of not embedded files (duplicates)': {
426 | topic: runOn('a{background:url(/test/data/gradient.png?embed)} p{background:url(/test/data/gradient.png?embed)}')().duplicates,
427 | 'should have one file': function(duplicates) {
428 | assert.equal(duplicates.length, 1);
429 | },
430 | 'should have gradient.png': function(duplicates) {
431 | assert.equal(duplicates[0], '/test/data/gradient.png');
432 | }
433 | }
434 | }).addBatch({
435 | 'parse absolute url': {
436 | topic: new EnhanceCSS({ rootPath: process.cwd() }).parseImageUrl('/test/data/gradient.png'),
437 | 'should get right relative path': function(parsed) {
438 | assert.equal(parsed.relative, '/test/data/gradient.png');
439 | },
440 | 'should get right absolute path': function(parsed) {
441 | assert.equal(parsed.absolute, path.join(process.cwd(), 'test', 'data', 'gradient.png'));
442 | },
443 | 'should exists': function(parsed) {
444 | assert.isTrue(parsed.exists);
445 | },
446 | 'should not have query options': function(parsed) {
447 | assert.isEmpty(parsed.query);
448 | }
449 | },
450 | 'parse absolute url with query string': {
451 | topic: new EnhanceCSS({ rootPath: process.cwd() }).parseImageUrl('/test/data/gradient.png?embed&x=y'),
452 | 'should get right relative path': function(parsed) {
453 | assert.equal(parsed.relative, '/test/data/gradient.png');
454 | },
455 | 'should get right absolute path': function(parsed) {
456 | assert.equal(parsed.absolute, path.join(process.cwd(), 'test', 'data', 'gradient.png'));
457 | },
458 | 'should exists': function(parsed) {
459 | assert.isTrue(parsed.exists);
460 | },
461 | 'should have query options': function(parsed) {
462 | assert.isNotNull(parsed.query.embed);
463 | assert.equal('y', parsed.query.x);
464 | }
465 | },
466 | 'parse non-canonical absolute urls': {
467 | topic: new EnhanceCSS({ rootPath: process.cwd() }).parseImageUrl('/test/data/../data/gradient.png'),
468 | 'should get right relative path': function(parsed) {
469 | assert.equal(parsed.relative, '/test/data/gradient.png');
470 | },
471 | 'should get right absolute path': function(parsed) {
472 | assert.equal(parsed.absolute, path.join(process.cwd(), 'test', 'data', 'gradient.png'));
473 | },
474 | 'should exists': function(parsed) {
475 | assert.isTrue(parsed.exists);
476 | },
477 | 'should not have query options': function(parsed) {
478 | assert.isEmpty(parsed.query);
479 | }
480 | },
481 | 'parse absolute urls with special characters': {
482 | topic: new EnhanceCSS({ rootPath: process.cwd() }).parseImageUrl('"/test/data/gradient.png"'),
483 | 'should get right relative path': function(parsed) {
484 | assert.equal(parsed.relative, '/test/data/gradient.png');
485 | },
486 | 'should get right absolute path': function(parsed) {
487 | assert.equal(parsed.absolute, path.join(process.cwd(), 'test', 'data', 'gradient.png'));
488 | },
489 | 'should exists': function(parsed) {
490 | assert.isTrue(parsed.exists);
491 | },
492 | 'should not have query options': function(parsed) {
493 | assert.isEmpty(parsed.query);
494 | }
495 | },
496 | 'parse relative url': {
497 | topic: new EnhanceCSS({ rootPath: process.cwd() }).parseImageUrl('test/data/gradient.png'),
498 | 'should get right relative path': function(parsed) {
499 | assert.equal(parsed.relative, 'test/data/gradient.png');
500 | },
501 | 'should get right absolute path': function(parsed) {
502 | assert.equal(parsed.absolute, path.join(process.cwd(), 'test', 'data', 'gradient.png'));
503 | },
504 | 'should exists': function(parsed) {
505 | assert.isTrue(parsed.exists);
506 | }
507 | }
508 | }).addBatch({
509 | 'get empty asset host': {
510 | topic: new EnhanceCSS().nextAssetHost(),
511 | 'from empty configuration': function(host) {
512 | assert.equal(host, null);
513 | }
514 | },
515 | 'get single asset host fixed with': {
516 | topic: new EnhanceCSS({ assetHosts: 'assets.example.com' }).nextAssetHost(),
517 | 'relative protocol': function(host) {
518 | assert.equal(host, '//assets.example.com');
519 | }
520 | },
521 | 'get single asset host not fixed if': [
522 | {
523 | topic: new EnhanceCSS({ assetHosts: '//assets.example.com' }).nextAssetHost(),
524 | 'relative protocol passed': function(host) {
525 | assert.equal(host, '//assets.example.com');
526 | }
527 | },
528 | {
529 | topic: new EnhanceCSS({ assetHosts: 'http://assets.example.com' }).nextAssetHost(),
530 | '"http" protocol passed': function(host) {
531 | assert.equal(host, 'http://assets.example.com');
532 | }
533 | },
534 | {
535 | topic: new EnhanceCSS({ assetHosts: 'https://assets.example.com' }).nextAssetHost(),
536 | '"https" protocol passed': function(host) {
537 | assert.equal(host, 'https://assets.example.com');
538 | }
539 | }
540 | ],
541 | 'get multiple asset hosts fixed with': {
542 | topic: function() {
543 | return new EnhanceCSS({ assetHosts: 'assets[0,1].example.com' });
544 | },
545 | 'relative protocol, for first in list': function(enhance) {
546 | assert.equal(enhance.nextAssetHost(), '//assets0.example.com');
547 | },
548 | 'relative protocol, for second in list': function(enhance) {
549 | assert.equal(enhance.nextAssetHost(), '//assets1.example.com');
550 | }
551 | },
552 | 'get multiple asset hosts not fixed if': [
553 | {
554 | topic: function() {
555 | return new EnhanceCSS({ assetHosts: '//assets[0,1].example.com' });
556 | },
557 | 'relative protocol passed, for first in list': function(enhance) {
558 | assert.equal(enhance.nextAssetHost(), '//assets0.example.com');
559 | },
560 | 'relative protocol passed, for second in list': function(enhance) {
561 | assert.equal(enhance.nextAssetHost(), '//assets1.example.com');
562 | }
563 | },
564 | {
565 | topic: function() {
566 | return new EnhanceCSS({ assetHosts: 'http://assets[0,1].example.com' });
567 | },
568 | '"http" protocol passed, for first in list': function(enhance) {
569 | assert.equal(enhance.nextAssetHost(), 'http://assets0.example.com');
570 | },
571 | '"http" protocol passed, for second in list': function(enhance) {
572 | assert.equal(enhance.nextAssetHost(), 'http://assets1.example.com');
573 | }
574 | },
575 | {
576 | topic: function() {
577 | return new EnhanceCSS({ assetHosts: 'https://assets[0,1].example.com' });
578 | },
579 | '"https" protocol passed, for first in list': function(enhance) {
580 | assert.equal(enhance.nextAssetHost(), 'https://assets0.example.com');
581 | },
582 | '"https" protocol passed, for second in list': function(enhance) {
583 | assert.equal(enhance.nextAssetHost(), 'https://assets1.example.com');
584 | }
585 | }
586 | ],
587 | 'get one asset host': {
588 | topic: new EnhanceCSS({ assetHosts: '//assets.example.com' }).nextAssetHost(),
589 | 'as first host from list': function(host) {
590 | assert.equal(host, '//assets.example.com');
591 | },
592 | 'as second host from list': function(host) {
593 | assert.equal(host, '//assets.example.com');
594 | }
595 | },
596 | 'get one asset host from multiple configuration - ': {
597 | topic: function() {
598 | return new EnhanceCSS({ assetHosts: '//assets[0,1,2].example.com' });
599 | },
600 | 'first': function(enhanceCSS) {
601 | assert.equal(enhanceCSS.nextAssetHost(), '//assets0.example.com');
602 | },
603 | 'second': function(enhanceCSS) {
604 | assert.equal(enhanceCSS.nextAssetHost(), '//assets1.example.com');
605 | },
606 | 'third': function(enhanceCSS) {
607 | assert.equal(enhanceCSS.nextAssetHost(), '//assets2.example.com');
608 | },
609 | 'fourth': function(enhanceCSS) {
610 | assert.equal(enhanceCSS.nextAssetHost(), '//assets0.example.com');
611 | }
612 | },
613 | 'get one asset host from list of different subdomains': {
614 | topic: function() {
615 | return new EnhanceCSS({ assetHosts: '//[alpha,beta,gamma].example.com' });
616 | },
617 | 'first': function(enhanceCSS) {
618 | assert.equal(enhanceCSS.nextAssetHost(), '//alpha.example.com');
619 | },
620 | 'second': function(enhanceCSS) {
621 | assert.equal(enhanceCSS.nextAssetHost(), '//beta.example.com');
622 | },
623 | 'third': function(enhanceCSS) {
624 | assert.equal(enhanceCSS.nextAssetHost(), '//gamma.example.com');
625 | },
626 | 'fourth': function(enhanceCSS) {
627 | assert.equal(enhanceCSS.nextAssetHost(), '//alpha.example.com');
628 | }
629 | }
630 | }).addBatch({
631 | 'not embedded files should not get mtime timestamp if "stamp" option equals false': {
632 | topic: runOn('div{background:url(/test/data/gradient.jpg)}', {
633 | stamp: false,
634 | noEmbedVersion: true
635 | }),
636 | 'in the "embedded" version': function(data) {
637 | assert.equal(data.embedded.plain, data.original);
638 | },
639 | 'in the "not embedded" version': function(data) {
640 | assert.equal(data.notEmbedded.plain, data.original);
641 | }
642 | }
643 | }).export(module);
644 |
--------------------------------------------------------------------------------