├── test ├── cases │ ├── jsdom.css │ ├── jsdom.html │ ├── empty.css │ ├── juice-content │ │ ├── important.css │ │ ├── font-quotes.css │ │ ├── media-preserve.css │ │ ├── width-attr.css │ │ ├── apply-link-to-style-tag.css │ │ ├── important.json │ │ ├── font-quotes.json │ │ ├── apply-link-to-style-tag-mq.css │ │ ├── apply-link-to-style-tag.json │ │ ├── width-attr.json │ │ ├── media-preserve.json │ │ ├── apply-link-to-style-tag-mq.json │ │ ├── apply-link-to-style-tag.html │ │ ├── apply-link-to-style-tag-mq.html │ │ ├── important.out │ │ ├── apply-link-to-style-tag.out │ │ ├── apply-link-to-style-tag-mq.out │ │ ├── font-quotes.out │ │ ├── font-quotes.html │ │ ├── important.html │ │ ├── media-preserve.out │ │ ├── width-attr.out │ │ ├── width-attr.html │ │ └── media-preserve.html │ ├── jsdom.out │ ├── alpha.css │ ├── empty.html │ ├── empty.out │ ├── id.css │ ├── class.css │ ├── preserve-events.css │ ├── css-quotes.css │ ├── regression-media.html │ ├── tag.html │ ├── css-quotes.html │ ├── style-preservation.css │ ├── alpha.html │ ├── regression-selector-newline.css │ ├── tag.css │ ├── id.html │ ├── media.out │ ├── alpha.out │ ├── ignore-pseudos.html │ ├── media.html │ ├── cascading.css │ ├── class+id.html │ ├── direct-descendents.html │ ├── id.out │ ├── css-quotes.out │ ├── direct-descendents.css │ ├── media.css │ ├── class+id.css │ ├── tag.out │ ├── regression-selector-newline.html │ ├── class.html │ ├── ignore-pseudos.out │ ├── important.html │ ├── yui-reset.html │ ├── normalize.html │ ├── style-preservation.html │ ├── important.out │ ├── regression-media.out │ ├── style-preservation.out │ ├── class+id.out │ ├── regression-media.css │ ├── preserve-events.html │ ├── regression-selector-newline.out │ ├── direct-descendents.out │ ├── class.out │ ├── specificity.html │ ├── preserve-events.out │ ├── ignore-pseudos.css │ ├── cascading.html │ ├── important.css │ ├── yui-reset.out │ ├── specificity.out │ ├── normalize.out │ ├── cascading.out │ ├── specificity.css │ ├── yui-reset.css │ └── normalize.css ├── html │ ├── no_css.in.html │ ├── Test.css │ ├── spaces in path │ │ └── Test.css │ ├── no_css.out.html │ ├── spaces_in_path.out.html │ ├── remote_url.in.html │ ├── doctype.in.html │ ├── doctype.out.html │ ├── spaces_in_path.in.html │ ├── remote_url.out.html │ ├── two_styles.out.html │ └── two_styles.in.html ├── test.js ├── juice.test.js └── run.js ├── .gitignore ├── .npmignore ├── index.js ├── .travis.yml ├── Makefile ├── package.json ├── bin └── juice ├── lib ├── property.js ├── selector.js ├── utils.js └── juice.js ├── LICENSE ├── CHANGELOG.md └── README.md /test/cases/jsdom.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/cases/jsdom.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/cases/empty.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *~ 3 | -------------------------------------------------------------------------------- /test/cases/juice-content/important.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/html/no_css.in.html: -------------------------------------------------------------------------------- 1 |

hi

2 | -------------------------------------------------------------------------------- /test/cases/juice-content/font-quotes.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/cases/juice-content/media-preserve.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/cases/juice-content/width-attr.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | support/ 3 | README.md 4 | -------------------------------------------------------------------------------- /test/cases/jsdom.out: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/juice'); 3 | -------------------------------------------------------------------------------- /test/cases/alpha.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/empty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/cases/empty.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/cases/id.css: -------------------------------------------------------------------------------- 1 | #test { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /test/html/Test.css: -------------------------------------------------------------------------------- 1 | strong { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /test/cases/class.css: -------------------------------------------------------------------------------- 1 | .woot { 2 | overflow: hidden; 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/preserve-events.css: -------------------------------------------------------------------------------- 1 | p { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/css-quotes.css: -------------------------------------------------------------------------------- 1 | p { 2 | background: url("/woot"); 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/regression-media.html: -------------------------------------------------------------------------------- 1 | Test 2 |

Test

3 | -------------------------------------------------------------------------------- /test/cases/tag.html: -------------------------------------------------------------------------------- 1 | 2 |

Test

3 | 4 | -------------------------------------------------------------------------------- /test/html/spaces in path/Test.css: -------------------------------------------------------------------------------- 1 | strong { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/css-quotes.html: -------------------------------------------------------------------------------- 1 | 2 |

woot

3 | 4 | -------------------------------------------------------------------------------- /test/cases/style-preservation.css: -------------------------------------------------------------------------------- 1 | p { color: blue; background: blue; } 2 | -------------------------------------------------------------------------------- /test/html/no_css.out.html: -------------------------------------------------------------------------------- 1 |

hi

2 | 3 | 4 | -------------------------------------------------------------------------------- /test/cases/alpha.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/cases/juice-content/apply-link-to-style-tag.css: -------------------------------------------------------------------------------- 1 | p { 2 | width: 200px; 3 | } -------------------------------------------------------------------------------- /test/cases/juice-content/important.json: -------------------------------------------------------------------------------- 1 | { 2 | "preserveImportant": true 3 | } -------------------------------------------------------------------------------- /test/cases/regression-selector-newline.css: -------------------------------------------------------------------------------- 1 | a, 2 | p { 3 | color: red; 4 | } 5 | -------------------------------------------------------------------------------- /test/cases/tag.css: -------------------------------------------------------------------------------- 1 | p { 2 | border: 1px solid #f00; 3 | margin: 1px; 4 | } 5 | -------------------------------------------------------------------------------- /test/cases/id.html: -------------------------------------------------------------------------------- 1 | 2 |
woot
3 | 4 | -------------------------------------------------------------------------------- /test/cases/media.out: -------------------------------------------------------------------------------- 1 | 2 | 3 |

hi

4 | 5 | 6 | -------------------------------------------------------------------------------- /test/cases/alpha.out: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/cases/ignore-pseudos.html: -------------------------------------------------------------------------------- 1 | 2 | Test 3 | 4 | -------------------------------------------------------------------------------- /test/cases/media.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

hi

4 | 5 | 6 | -------------------------------------------------------------------------------- /test/cases/cascading.css: -------------------------------------------------------------------------------- 1 | ul li { 2 | display: block; 3 | } 4 | li { 5 | color: #f00; 6 | } 7 | -------------------------------------------------------------------------------- /test/cases/class+id.html: -------------------------------------------------------------------------------- 1 | 2 |

Woot

3 | 4 | -------------------------------------------------------------------------------- /test/cases/juice-content/font-quotes.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true 4 | } -------------------------------------------------------------------------------- /test/cases/direct-descendents.html: -------------------------------------------------------------------------------- 1 | 2 |

woot

3 | 4 | -------------------------------------------------------------------------------- /test/cases/id.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/cases/css-quotes.out: -------------------------------------------------------------------------------- 1 | 2 |

woot

3 | 4 | -------------------------------------------------------------------------------- /test/cases/direct-descendents.css: -------------------------------------------------------------------------------- 1 | p em { 2 | background: blue; 3 | } 4 | p > em { 5 | color: red; 6 | } 7 | -------------------------------------------------------------------------------- /test/cases/juice-content/apply-link-to-style-tag-mq.css: -------------------------------------------------------------------------------- 1 | p { width: 200px; } 2 | @media all { p { width: 100px; } } -------------------------------------------------------------------------------- /test/cases/media.css: -------------------------------------------------------------------------------- 1 | @media only screen and (-webkit-min-device-pixel-ratio: 2) { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/class+id.css: -------------------------------------------------------------------------------- 1 | #test { 2 | border: 1px solid #f00; 3 | } 4 | .woot { 5 | display: inline-block; 6 | } 7 | -------------------------------------------------------------------------------- /test/cases/tag.out: -------------------------------------------------------------------------------- 1 | 2 |

Test

3 | 4 | -------------------------------------------------------------------------------- /test/cases/regression-selector-newline.html: -------------------------------------------------------------------------------- 1 | 2 | Test 3 |

Test

4 | 5 | -------------------------------------------------------------------------------- /test/cases/class.html: -------------------------------------------------------------------------------- 1 | 2 |
Test
3 |
Test
4 | 5 | 6 | -------------------------------------------------------------------------------- /test/cases/ignore-pseudos.out: -------------------------------------------------------------------------------- 1 | 2 | Test 3 | 4 | -------------------------------------------------------------------------------- /test/cases/important.html: -------------------------------------------------------------------------------- 1 | 2 | woot 3 | 4 | -------------------------------------------------------------------------------- /test/cases/yui-reset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

hello

6 | 7 | 8 | -------------------------------------------------------------------------------- /test/cases/juice-content/apply-link-to-style-tag.json: -------------------------------------------------------------------------------- 1 | { 2 | "applyLinksToStyleTags": true, 3 | "removeStyleTags": false 4 | } -------------------------------------------------------------------------------- /test/cases/normalize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

normalize.css

6 | 7 | 8 | -------------------------------------------------------------------------------- /test/cases/style-preservation.html: -------------------------------------------------------------------------------- 1 | 2 |

The color should stay intact

3 | 4 | -------------------------------------------------------------------------------- /test/cases/juice-content/width-attr.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true, 4 | "applyWidthAttributes": true 5 | } -------------------------------------------------------------------------------- /test/cases/important.out: -------------------------------------------------------------------------------- 1 | 2 | woot 3 | 4 | -------------------------------------------------------------------------------- /test/cases/juice-content/media-preserve.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true, 4 | "preserveMediaQueries": true 5 | } -------------------------------------------------------------------------------- /test/cases/regression-media.out: -------------------------------------------------------------------------------- 1 | Test 2 |

Test

3 | -------------------------------------------------------------------------------- /test/cases/style-preservation.out: -------------------------------------------------------------------------------- 1 | 2 |

The color should stay intact

3 | 4 | -------------------------------------------------------------------------------- /test/cases/class+id.out: -------------------------------------------------------------------------------- 1 | 2 |

Woot

3 | 4 | -------------------------------------------------------------------------------- /test/cases/regression-media.css: -------------------------------------------------------------------------------- 1 | a, 2 | p { 3 | color: green; 4 | } 5 | 6 | @media only screen and (max-width: 600px) { 7 | a { color: red; } 8 | } -------------------------------------------------------------------------------- /test/cases/preserve-events.html: -------------------------------------------------------------------------------- 1 | 2 | Google 3 |

Google

4 | 5 | -------------------------------------------------------------------------------- /test/cases/regression-selector-newline.out: -------------------------------------------------------------------------------- 1 | 2 | Test 3 |

Test

4 | 5 | -------------------------------------------------------------------------------- /test/cases/direct-descendents.out: -------------------------------------------------------------------------------- 1 | 2 |

woot

3 | 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @node test/run.js 4 | @./node_modules/.bin/expresso \ 5 | -t 3000 \ 6 | --serial \ 7 | test/juice.test.js 8 | 9 | .PHONY: test 10 | -------------------------------------------------------------------------------- /test/cases/juice-content/apply-link-to-style-tag-mq.json: -------------------------------------------------------------------------------- 1 | { 2 | "applyLinksToStyleTags": true, 3 | "removeStyleTags": true, 4 | "preserveMediaQueries": true 5 | } -------------------------------------------------------------------------------- /test/cases/class.out: -------------------------------------------------------------------------------- 1 | 2 |
Test
3 |
Test
4 | 5 | -------------------------------------------------------------------------------- /test/cases/specificity.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |
7 | 8 | -------------------------------------------------------------------------------- /test/cases/preserve-events.out: -------------------------------------------------------------------------------- 1 | 2 | Google 3 |

Google

4 | 5 | -------------------------------------------------------------------------------- /test/cases/ignore-pseudos.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: underline; 3 | } 4 | 5 | a:hover { 6 | text-decoration: none; 7 | } 8 | 9 | a:link, a:visited { 10 | font-weight: bold; 11 | } 12 | -------------------------------------------------------------------------------- /test/cases/juice-content/apply-link-to-style-tag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

none

7 | 8 | 9 | -------------------------------------------------------------------------------- /test/cases/juice-content/apply-link-to-style-tag-mq.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

none

7 | 8 | 9 | -------------------------------------------------------------------------------- /test/cases/juice-content/important.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hi

7 | there 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/html/spaces_in_path.out.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hello, you are my favorite person

7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/html/remote_url.in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hello

7 | 8 | 9 | -------------------------------------------------------------------------------- /test/cases/juice-content/apply-link-to-style-tag.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 |

none

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/cases/cascading.html: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | -------------------------------------------------------------------------------- /test/html/doctype.in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hello

7 | 8 | -------------------------------------------------------------------------------- /test/html/doctype.out.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hello

7 | 8 | -------------------------------------------------------------------------------- /test/html/spaces_in_path.in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hello, you are my favorite person

7 | 8 | 9 | -------------------------------------------------------------------------------- /test/cases/important.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: red !important; 3 | background: blue !important; 4 | border-style: dashed !important; 5 | } 6 | 7 | #a { 8 | color: purple; 9 | background: purple; 10 | border-style: solid; 11 | } 12 | -------------------------------------------------------------------------------- /test/cases/yui-reset.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

hello

6 | 7 | 8 | -------------------------------------------------------------------------------- /test/cases/juice-content/apply-link-to-style-tag-mq.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 |

none

13 | 14 | 15 | -------------------------------------------------------------------------------- /test/cases/juice-content/font-quotes.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hi

7 |

there

8 | 9 | 10 | -------------------------------------------------------------------------------- /test/html/remote_url.out.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hello

7 | 8 | 9 | -------------------------------------------------------------------------------- /test/cases/specificity.out: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |
7 | 8 | -------------------------------------------------------------------------------- /test/cases/normalize.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

normalize.css

6 | 7 | 8 | -------------------------------------------------------------------------------- /test/html/two_styles.out.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

hello, you are my favorite person

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/cases/juice-content/font-quotes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 |

hi

11 |

there

12 | 13 | 14 | -------------------------------------------------------------------------------- /test/cases/juice-content/important.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 |

hi

14 | there 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/cases/cascading.out: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | -------------------------------------------------------------------------------- /test/cases/juice-content/media-preserve.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 |

hi

17 | 18 | 19 | -------------------------------------------------------------------------------- /test/cases/juice-content/width-attr.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

none

7 | 8 | 9 | 12 | 13 |
10 | wide 11 |
14 | wide 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/cases/juice-content/width-attr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 |

none

11 | 12 | 13 | 16 | 17 |
14 | wide 15 |
18 | wide 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/html/two_styles.in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 16 | 17 | 18 | 19 |

hello, you are my favorite person

20 | 21 | 22 | -------------------------------------------------------------------------------- /test/cases/juice-content/media-preserve.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 |

hi

23 | 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juice2", 3 | "version": "1.3.2", 4 | "description": "Inlines css into html source", 5 | "bin": "./bin/juice", 6 | "scripts": { 7 | "test": "mocha --reporter spec" 8 | }, 9 | "engines": { 10 | "node": ">=0.10.20" 11 | }, 12 | "dependencies": { 13 | "slick": "~1.12.1", 14 | "commander": "~2.3.0", 15 | "cssom": "~0.3.0", 16 | "superagent": "~0.18.2", 17 | "jsdom": "~0.11.1", 18 | "pend": "~1.1.2" 19 | }, 20 | "devDependencies": { 21 | "should": "~4.0.4", 22 | "mocha": "~1.20.1", 23 | "expresso": "~0.9.2" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/andrewrk/juice.git" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /bin/juice: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var juice = require('..'); 4 | var program = require('commander'); 5 | var package = require('../package'); 6 | var fs = require('fs'); 7 | 8 | program.name = package.name; 9 | 10 | program.version(package.version) 11 | .usage('input.html output.html') 12 | .parse(process.argv); 13 | 14 | if (program.args < 2) program.help(); 15 | 16 | var inputFile = program.args[0] 17 | , outputFile = program.args[1]; 18 | 19 | juice(inputFile, function(err, html) { 20 | if (err) { 21 | console.error(err.stack) 22 | process.exit(1); 23 | } else { 24 | fs.writeFile(outputFile, html, function(err) { 25 | if (err) { 26 | console.error(err.stack); 27 | process.exit(1); 28 | } 29 | }); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /test/cases/specificity.css: -------------------------------------------------------------------------------- 1 | #wrap #a { /* [2, 0, 0] - winner */ 2 | color: red; 3 | } 4 | #wrap a { /* [1, 0, 1] */ 5 | color: #000; 6 | } 7 | #a { /* [1, 0, 0] */ 8 | color: #00f; 9 | } 10 | div[a=b] .wrap div { /* [0, 2, 2] */ 11 | display: block; 12 | } 13 | #wrap .wrap { /* [1, 1, 0] - winner */ 14 | display: inline-block; 15 | } 16 | #wrap div.wrap #a { /* [2, 1, 1] */ 17 | background: #000; 18 | } 19 | #wrap[a=b] div.wrap #a { /* [2, 2, 1] - winner */ 20 | background: green; 21 | } 22 | #wrap[a=c] div.wrap #a { /* [2, 2, 1] - no match */ 23 | background: #00f; 24 | } 25 | 26 | #wrap .wrap #a { /* [2, 1, 0] */ 27 | background: #f00; 28 | } 29 | #wrap .wrap #a { /* [2, 1, 0] - winner */ 30 | border-color: green; 31 | } 32 | div #wrap-2 #a { /* [2, 0, 1] */ 33 | border-color: #008000; 34 | } 35 | -------------------------------------------------------------------------------- /test/cases/yui-reset.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010, Yahoo! Inc. All rights reserved. 3 | * Code licensed under the BSD License: 4 | * http://developer.yahoo.com/yui/license.html 5 | * version: 3.3.0 6 | * build: 3167 7 | * */ 8 | html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:text-top;}sub{vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;} 9 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var juice = require('../'); 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | var Pend = require('pend'); 5 | var assert = require('assert'); 6 | 7 | var it = global.it; 8 | 9 | var tests = [ 10 | "doctype", 11 | "no_css", 12 | "two_styles", 13 | "remote_url", 14 | "spaces_in_path", 15 | ]; 16 | 17 | 18 | tests.forEach(function(testName) { 19 | it(testName, createIt(testName)); 20 | }); 21 | 22 | function createIt(testName) { 23 | return function(cb) { 24 | var pend = new Pend(); 25 | var expected, actual; 26 | pend.go(function(cb) { 27 | var filePath = path.join(__dirname, "html", testName + ".in.html"); 28 | juice(filePath, function(err, val) { 29 | actual = val; 30 | cb(err); 31 | }); 32 | }); 33 | pend.go(function(cb) { 34 | fs.readFile(path.join(__dirname, "html", testName + ".out.html"), 'utf8', function(err, val) { 35 | expected = val; 36 | cb(err); 37 | }); 38 | }); 39 | pend.wait(function(err) { 40 | if (err) return cb(err); 41 | assert.strictEqual(actual.trim(), expected.trim()); 42 | cb(); 43 | }); 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /lib/property.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * juice 4 | * Copyright(c) 2011 LearnBoost 5 | * MIT Licensed 6 | */ 7 | 8 | module.exports = exports = Property; 9 | 10 | /** 11 | * Module dependencies. 12 | */ 13 | 14 | var compare = require('./utils').compare 15 | 16 | /** 17 | * CSS property constructor. 18 | * 19 | * @param {String} property 20 | * @param {String} value 21 | * @param {Selector} selector the property originates from 22 | * @api public 23 | */ 24 | 25 | function Property (prop, value, selector) { 26 | this.prop = prop; 27 | this.value = value; 28 | this.selector = selector 29 | } 30 | 31 | /** 32 | * Compares with another Property based on Selector#specificity. 33 | * 34 | * @api public 35 | */ 36 | 37 | Property.prototype.compare = function (property) { 38 | var a = this.selector.specificity() 39 | , b = property.selector.specificity() 40 | , winner = compare(a, b) 41 | 42 | if (winner == a) return this; 43 | return property; 44 | }; 45 | 46 | /** 47 | * Returns CSS property 48 | * 49 | * @api public 50 | */ 51 | 52 | Property.prototype.toString = function () { 53 | return this.prop + ': ' + this.value.replace(/['"]+/g, '') + ';'; 54 | }; 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 Guillermo Rauch 4 | Copyright (c) 2014 Andrew Kelley 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | -------------------------------------------------------------------------------- /lib/selector.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var parser = require('slick').parse 7 | 8 | /** 9 | * Module exports. 10 | */ 11 | 12 | module.exports = exports = Selector; 13 | 14 | /** 15 | * CSS selector constructor. 16 | * 17 | * @param {String} selector text 18 | * @param {Array} optionally, precalculated specificity 19 | * @api public 20 | */ 21 | 22 | function Selector (text, spec) { 23 | this.text = text; 24 | this.spec = spec; 25 | } 26 | 27 | /** 28 | * Get parsed selector. 29 | * 30 | * @api public 31 | */ 32 | 33 | Selector.prototype.parsed = function () { 34 | if (!this.tokens) this.tokens = parse(this.text); 35 | return this.tokens; 36 | }; 37 | 38 | /** 39 | * Lazy specificity getter 40 | * 41 | * @api public 42 | */ 43 | 44 | Selector.prototype.specificity = function () { 45 | if (!this.spec) this.spec = specificity(this.text, this.parsed()); 46 | return this.spec; 47 | 48 | function specificity (text, parsed) { 49 | var expressions = parsed || parse(text) 50 | , spec = [0, 0, 0, 0] 51 | , nots = [] 52 | 53 | for (var i = 0; i < expressions.length; i++) { 54 | var expression = expressions[i] 55 | , pseudos = expression.pseudos 56 | 57 | // id awards a point in the second column 58 | if (expression.id) spec[1]++; 59 | 60 | // classes and attributes award a point each in the third column 61 | if (expression.attributes) spec[2] += expression.attributes.length; 62 | if (expression.classList) spec[2] += expression.classList.length; 63 | 64 | // tag awards a point in the fourth column 65 | if (expression.tag && expression.tag !== '*') spec[3]++; 66 | 67 | // pseudos award a point each in the fourth column 68 | if (pseudos) { 69 | spec[3] += pseudos.length; 70 | 71 | for (var p = 0; p < pseudos.length; p++) { 72 | if (pseudos[p].key === 'not'){ 73 | nots.push(pseudos[p].value); 74 | spec[3]--; 75 | } 76 | } 77 | } 78 | } 79 | 80 | for (var ii = nots.length; ii--;) { 81 | var not = specificity(nots[ii]); 82 | for (var jj = 4; jj--;) spec[jj] += not[jj]; 83 | } 84 | 85 | return spec; 86 | } 87 | } 88 | 89 | /** 90 | * Parses a selector and returns the tokens. 91 | * 92 | * @param {String} selector 93 | * @api private. 94 | */ 95 | 96 | function parse (text) { 97 | try { 98 | return parser(text)[0]; 99 | } catch (e) { 100 | return []; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/juice.test.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Juice unit tests. 4 | */ 5 | 6 | /** 7 | * Test dependencies. 8 | */ 9 | 10 | var juice = require('../') 11 | , Selector = juice.Selector 12 | , Property = juice.Property 13 | , utils = juice.utils 14 | , should = require('should') 15 | 16 | /** 17 | * Tests. 18 | */ 19 | 20 | module.exports = { 21 | 22 | 'test extracting selectors': function () { 23 | var extract = utils.extract; 24 | 25 | extract('#a').should.eql(['#a']); 26 | extract('#a, .b').should.eql(['#a', '.b']); 27 | extract('#a, .b,').should.eql(['#a', '.b']); 28 | extract('.test.a, #a.b').should.eql(['.test.a', '#a.b']); 29 | extract('a[type=text, a=b], .a, .b, #c #d').should 30 | .eql(['a[type=text, a=b]', '.a', '.b', '#c #d']); 31 | extract('a:not(.a,.b,.c)').should.eql(['a:not(.a,.b,.c)']); 32 | extract('a:not(.a,.b,.c), .b').should.eql(['a:not(.a,.b,.c)', '.b']); 33 | extract('a:not(.a,.b,[type=text]), .b').should 34 | .eql(['a:not(.a,.b,[type=text])', '.b']); 35 | extract('a:not(.a,.b,[type=text, a=b]), .b').should 36 | .eql(['a:not(.a,.b,[type=text, a=b])', '.b']); 37 | }, 38 | 39 | 'test selector specificity comparison': function () { 40 | var compare = utils.compare; 41 | 42 | compare([0, 1, 2, 3], [0, 2, 0, 0]).should.eql([0, 2, 0, 0]); 43 | compare([0, 2, 0, 0], [0, 1, 2, 3]).should.eql([0, 2, 0, 0]); 44 | 45 | // check that the second reference is returned upon draws 46 | var b = [0, 1, 1, 4]; 47 | compare([0, 1, 1, 4], b).should.equal(b); 48 | 49 | compare([0, 0, 0, 4], [0, 0, 0, 10]).should.eql([0, 0, 0, 10]); 50 | compare([0, 0, 0, 10], [0, 0, 0, 4]).should.eql([0, 0, 0, 10]); 51 | 52 | compare([0, 4, 0, 0], [0, 0, 100, 4]).should.eql([0, 4, 0, 0]); 53 | compare([0, 0, 100, 4], [0, 4, 0, 0]).should.eql([0, 4, 0, 0]); 54 | 55 | compare([0, 1, 1, 5], [0, 1, 1, 15]).should.eql([0, 1, 1, 15]); 56 | compare([0, 1, 1, 15], [0, 1, 1, 5]).should.eql([0, 1, 1, 15]); 57 | }, 58 | 59 | 'test selector specificity calculator': function () { 60 | function spec (selector) { 61 | return new Selector(selector).specificity(); 62 | }; 63 | 64 | spec('#test').should.eql([0, 1, 0, 0]); 65 | spec('#a #b #c').should.eql([0, 3, 0, 0]); 66 | spec('.a .b .c').should.eql([0, 0, 3, 0]); 67 | spec('div.a div.b div.c').should.eql([0, 0, 3, 3]); 68 | spec('div a span').should.eql([0, 0, 0, 3]); 69 | spec('#test input[type=text]').should.eql([0, 1, 1, 1]); 70 | spec('[type=text]', [0, 0, 1, 0]); 71 | spec('*').should.eql([0, 0, 0, 0]); 72 | spec('div *').should.eql([0, 0, 0, 1]); 73 | }, 74 | 75 | 'test property comparison based on selector specificity': function () { 76 | function prop (k, v, sel) { 77 | return new Property(k, v, new Selector(sel)); 78 | } 79 | 80 | var a = prop('color', 'white', '#woot') 81 | , b = prop('color', 'red', '#a #woot') 82 | 83 | a.compare(b).should.equal(b); 84 | 85 | var a = prop('background-color', 'red', '#a') 86 | , b = prop('background-color', 'red', '.a.b.c') 87 | 88 | a.compare(b).should.equal(a); 89 | 90 | var a = prop('background-color', 'red', '#a .b.c') 91 | , b = prop('background-color', 'red', '.a.b.c #c') 92 | 93 | a.compare(b).should.equal(b); 94 | }, 95 | 96 | 'test parsing css into a object structure': function () { 97 | var parse = utils.parseCSS; 98 | 99 | parse('a, b { c: e; }').should.eql([ 100 | ['a', { '0': 'c', length: 1, _importants: { c: '' }, __starts: 5, c: 'e' } ] 101 | , ['b', { '0': 'c', length: 1, _importants: { c: '' }, __starts: 5, c: 'e' } ] 102 | ]); 103 | 104 | parse([ 105 | 'a, b { c: e; }' 106 | , 'b.e #d { d: e; }' 107 | , 'c[a=b] { d: e; }' 108 | ].join('\n')).should.eql([ 109 | ['a', { '0': 'c', length: 1, _importants: { c: '' }, __starts: 5, c: 'e' } ] 110 | , ['b', { '0': 'c', length: 1, _importants: { c: '' }, __starts: 5, c: 'e' } ] 111 | , ['b.e #d', { '0': 'd', length: 1, _importants: { d: '' }, __starts: 22, d: 'e' }] 112 | , ['c[a=b]', { '0': 'd', length: 1, _importants: { d: '' }, __starts: 39, d: 'e' }] 113 | ]); 114 | }, 115 | 116 | 'test juice': function () { 117 | juice('
woot
', 'div { color: red; }') 118 | .should.equal('
woot
'); 119 | } 120 | 121 | }; 122 | -------------------------------------------------------------------------------- /test/run.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Test runner based on Stylus test runner. 4 | */ 5 | 6 | /** 7 | * Module dependencies. 8 | */ 9 | 10 | var juice = require('../') 11 | , basename = require('path').basename 12 | , fs = require('fs') 13 | , path = require('path'); 14 | 15 | /** 16 | * Test count. 17 | */ 18 | 19 | var count = 0; 20 | 21 | /** 22 | * Failure count. 23 | */ 24 | 25 | var failures = 0; 26 | 27 | /** 28 | * Test the given `test`. 29 | * 30 | * @param {String} test 31 | * @param {Function} fn 32 | */ 33 | 34 | function test (testName, fn, options) { 35 | var base = __dirname + '/cases/' + testName 36 | , html = read(base + '.html') 37 | , css = read( base + '.css' ) 38 | , config = options ? JSON.parse( read( base + '.json' ) ) : null 39 | 40 | function read (file) { 41 | return fs.readFileSync(file, 'utf8'); 42 | } 43 | 44 | var onDone = function ( err, actual ) 45 | { 46 | var expected = read( base + '.out' ); 47 | 48 | if ( actual.trim() === expected.trim() ) 49 | { 50 | fn(); 51 | } else 52 | { 53 | fn( actual, expected ); 54 | } 55 | }; 56 | 57 | if ( config === null ) 58 | { 59 | // use the legacy invocation to test backward compatibility 60 | var actual = juice( html, css ); 61 | onDone( null, actual ); 62 | } 63 | else 64 | { 65 | var slashes = path.sep + path.sep; 66 | config.url = "file:" + slashes + path.dirname(path.resolve(process.cwd(), base + '.html')); 67 | juice.juiceContent( html, config, onDone ); 68 | } 69 | 70 | return testName; 71 | } 72 | 73 | /** 74 | * Auto-load and run tests. 75 | */ 76 | 77 | fs.readdir( __dirname + '/cases', function ( err, files ) { 78 | if ( err ) throw err; 79 | var tests = []; 80 | 81 | files.forEach( function ( file ) 82 | { 83 | if ( /\.html$/.test( file ) ) { 84 | ++count; 85 | tests.push( { basename: basename( file, '.html' ) } ); 86 | } 87 | } ); 88 | 89 | fs.readdir( __dirname + '/cases/juice-content', function ( err, files ) 90 | { 91 | if ( err ) throw err; 92 | 93 | files.forEach( function ( file ) 94 | { 95 | if ( /\.html$/.test( file ) ) { 96 | ++count; 97 | tests.push( { basename: 'juice-content/' + basename( file, '.html' ), options: true } ); 98 | } 99 | } ); 100 | 101 | nextTest( tests ); 102 | } ); 103 | } ); 104 | 105 | 106 | function nextTest( tests ) 107 | { 108 | curr = tests.shift(); 109 | if ( !curr ) return done(); 110 | process.stderr.write( ' \033[90m' + curr.basename + '\033[0m' ); 111 | test( curr.basename, function ( actual, expected ) 112 | { 113 | if ( actual ) { 114 | ++failures; 115 | console.error( '\r \033[31m✖\033[0m \033[90m' + curr.basename + '\033[0m\n' ); 116 | diff( actual, expected ); 117 | console.error(); 118 | } else { 119 | console.error( '\r \033[36m✔\033[0m \033[90m' + curr.basename + '\033[0m' ); 120 | } 121 | nextTest( tests ); 122 | }, curr.options ); 123 | } 124 | 125 | /** 126 | * Diff `actual` / `expected`. 127 | * 128 | * @param {String} actual 129 | * @param {String} expected 130 | */ 131 | 132 | function diff (actual, expected) { 133 | actual = actual.split('\n'); 134 | expected = expected.split('\n'); 135 | var len = largestLineIn(expected); 136 | 137 | expected.forEach(function(line, i){ 138 | if (!line.length && i === expected.length - 1) return; 139 | var other = actual[i] 140 | , pad = len - line.length; 141 | pad = (new Array(++pad)).join(' '); 142 | var same = line === other; 143 | if (same) { 144 | console.error(' %d| %j%s | %j', i+1, line, pad, other); 145 | } else { 146 | console.error(' \033[31m%d| %j%s | %j\033[0m', i+1, line, pad, other); 147 | } 148 | }); 149 | } 150 | 151 | /** 152 | * Return the length of the largest line in `lines`. 153 | * 154 | * @param {Array} lines 155 | * @return {Number} 156 | */ 157 | 158 | function largestLineIn(lines) { 159 | return lines.reduce(function(n, line){ 160 | return Math.max(n, line.length); 161 | }, 0); 162 | } 163 | 164 | /** 165 | * Done!!! 166 | */ 167 | 168 | function done () { 169 | console.log(); 170 | console.log( 171 | ' \033[90mcompleted\033[0m' + 172 | ' \033[32m%d\033[0m' + 173 | ' \033[90mtests\033[0m', count); 174 | 175 | if (failures) { 176 | console.error(' \033[90mfailed\033[0m' + 177 | ' \033[31m%d\033[0m' + 178 | ' \033[90mtests\033[0m', failures); 179 | process.exit(failures); 180 | } 181 | 182 | console.log(); 183 | } 184 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var fs = require('fs') 7 | , cssom = require('cssom') 8 | , jsdom = require('jsdom') 9 | , own = {}.hasOwnProperty 10 | , os = require('os'); 11 | 12 | /** 13 | * Returns an array of the selectors. 14 | * 15 | * @license Sizzle CSS Selector Engine - MIT 16 | * @param {String} selectorText from cssom 17 | * @api public 18 | */ 19 | 20 | exports.extract = function extract (selectorText) { 21 | var attr = 0 22 | , sels = [] 23 | , sel = '' 24 | 25 | for (var i = 0, l = selectorText.length; i < l; i++) { 26 | var c = selectorText.charAt(i); 27 | 28 | if (attr) { 29 | if (']' === c || ')' === c) attr--; 30 | sel += c; 31 | } else { 32 | if (',' === c) { 33 | sels.push(sel); 34 | sel = ''; 35 | } else { 36 | if ('[' === c || '(' === c) attr++; 37 | if (sel.length || (c !== ',' && c !== '\n' && c !== ' ')) sel += c; 38 | } 39 | } 40 | } 41 | 42 | if (sel.length) sels.push(sel); 43 | 44 | return sels; 45 | } 46 | 47 | /** 48 | * Returns a parse tree for a CSS source. 49 | * If it encounters multiple selectors separated by a comma, it splits the 50 | * tree. 51 | * 52 | * @param {String} css source 53 | * @api public 54 | */ 55 | 56 | exports.parseCSS = function (css) { 57 | var rules = cssom.parse(css).cssRules || [] 58 | , ret = [] 59 | 60 | for (var i = 0, l = rules.length; i < l; i++) { 61 | if (rules[i].selectorText) { // media queries don't have selectorText 62 | var rule = rules[i] 63 | , selectors = exports.extract(rule.selectorText) 64 | 65 | for (var ii = 0, ll = selectors.length; ii < ll; ii++) { 66 | ret.push([selectors[ii], rule.style]); 67 | } 68 | } 69 | } 70 | 71 | return ret; 72 | } 73 | 74 | 75 | /** 76 | * Returns Media Query text for a CSS source. 77 | * 78 | * @param {String} css source 79 | * @api public 80 | */ 81 | 82 | exports.getMediaQueryText = function ( css ) 83 | { 84 | var rules = cssom.parse( css ).cssRules || [] 85 | var queries = []; 86 | 87 | for ( var i = 0, l = rules.length; i < l; i++ ) 88 | { 89 | /* CSS types 90 | STYLE: 1, 91 | IMPORT: 3, 92 | MEDIA: 4, 93 | FONT_FACE: 5, 94 | */ 95 | 96 | if ( rules[i].type === cssom.CSSMediaRule.prototype.type ) 97 | { 98 | var query = rules[i]; 99 | var queryString = []; 100 | 101 | queryString.push( os.EOL + "@media " + query.media[0] + " {" ); 102 | 103 | for ( var ii = 0, ll = query.cssRules.length; ii < ll; ii++ ) 104 | { 105 | var rule = query.cssRules[ii]; 106 | 107 | if ( rule.type === cssom.CSSStyleRule.prototype.type || rule.type === cssom.CSSFontFaceRule.prototype.type ) 108 | { 109 | queryString.push( " " + ( rule.type === cssom.CSSStyleRule.prototype.type ? rule.selectorText : "@font-face" ) + " {" ); 110 | 111 | for ( var style = 0; style < rule.style.length; style++ ) 112 | { 113 | var property = rule.style[style]; 114 | var value = rule.style[property]; 115 | var important = rule.style._importants[property] ? " !important" : ""; 116 | queryString.push( " " + property + ": " + value + important + ";" ); 117 | } 118 | queryString.push( " }" ); 119 | } 120 | } 121 | 122 | queryString.push( "}" ); 123 | var result = queryString.length ? queryString.join( os.EOL ) + os.EOL : ""; 124 | 125 | queries.push( result ); 126 | } 127 | } 128 | 129 | return queries.join( os.EOL ); 130 | } 131 | 132 | /** 133 | * Returns a JSDom jQuery object 134 | * 135 | * api public 136 | */ 137 | 138 | exports.jsdom = function (html) { 139 | return jsdom.html(html, null, { 140 | features: { 141 | QuerySelector: ['1.0'] 142 | , FetchExternalResources: false 143 | , ProcessExternalResources: false 144 | , MutationEvents: false 145 | } 146 | }); 147 | }; 148 | 149 | /** 150 | * Converts to array 151 | * 152 | * @api public 153 | */ 154 | 155 | exports.toArray = function (arr) { 156 | var ret = []; 157 | 158 | for (var i = 0, l = arr.length; i < l; i++) 159 | ret.push(arr[i]); 160 | 161 | return ret; 162 | }; 163 | 164 | /** 165 | * Compares two specificity vectors, returning the winning one. 166 | * 167 | * @param {Array} vector a 168 | * @param {Array} vector b 169 | * @return {Array} 170 | * @api public 171 | */ 172 | 173 | exports.compare = function (a, b) { 174 | for (var i = 0; i < 4; i++) { 175 | if (a[i] === b[i]) continue; 176 | if (a[i] > b[i]) return a; 177 | return b; 178 | } 179 | 180 | return b; 181 | } 182 | 183 | exports.extend = function (obj, src) { 184 | for (var key in src) if (own.call(src, key)) obj[key] = src[key]; 185 | return obj; 186 | } 187 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.3.2 / 2014-11-24 2 | ================== 3 | 4 | * Add option preserveImportant [jrit] 5 | 6 | 1.3.1 / 2014-11-24 7 | ================== 8 | 9 | * Add option applyLinksToStyleTags [jrit] 10 | 11 | 1.3.0 / 2014-07-22 12 | ================== 13 | 14 | * Update dependencies and code cleanup 15 | * heads up, xhtml might be handled differently 16 | * extract LICENSE from README 17 | * rename History.md to CHANGELOG.md 18 | 19 | 1.2.0 / 2014-05-22 20 | ================== 21 | 22 | * Add option applyWidthAttributes to API [jrit] 23 | 24 | 1.1.0 / 2014-05-22 25 | ================== 26 | 27 | * preserve single quotes for values [jrit] 28 | 29 | 1.0.0 / 2014-05-07 30 | ================== 31 | 32 | * order styles alphabetically to preserve expected style priority [jrit] 33 | * major version bumped in case people expected styles to not be sorted alphabetically 34 | 35 | 0.7.0 / 2014-05-06 36 | ================== 37 | 38 | * add preserveMediaQueries API [jrit] 39 | 40 | 0.6.0 / 2014-04-25 41 | ================== 42 | 43 | * update dependencies [jonkemp] 44 | 45 | 0.5.1 / 2014-03-12 46 | ================== 47 | 48 | * fix not including all stylesheets [andxyz] 49 | 50 | 0.5.0 / 2013-04-15 51 | ================== 52 | 53 | * update dependencies to latest 54 | * fix not preserving DOCTYPE [jgannonjr] 55 | * fix decoding local file paths [alexluke] 56 | 57 | 0.4.0 / 2013-04-15 58 | ================== 59 | 60 | * update jsdom dependency to 0.6.0 61 | 62 | 0.3.3 / 2013-04-08 63 | ================== 64 | 65 | * fix resolving file:// paths on windows (thanks Mirco Zeiss) 66 | * fix crash during cleanup. (thanks Ger Hobbelt) 67 | * update superagent to 0.14.0 68 | 69 | 0.3.2 / 2013-03-26 70 | ================== 71 | 72 | * fix regression: not ignoring pseudos 73 | 74 | 0.3.1 / 2013-03-26 75 | ================== 76 | 77 | * do not crash on ::selectors (covered by normalize.css test case) 78 | 79 | 0.3.0 / 2013-03-26 80 | ================== 81 | 82 | * update jsdom dependency to 0.5.4 83 | * support node v0.10 84 | * switch dependency to slick instead of mootools which was rudely unpublished 85 | 86 | 0.2.0 / 2013-02-13 87 | ================== 88 | 89 | * update jsdom dependency to 0.5.0 90 | 91 | 0.1.3 / 2013-02-12 92 | ================== 93 | 94 | * fix specificity test. all test cases passed now. 95 | * add a command line `juice` program 96 | 97 | 0.1.2 / 2013-02-11 98 | ================== 99 | 100 | * fix incorrectly lowercasing href 101 | 102 | 0.1.1 / 2013-02-11 103 | ================== 104 | 105 | * explicitly document which node versions are supported 106 | with `engines` and travis-ci. 107 | * expose `juice.inlineDocument` and `juice.inlineContent` 108 | 109 | 0.1.0 / 2013-02-07 110 | ================== 111 | 112 | * fix / test case for @media queries 113 | * merge [boost](https://github.com/andrewrk/boost) into juice 114 | * legacy `juice` function still works as is 115 | * add `juice(filePath, [options], callback)` 116 | * add `juice.juiceDocument(document, options, callback)` 117 | * add `juice.juiceContent(html, options, callback)` 118 | * remove `juice.juiceDom` 119 | 120 | 0.0.9 / 2013-02-07 121 | ================== 122 | 123 | * update jsdom dependency to 0.4.0 124 | * update cssom dependency to 0.2.5 125 | 126 | 0.0.8 / 2013-02-06 127 | ================== 128 | 129 | * expose a lower level export so you can operate on a jsdom document [andrewrk] 130 | * fix exports not working [andrewrk] 131 | * fix jshint problems [andrewrk] 132 | 133 | 0.0.7 / 2013-02-06 134 | ================== 135 | 136 | * fixed test case expected outputs to have starting and ending and tags as jsdom appends them in its html() function if they do not exist 137 | * regression test for previous fix for media queries. note i had to wrap my test .out content in tags in order to pass tests, it looks like they are appended at some point for partial html content which just guessing is new behavior from when these tests were written 138 | * make sure the css rule has selectorText to prevent parsing exception. hit this on @media rules which do not have selector text. afaik this means media queries will not be inlined. however everything else is. 139 | * bump jsdom 140 | * Added note regarding node-email-templates to README 141 | 142 | 0.0.6 / 2011-12-20 143 | ================== 144 | 145 | * Corrected juice unit tests for latest cssom. 146 | * Fixed presence of \n in selectors. 147 | * Fixed unneeded removal of inline event handlers in html. 148 | * Bumped jsdom. 149 | 150 | 0.0.5 / 2011-10-10 151 | ================== 152 | 153 | * Added whitelist of pseudos to ignore (fixes `:first-child` etc) 154 | * Fine-tuned jsdom for speed (disabled unneded features). 155 | * Added caching of parsed selectors. 156 | 157 | 0.0.4 / 2011-10-10 158 | ================== 159 | 160 | * Fixed `:hover`. 161 | 162 | 0.0.3 / 2011-10-09 163 | ================== 164 | 165 | * Fixed specificity :not recursion. 166 | 167 | 0.0.2 / 2011-10-09 168 | ================== 169 | 170 | * Fixed specificity calculation for `not()` and pseudos. [arian] 171 | 172 | 0.0.1 / 2011-10-09 173 | ================== 174 | 175 | * Initial release. 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/andrewrk/juice.png?branch=master)](https://travis-ci.org/andrewrk/juice) 2 | # Juice ![](http://i.imgur.com/jN8Ht.gif) 3 | 4 | Given HTML, juice will inline your CSS properties into the `style` 5 | attribute. 6 | 7 | ## How to use 8 | 9 | ```js 10 | var juice = require('juice2'); 11 | juice("/path/to/file.html", function(err, html) { 12 | console.log(html); 13 | }); 14 | ``` 15 | 16 | `/path/to/file.html`: 17 | ```html 18 | 19 | 20 | 23 | 24 | 25 | 26 |

Test

27 | 28 | 29 | ``` 30 | 31 | `style.css` 32 | ```css 33 | p { 34 | text-decoration: underline; 35 | } 36 | ``` 37 | 38 | Output: 39 | ```html 40 | 41 | 42 | 43 | 44 |

Test

45 | 46 | 47 | ``` 48 | 49 | ## What is this useful for ? 50 | 51 | - HTML emails. For a comprehensive list of supported selectors see 52 | [here](http://www.campaignmonitor.com/css/) 53 | - Embedding HTML in 3rd-party websites. 54 | 55 | ## Projects using juice 56 | 57 | * [node-email-templates][1] - Node.js module for rendering beautiful emails with [ejs][2] templates and email-friendly inline CSS using [juice][3]. 58 | * [swig-email-templates][4] - Uses [swig][5], which gives you [template inheritance][6], and 59 | can generate a [dummy context][7] from a template. 60 | 61 | [1]: https://github.com/niftylettuce/node-email-templates 62 | [2]: https://github.com/visionmedia/ejs 63 | [3]: https://github.com/LearnBoost/juice 64 | [4]: https://github.com/andrewrk/swig-email-templates 65 | [5]: https://github.com/paularmstrong/swig 66 | [6]: https://docs.djangoproject.com/en/dev/topics/templates/#template-inheritance 67 | [7]: https://github.com/andrewrk/swig-dummy-context 68 | 69 | ## Documentation 70 | 71 | ### juice(filePath, [options], callback) 72 | 73 | * `filePath` - html file 74 | * `options` - (optional) object containing these properties: 75 | - `extraCss` - extra css to apply to the file. Defaults to `""`. 76 | - `applyStyleTags` - whether to inline styles in `` 77 | Defaults to `true`. 78 | - `applyLinkTags` [Deprecated, use applyLinksToStyleTags] - whether to resolve `` tags 79 | and inline the resulting styles into document `head`. Defaults to `true`. 80 | - `applyLinksToStyleTags` - whether to resolve `` tags 81 | and replace them with `style` tags. This works with `preserveMediaQueries` 82 | unlike the deprecated `applyLinkTags`. Defaults to `true`. 83 | - `removeStyleTags` - whether to remove the original `` 84 | tags after (possibly) inlining the css from them. Defaults to `true`. 85 | - `preserveMediaQueries` - preserves all media queries (and contained styles) 86 | within `` tags as a refinement when `removeStyleTags` is `true`. 87 | Other styles are removed. Defaults to `false`. 88 | - `preserveImportant` - preserves `!important` in values. Defaults to `false`. 89 | - `applyWidthAttributes` - whether to use any CSS pixel widths to create 90 | `width` attributes on elements set in `juice.widthElements` 91 | - `removeLinkTags` - whether to remove the original `` 92 | tags after (possibly) inlining the css from them. Defaults to `true`. 93 | - `url` - how to resolve hrefs. Defaults to using `filePath`. If you want 94 | to override, be sure your `url` has the protocol at the beginning, e.g. 95 | `http://` or `file://`. 96 | * `callback(err, html)` 97 | - `err` - `Error` object or `null`. 98 | - `html` - contains the html from `filePath`, with potentially `