├── index.js ├── .travis.yml ├── .npmignore ├── test ├── helpers.js └── plugin.js ├── .eslintrc ├── .gitignore ├── lib ├── inlineCss.es6 └── helpers.es6 ├── LICENSE.txt ├── package.json └── README.md /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/inlineCss').default; 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - stable 5 | - "0.12" 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .eslintrc 3 | .travis.yml 4 | node_modules/ 5 | npm-debug.log 6 | /lib/*.es6 7 | /test/ 8 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { sortCssNodesBySpecificity } from '../lib/helpers'; 3 | 4 | describe('Helpers', () => { 5 | it('sortCssNodesBySpecificity()', () => { 6 | const cssNodes = [{selector: '#foo'}, {selector: '.bar'}, {selector: 'div'}]; 7 | const sortedCssNodes = [{selector: 'div'}, {selector: '.bar'}, {selector: '#foo'}]; 8 | 9 | expect(sortCssNodesBySpecificity(cssNodes)).toEqual(sortedCssNodes); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "indent": [2, 4, {"SwitchCase": 1}], 5 | "quotes": [2, "single"], 6 | "linebreak-style": [2, "unix"], 7 | "semi": [2, "always"], 8 | "camelcase": [2, {"properties": "always"}], 9 | "brace-style": [2, "1tbs", {"allowSingleLine": true}] 10 | }, 11 | "env": { 12 | "es6": true, 13 | "node": true, 14 | "browser": false, 15 | "mocha": true 16 | }, 17 | "extends": "eslint:recommended", 18 | "ecmaFeatures": { 19 | "experimentalObjectRestSpread": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Source: https://github.com/github/gitignore/blob/master/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 30 | node_modules 31 | 32 | # Optional npm cache directory 33 | .npm 34 | 35 | # Optional REPL history 36 | .node_repl_history 37 | 38 | 39 | 40 | # Custom 41 | /lib/*.js 42 | -------------------------------------------------------------------------------- /lib/inlineCss.es6: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss'; 2 | import matchHelper from 'posthtml-match-helper'; 3 | import { extendStyle, sortCssNodesBySpecificity, getCssFromStyleTags } from './helpers'; 4 | 5 | 6 | export default css => { 7 | return function inlineCss(tree) { 8 | if (!css || (typeof css !== 'string' && !Object.keys(css).length)) { 9 | css = getCssFromStyleTags(tree); 10 | } 11 | 12 | var postcssObj = css.then ? css : postcss().process(css); 13 | return postcssObj 14 | .then(result => sortCssNodesBySpecificity(result.root.nodes)) 15 | .then(cssNodes => { 16 | cssNodes.forEach(cssNode => { 17 | tree.match(matchHelper(cssNode.selector), htmlNode => { 18 | return extendStyle(htmlNode, cssNode); 19 | }); 20 | }); 21 | }); 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kirill Maltsev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "posthtml-inline-css", 3 | "version": "1.2.3", 4 | "description": "PostHTML CSS Inliner", 5 | "main": "index.js", 6 | "author": "Kirill Maltsev ", 7 | "license": "MIT", 8 | "scripts": { 9 | "compile": "rm -f lib/*.js && babel -d lib/ lib/", 10 | "lint": "eslint *.js lib/*.es6 test/", 11 | "pretest": "npm run lint && npm run compile", 12 | "test": "_mocha --compilers js:babel-core/register --check-leaks", 13 | "prepublish": "npm run compile" 14 | }, 15 | "keywords": [ 16 | "posthtml", 17 | "posthtml-plugin", 18 | "html", 19 | "css", 20 | "inliner", 21 | "inline", 22 | "postproccessor", 23 | "style" 24 | ], 25 | "babel": { 26 | "presets": [ 27 | "es2015" 28 | ] 29 | }, 30 | "dependencies": { 31 | "object-assign": "^4.0.1", 32 | "postcss": "^5.0.12", 33 | "posthtml-attrs-parser": "^0.1.0", 34 | "posthtml-match-helper": "^1.0.1", 35 | "specificity": "^0.2.0" 36 | }, 37 | "devDependencies": { 38 | "babel-cli": "^6.1.18", 39 | "babel-core": "^6.1.21", 40 | "babel-eslint": "^6.0.0", 41 | "babel-preset-es2015": "^6.1.18", 42 | "eslint": "^2.13.1", 43 | "expect": "^1.13.0", 44 | "lodash": "^4.0.0", 45 | "mocha": "^3.0.0", 46 | "posthtml": "^0.9.0" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "git://github.com/posthtml/posthtml-inline-css.git" 51 | }, 52 | "bugs": { 53 | "url": "https://github.com/posthtml/posthtml-inline-css/issues" 54 | }, 55 | "homepage": "https://github.com/posthtml/posthtml-inline-css" 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # posthtml-inline-css [![npm version](https://badge.fury.io/js/posthtml-inline-css.svg)](http://badge.fury.io/js/posthtml-inline-css) [![Build Status](https://travis-ci.org/posthtml/posthtml-inline-css.svg?branch=master)](https://travis-ci.org/posthtml/posthtml-inline-css) 2 | 3 | [PostHTML](https://github.com/posthtml/posthtml) plugin for inlining CSS to style attrs. 4 | 5 | Many modern email clients nowadays support CSS in a `
Hello!
'; 28 | 29 | posthtml([require('posthtml-inline-css')()]) 30 | .process(html) 31 | .then(function (result) { 32 | console.log(result.html); 33 | }); 34 | 35 | //
Hello!
36 | ``` 37 | 38 | 39 | ### PostCSS 40 | ```js 41 | var posthtml = require('posthtml'), 42 | postcss = require('postcss'), 43 | postcssObj = postcss(/* some PostCSS plugins */).process('div { color: white }'); 44 | 45 | 46 | posthtml([require('posthtml-inline-css')(postcssObj)]) 47 | .process('
Hello!
') 48 | .then(function (result) { 49 | console.log(result.html); 50 | }); 51 | 52 | //
Hello!
53 | ``` 54 | -------------------------------------------------------------------------------- /lib/helpers.es6: -------------------------------------------------------------------------------- 1 | import objectAssign from 'object-assign'; 2 | import specificity from 'specificity'; 3 | import parseAttrs from 'posthtml-attrs-parser'; 4 | 5 | 6 | export function extendStyle(htmlNode, cssNode) { 7 | var attrs = parseAttrs(htmlNode.attrs); 8 | const cssNodeCss = parseCssFromNode(cssNode); 9 | attrs.style = objectAssign(attrs.style || {}, cssNodeCss); 10 | htmlNode.attrs = attrs.compose(); 11 | 12 | return htmlNode; 13 | } 14 | 15 | 16 | export function sortCssNodesBySpecificity(nodes) { 17 | // Sort CSS nodes by specificity (ascending): div - .foo - #bar 18 | return nodes.sort((a, b) => { 19 | a = typeof a.selector == 'string' ? getSpecificity(a.selector) : 0; 20 | b = typeof b.selector == 'string' ? getSpecificity(b.selector) : 0; 21 | 22 | if (a > b) { 23 | return 1; 24 | } else if (a < b) { 25 | return -1; 26 | } else { 27 | return 0; 28 | } 29 | }); 30 | } 31 | 32 | 33 | export function getCssFromStyleTags(htmlTree) { 34 | var css = []; 35 | htmlTree.match({tag: 'style'}, tag => { 36 | css = css.concat(tag.content || []); 37 | return tag; 38 | }); 39 | 40 | return css.join(' '); 41 | } 42 | 43 | 44 | var specificityCache = {}; 45 | function getSpecificity(selector) { 46 | if (specificityCache[selector] !== undefined) { 47 | return specificityCache[selector]; 48 | } 49 | 50 | const specificityResult = specificity.calculate(selector)[0]; 51 | const specificityParts = specificityResult.specificity.split(',').reverse(); 52 | // Convert "0,1,3,2" to 10302 (2*1 + 3*100 + 1*10'000 + 0*1'000'000) 53 | const totalSpecificity = specificityParts.reduce((totalSpecificity, specificity, i) => { 54 | specificity = parseInt(specificity, 10); 55 | return totalSpecificity + (specificity * Math.pow(10, i * 2)); 56 | }, 0); 57 | 58 | specificityCache[selector] = totalSpecificity; 59 | 60 | return totalSpecificity; 61 | } 62 | 63 | 64 | function parseCssFromNode(cssNode) { 65 | var css = {}; 66 | cssNode.nodes.forEach(node => { 67 | css[node.prop] = node.value; 68 | }); 69 | 70 | return css; 71 | } 72 | -------------------------------------------------------------------------------- /test/plugin.js: -------------------------------------------------------------------------------- 1 | import { format } from 'util'; 2 | import expect from 'expect'; 3 | import posthtml from 'posthtml'; 4 | import postcss from 'postcss'; 5 | import inlineCss from '..'; 6 | 7 | const css = '.lead { font-size: 14px; color: blue }\n' + 8 | 'div { color: red; padding: 1px }'; 9 | 10 | const html = '
hello
' + 11 | '
world
'; 12 | 13 | const expectedHtml = '
hello
' + 14 | '
world
'; 15 | 16 | describe('Plugin', () => { 17 | it('should not broken if options empty object', () => { 18 | return initPlugin({}, '
').then(html => expect(html).toBe('
')); 19 | }); 20 | 21 | it('should inline plain CSS', () => { 22 | return initPlugin(css, html).then(html => expect(html).toBe(expectedHtml)); 23 | }); 24 | 25 | 26 | it('should inline PostCSS', () => { 27 | return initPlugin(postcss().process(css), html).then(html => { 28 | expect(html).toBe(expectedHtml); 29 | }); 30 | }); 31 | 32 | 33 | it('should inline CSS from %s '; 36 | const htmlWithStyles = format(htmlTemplate, cssParts[0], html, cssParts[1]); 37 | const expectedHtmlWithStyle = format(htmlTemplate, cssParts[0], expectedHtml, cssParts[1]); 38 | 39 | return initPlugin(undefined, htmlWithStyles).then(html => { 40 | expect(html).toBe(expectedHtmlWithStyle); 41 | }); 42 | }); 43 | 44 | it('should properly process any other nodes', () => { 45 | var css = '@media {}/* comment */.test {color: red;}'; 46 | var html = '
olala
'; 47 | var expectedHtml = '
olala
'; 48 | return initPlugin(css, html).then(html => expect(html).toBe(expectedHtml)); 49 | }); 50 | }); 51 | 52 | 53 | function initPlugin(options, html) { 54 | return posthtml() 55 | .use(inlineCss(options)) 56 | .process(html) 57 | .then(result => result.html); 58 | } 59 | --------------------------------------------------------------------------------