├── .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 | [](https://travis-ci.org/lexich/webpcss)
2 | [](http://badge.fury.io/js/webpcss)
3 | [](https://coveralls.io/r/lexich/webpcss)
4 | [](https://david-dm.org/lexich/webpcss)
5 | [](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 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------