├── .babelrc ├── .editorconfig ├── .eslintrc ├── .github └── workflows │ └── npmpublish.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── WebpBase64.js ├── Webpcss.js └── index.js ├── index.js ├── lib ├── WebpBase64.js ├── Webpcss.js └── index.js ├── package-lock.json ├── package.json └── test ├── base64_spec.js ├── fixtures ├── avatar.png ├── avatar.webp ├── base64.js ├── circle.svg ├── kitten.jpg └── kitten.webp └── main_spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": ["add-module-exports"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | [*] 7 | # Change these settings to your own preference 8 | indent_style = space 9 | indent_size = 2 10 | 11 | # We recommend you to keep these unchanged 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "prettier" 5 | ], 6 | "parser": "babel-eslint", 7 | "env": { 8 | "browser": true, 9 | "node": true 10 | }, 11 | "plugins": [ 12 | "prettier" 13 | ], 14 | "rules": { 15 | "no-multi-spaces": 0, 16 | "space-infix-ops": 0, 17 | "quotes": [ 18 | 2, "double", "avoid-escape" // http://eslint.org/docs/rules/quotes 19 | ], 20 | "func-names": 0, 21 | "vars-on-top": 0, 22 | "strict": 0, 23 | "no-unused-expressions": 0, 24 | "consistent-return": 0, 25 | "one-var": 0, 26 | "new-cap": 0, 27 | "no-else-return": 0, 28 | "semi-spacing": 0, 29 | "no-nested-ternary": 0, 30 | "no-shadow": 0, 31 | "no-param-reassign": 0, 32 | "no-extend-native": 0, 33 | "no-empty": 0, 34 | "guard-for-in": 0, 35 | "comma-dangle": 0, 36 | "space-before-function-paren": 0, 37 | "prefer-template": 0, 38 | "no-useless-concat": 0, 39 | "no-confusing-arrow": 0, 40 | "arrow-parens": 0, 41 | "no-extra-boolean-cast": 0 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | - run: npm ci 16 | - run: npm test 17 | 18 | publish-npm: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v1 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: 12 26 | registry-url: https://registry.npmjs.org/ 27 | - run: npm ci 28 | - run: npm publish 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 31 | 32 | publish-gpr: 33 | needs: build 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v1 37 | - uses: actions/setup-node@v1 38 | with: 39 | node-version: 12 40 | registry-url: https://npm.pkg.github.com/ 41 | scope: '@your-github-username' 42 | - run: npm ci 43 | - run: npm publish 44 | env: 45 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | tmp 4 | coverage/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .eslintrc 3 | .git 4 | .gitignore 5 | .jscsrc 6 | .jshintrc 7 | .npmignore 8 | .travis.yml 9 | 10 | coverage/ 11 | lib/ 12 | node_modules/ 13 | test/ 14 | /index.js 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | useTabs: false, 4 | semi: true, 5 | singleQuote: false, 6 | trailingComma: "es5", 7 | bracketSpacing: true, 8 | arrowParens: "avoid", 9 | }; 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: true 3 | before_install: 4 | - curl -s https://raw.githubusercontent.com/Intervox/node-webp/latest/bin/install_webp | sudo bash 5 | node_js: 6 | - 5.0 7 | - 6.0 8 | - 7.0 9 | - 8.0 10 | - 10.0 11 | after_script: 12 | - npm run coveralls 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.3.4](https://github.com/lexich/webpcss/compare/v1.3.3...v1.3.4) (2019-12-09) 6 | 7 | 8 | ## [1.3.3](https://github.com/lexich/webpcss/compare/v1.3.2...v1.3.3) (2019-03-14) 9 | 10 | 11 | 12 | 13 | ## [1.3.2](https://github.com/lexich/webpcss/compare/v1.3.1...v1.3.2) (2019-03-14) 14 | 15 | 16 | 17 | 18 | ## [1.3.1](https://github.com/lexich/webpcss/compare/v1.3.0...v1.3.1) (2018-12-17) 19 | 20 | 21 | 22 | 23 | # [1.3.0](https://github.com/lexich/webpcss/compare/v1.2.2...v1.3.0) (2018-12-17) 24 | 25 | 26 | 27 | 28 | ## 1.2.2 (2018-07-27) 29 | 30 | 31 | 32 | 33 | ## 1.1.3 (2016-11-06) 34 | 35 | 36 | 37 | 38 | ## 1.1.2 (2016-05-13) 39 | 40 | 41 | 42 | 43 | ## 1.1.1 (2015-12-21) 44 | 45 | 46 | 47 | 48 | # 1.1.0 (2015-09-18) 49 | 50 | 51 | 52 | 53 | ## 1.0.8 (2015-09-08) 54 | 55 | 56 | 57 | 58 | ## 1.0.7 (2015-08-31) 59 | 60 | 61 | 62 | 63 | ## 1.0.6 (2015-06-10) 64 | 65 | 66 | 67 | 68 | ## 1.0.5 (2015-06-10) 69 | 70 | 71 | 72 | 73 | ## 1.0.3 (2015-06-10) 74 | 75 | 76 | 77 | 78 | ## 1.0.2 (2015-06-10) 79 | 80 | 81 | 82 | 83 | ## 1.0.1 (2015-05-06) 84 | 85 | 86 | 87 | 88 | # 1.0.0 (2015-04-27) 89 | 90 | 91 | ### Bug Fixes 92 | 93 | * travis ([641c927](https://github.com/lexich/webpcss/commit/641c927)) 94 | 95 | 96 | 97 | 98 | ## 0.0.14 (2015-01-31) 99 | 100 | 101 | 102 | 103 | ## 0.0.13 (2015-01-26) 104 | 105 | 106 | 107 | 108 | ## 0.0.12 (2015-01-16) 109 | 110 | 111 | 112 | 113 | ## 0.0.11 (2014-11-26) 114 | 115 | 116 | 117 | 118 | ## 0.0.10 (2014-11-14) 119 | 120 | 121 | 122 | 123 | ## 0.0.9 (2014-11-10) 124 | 125 | 126 | 127 | 128 | ## 0.0.7 (2014-10-13) 129 | 130 | 131 | 132 | 133 | ## 0.0.6 (2014-10-13) 134 | 135 | 136 | 137 | 138 | ## 0.0.5 (2014-09-29) 139 | 140 | 141 | 142 | 143 | ## 0.0.4 (2014-09-29) 144 | 145 | 146 | 147 | 148 | ## 0.0.2 (2014-09-26) 149 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Efremov Alex 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/lexich/webpcss.svg)](https://travis-ci.org/lexich/webpcss) 2 | [![NPM version](https://badge.fury.io/js/webpcss.svg)](http://badge.fury.io/js/webpcss) 3 | [![Coverage Status](https://coveralls.io/repos/lexich/webpcss/badge.png)](https://coveralls.io/r/lexich/webpcss) 4 | [![Dependency Status](https://david-dm.org/lexich/webpcss.png)](https://david-dm.org/lexich/webpcss) 5 | [![devDependency Status](https://david-dm.org/lexich/webpcss/dev-status.png)](https://david-dm.org/lexich/webpcss) 6 | 7 | ### About 8 | [PostCSS](https://github.com/postcss/postcss) processor to add links to WebP images for browsers that support it. 9 | 10 | WebP is image format that is smaller, that PNG or JPEG, but it is [supported](http://caniuse.com/webp) only by Chrome. 11 | 12 | ### Plugins for intergation with popular frontend build systems 13 | * [grunt-webpcss](https://github.com/lexich/grunt-webpcss) 14 | * [gulp-webpcss](https://github.com/lexich/gulp-webpcss) 15 | 16 | ### Install 17 | This plugin use [cwebp](https://github.com/Intervox/node-webp) for processing images. If you want to use this functionality read [Installation Guide](https://github.com/Intervox/node-webp#installation) 18 | 19 | ### Support 20 | Postcss drop support node 0.10.0 by default. But if your need this version 21 | use Promise polyfill. 22 | ```js 23 | var Promise = require("es6-promise"); 24 | Promise.polyfill(); 25 | ``` 26 | Versions >= 0.12 including 4.0.0 and iojs works without polyfills 27 | 28 | ### Examples 29 | Using with [webpack](https://webpack.github.io/) and [postcss-loader](https://github.com/postcss/postcss-loader): 30 | *[https://github.com/lexich/example-webpack-postcss-loader-webpcss](https://github.com/lexich/example-webpack-postcss-loader-webpcss)* 31 | 32 | 33 | Using with [gulp-postcss](https://github.com/w0rm/gulp-postcss): 34 | 35 | ```js 36 | var gulp = require('gulp'); 37 | var webp = require('gulp-webp'); 38 | var postcss = require('gulp-postcss'); 39 | var autoprefixer = require('autoprefixer-core'); 40 | var webpcss = require('webpcss'); 41 | 42 | gulp.task('webp', function () { 43 | return gulp.src('./images/*.{png,jpg,jpeg}') 44 | .pipe(webp()) 45 | .pipe(gulp.dest('./images')); 46 | }); 47 | 48 | gulp.task('css', function () { 49 | var processors = [ 50 | autoprefixer, 51 | webpcss.default 52 | ]; 53 | return gulp.src('./src/*.css') 54 | .pipe( postcss(processors) ) 55 | .pipe( gulp.dest('./dist') ); 56 | }); 57 | gulp.task('default',['webp', 'css']); 58 | ``` 59 | 60 | Results of webpcss processor. 61 | 62 | ```css 63 | /* Source */ 64 | .icon { color: #222; background-image: url('../images/icon.png'); } 65 | 66 | /* Result */ 67 | .icon { background-image: url('../images/icon.png'); } 68 | .icon { color: #222; } 69 | .webp .icon { background-image: url('../images/icon.webp'); } 70 | ``` 71 | 72 | Results of webp task. 73 | webp task appends .webp images for every .png image. 74 | 75 | ```sh 76 | #Source 77 | > ls images 78 | icon.png 79 | 80 | #Result 81 | > ls images 82 | icon.png icon.webp 83 | ``` 84 | 85 | ### Options 86 | 87 | - `webpClass` 88 | Type: String 89 | Default: '.webp' 90 | Class which prepend selector. For expample: 91 | before 92 | 93 | ```css 94 | .test { background-image:url('test.png'); } 95 | ``` 96 | 97 | after 98 | 99 | ```css 100 | .test { background-image:url('test.png'); } 101 | .webp .test { background-image:url('test.webp'); } 102 | ``` 103 | .webp class indicate webp browser support. Recommends to use [Modernizr](http://modernizr.com/) 104 | 105 | - `noWebpClass` 106 | Type: String 107 | Default: "" 108 | Class which prepend selector without webp content. For expample: 109 | `noWebpClass=".no-webp"` 110 | before 111 | 112 | ```css 113 | .test { background-image:url('test.png'); } 114 | ``` 115 | 116 | after 117 | 118 | ```css 119 | .no-webp .test { background-image:url('test.png'); } 120 | .webp .test { background-image:url('test.webp'); } 121 | ``` 122 | 123 | - `replace_from` 124 | Type: RegExp 125 | Default: /\.(png|jpg|jpeg)/ 126 | RegExp pattern for replace 127 | 128 | - `replace_to` 129 | Type: String or Function 130 | Default: .webp 131 | The contents of `replace_from` will be replaced by `replace_to`. They will be replaced with ".webp" by default. 132 | 133 | If `replace_to` is a Function, not `replace_from` but the whole url will be replaced with the return value of the function. 134 | 135 | The function will have a argument object, which has the following properties: 136 | > `url`: The whole original url. 137 | 138 | To checks browser support of webp format need to use [Modernizr](http://modernizr.com/) which adds `.webp` class to `body` if browser support WebP and browser will download smaller WebP image instead of bigger PNG. 139 | 140 | ```html 141 | 144 | ``` 145 | 146 | 147 | - `process_selector` 148 | Type: function(selector, baseClass) 149 | modify `selector` with `baseClass` 150 | 151 | - `inline` 152 | Type: Boolean 153 | Default: false 154 | Turn on inline images mode. You need setup `image_path` and `css_path` for 155 | correct resolving image path. 156 | 157 | ```css 158 | .test { background-image:url('test.png'); } // `${inline}/`test.png 159 | ``` 160 | after 161 | ```css 162 | .test { background-image:url('test.png'); } // `${inline}/`test.png 163 | .webp .test { background-image: url(data:image/webp;base64,UklGRmAAAABXRUJQVlA4IFQAAADwAQCdASoKAAgAAgA0JQBOgB6XKgsI3ogA/gEAtARF3E8iPiuncdF4zSgVjkZEgIatdknUme0fy3LBWFwbOjWUoaOOso78HmdNsa5gir1gmEwgAAA=); } 164 | ``` 165 | 166 | - `image_root` 167 | Type: String 168 | Default: "" 169 | This property needs to resolve absolute paths `url(/images/1.png)` while inlining images or other file info options. 170 | 171 | - `css_root` 172 | Type: String 173 | Default: "" 174 | This property needs to resolve relative paths `url(../images/1.png)` `url(image.png)` while inlining images or other file info options. 175 | 176 | - `minAddClassFileSize` 177 | Type: Number 178 | Default: 0 179 | `webpClass` will be added when images only of which greater than certain certain file size in bytes. It only works when the file path can be resolved(Either `image_root` or `css_root` or `resolveUrlRelativeToFile`) if they are files but not base64 encoded content. 180 | 181 | - `resolveUrlRelativeToFile` 182 | Type: Boolean 183 | Default: false 184 | This property is needed to resolve relative paths `url(../images/1.png)` `url(image.png)` while inlining images or other options which are relative to file info . It will try to find resource file relative to current css file when it's true and `css_root` is not set. 185 | 186 | - `localImgFileLocator` 187 | Type: Function 188 | Default: null 189 | When this property is set, it will be used to resolve the file path of image from the css url value while inlining images or other options which are relative to file info. In addition, `resolveUrlRelativeToFile`, `css_root`, `image_root` will be ignored. 190 | This function should return the exact file path in the file system and it has an argument object, which contains the following properties: 191 | ```javascript 192 | { 193 | url, // The original url in the css value 194 | cssFilePath, // The absolute file path of the css file 195 | } 196 | ``` 197 | 198 | - `copyBackgroundSize` 199 | Type: Boolean 200 | Default: false 201 | It will copy the `background-size` rule of same scope into the webp class rules if it's true 202 | 203 | - `replaceRemoteImage` 204 | Type: Boolean 205 | Default: true 206 | It will add webp class when the url it's with host(eg. `url(//foo.com/image.png)` or `url(http://foo.com/image.png)` or `url(https://foo.com/image.png)`) if it's true 207 | 208 | - `cwebp_configurator` 209 | Type: function(encoder){} 210 | Default: null 211 | You can configure cwebp encoder according [cwebp documentation](https://github.com/Intervox/node-webp#specifying-conversion-options) 212 | 213 | ### Changelog 214 | - 1.3.0 - Add option `localImgFileLocator` 215 | - 1.2.1 - Add options `copyBackgroundSize`, `replaceRemoteImage`, bug fixes for absolute URL detection and unsupported based64 encoded content. 216 | - 1.2.0 - Improve cross platform compatibility, add Function type as replace_to option, add options `minAddClassFileSize`, `resolveUrlRelativeToFile` 217 | - 1.1.0 - add webpClass, noWebpClass options deprecate baseClass option 218 | - 1.0.0 - add suport CWeb for automatic inline images in webp format 219 | - 0.0.11 - add support of border-image, update deps 220 | - 0.0.10 - update deps 221 | - 0.0.9 - update postcss to 2.2.6 222 | - 0.0.8 - fix bug with using @media-queryes and @support statement 223 | - 0.0.7 - fix bug with multiple selectors 224 | - 0.0.6 - add process_selector options for transform selectors 225 | - 0.0.5 - update api according postcss convention 226 | 227 | 228 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/lexich/webpcss/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 229 | 230 | -------------------------------------------------------------------------------- /dist/WebpBase64.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * webpcss 5 | * https://github.com/lexich/webpcss 6 | * 7 | * Copyright (c) 2015 Efremov Alexey 8 | * Licensed under the MIT license. 9 | */ 10 | 11 | /* eslint class-methods-use-this: 0 */ 12 | 13 | Object.defineProperty(exports, "__esModule", { 14 | value: true 15 | }); 16 | 17 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 18 | 19 | var _urldata = require("urldata"); 20 | 21 | var _urldata2 = _interopRequireDefault(_urldata); 22 | 23 | var _cwebp = require("cwebp"); 24 | 25 | var _parseDataUri2 = require("parse-data-uri"); 26 | 27 | var _parseDataUri3 = _interopRequireDefault(_parseDataUri2); 28 | 29 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 30 | 31 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 32 | 33 | var webpBinPath = require("webp-converter/cwebp")(); 34 | 35 | var base64pattern = "data:"; 36 | var base64patternEnd = ";base64,"; 37 | 38 | var WebpBase64 = function () { 39 | function WebpBase64() { 40 | _classCallCheck(this, WebpBase64); 41 | } 42 | 43 | _createClass(WebpBase64, [{ 44 | key: "extract", 45 | value: function extract(value, isUrl) { 46 | var result = []; 47 | if (!!isUrl) { 48 | var data = (0, _urldata2.default)(value); 49 | for (var i = 0; i < data.length; i += 1) { 50 | /* eslint no-continue: 0 */ 51 | if (!data[i]) { 52 | continue; 53 | } 54 | result[result.length] = WebpBase64.extractor(data[i], isUrl); 55 | } 56 | } else { 57 | var res = WebpBase64.extractor(value, isUrl); 58 | if (res) { 59 | result[result.length] = res; 60 | } 61 | } 62 | return result; 63 | } 64 | }, { 65 | key: "convert", 66 | value: function convert(data, fConfig) { 67 | var buffer = data instanceof Buffer ? data : Buffer.from(data, "base64"); 68 | var encoderBase = new _cwebp.CWebp(buffer, webpBinPath); 69 | var encoder = fConfig ? fConfig(encoderBase) : encoderBase; 70 | return encoder.toBuffer(); 71 | } 72 | }], [{ 73 | key: "extractor", 74 | value: function extractor(value) { 75 | if (!value) { 76 | return; 77 | } 78 | var base64pos = value.indexOf(base64pattern); 79 | if (base64pos >= 0) { 80 | var base64posEnd = value.indexOf(base64patternEnd); 81 | 82 | if (base64posEnd < 0) { 83 | var _parseDataUri = (0, _parseDataUri3.default)(value), 84 | mimeType = _parseDataUri.mimeType, 85 | data = _parseDataUri.data; 86 | 87 | return { mimetype: mimeType, data: data }; 88 | } else { 89 | var mimetype = value.slice(base64pos + base64pattern.length, base64posEnd); 90 | var _data = value.slice(base64posEnd + base64patternEnd.length); 91 | return { mimetype: mimetype, data: _data }; 92 | } 93 | } else { 94 | return { mimetype: "url", data: value }; 95 | } 96 | } 97 | }]); 98 | 99 | return WebpBase64; 100 | }(); 101 | 102 | exports.default = WebpBase64; 103 | module.exports = exports.default; -------------------------------------------------------------------------------- /dist/Webpcss.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * webpcss 5 | * https://github.com/lexich/webpcss 6 | * 7 | * Copyright (c) 2015 Efremov Alexey 8 | * Licensed under the MIT license. 9 | */ 10 | 11 | /* eslint no-useless-escape: 0 */ 12 | 13 | Object.defineProperty(exports, "__esModule", { 14 | value: true 15 | }); 16 | 17 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 18 | 19 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 20 | 21 | var _mimeTypes = require("mime-types"); 22 | 23 | var _mimeTypes2 = _interopRequireDefault(_mimeTypes); 24 | 25 | var _fileType = require("file-type"); 26 | 27 | var _fileType2 = _interopRequireDefault(_fileType); 28 | 29 | var _path = require("path"); 30 | 31 | var _path2 = _interopRequireDefault(_path); 32 | 33 | var _fs = require("fs"); 34 | 35 | var _fs2 = _interopRequireDefault(_fs); 36 | 37 | var _lodash = require("lodash"); 38 | 39 | var _WebpBase = require("./WebpBase64"); 40 | 41 | var _WebpBase2 = _interopRequireDefault(_WebpBase); 42 | 43 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 44 | 45 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 46 | 47 | function readFileAsync(path) { 48 | return new Promise(function (resolve, reject) { 49 | (0, _fs.readFile)(path, function (err, data) { 50 | return err ? reject(err) : resolve(data); 51 | }); 52 | }); 53 | } 54 | 55 | var rxHtml = /^html[_\.#\[]{1}/; 56 | var DEFAULTS = { 57 | webpClass: ".webp", 58 | noWebpClass: "", 59 | replace_from: /\.(png|jpg|jpeg)/g, 60 | replace_to: ".webp", 61 | inline: false /* root path to folder */ 62 | , image_root: "", 63 | css_root: "", 64 | minAddClassFileSize: 0, 65 | resolveUrlRelativeToFile: false, 66 | copyBackgroundSize: false, 67 | replaceRemoteImage: true, 68 | cwebp_configurator: null, 69 | process_selector: function process_selector(selector, baseClass) { 70 | if (baseClass) { 71 | return rxHtml.test(selector) ? selector.replace("html", "html" + baseClass) : baseClass + " " + selector; 72 | } 73 | return selector; 74 | } 75 | }; 76 | 77 | function deprecate(msg) { 78 | /* eslint no-console: 0 */ 79 | typeof console !== "undefined" && console.warn && console.warn(msg); 80 | } 81 | 82 | function canURLLocalResolve(url) { 83 | /* url(//foo.com/image.png) or url(http://foo.com/image.png) or url(https://foo.com/image.png) abs path with host */ 84 | return !/^(https?:)?\/\//i.test(url); 85 | } 86 | 87 | var Webpcss = function () { 88 | function Webpcss(opts) { 89 | _classCallCheck(this, Webpcss); 90 | 91 | if (!opts) { 92 | this.options = DEFAULTS; 93 | } else { 94 | this.options = _extends({}, DEFAULTS, opts); 95 | if (opts.baseClass) { 96 | this.options.webpClass = opts.baseClass; 97 | delete opts.baseClass; 98 | deprecate("Option `baseClass` is deprecated. Use webpClass instead."); 99 | } 100 | } 101 | this.base64 = new _WebpBase2.default(); 102 | } 103 | 104 | _createClass(Webpcss, [{ 105 | key: "postcss", 106 | value: function postcss(css, cb) { 107 | var _this = this; 108 | 109 | var asyncNodes = []; 110 | css.walkDecls(function (decl) { 111 | if ((decl.prop.indexOf("background") === 0 || decl.prop.indexOf("border-image") === 0) && decl.value.indexOf("url") >= 0) { 112 | asyncNodes[asyncNodes.length] = _this.asyncProcessNode(decl); 113 | } 114 | }); 115 | return Promise.all(asyncNodes).then(function (nodes) { 116 | nodes.filter(function (decl) { 117 | return decl; 118 | }).forEach(function (decl) { 119 | return css.append(decl); 120 | }); 121 | cb(); 122 | }).catch(function () { 123 | return cb(); 124 | }); 125 | } 126 | }, { 127 | key: "asyncProcessNode", 128 | value: function asyncProcessNode(decl) { 129 | var options = this.options, 130 | base64 = this.base64; 131 | 132 | 133 | function resolveUrlPath(url) { 134 | var urlPath = url; 135 | var canLocalResolve = canURLLocalResolve(url); 136 | 137 | if (canLocalResolve) { 138 | var localImgFileLocator = options.localImgFileLocator; 139 | 140 | if (localImgFileLocator) { 141 | var input = decl.source.input; 142 | 143 | if (input && input.file) { 144 | var cssFilePath = _path2.default.resolve(input.file); 145 | urlPath = localImgFileLocator({ 146 | url: url, 147 | cssFilePath: cssFilePath 148 | }); 149 | } else { 150 | console.warn("Source input not found: " + url); 151 | } 152 | } else if (url[0] === "/") { 153 | /* url(/image.png) abs path */ 154 | urlPath = _path2.default.resolve(_path2.default.join(options.image_root, url)); 155 | } else { 156 | /* url(../images.png) or url(image.png) - relative css path */ 157 | var resolveUrlRelativeToFile = options.resolveUrlRelativeToFile; 158 | 159 | if (options.css_root || !resolveUrlRelativeToFile) { 160 | urlPath = _path2.default.resolve(_path2.default.join(options.css_root, url)); 161 | } else if (resolveUrlRelativeToFile) { 162 | // resolve relative path automatically 163 | var _input = decl.source.input; 164 | 165 | if (_input && _input.file) { 166 | var file = _input.file; 167 | 168 | urlPath = _path2.default.resolve(_path2.default.join(_path2.default.dirname(file), url)); 169 | } else { 170 | console.warn("Source input not found: " + url); 171 | } 172 | } 173 | } 174 | } 175 | return { 176 | urlPath: urlPath, 177 | canLocalResolve: canLocalResolve 178 | }; 179 | } 180 | 181 | var breaks = 0; 182 | var selector = decl.parent.selectors.map(function (sel) { 183 | return options.process_selector(sel, options.webpClass); 184 | }).join(", "); 185 | var urls = base64.extract(decl.value, true); 186 | if (!urls.length) { 187 | return; 188 | } 189 | var rx = options.replace_from instanceof RegExp ? options.replace_from : new RegExp(options.replace_from, "g"); 190 | var asyncUrls = urls.map(function (item) { 191 | var url = item.data; 192 | var minAddClassFileSize = options.minAddClassFileSize; 193 | 194 | if (item.mimetype === "url") { 195 | var shouldAddClass = true; 196 | 197 | if (minAddClassFileSize > 0) { 198 | var _resolveUrlPath = resolveUrlPath(url), 199 | urlPath = _resolveUrlPath.urlPath, 200 | canLocalResolve = _resolveUrlPath.canLocalResolve; 201 | 202 | if (canLocalResolve) { 203 | try { 204 | var fileSize = _fs2.default.statSync(urlPath).size; 205 | if (fileSize < minAddClassFileSize) { 206 | shouldAddClass = false; 207 | } 208 | } catch (e) { 209 | console.warn("Analyze file " + urlPath + " size failed", e); 210 | } 211 | } 212 | } 213 | 214 | if (!options.inline) { 215 | var replaceTo = options.replace_to; 216 | 217 | var src = url; 218 | 219 | if (shouldAddClass) { 220 | var replaceRemoteImage = options.replaceRemoteImage; 221 | 222 | if (replaceRemoteImage || canURLLocalResolve(url)) { 223 | src = (0, _lodash.isFunction)(replaceTo) ? replaceTo({ 224 | url: url 225 | }) : url.replace(rx, replaceTo); 226 | } 227 | } 228 | breaks += +(src === url); 229 | return "url(" + src + ")"; 230 | } else { 231 | // eslint-disable-next-line no-lonely-if 232 | if (shouldAddClass) { 233 | var _resolveUrlPath2 = resolveUrlPath(url), 234 | _urlPath = _resolveUrlPath2.urlPath, 235 | _canLocalResolve = _resolveUrlPath2.canLocalResolve; 236 | 237 | if (_canLocalResolve) { 238 | return readFileAsync(_urlPath).then(function (data) { 239 | return base64.convert(data, options.cwebp_configurator).then(function (buffer) { 240 | return buffer && "url(data:image/webp;base64," + buffer.toString("base64") + ")"; 241 | }).catch(function () { 242 | return "url(" + item.data + ")"; 243 | }); 244 | }).catch(function () { 245 | breaks += 1; 246 | return "url(" + item.data + ")"; 247 | }); 248 | } else { 249 | breaks += 1; 250 | return "url(" + item.data + ")"; 251 | } 252 | } else { 253 | breaks += 1; 254 | return "url(" + item.data + ")"; 255 | } 256 | } 257 | } else { 258 | var buffer = url instanceof Buffer ? url : Buffer.from(url, "base64"); 259 | 260 | var _shouldAddClass = true; 261 | var ext = _mimeTypes2.default.extension(item.mimetype); 262 | if (!ext) { 263 | var ft = (0, _fileType2.default)(buffer); 264 | if (ft) { 265 | ext = ft.ext; 266 | } 267 | } 268 | // Unsupported types guarding 269 | if (!/png|jpg|jpeg|gif/i.test(ext)) { 270 | _shouldAddClass = false; 271 | } else if (minAddClassFileSize > 0) { 272 | if (Buffer.byteLength(buffer) < minAddClassFileSize) { 273 | _shouldAddClass = false; 274 | } 275 | } 276 | 277 | if (_shouldAddClass) { 278 | return base64.convert(url).then(function (buffer) { 279 | if (buffer) { 280 | return "url(data:image/webp;base64," + buffer.toString("base64") + ")"; 281 | } 282 | }).catch(function () { 283 | breaks += 1; 284 | return "url(" + item.data + ")"; 285 | }); 286 | } else { 287 | breaks += 1; 288 | return "url(" + item.data + ")"; 289 | } 290 | } 291 | }); 292 | return Promise.all(asyncUrls).then(function (urls) { 293 | if (breaks !== urls.length) { 294 | var originalRule = decl.parent; 295 | var copyBackgroundSize = options.copyBackgroundSize; 296 | 297 | 298 | if (options.noWebpClass) { 299 | // add .no-webp 300 | var selectorNoWebP = originalRule.selectors.map(function (sel) { 301 | return options.process_selector(sel, options.noWebpClass); 302 | }).join(", "); 303 | 304 | var noWebpRule = Webpcss.formatRule(originalRule.cloneBefore({ 305 | selector: selectorNoWebP 306 | })); 307 | 308 | decl.raws.before = " "; 309 | decl.moveTo(noWebpRule); 310 | } 311 | 312 | // add .webp 313 | var value = decl.value.split(" ").map(function (val) { 314 | return val.indexOf("url") >= 0 ? val.replace(/(url)\(.*\)/, urls.shift()) : val; 315 | }).join(" "); 316 | 317 | var webpRule = Webpcss.formatRule(originalRule.clone({ 318 | selector: selector 319 | })); 320 | 321 | var webpDecl = decl.clone({ 322 | prop: decl.prop, 323 | value: value 324 | }); 325 | webpDecl.raws.semicolon = true; 326 | webpDecl.raws.before = " "; 327 | 328 | webpRule.append(webpDecl); 329 | var webpTreeRule = Webpcss.appendToCopyTree(originalRule.parent, webpRule); 330 | 331 | originalRule.walkDecls(function (decl) { 332 | if (copyBackgroundSize) { 333 | if (decl.prop === "background-size") { 334 | webpRule.append(decl.clone()); 335 | } 336 | } 337 | }); 338 | 339 | // clean if original rule is empty 340 | !originalRule.nodes.length && originalRule.remove(); 341 | return webpTreeRule; 342 | } 343 | }); 344 | } 345 | }], [{ 346 | key: "appendToCopyTree", 347 | value: function appendToCopyTree(aRoot, aRule) { 348 | var root = aRoot; 349 | var rule = aRule; 350 | while (root.type !== "root") { 351 | rule = root.clone().removeAll().append(rule); 352 | root = root.parent; 353 | } 354 | return rule; 355 | } 356 | }, { 357 | key: "formatRule", 358 | value: function formatRule(rule) { 359 | var isRemove = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; 360 | 361 | rule.raws.semicolon = true; 362 | rule.raws.after = " "; 363 | return isRemove ? rule.removeAll() : rule; 364 | } 365 | }]); 366 | 367 | return Webpcss; 368 | }(); 369 | 370 | exports.default = Webpcss; 371 | module.exports = exports.default; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * webpcss 3 | * https://github.com/lexich/webpcss 4 | * 5 | * Copyright (c) 2015 Efremov Alexey 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | "use strict"; 10 | 11 | Object.defineProperty(exports, "__esModule", { 12 | value: true 13 | }); 14 | exports.transform = transform; 15 | 16 | var _postcss = require("postcss"); 17 | 18 | var _postcss2 = _interopRequireDefault(_postcss); 19 | 20 | var _Webpcss = require("./Webpcss"); 21 | 22 | var _Webpcss2 = _interopRequireDefault(_Webpcss); 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | var defaultWebpcss = null; 27 | 28 | var plugin = _postcss2.default.plugin("webpcss", function (options) { 29 | var pt = options ? new _Webpcss2.default(options) : defaultWebpcss || (defaultWebpcss = new _Webpcss2.default()); 30 | return function (css) { 31 | return new Promise(function (resolve, reject) { 32 | return pt.postcss(css, function (err, data) { 33 | return err ? reject(err, data) : resolve(data); 34 | }); 35 | }); 36 | }; 37 | }); 38 | 39 | exports.default = plugin; 40 | function transform(data, options, processOptions) { 41 | return (0, _postcss2.default)([plugin(options)]).process(data, processOptions); 42 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* eslint no-var: 0, import/export: 0, prefer-arrow-callback: 0 */ 4 | /* eslint import/no-extraneous-dependencies: 0, no-useless-escape: 0 */ 5 | var path = require("path"); 6 | 7 | var escape = function(str) { 8 | return str.replace(/[\[\]\/{}()*+?.\\^$|-]/g, "\\$&"); 9 | }; 10 | 11 | var regexp = ["lib", "test"] 12 | .map(function(i) { 13 | return "^" + escape(path.join(__dirname, i) + path.sep); 14 | }) 15 | .join("|"); 16 | 17 | require("babel-core/register")({ 18 | only: new RegExp("(" + regexp + ")"), 19 | ignore: false, 20 | loose: "all", 21 | }); 22 | module.exports = require("./lib"); 23 | -------------------------------------------------------------------------------- /lib/WebpBase64.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * webpcss 5 | * https://github.com/lexich/webpcss 6 | * 7 | * Copyright (c) 2015 Efremov Alexey 8 | * Licensed under the MIT license. 9 | */ 10 | 11 | /* eslint class-methods-use-this: 0 */ 12 | 13 | import urldata from "urldata"; 14 | import { CWebp } from "cwebp"; 15 | import parseDataUri from "parse-data-uri"; 16 | 17 | const webpBinPath = require("webp-converter/cwebp")(); 18 | 19 | const base64pattern = "data:"; 20 | const base64patternEnd = ";base64,"; 21 | 22 | class WebpBase64 { 23 | extract(value, isUrl) { 24 | const result = []; 25 | if (!!isUrl) { 26 | const data = urldata(value); 27 | for (let i = 0; i < data.length; i += 1) { 28 | /* eslint no-continue: 0 */ 29 | if (!data[i]) { 30 | continue; 31 | } 32 | result[result.length] = WebpBase64.extractor(data[i], isUrl); 33 | } 34 | } else { 35 | const res = WebpBase64.extractor(value, isUrl); 36 | if (res) { 37 | result[result.length] = res; 38 | } 39 | } 40 | return result; 41 | } 42 | 43 | convert(data, fConfig) { 44 | const buffer = data instanceof Buffer ? data : Buffer.from(data, "base64"); 45 | const encoderBase = new CWebp(buffer, webpBinPath); 46 | const encoder = fConfig ? fConfig(encoderBase) : encoderBase; 47 | return encoder.toBuffer(); 48 | } 49 | 50 | static extractor(value) { 51 | if (!value) { 52 | return; 53 | } 54 | const base64pos = value.indexOf(base64pattern); 55 | if (base64pos >= 0) { 56 | const base64posEnd = value.indexOf(base64patternEnd); 57 | 58 | if (base64posEnd < 0) { 59 | const { mimeType, data } = parseDataUri(value); 60 | return { mimetype: mimeType, data }; 61 | } else { 62 | const mimetype = value.slice(base64pos + base64pattern.length, base64posEnd); 63 | const data = value.slice(base64posEnd + base64patternEnd.length); 64 | return { mimetype, data }; 65 | } 66 | } else { 67 | return { mimetype: "url", data: value }; 68 | } 69 | } 70 | } 71 | 72 | export default WebpBase64; 73 | -------------------------------------------------------------------------------- /lib/Webpcss.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * webpcss 5 | * https://github.com/lexich/webpcss 6 | * 7 | * Copyright (c) 2015 Efremov Alexey 8 | * Licensed under the MIT license. 9 | */ 10 | 11 | /* eslint no-useless-escape: 0 */ 12 | 13 | import mime from "mime-types"; 14 | import fileType from "file-type"; 15 | import libpath from "path"; 16 | import fs, { readFile } from "fs"; 17 | import { isFunction } from "lodash"; 18 | import WebpBase64 from "./WebpBase64"; 19 | 20 | function readFileAsync(path) { 21 | return new Promise((resolve, reject) => { 22 | readFile(path, (err, data) => (err ? reject(err) : resolve(data))); 23 | }); 24 | } 25 | 26 | const rxHtml = /^html[_\.#\[]{1}/; 27 | const DEFAULTS = { 28 | webpClass: ".webp", 29 | noWebpClass: "", 30 | replace_from: /\.(png|jpg|jpeg)/g, 31 | replace_to: ".webp", 32 | inline: false /* root path to folder */, 33 | image_root: "", 34 | css_root: "", 35 | minAddClassFileSize: 0, 36 | resolveUrlRelativeToFile: false, 37 | copyBackgroundSize: false, 38 | replaceRemoteImage: true, 39 | cwebp_configurator: null, 40 | process_selector(selector, baseClass) { 41 | if (baseClass) { 42 | return rxHtml.test(selector) ? selector.replace("html", "html" + baseClass) : baseClass + " " + selector; 43 | } 44 | return selector; 45 | }, 46 | }; 47 | 48 | function deprecate(msg) { 49 | /* eslint no-console: 0 */ 50 | typeof console !== "undefined" && console.warn && console.warn(msg); 51 | } 52 | 53 | function canURLLocalResolve(url) { 54 | /* url(//foo.com/image.png) or url(http://foo.com/image.png) or url(https://foo.com/image.png) abs path with host */ 55 | return !/^(https?:)?\/\//i.test(url); 56 | } 57 | 58 | class Webpcss { 59 | constructor(opts) { 60 | if (!opts) { 61 | this.options = DEFAULTS; 62 | } else { 63 | this.options = { 64 | ...DEFAULTS, 65 | ...opts, 66 | }; 67 | if (opts.baseClass) { 68 | this.options.webpClass = opts.baseClass; 69 | delete opts.baseClass; 70 | deprecate("Option `baseClass` is deprecated. Use webpClass instead."); 71 | } 72 | } 73 | this.base64 = new WebpBase64(); 74 | } 75 | 76 | postcss(css, cb) { 77 | const asyncNodes = []; 78 | css.walkDecls(decl => { 79 | if ( 80 | (decl.prop.indexOf("background") === 0 || decl.prop.indexOf("border-image") === 0) && 81 | decl.value.indexOf("url") >= 0 82 | ) { 83 | asyncNodes[asyncNodes.length] = this.asyncProcessNode(decl); 84 | } 85 | }); 86 | return Promise.all(asyncNodes) 87 | .then(nodes => { 88 | nodes.filter(decl => decl).forEach(decl => css.append(decl)); 89 | cb(); 90 | }) 91 | .catch(() => cb()); 92 | } 93 | 94 | asyncProcessNode(decl) { 95 | const { options, base64 } = this; 96 | 97 | function resolveUrlPath(url) { 98 | let urlPath = url; 99 | const canLocalResolve = canURLLocalResolve(url); 100 | 101 | if (canLocalResolve) { 102 | const { localImgFileLocator } = options; 103 | if (localImgFileLocator) { 104 | const { input } = decl.source; 105 | if (input && input.file) { 106 | const cssFilePath = libpath.resolve(input.file); 107 | urlPath = localImgFileLocator({ 108 | url, 109 | cssFilePath, 110 | }); 111 | } else { 112 | console.warn(`Source input not found: ${url}`); 113 | } 114 | } else if (url[0] === "/") { 115 | /* url(/image.png) abs path */ 116 | urlPath = libpath.resolve(libpath.join(options.image_root, url)); 117 | } else { 118 | /* url(../images.png) or url(image.png) - relative css path */ 119 | const { resolveUrlRelativeToFile } = options; 120 | if (options.css_root || !resolveUrlRelativeToFile) { 121 | urlPath = libpath.resolve(libpath.join(options.css_root, url)); 122 | } else if (resolveUrlRelativeToFile) { 123 | // resolve relative path automatically 124 | const { input } = decl.source; 125 | if (input && input.file) { 126 | const { file } = input; 127 | urlPath = libpath.resolve(libpath.join(libpath.dirname(file), url)); 128 | } else { 129 | console.warn(`Source input not found: ${url}`); 130 | } 131 | } 132 | } 133 | } 134 | return { 135 | urlPath, 136 | canLocalResolve, 137 | }; 138 | } 139 | 140 | let breaks = 0; 141 | const selector = decl.parent.selectors.map(sel => options.process_selector(sel, options.webpClass)).join(", "); 142 | const urls = base64.extract(decl.value, true); 143 | if (!urls.length) { 144 | return; 145 | } 146 | const rx = options.replace_from instanceof RegExp ? options.replace_from : new RegExp(options.replace_from, "g"); 147 | const asyncUrls = urls.map(item => { 148 | const url = item.data; 149 | const { minAddClassFileSize } = options; 150 | if (item.mimetype === "url") { 151 | let shouldAddClass = true; 152 | 153 | if (minAddClassFileSize > 0) { 154 | const { urlPath, canLocalResolve } = resolveUrlPath(url); 155 | 156 | if (canLocalResolve) { 157 | try { 158 | const fileSize = fs.statSync(urlPath).size; 159 | if (fileSize < minAddClassFileSize) { 160 | shouldAddClass = false; 161 | } 162 | } catch (e) { 163 | console.warn(`Analyze file ${urlPath} size failed`, e); 164 | } 165 | } 166 | } 167 | 168 | if (!options.inline) { 169 | const { replace_to: replaceTo } = options; 170 | let src = url; 171 | 172 | if (shouldAddClass) { 173 | const { replaceRemoteImage } = options; 174 | if (replaceRemoteImage || canURLLocalResolve(url)) { 175 | src = isFunction(replaceTo) 176 | ? replaceTo({ 177 | url, 178 | }) 179 | : url.replace(rx, replaceTo); 180 | } 181 | } 182 | breaks += +(src === url); 183 | return `url(${src})`; 184 | } else { 185 | // eslint-disable-next-line no-lonely-if 186 | if (shouldAddClass) { 187 | const { urlPath, canLocalResolve } = resolveUrlPath(url); 188 | 189 | if (canLocalResolve) { 190 | return readFileAsync(urlPath) 191 | .then(data => 192 | base64 193 | .convert(data, options.cwebp_configurator) 194 | .then(buffer => buffer && `url(data:image/webp;base64,${buffer.toString("base64")})`) 195 | .catch(() => `url(${item.data})`) 196 | ) 197 | .catch(() => { 198 | breaks += 1; 199 | return `url(${item.data})`; 200 | }); 201 | } else { 202 | breaks += 1; 203 | return `url(${item.data})`; 204 | } 205 | } else { 206 | breaks += 1; 207 | return `url(${item.data})`; 208 | } 209 | } 210 | } else { 211 | const buffer = url instanceof Buffer ? url : Buffer.from(url, "base64"); 212 | 213 | let shouldAddClass = true; 214 | let ext = mime.extension(item.mimetype); 215 | if (!ext) { 216 | const ft = fileType(buffer); 217 | if (ft) { 218 | ext = ft.ext; 219 | } 220 | } 221 | // Unsupported types guarding 222 | if (!/png|jpg|jpeg|gif/i.test(ext)) { 223 | shouldAddClass = false; 224 | } else if (minAddClassFileSize > 0) { 225 | if (Buffer.byteLength(buffer) < minAddClassFileSize) { 226 | shouldAddClass = false; 227 | } 228 | } 229 | 230 | if (shouldAddClass) { 231 | return base64 232 | .convert(url) 233 | .then(buffer => { 234 | if (buffer) { 235 | return `url(data:image/webp;base64,${buffer.toString("base64")})`; 236 | } 237 | }) 238 | .catch(() => { 239 | breaks += 1; 240 | return `url(${item.data})`; 241 | }); 242 | } else { 243 | breaks += 1; 244 | return `url(${item.data})`; 245 | } 246 | } 247 | }); 248 | return Promise.all(asyncUrls).then(urls => { 249 | if (breaks !== urls.length) { 250 | const originalRule = decl.parent; 251 | const { copyBackgroundSize } = options; 252 | 253 | if (options.noWebpClass) { 254 | // add .no-webp 255 | const selectorNoWebP = originalRule.selectors 256 | .map(sel => options.process_selector(sel, options.noWebpClass)) 257 | .join(", "); 258 | 259 | const noWebpRule = Webpcss.formatRule( 260 | originalRule.cloneBefore({ 261 | selector: selectorNoWebP, 262 | }) 263 | ); 264 | 265 | decl.raws.before = " "; 266 | decl.moveTo(noWebpRule); 267 | } 268 | 269 | // add .webp 270 | const value = decl.value 271 | .split(" ") 272 | .map(val => (val.indexOf("url") >= 0 ? val.replace(/(url)\(.*\)/, urls.shift()) : val)) 273 | .join(" "); 274 | 275 | const webpRule = Webpcss.formatRule( 276 | originalRule.clone({ 277 | selector, 278 | }) 279 | ); 280 | 281 | const webpDecl = decl.clone({ 282 | prop: decl.prop, 283 | value, 284 | }); 285 | webpDecl.raws.semicolon = true; 286 | webpDecl.raws.before = " "; 287 | 288 | webpRule.append(webpDecl); 289 | const webpTreeRule = Webpcss.appendToCopyTree(originalRule.parent, webpRule); 290 | 291 | originalRule.walkDecls(decl => { 292 | if (copyBackgroundSize) { 293 | if (decl.prop === "background-size") { 294 | webpRule.append(decl.clone()); 295 | } 296 | } 297 | }); 298 | 299 | // clean if original rule is empty 300 | !originalRule.nodes.length && originalRule.remove(); 301 | return webpTreeRule; 302 | } 303 | }); 304 | } 305 | 306 | static appendToCopyTree(aRoot, aRule) { 307 | let root = aRoot; 308 | let rule = aRule; 309 | while (root.type !== "root") { 310 | rule = root 311 | .clone() 312 | .removeAll() 313 | .append(rule); 314 | root = root.parent; 315 | } 316 | return rule; 317 | } 318 | 319 | static formatRule(rule, isRemove = true) { 320 | rule.raws.semicolon = true; 321 | rule.raws.after = " "; 322 | return isRemove ? rule.removeAll() : rule; 323 | } 324 | } 325 | 326 | export default Webpcss; 327 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * webpcss 3 | * https://github.com/lexich/webpcss 4 | * 5 | * Copyright (c) 2015 Efremov Alexey 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | "use strict"; 10 | 11 | import postcss from "postcss"; 12 | import Webpcss from "./Webpcss"; 13 | 14 | let defaultWebpcss = null; 15 | 16 | const plugin = postcss.plugin("webpcss", options => { 17 | const pt = options ? new Webpcss(options) : defaultWebpcss || (defaultWebpcss = new Webpcss()); 18 | return css => 19 | new Promise((resolve, reject) => pt.postcss(css, (err, data) => (err ? reject(err, data) : resolve(data)))); 20 | }); 21 | 22 | export default plugin; 23 | 24 | export function transform(data, options, processOptions) { 25 | return postcss([plugin(options)]).process(data, processOptions); 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpcss", 3 | "version": "1.3.4", 4 | "description": "postcss processor for prepare css to use webp images", 5 | "main": "dist/index.js", 6 | "repository": "http://github.com/lexich/webpcss", 7 | "scripts": { 8 | "mocha": "istanbul test node_modules/mocha/bin/_mocha --report html -- --compilers js:babel-core/register --timeout 8000 test/*_spec.js --reporter spec", 9 | "test": "npm run eslint && npm run mocha", 10 | "coveralls": "istanbul cover node_modules/mocha/bin/_mocha --report html --report lcovonly -- --compilers js:babel-core/register test/*_spec.js && cat ./coverage/lcov.info | node ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", 11 | "eslint": "node_modules/.bin/eslint index.js lib test", 12 | "eslintFix": "node_modules/.bin/eslint --fix index.js lib test", 13 | "compile": "node_modules/.bin/babel lib --out-dir dist", 14 | "precommit": "npm run prettier && npm test && npm run compile", 15 | "prepush": "npm test && npm run compile", 16 | "postmerge": "npm install", 17 | "prettier": "prettier --write \"./**/*.js\"", 18 | "release": "npm test && standard-version && git push --follow-tags origin master --no-verify && npm publish" 19 | }, 20 | "keywords": [ 21 | "webp", 22 | "node", 23 | "postcss" 24 | ], 25 | "author": { 26 | "name": "Efremov Alex", 27 | "email": "lexich121@gmail.com", 28 | "url": "https://github.com/lexich" 29 | }, 30 | "license": "MIT", 31 | "dependencies": { 32 | "cwebp": "^2.0.4", 33 | "file-type": "^8.1.0", 34 | "lodash": "^4.17.15", 35 | "mime-types": "^2.1.19", 36 | "parse-data-uri": "^0.2.0", 37 | "postcss": "5.2.5", 38 | "urldata": "0.0.4", 39 | "webp-converter": "^2.1.6" 40 | }, 41 | "devDependencies": { 42 | "babel": "6.23.0", 43 | "babel-cli": "6.26.0", 44 | "babel-core": "6.26.3", 45 | "babel-eslint": "10.0.3", 46 | "babel-plugin-add-module-exports": "1.0.2", 47 | "babel-preset-es2015": "6.24.1", 48 | "babel-preset-stage-0": "6.24.1", 49 | "chai": "3.5.0", 50 | "coveralls": "2.11.14", 51 | "cryptiles": ">=4.1.2", 52 | "es6-promise": "4.0.5", 53 | "eslint": "6.7.2", 54 | "eslint-config-airbnb": "18.0.1", 55 | "eslint-config-prettier": "6.7.0", 56 | "eslint-plugin-import": "2.19.1", 57 | "eslint-plugin-jsx-a11y": "6.2.3", 58 | "eslint-plugin-prettier": "3.1.1", 59 | "eslint-plugin-react": "^7.17.0", 60 | "growl": ">=1.10.0", 61 | "husky": "3.1.0", 62 | "istanbul": "0.4.5", 63 | "mocha": "3.1.2", 64 | "mocha-lcov-reporter": "1.2.0", 65 | "prettier": "1.19.1", 66 | "sinon": "^7.2.2", 67 | "standard-version": "7.0.1" 68 | }, 69 | "resolutions": { 70 | "extend": ">=3.0.2", 71 | "just-extend": ">=4.0.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/base64_spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global describe, it */ 4 | /* eslint no-var: 0, import/no-extraneous-dependencies: 0 */ 5 | 6 | import { expect } from "chai"; 7 | import Promise from "es6-promise"; 8 | 9 | import WebpBase64 from "../lib/WebpBase64"; 10 | import base64stub from "./fixtures/base64"; 11 | 12 | Promise.polyfill(); 13 | describe("base64", () => { 14 | var base64 = new WebpBase64(); 15 | 16 | it("test base64 data", () => { 17 | expect(Buffer.from(base64stub.png_base64, "base64").toString()).to.eql(base64stub.png_bin.toString()); 18 | }); 19 | 20 | it("extract png", () => { 21 | var png = "data:image/png;base64,iVBORw"; 22 | var urlPng = "url(" + png + ")"; 23 | var res = base64.extract(png); 24 | expect(res) 25 | .to.be.instanceof(Array) 26 | .and.have.lengthOf(1); 27 | expect([{ mimetype: "image/png", data: "iVBORw" }]).to.eql(res); 28 | 29 | res = base64.extract(urlPng, true); 30 | expect(res) 31 | .to.be.instanceof(Array) 32 | .and.have.lengthOf(1); 33 | expect([{ mimetype: "image/png", data: "iVBORw" }]).to.eql(res); 34 | 35 | res = base64.extract(base64stub.png_uri); 36 | expect(res) 37 | .to.be.instanceof(Array) 38 | .and.have.lengthOf(1); 39 | expect([{ mimetype: "image/png", data: base64stub.png_base64 }]).to.eql(res); 40 | 41 | res = base64.extract(base64stub.png_css, true); 42 | expect(res) 43 | .to.be.instanceof(Array) 44 | .and.have.lengthOf(1); 45 | expect([{ mimetype: "image/png", data: base64stub.png_base64 }]).to.eql(res); 46 | }); 47 | 48 | it("extract svg", () => { 49 | var res = WebpBase64.extractor(base64stub.svg_content_uri); 50 | expect(res.mimetype).to.be.eql("image/svg+xml"); 51 | expect(decodeURIComponent(res.data)).to.be.eql(base64stub.svg_content); 52 | 53 | res = WebpBase64.extractor(base64stub.svg_base64_uri); 54 | expect(res.mimetype).to.be.eql("image/svg+xml"); 55 | expect(res.data.toString("base64")).to.be.eql(base64stub.svg_base64); 56 | }); 57 | 58 | it("extract multiple png", () => { 59 | var png = "data:image/png;base64,iVBORw"; 60 | var urlPng2 = "url(" + png + "), url(" + png + ")"; 61 | var res = base64.extract(urlPng2, true); 62 | expect(res).to.be.ok; 63 | }); 64 | 65 | it("extract breaking data", () => { 66 | expect([{ mimetype: "_image/png", data: "iVBORw" }]).to.eql(base64.extract("data:_image/png;base64,iVBORw")); 67 | 68 | expect([{ mimetype: "url", data: "data_:image/png;base64,iVBORw" }]).to.eql( 69 | base64.extract("data_:image/png;base64,iVBORw") 70 | ); 71 | 72 | expect([{ mimetype: "url", data: "data:image/png;base64iVBORw" }]).to.throw; 73 | }); 74 | 75 | it("test convert data with node-webp png", () => 76 | base64 77 | .convert(base64stub.png_bin) 78 | .catch(err => expect(err).to.not.exist) 79 | .done(buffer => { 80 | expect(buffer).to.be.instanceof(Buffer); 81 | expect(buffer).to.be.eql(base64stub.webp); 82 | })); 83 | 84 | it("test convert data with node-webp jpg", () => 85 | base64 86 | .convert(base64stub.jpg_bin) 87 | .catch(err => expect(err).to.not.exist) 88 | .done(buffer => { 89 | expect(buffer).to.be.instanceof(Buffer); 90 | expect(buffer).to.be.eql(base64stub.webp_jpg_bin); 91 | })); 92 | }); 93 | -------------------------------------------------------------------------------- /test/fixtures/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexich/webpcss/be6eff98015b9c3055120c59db1af122cea43e4b/test/fixtures/avatar.png -------------------------------------------------------------------------------- /test/fixtures/avatar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexich/webpcss/be6eff98015b9c3055120c59db1af122cea43e4b/test/fixtures/avatar.webp -------------------------------------------------------------------------------- /test/fixtures/base64.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* eslint no-var: 0 */ 4 | 5 | var fs = require("fs"); 6 | var libpath = require("path"); 7 | 8 | var pngbinary = fs.readFileSync(libpath.join(__dirname, "avatar.png")); 9 | var jpgbinary = fs.readFileSync(libpath.join(__dirname, "kitten.jpg")); 10 | var svgContent = fs.readFileSync(libpath.join(__dirname, "circle.svg"), { 11 | encoding: "utf-8", 12 | }); 13 | var svgbinary = Buffer.from(svgContent); 14 | 15 | var pngbase64 = pngbinary.toString("base64"); 16 | var jpgbase64 = jpgbinary.toString("base64"); 17 | var svgbase64 = svgbinary.toString("base64"); 18 | 19 | var pngUri = "data:image/png;base64," + pngbase64; 20 | var jpgUri = "data:image/jpg;base64," + jpgbase64; 21 | var svgContentUri = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svgContent); 22 | var svgBase64Uri = "data:image/svg+xml;base64," + svgbase64; 23 | 24 | var webpPngbinary = fs.readFileSync(libpath.join(__dirname, "avatar.webp")); 25 | var webpJpgbinary = fs.readFileSync(libpath.join(__dirname, "kitten.webp")); 26 | var webpPngbase64 = webpPngbinary.toString("base64"); 27 | 28 | module.exports = { 29 | png_bin: pngbinary, 30 | png_base64: pngbase64, 31 | png_uri: pngUri, 32 | png_css: "url(" + pngUri + ")", 33 | webp: webpPngbinary, 34 | webp_jpg_bin: webpJpgbinary, 35 | webp_base64: webpPngbase64, 36 | webp_uri: "data:image/webp;base64," + webpPngbase64, 37 | jpg_bin: jpgbinary, 38 | jpg_uri: jpgUri, 39 | jpg_css: "url(" + jpgUri + ")", 40 | svg_content: svgContent, 41 | svg_base64: svgbase64, 42 | svg_content_uri: svgContentUri, 43 | svg_base64_uri: svgBase64Uri, 44 | }; 45 | -------------------------------------------------------------------------------- /test/fixtures/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/fixtures/kitten.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexich/webpcss/be6eff98015b9c3055120c59db1af122cea43e4b/test/fixtures/kitten.jpg -------------------------------------------------------------------------------- /test/fixtures/kitten.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexich/webpcss/be6eff98015b9c3055120c59db1af122cea43e4b/test/fixtures/kitten.webp -------------------------------------------------------------------------------- /test/main_spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | /* eslint import/no-extraneous-dependencies: 0 */ 3 | 4 | "use strict"; 5 | 6 | import libpath from "path"; 7 | import { expect } from "chai"; 8 | import sinon from "sinon"; 9 | import Promise from "es6-promise"; 10 | import { transform } from "../lib"; 11 | import base64stub from "./fixtures/base64"; 12 | 13 | Promise.polyfill(); 14 | 15 | describe("webpcss", () => { 16 | it("not modify sample", () => { 17 | const input = ".test { backround: red; }"; 18 | return transform(input).then(res => { 19 | expect(input).to.be.eql(res.css); 20 | }); 21 | }); 22 | 23 | it("html tag", () => { 24 | const input = "html.test { background: url('test.png'); }"; 25 | return transform(input).then(res => { 26 | expect(input + "\nhtml.webp.test { background: url(test.webp); }").to.be.eql(res.css); 27 | }); 28 | }); 29 | 30 | it("border-radius css property", () => { 31 | const input = ".test { border-image: url('test.png'); }"; 32 | return transform(input).then(res => { 33 | expect(input + "\n.webp .test { border-image: url(test.webp); }").to.be.eql(res.css); 34 | }); 35 | }); 36 | 37 | it(".html classname", () => { 38 | const input = ".html.test { background: url('test.png'); }"; 39 | return transform(input).then(res => { 40 | expect(input + "\n.webp .html.test { background: url(test.webp); }").to.be.eql(res.css); 41 | }); 42 | }); 43 | 44 | it("multiple selectors", () => { 45 | const input = ".test1, .test2 { background: url('test.png'); }"; 46 | return transform(input).then(res => { 47 | expect(input + "\n.webp .test1, .webp .test2 { background: url(test.webp); }").to.be.eql(res.css); 48 | }); 49 | }); 50 | 51 | it("default options background-image with url", () => { 52 | const input = ".test { background-image: url(test.jpg); }"; 53 | return transform(input).then(res => { 54 | expect(input + "\n.webp .test { background-image: url(test.webp); }").to.be.eql(res.css); 55 | }); 56 | }); 57 | 58 | it("default options background with url", () => { 59 | const input = ".test { background: url(test.jpeg); }"; 60 | transform(input).then(res => { 61 | expect(input + "\n.webp .test { background: url(test.webp); }").to.be.eql(res.css); 62 | }); 63 | }); 64 | 65 | it("default options background with url and params", () => { 66 | const input = ".test { background: transparent url(test.png) no-repeat; }"; 67 | return transform(input).then(res => { 68 | expect(input + "\n.webp .test { background: transparent url(test.webp) no-repeat; }").to.be.eql(res.css); 69 | }); 70 | }); 71 | 72 | it("default options background multiple urls", () => { 73 | const input = 74 | ".img_play_photo_multiple { background: url(number.png) 600px 10px no-repeat,\nurl(\"thingy.png\") 10px 10px no-repeat,\nurl('Paper-4.png');\n}"; 75 | const output = 76 | input + 77 | "\n.webp .img_play_photo_multiple { background: url(number.webp) 600px 10px no-repeat,\nurl(thingy.webp) 10px 10px no-repeat,\nurl(Paper-4.webp); }"; 78 | return transform(input).then(res => { 79 | expect(output).to.be.eql(res.css); 80 | }); 81 | }); 82 | 83 | it("default options multiple mixed clasess", () => { 84 | const input = '.test1 { background: url("test1.jpeg"); }' + ".test2 { background-image: url('test2.png'); }"; 85 | const output = 86 | '.test1 { background: url("test1.jpeg"); }' + 87 | ".test2 { background-image: url('test2.png'); }" + 88 | ".webp .test1 { background: url(test1.webp); }" + 89 | ".webp .test2 { background-image: url(test2.webp); }"; 90 | 91 | return transform(input).then(res => { 92 | expect(output).to.be.eql(res.css); 93 | }); 94 | }); 95 | 96 | it("default options background with gif", () => { 97 | const input = ".test { background: url(test.gif); }"; 98 | 99 | return transform(input).then(res => { 100 | expect(input).to.be.eql(res.css); 101 | }); 102 | }); 103 | 104 | it("default options background with gif and jpg", () => { 105 | const input = '.test { background: url(test.gif), url("test1.jpg"); }'; 106 | return transform(input).then(res => { 107 | expect(input + "\n.webp .test { background: url(test.gif), url(test1.webp); }").to.be.eql(res.css); 108 | }); 109 | }); 110 | 111 | it("default options background data uri", () => { 112 | const input = ".test { background: url(" + base64stub.png + ") no-repeat; }"; 113 | return transform(input).then(res => { 114 | expect(input).to.be.eql(res.css); 115 | }); 116 | }); 117 | 118 | it("custom options webpClass", () => { 119 | const input = ".test { background-image: url(test.png); }"; 120 | return transform(input, { webpClass: ".webp1" }).then(res => { 121 | expect(input + "\n.webp1 .test { background-image: url(test.webp); }").to.be.eql(res.css); 122 | }); 123 | }); 124 | 125 | it("custom options noWebpClass with example background-image", () => { 126 | const input = ".test { background-image: url(test.png); }"; 127 | return transform(input, { noWebpClass: ".no-webp" }).then(res => { 128 | expect( 129 | ".no-webp .test { background-image: url(test.png); }" + "\n.webp .test { background-image: url(test.webp); }" 130 | ).to.be.eql(res.css); 131 | }); 132 | }); 133 | 134 | it("custom options noWebpClass example background", () => { 135 | const input = ".test { background: transparent url(test.png); }"; 136 | return transform(input, { noWebpClass: ".no-webp" }).then(res => { 137 | expect( 138 | ".no-webp .test { background: transparent url(test.png); }" + 139 | "\n.webp .test { background: transparent url(test.webp); }" 140 | ).to.be.eql(res.css); 141 | }); 142 | }); 143 | 144 | it("custom options noWebpClass example background with other decl", () => { 145 | const input = ".test { background: transparent url(test.png); color: red; }"; 146 | return transform(input, { noWebpClass: ".no-webp" }).then(res => { 147 | expect( 148 | ".no-webp .test { background: transparent url(test.png); }" + 149 | "\n.test { color: red; }" + 150 | "\n.webp .test { background: transparent url(test.webp); }" 151 | ).to.be.eql(res.css); 152 | }); 153 | }); 154 | 155 | it("custom options noWebpClass example background with other decl with @media query", () => { 156 | const input = 157 | "@media screen and (min-width: 500px) { .test { background: transparent url(test.png); color: red; } }"; 158 | return transform(input, { noWebpClass: ".no-webp" }).then(res => { 159 | expect( 160 | "@media screen and (min-width: 500px) { .no-webp .test { background: transparent url(test.png); } .test { color: red; } } " + 161 | "@media screen and (min-width: 500px) { .webp .test { background: transparent url(test.webp); } }" 162 | ).to.be.eql(res.css); 163 | }); 164 | }); 165 | 166 | it("custom options replace_from background with gif", () => { 167 | const input = ".test { background: url(test.gif); }"; 168 | return transform(input, { replace_from: /\.gif/g }).then(res => { 169 | expect(input + "\n.webp .test { background: url(test.webp); }").to.be.eql(res.css); 170 | }); 171 | }); 172 | 173 | it("custom options replaceRemoteImage to true background-image with remote url '//foo.com/test.jpg'", () => { 174 | const input = ".test { background-image: url(//foo.com/test.jpg); }"; 175 | return transform(input, {}).then(res => { 176 | expect(input + "\n.webp .test { background-image: url(//foo.com/test.webp); }").to.be.eql(res.css); 177 | }); 178 | }); 179 | 180 | it("custom options replaceRemoteImage to true background-image with remote url 'http://foo.com/test.jpg'", () => { 181 | const input = ".test { background-image: url(http://foo.com/test.jpg); }"; 182 | return transform(input, {}).then(res => { 183 | expect(input + "\n.webp .test { background-image: url(http://foo.com/test.webp); }").to.be.eql(res.css); 184 | }); 185 | }); 186 | 187 | it("custom options replaceRemoteImage to true background-image with remote url 'https://foo.com/test.jpg'", () => { 188 | const input = ".test { background-image: url(https://foo.com/test.jpg); }"; 189 | return transform(input, {}).then(res => { 190 | expect(input + "\n.webp .test { background-image: url(https://foo.com/test.webp); }").to.be.eql(res.css); 191 | }); 192 | }); 193 | 194 | it("custom options replaceRemoteImage to false background-image with remote url '//foo.com/test.jpg'", () => { 195 | const input = ".test { background-image: url(//foo.com/test.jpg); }"; 196 | return transform(input, { replaceRemoteImage: false }).then(res => { 197 | expect(input).to.be.eql(res.css); 198 | }); 199 | }); 200 | 201 | it("custom options replaceRemoteImage to false background-image with remote url 'http://foo.com/test.jpg'", () => { 202 | const input = ".test { background-image: url(http://foo.com/test.jpg); }"; 203 | return transform(input, { replaceRemoteImage: false }).then(res => { 204 | expect(input).to.be.eql(res.css); 205 | }); 206 | }); 207 | 208 | it("custom options replaceRemoteImage to false background-image with remote url 'https://foo.com/test.jpg'", () => { 209 | const input = ".test { background-image: url(https://foo.com/test.jpg); }"; 210 | return transform(input, { replaceRemoteImage: false }).then(res => { 211 | expect(input).to.be.eql(res.css); 212 | }); 213 | }); 214 | 215 | it("custom options copyBackgroundSize to false with background-size rule", () => { 216 | const input = ".test { background-image: url(test.jpg); background-size: auto; }"; 217 | return transform(input, {}).then(res => { 218 | expect(input + "\n.webp .test { background-image: url(test.webp); }").to.be.eql(res.css); 219 | }); 220 | }); 221 | 222 | it("custom options copyBackgroundSize to true with background-size rule", () => { 223 | const input = ".test { background-image: url(test.jpg); background-size: auto; }"; 224 | return transform(input, { copyBackgroundSize: true }).then(res => { 225 | expect(input + "\n.webp .test { background-image: url(test.webp); background-size: auto; }").to.be.eql(res.css); 226 | }); 227 | }); 228 | 229 | it("custom options replace_to background-image with url", () => { 230 | const input = ".test { background-image: url(test.jpg); }"; 231 | return transform(input, { replace_to: ".other" }).then(res => { 232 | expect(input + "\n.webp .test { background-image: url(test.other); }").to.be.eql(res.css); 233 | }); 234 | }); 235 | 236 | it("custom options replace_to function background-image with url", () => { 237 | const input = ".test { background-image: url(test.jpg); }"; 238 | return transform(input, { 239 | replace_to(data) { 240 | expect(data.url).to.be.eql("test.jpg"); 241 | return "hello.world?text=test"; 242 | }, 243 | }).then(res => { 244 | expect(input + "\n.webp .test { background-image: url(hello.world?text=test); }").to.be.eql(res.css); 245 | }); 246 | }); 247 | 248 | it("replace_to && replace_from", () => { 249 | const input = ".icon { color: #222; background-image: url('../images/icon.png'); }"; 250 | return transform(input, { replace_to: ".$1.webp" }).then(res => { 251 | expect(input + "\n.webp .icon { background-image: url(../images/icon.png.webp); }").to.be.eql(res.css); 252 | }); 253 | }); 254 | 255 | it("check with @media-query", () => { 256 | const input = "@media all and (min-width:100px){ .test { background-image: url(test.jpg); } }"; 257 | const output = input + " @media all and (min-width:100px){ .webp .test{ background-image: url(test.webp); } }"; 258 | return transform(input).then(res => { 259 | expect(output).to.be.eql(res.css); 260 | }); 261 | }); 262 | 263 | it("check with multiple @media-query", () => { 264 | const input = 265 | "@media all and (max-width:200px){ @media all and (min-width:100px){ .test { background-image: url(test.jpg); } } }"; 266 | const output = 267 | "@media all and (max-width:200px){ @media all and (min-width:100px){ .test { background-image: url(test.jpg); } } }" + 268 | " @media all and (max-width:200px){ @media all and (min-width:100px){ .webp .test{ background-image: url(test.webp); } } }"; 269 | transform(input).then(res => { 270 | expect(output).to.be.eql(res.css); 271 | }); 272 | }); 273 | 274 | it("check with multiple @media-query with other rule and decls", () => { 275 | const input = 276 | "@media all and (max-width:200px){" + 277 | " .garbage{ color: blue; } " + 278 | "@media all and (min-width:100px){" + 279 | " .test { " + 280 | "background-image: url(test.jpg); color: red; " + 281 | "} } }"; 282 | const output = 283 | input + 284 | " @media all and (max-width:200px){ @media all and (min-width:100px){ .webp .test{ background-image: url(test.webp); } } }"; 285 | transform(input).then(res => { 286 | expect(output).to.be.eql(res.css); 287 | }); 288 | }); 289 | 290 | it("check convert base64 png webp options background data uri", () => { 291 | const input = ".test { background: " + base64stub.png_css + " no-repeat; }"; 292 | return transform(input).then(res => { 293 | const { css } = res; 294 | expect(css).to.match(/data:image\/png;base64,/); 295 | expect(css).to.match(/\.test { background: url\(data:image\/png;base64,/); 296 | 297 | expect(css).to.not.match(/\.test { }/); 298 | 299 | expect(css).to.match(/data:image\/webp;base64,/); 300 | expect(css).to.match(/\.webp \.test { background: url\(data:image\/webp;base64,/); 301 | }); 302 | }); 303 | 304 | it("check convert base64 jpg webp options background data uri", () => { 305 | const input = ".test { background: " + base64stub.jpg_css + " no-repeat; }"; 306 | return transform(input).then(res => { 307 | const { css } = res; 308 | expect(css).to.match(/data:image\/jpg;base64,/); 309 | expect(css).to.match(/\.test { background: url\(data:image\/jpg;base64,/); 310 | 311 | expect(css).to.not.match(/\.test { }/); 312 | 313 | expect(css).to.match(/data:image\/webp;base64,/); 314 | expect(css).to.match(/\.webp \.test { background: url\(data:image\/webp;base64,/); 315 | }); 316 | }); 317 | 318 | it("check convert inline base64 svg and should do nothing", () => { 319 | const input = ".test { background: url(" + base64stub.svg_base64_uri + ") no-repeat; }"; 320 | return transform(input).then(res => { 321 | const { css } = res; 322 | expect(css).to.be.eql(input); 323 | }); 324 | }); 325 | 326 | it("check convert inline content uri svg and should do nothing", () => { 327 | const input = ".test { background: url(" + base64stub.svg_content_uri + ") no-repeat; }"; 328 | return transform(input).then(res => { 329 | const { css } = res; 330 | expect(css).to.be.eql(input); 331 | }); 332 | }); 333 | 334 | it("check convert base64 webp options background data uri and should do nothing", () => { 335 | const input = ".test { background: url(" + base64stub.webp_uri + ") no-repeat; }"; 336 | return transform(input).then(res => { 337 | const { css } = res; 338 | expect(css).to.be.eql(input); 339 | }); 340 | }); 341 | 342 | it("check resolveUrlRelativeToFile and file size above minAddClassFileSize", () => { 343 | const input = ".test { background: url(avatar.png); }"; 344 | const fixturesPath = libpath.join(__dirname, "fixtures"); 345 | return transform( 346 | input, 347 | { resolveUrlRelativeToFile: true, minAddClassFileSize: 1 }, 348 | { 349 | from: libpath.join(fixturesPath, "test.css"), 350 | } 351 | ).then(res => { 352 | const { css } = res; 353 | expect(input + "\n.webp .test { background: url(avatar.webp); }").to.be.eql(css); 354 | }); 355 | }); 356 | 357 | it("check resolveUrlRelativeToFile and file size below minAddClassFileSize", () => { 358 | const input = ".test { background: url(avatar.png); }"; 359 | const fixturesPath = libpath.join(__dirname, "fixtures"); 360 | return transform( 361 | input, 362 | { resolveUrlRelativeToFile: true, minAddClassFileSize: 1024 * 1024 }, 363 | { 364 | from: libpath.join(fixturesPath, "test.css"), 365 | } 366 | ).then(res => { 367 | const { css } = res; 368 | expect(input).to.be.eql(css); 369 | }); 370 | }); 371 | 372 | it("check resolveUrlRelativeToFile and file size above minAddClassFileSize with inline", () => { 373 | const input = ".test { background: url(avatar.png); }"; 374 | const fixturesPath = libpath.join(__dirname, "fixtures"); 375 | return transform( 376 | input, 377 | { inline: true, resolveUrlRelativeToFile: true, minAddClassFileSize: 1 }, 378 | { 379 | from: libpath.join(fixturesPath, "test.css"), 380 | } 381 | ).then(res => { 382 | const { css } = res; 383 | expect(css).to.contain(".test { background: url(avatar.png); }"); 384 | expect(css).to.contain(".webp .test { background: url(data:image/webp;base64,"); 385 | }); 386 | }); 387 | 388 | it("check resolveUrlRelativeToFile and file size below minAddClassFileSize with inline", () => { 389 | const input = ".test { background: url(avatar.png); }"; 390 | const fixturesPath = libpath.join(__dirname, "fixtures"); 391 | return transform( 392 | input, 393 | { resolveUrlRelativeToFile: true, minAddClassFileSize: 1024 * 1024 }, 394 | { 395 | from: libpath.join(fixturesPath, "test.css"), 396 | } 397 | ).then(res => { 398 | const { css } = res; 399 | expect(input).to.be.eql(css); 400 | }); 401 | }); 402 | 403 | it("check localImgFileLocator with url of special grammar of other css preprocessor and file size above minAddClassFileSize", () => { 404 | const urlWithoutExt = "~/path/to/avatar"; 405 | const url = urlWithoutExt + ".png"; 406 | const input = ".test { background: url(" + url + "); }"; 407 | const fixturesPath = libpath.join(__dirname, "fixtures"); 408 | const pathFrom = libpath.join(fixturesPath, "test.css"); 409 | const expectedPath = libpath.resolve(pathFrom); 410 | const fileLocation = libpath.resolve(__dirname, "fixtures/avatar.png"); 411 | const localImgFileLocator = sinon.spy(() => fileLocation); 412 | return transform( 413 | input, 414 | { 415 | // should be ignore 416 | resolveUrlRelativeToFile: true, 417 | // should be ignore 418 | img_root: "/path-not-exists", 419 | // should be ignore 420 | css_root: "/path-not-exists", 421 | localImgFileLocator, 422 | minAddClassFileSize: 1, 423 | }, 424 | { 425 | from: pathFrom, 426 | } 427 | ).then(res => { 428 | const { css } = res; 429 | expect( 430 | localImgFileLocator.alwaysCalledWith({ 431 | url, 432 | cssFilePath: expectedPath, 433 | }) 434 | ); 435 | expect(input + "\n.webp .test { background: url(" + urlWithoutExt + ".webp); }").to.be.eql(css); 436 | }); 437 | }); 438 | 439 | it("check localImgFileLocator with url of special grammar of other css preprocessor and file size below minAddClassFileSize", () => { 440 | const urlWithoutExt = "~/path/to/avatar"; 441 | const url = urlWithoutExt + ".png"; 442 | const input = ".test { background: url(" + url + "); }"; 443 | const fixturesPath = libpath.join(__dirname, "fixtures"); 444 | const pathFrom = libpath.join(fixturesPath, "test.css"); 445 | const expectedPath = libpath.resolve(pathFrom); 446 | const fileLocation = libpath.resolve(__dirname, "fixtures/avatar.png"); 447 | const localImgFileLocator = sinon.spy(() => fileLocation); 448 | return transform( 449 | input, 450 | { 451 | // should be ignore 452 | resolveUrlRelativeToFile: true, 453 | // should be ignore 454 | img_root: "/path-not-exists", 455 | // should be ignore 456 | css_root: "/path-not-exists", 457 | localImgFileLocator, 458 | minAddClassFileSize: 1024 * 1024, 459 | }, 460 | { 461 | from: pathFrom, 462 | } 463 | ).then(res => { 464 | const { css } = res; 465 | expect( 466 | localImgFileLocator.alwaysCalledWith({ 467 | url, 468 | cssFilePath: expectedPath, 469 | }) 470 | ); 471 | expect(input).to.be.eql(css); 472 | }); 473 | }); 474 | 475 | it("check localImgFileLocator with url of special grammar of other css preprocessor and file size above minAddClassFileSize with inline", () => { 476 | const urlWithoutExt = "~/path/to/avatar"; 477 | const url = urlWithoutExt + ".png"; 478 | const input = ".test { background: url(" + url + "); }"; 479 | const fixturesPath = libpath.join(__dirname, "fixtures"); 480 | const pathFrom = libpath.join(fixturesPath, "test.css"); 481 | const expectedPath = libpath.resolve(pathFrom); 482 | const fileLocation = libpath.resolve(__dirname, "fixtures/avatar.png"); 483 | const localImgFileLocator = sinon.spy(() => fileLocation); 484 | return transform( 485 | input, 486 | { 487 | // should be ignore 488 | resolveUrlRelativeToFile: true, 489 | // should be ignore 490 | img_root: "/path-not-exists", 491 | // should be ignore 492 | css_root: "/path-not-exists", 493 | localImgFileLocator, 494 | minAddClassFileSize: 1, 495 | inline: true, 496 | }, 497 | { 498 | from: pathFrom, 499 | } 500 | ).then(res => { 501 | const { css } = res; 502 | expect( 503 | localImgFileLocator.alwaysCalledWith({ 504 | url, 505 | cssFilePath: expectedPath, 506 | }) 507 | ); 508 | expect(css).to.contain(".test { background: url(" + urlWithoutExt + ".png); }"); 509 | expect(css).to.contain(".webp .test { background: url(data:image/webp;base64,"); 510 | }); 511 | }); 512 | 513 | it("check localImgFileLocator with url of special grammar of other css preprocessor and file size below minAddClassFileSize with inline", () => { 514 | const urlWithoutExt = "~/path/to/avatar"; 515 | const url = urlWithoutExt + ".png"; 516 | const input = ".test { background: url(" + url + "); }"; 517 | const fixturesPath = libpath.join(__dirname, "fixtures"); 518 | const pathFrom = libpath.join(fixturesPath, "test.css"); 519 | const expectedPath = libpath.resolve(pathFrom); 520 | const fileLocation = libpath.resolve(__dirname, "fixtures/avatar.png"); 521 | const localImgFileLocator = sinon.spy(() => fileLocation); 522 | return transform( 523 | input, 524 | { 525 | // should be ignore 526 | resolveUrlRelativeToFile: true, 527 | // should be ignore 528 | img_root: "/path-not-exists", 529 | // should be ignore 530 | css_root: "/path-not-exists", 531 | localImgFileLocator, 532 | minAddClassFileSize: 1024 * 1024, 533 | inline: true, 534 | }, 535 | { 536 | from: pathFrom, 537 | } 538 | ).then(res => { 539 | const { css } = res; 540 | expect( 541 | localImgFileLocator.alwaysCalledWith({ 542 | url, 543 | cssFilePath: expectedPath, 544 | }) 545 | ); 546 | expect(input).to.be.eql(css); 547 | }); 548 | }); 549 | 550 | it("check file size below minAddClassFileSize with base64 encoded content", () => { 551 | const input = ".test { background: " + base64stub.png_css + " no-repeat; }"; 552 | return transform(input, { minAddClassFileSize: 1 }).then(res => { 553 | const { css } = res; 554 | expect(css).to.match(/data:image\/png;base64,/); 555 | expect(css).to.match(/\.test { background: url\(data:image\/png;base64,/); 556 | 557 | expect(css).to.not.match(/\.test { }/); 558 | 559 | expect(css).to.match(/data:image\/webp;base64,/); 560 | expect(css).to.match(/\.webp \.test { background: url\(data:image\/webp;base64,/); 561 | }); 562 | }); 563 | 564 | it("check file size above minAddClassFileSize with base64 encoded content", () => { 565 | const input = ".test { background: " + base64stub.png_css + " no-repeat; }"; 566 | return transform(input, { minAddClassFileSize: 1024 * 1024 }).then(res => { 567 | const { css } = res; 568 | expect(input).to.be.eql(css); 569 | }); 570 | }); 571 | 572 | it("check inline property for png source", () => { 573 | const input = ".test { background: url(avatar.png); }"; 574 | const fixturesPath = libpath.join(__dirname, "fixtures"); 575 | return transform(input, { inline: true, css_root: fixturesPath }).then(res => { 576 | const { css } = res; 577 | expect(css).to.contain(".test { background: url(avatar.png); }"); 578 | expect(css).to.contain(".webp .test { background: url(data:image/webp;base64,"); 579 | }); 580 | }); 581 | 582 | it("check inline property for jpg source", () => { 583 | const input = ".test { background: url(kitten.jpg); }"; 584 | const fixturesPath = libpath.join(__dirname, "fixtures"); 585 | return transform(input, { inline: true, css_root: fixturesPath }).then(res => { 586 | const { css } = res; 587 | expect(css).to.contain(".test { background: url(kitten.jpg); }"); 588 | expect(css).to.contain(".webp .test { background: url(data:image/webp;base64,"); 589 | }); 590 | }); 591 | 592 | it("check inline property for invalid path source", () => { 593 | const input = ".test { background: url(kitten1.jpg); }"; 594 | const fixturesPath = libpath.join(__dirname, "fixtures"); 595 | return transform(input, { inline: true, css_root: fixturesPath }).then(res => { 596 | const { css } = res; 597 | expect(css).to.eql(input); 598 | }); 599 | }); 600 | 601 | it("check inline property for jpg source with relative path", () => { 602 | const input = ".test { background: url(kitten.jpg); }"; 603 | const fixturesPath = libpath.join(__dirname, "fixtures"); 604 | return transform(input, { inline: true, css_root: fixturesPath }).then(res => { 605 | const { css } = res; 606 | expect(css).to.match(/data:image\/webp;base64,/); 607 | expect(css).to.match(/\.webp \.test { background: url\(data:image\/webp;base64,/); 608 | }); 609 | }); 610 | 611 | it("check inline property for jpg source with relative path", () => { 612 | const input = ".test { background: url(../fixtures/kitten.jpg); }"; 613 | const fixturesPath = libpath.join(__dirname, "css"); 614 | return transform(input, { inline: true, css_root: fixturesPath }).then(res => { 615 | const { css } = res; 616 | expect(css).to.match(/data:image\/webp;base64,/); 617 | expect(css).to.match(/\.webp \.test { background: url\(data:image\/webp;base64,/); 618 | }); 619 | }); 620 | 621 | it("check inline property for jpg source with relative path", () => { 622 | const input = ".test { background: url(/kitten.jpg); }"; 623 | const fixturesPath = libpath.join(__dirname, "fixtures"); 624 | return transform(input, { inline: true, image_root: fixturesPath }).then(res => { 625 | const { css } = res; 626 | expect(css).to.match(/data:image\/webp;base64,/); 627 | expect(css).to.match(/\.webp \.test { background: url\(data:image\/webp;base64,/); 628 | }); 629 | }); 630 | 631 | it("invalid css", () => { 632 | const input = `foo { 633 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e")\` 634 | };`; 635 | return transform(input).then(res => { 636 | const { css } = res; 637 | expect(css).to.be.eql(input); 638 | }); 639 | }); 640 | }); 641 | --------------------------------------------------------------------------------