├── .eslintignore
├── test
├── units
│ └── .gitkeep
├── cases
│ ├── default
│ │ ├── index.js
│ │ ├── webpack.config.js
│ │ ├── index.html
│ │ └── style.css
│ ├── retina
│ │ ├── index.js
│ │ ├── webpack.config.js
│ │ ├── index.html
│ │ └── style.css
│ ├── smart
│ │ ├── index.js
│ │ ├── webpack.config.js
│ │ ├── index.html
│ │ └── style.css
│ ├── background
│ │ ├── index.js
│ │ ├── webpack.config.js
│ │ ├── webpack.config.test.js
│ │ ├── index.html
│ │ ├── test.js
│ │ └── style.css
│ ├── image-set
│ │ ├── index.js
│ │ ├── webpack.config.js
│ │ ├── media-query-order.html
│ │ ├── index.html
│ │ ├── style.css
│ │ ├── test.js
│ │ └── test.html
│ ├── public-path
│ │ ├── index.js
│ │ ├── webpack.config.js
│ │ ├── index.html
│ │ └── style.css
│ ├── postcss-plugins
│ │ ├── index.js
│ │ ├── index.html
│ │ ├── style.css
│ │ └── webpack.config.js
│ └── extract-text-webpack-plugin
│ │ ├── index.js
│ │ ├── webpack.config.js
│ │ ├── index.html
│ │ └── style.css
├── fixtures
│ └── images
│ │ ├── gift.png
│ │ ├── home.png
│ │ ├── html.png
│ │ ├── tag.png
│ │ ├── light.png
│ │ ├── tag@2x.png
│ │ ├── adaptive.png
│ │ ├── gift@2x.png
│ │ ├── home@2x.png
│ │ ├── html@2x.png
│ │ ├── light@2x.png
│ │ ├── lollipop.png
│ │ ├── adaptive@2x.png
│ │ ├── calculator.png
│ │ ├── lollipop@2x.png
│ │ ├── calculator@2x.png
│ │ └── retina
│ │ ├── minion.png
│ │ ├── radio.png
│ │ ├── minion@2x.png
│ │ ├── minion@3x.png
│ │ ├── minion@4x.png
│ │ ├── mountains.png
│ │ ├── radio@2x.png
│ │ ├── radio@3x.png
│ │ ├── radio@4x.png
│ │ ├── satelite.png
│ │ ├── angry-birds.png
│ │ ├── mountains@2x.png
│ │ ├── mountains@3x.png
│ │ ├── mountains@4x.png
│ │ ├── satelite@2x.png
│ │ ├── satelite@3x.png
│ │ ├── satelite@4x.png
│ │ ├── angry-birds@1x.png
│ │ ├── angry-birds@2x.png
│ │ ├── angry-birds@3x.png
│ │ ├── angry-birds@4x.png
│ │ ├── captain-america.png
│ │ ├── captain-america@1x.png
│ │ ├── captain-america@2x.png
│ │ ├── captain-america@3x.png
│ │ └── captain-america@4x.png
├── final.test.js
├── integration.test.js
├── default.test.js
├── retina.test.js
├── dist
│ └── default.test.dev.js
└── extract-text-webpack-plugin.test.js
├── example
├── index.js
├── screenshot.png
├── webpack.config.js
├── index.html
└── style.css
├── index.js
├── .eslintrc
├── src
├── meta.js
├── loader.js
├── README.md
├── Plugin.js
├── computeNewBackground.js
└── postcssPlugin.js
├── .gitignore
├── LICENSE
├── .circleci
└── config.yml
├── package.json
├── README.zh-CN.md
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | test/
2 |
--------------------------------------------------------------------------------
/test/units/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | import './style.css';
2 |
--------------------------------------------------------------------------------
/test/cases/default/index.js:
--------------------------------------------------------------------------------
1 | import './style.css';
2 |
--------------------------------------------------------------------------------
/test/cases/retina/index.js:
--------------------------------------------------------------------------------
1 | import './style.css';
2 |
--------------------------------------------------------------------------------
/test/cases/smart/index.js:
--------------------------------------------------------------------------------
1 | import './style.css';
2 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/loader');
2 |
--------------------------------------------------------------------------------
/test/cases/background/index.js:
--------------------------------------------------------------------------------
1 | import './style.css';
2 |
--------------------------------------------------------------------------------
/test/cases/image-set/index.js:
--------------------------------------------------------------------------------
1 | import './style.css';
2 |
--------------------------------------------------------------------------------
/test/cases/public-path/index.js:
--------------------------------------------------------------------------------
1 | import './style.css';
2 |
--------------------------------------------------------------------------------
/test/cases/postcss-plugins/index.js:
--------------------------------------------------------------------------------
1 | import './style.css';
2 |
--------------------------------------------------------------------------------
/test/cases/extract-text-webpack-plugin/index.js:
--------------------------------------------------------------------------------
1 | import './style.css';
2 |
--------------------------------------------------------------------------------
/example/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/example/screenshot.png
--------------------------------------------------------------------------------
/test/fixtures/images/gift.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/gift.png
--------------------------------------------------------------------------------
/test/fixtures/images/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/home.png
--------------------------------------------------------------------------------
/test/fixtures/images/html.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/html.png
--------------------------------------------------------------------------------
/test/fixtures/images/tag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/tag.png
--------------------------------------------------------------------------------
/test/fixtures/images/light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/light.png
--------------------------------------------------------------------------------
/test/fixtures/images/tag@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/tag@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/adaptive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/adaptive.png
--------------------------------------------------------------------------------
/test/fixtures/images/gift@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/gift@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/home@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/home@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/html@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/html@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/light@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/light@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/lollipop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/lollipop.png
--------------------------------------------------------------------------------
/test/fixtures/images/adaptive@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/adaptive@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/calculator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/calculator.png
--------------------------------------------------------------------------------
/test/fixtures/images/lollipop@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/lollipop@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/calculator@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/calculator@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/minion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/minion.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/radio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/radio.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/minion@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/minion@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/minion@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/minion@3x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/minion@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/minion@4x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/mountains.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/mountains.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/radio@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/radio@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/radio@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/radio@3x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/radio@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/radio@4x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/satelite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/satelite.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/angry-birds.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/angry-birds.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/mountains@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/mountains@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/mountains@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/mountains@3x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/mountains@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/mountains@4x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/satelite@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/satelite@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/satelite@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/satelite@3x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/satelite@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/satelite@4x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/angry-birds@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/angry-birds@1x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/angry-birds@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/angry-birds@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/angry-birds@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/angry-birds@3x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/angry-birds@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/angry-birds@4x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/captain-america.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/captain-america.png
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "vusion",
3 | "env": {
4 | "commonjs": true,
5 | "node": true,
6 | "mocha": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/test/fixtures/images/retina/captain-america@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/captain-america@1x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/captain-america@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/captain-america@2x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/captain-america@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/captain-america@3x.png
--------------------------------------------------------------------------------
/test/fixtures/images/retina/captain-america@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/captain-america@4x.png
--------------------------------------------------------------------------------
/test/final.test.js:
--------------------------------------------------------------------------------
1 | require('./default.test.js');
2 | // require('./extract-text-webpack-plugin.test.js');
3 | require('./retina.test.js');
4 | require('./integration.test.js');
5 |
6 | // require('./cases/background/test');
7 |
--------------------------------------------------------------------------------
/src/meta.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | PLUGIN_NAME: 'cssSpritePlugin',
3 | MODULE_MARK: 'isCSSSpriteModule',
4 | REPLACER_NAME: 'CSS_SPRITE_LOADER_IMAGE',
5 | REPLACER_RE: /CSS_SPRITE_LOADER_IMAGE\(([^)]*?),\s*([^)]*)\)/g,
6 | };
7 |
--------------------------------------------------------------------------------
/src/loader.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const postcssPlugin = require('./postcssPlugin');
4 | const { createLoader } = require('base-css-image-loader');
5 |
6 | const loader = createLoader([postcssPlugin]);
7 | loader.Plugin = require('./Plugin');
8 |
9 | module.exports = loader;
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Locks
9 | package-lock.json
10 | yarn.lock
11 |
12 | # Cache
13 | .DS_Store
14 | .cache
15 | .npm
16 |
17 | # Environment
18 | .env
19 |
20 | # Dependency directories
21 | node_modules/
22 |
23 | # Editors
24 | .vscode/
25 | .idea/
26 |
27 | # Build Output
28 | dest/
29 |
30 | # Coverage
31 | lib-cov
32 | coverage
33 | .nyc_output
34 |
--------------------------------------------------------------------------------
/test/integration.test.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const runWebpack = require('base-css-image-loader/test/fixtures/runWebpack');
3 |
4 | const cases = ['background', 'image-set', 'postcss-plugins', 'public-path', 'smart'];
5 |
6 | describe('Webpack Integration Tests', () => {
7 | cases.forEach((caseName) => {
8 | it('#test webpack integration case: ' + caseName, (done) => {
9 | runWebpack(caseName, { casesPath: path.resolve(__dirname, './cases') }, done);
10 | });
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | const CSSSpritePlugin = require('../index').Plugin;
2 |
3 | module.exports = {
4 | entry: {
5 | bundle: './index.js',
6 | },
7 | output: {
8 | path: __dirname + '/dest',
9 | filename: '[name].js',
10 | publicPath: 'dest/',
11 | },
12 | module: {
13 | rules: [
14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../index')] },
15 | { test: /\.png$/, use: ['file-loader'] },
16 | ],
17 | },
18 | plugins: [new CSSSpritePlugin()],
19 | };
20 |
--------------------------------------------------------------------------------
/test/cases/retina/webpack.config.js:
--------------------------------------------------------------------------------
1 | const CSSSpritePlugin = require('../../../index').Plugin;
2 |
3 | module.exports = {
4 | entry: {
5 | bundle: './index.js',
6 | },
7 | output: {
8 | path: __dirname + '/dest',
9 | filename: '[name].js',
10 | publicPath: 'dest/',
11 | },
12 | module: {
13 | rules: [
14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] },
15 | { test: /\.png$/, use: ['file-loader'] },
16 | ],
17 | },
18 | plugins: [new CSSSpritePlugin()],
19 | };
20 |
--------------------------------------------------------------------------------
/test/cases/background/webpack.config.js:
--------------------------------------------------------------------------------
1 | const CSSSpritePlugin = require('../../../index').Plugin;
2 |
3 | module.exports = {
4 | entry: {
5 | bundle: './index.js',
6 | },
7 | output: {
8 | path: __dirname + '/dest',
9 | filename: '[name].js',
10 | publicPath: 'dest/',
11 | },
12 | module: {
13 | rules: [
14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] },
15 | { test: /\.png$/, use: ['file-loader'] },
16 | ],
17 | },
18 | plugins: [new CSSSpritePlugin()],
19 | };
20 |
--------------------------------------------------------------------------------
/test/cases/default/webpack.config.js:
--------------------------------------------------------------------------------
1 | const CSSSpritePlugin = require('../../../index').Plugin;
2 |
3 | module.exports = {
4 | entry: {
5 | bundle: './index.js',
6 | },
7 | output: {
8 | path: __dirname + '/dest',
9 | filename: '[name].js',
10 | publicPath: 'dest/',
11 | },
12 | module: {
13 | rules: [
14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] },
15 | { test: /\.png$/, use: ['file-loader'] },
16 | ],
17 | },
18 | plugins: [new CSSSpritePlugin()],
19 | };
20 |
--------------------------------------------------------------------------------
/test/cases/background/webpack.config.test.js:
--------------------------------------------------------------------------------
1 | const CSSSpritePlugin = require('../../../index').Plugin;
2 |
3 | module.exports = {
4 | entry: {
5 | bundle: './index.js',
6 | },
7 | output: {
8 | path: __dirname + '/',
9 | filename: '[name].js',
10 | publicPath: '/',
11 | },
12 | context: __dirname,
13 | module: {
14 | rules: [
15 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] },
16 | { test: /\.png$/, use: ['file-loader'] },
17 | ],
18 | },
19 | plugins: [new CSSSpritePlugin()],
20 | };
21 |
--------------------------------------------------------------------------------
/test/cases/smart/webpack.config.js:
--------------------------------------------------------------------------------
1 | const CSSSpritePlugin = require('../../../index').Plugin;
2 |
3 | module.exports = {
4 | entry: {
5 | bundle: './index.js',
6 | },
7 | output: {
8 | path: __dirname + '/dest',
9 | filename: '[name].js',
10 | publicPath: 'dest/',
11 | },
12 | module: {
13 | rules: [
14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] },
15 | { test: /\.png$/, use: ['file-loader'] },
16 | ],
17 | },
18 | plugins: [new CSSSpritePlugin({
19 | publicPath: 'dest/',
20 | imageSetFallback: true,
21 | })],
22 | };
23 |
--------------------------------------------------------------------------------
/test/cases/image-set/webpack.config.js:
--------------------------------------------------------------------------------
1 | const CSSSpritePlugin = require('../../../index').Plugin;
2 |
3 | module.exports = {
4 | entry: {
5 | bundle: './index.js',
6 | },
7 | output: {
8 | path: __dirname + '/dest',
9 | filename: '[name].js',
10 | publicPath: 'dest/',
11 | },
12 | module: {
13 | rules: [
14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] },
15 | { test: /\.png$/, use: ['file-loader'] },
16 | ],
17 | },
18 | plugins: [new CSSSpritePlugin({
19 | publicPath: 'dest/',
20 | imageSetFallback: true,
21 | })],
22 | };
23 |
--------------------------------------------------------------------------------
/test/cases/public-path/webpack.config.js:
--------------------------------------------------------------------------------
1 | const CSSSpritePlugin = require('../../../index').Plugin;
2 |
3 | module.exports = {
4 | entry: {
5 | bundle: './index.js',
6 | },
7 | output: {
8 | path: __dirname + '/dest',
9 | filename: '[name].js',
10 | publicPath: '/some/public/',
11 | },
12 | module: {
13 | rules: [
14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] },
15 | { test: /\.png$/, use: ['file-loader'] },
16 | ],
17 | },
18 | plugins: [new CSSSpritePlugin({
19 | output: 'static',
20 | publicPath: 'http://cdn.163.com/cdn/static',
21 | })],
22 | };
23 |
--------------------------------------------------------------------------------
/src/README.md:
--------------------------------------------------------------------------------
1 | # 说明
2 | + backgroundParser: 单行background解析器
3 | + backgroundBlockParser: cssBlock 中background属性集解析器,生成parsedRule
4 |
5 | + Plugin2 修改imageList结构到cssBlockList
6 | ```
7 | cssBlockList{
8 | 'cssRule-hash':{
9 | parsedRule: backgroundBlockParser生成
10 | hash: 原css属性字符串生成的哈希码
11 | divWidth: 容器宽度
12 | divHeight: 容器高度
13 | images: cssblock中所有的图片引用
14 | }
15 | }
16 | ```
17 | 修改position、size算法
18 | ```
19 | r = css设置的size 与 雪碧图中的图片size比例
20 | background-size = r * 雪碧图的长宽
21 | background-postion = -r* 雪碧图中的图片偏移 + css设置的position
22 |
23 | ```
24 | 修改imageSet兼容方式为media query,重算雪碧图的size和position
25 |
26 | 兼容老版本retina写法
27 |
28 |
29 | # 局限
30 |
31 | + backgroundBlockParser暂不支持多背景图解析(backgroundParser可以解析单行多图)
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/test/cases/default/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CSS Sprite Loader
6 |
7 |
8 |
9 |
10 |
Source images display
11 |
12 |
13 |
14 |
15 |
16 |
Sprite display
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/test/cases/image-set/media-query-order.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/test/cases/postcss-plugins/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CSS Sprite Loader
6 |
7 |
8 |
9 |
10 |
Source images display
11 |
12 |
13 |
14 |
15 |
16 |
Sprite display
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/test/cases/public-path/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CSS Sprite Loader
6 |
7 |
8 |
9 |
10 |
11 |
Source images display
12 |
13 |
14 |
15 |
16 |
17 |
Sprite display
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CSS Sprite Loader
6 |
7 |
8 |
9 |
10 |
Source Image Display
11 |
12 |
13 |
14 |
15 |
16 |
Sprite Image Display
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/test/cases/extract-text-webpack-plugin/webpack.config.js:
--------------------------------------------------------------------------------
1 | const CSSSpritePlugin = require('../../../index').Plugin;
2 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
3 |
4 | module.exports = {
5 | entry: {
6 | bundle: './index.js',
7 | },
8 | output: {
9 | path: __dirname + '/dest',
10 | filename: '[name].js',
11 | },
12 | module: {
13 | rules: [
14 | { test: /\.css$/, use: ExtractTextPlugin.extract({
15 | fallback: 'style-loader',
16 | use: ['css-loader', require.resolve('../../../index')],
17 | }) },
18 | { test: /\.png$/, use: ['file-loader'] },
19 | ],
20 | },
21 | plugins: [
22 | new CSSSpritePlugin(),
23 | new ExtractTextPlugin('bundle.css'),
24 | ],
25 | };
26 |
--------------------------------------------------------------------------------
/test/cases/extract-text-webpack-plugin/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CSS Sprite Loader
6 |
7 |
8 |
9 |
10 |
11 |
Source images display
12 |
13 |
14 |
15 |
16 |
17 |
Sprite display
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/test/cases/retina/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CSS Sprite Loader
6 |
7 |
8 |
9 |
10 |
Source images display
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
Sprite display
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018-present NetEase Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/cases/smart/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CSS Sprite Loader
6 |
7 |
8 |
9 |
10 |
Source images display
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
Sprite display
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/test/cases/image-set/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CSS Sprite Loader
6 |
7 |
8 |
9 |
10 |
Source images display
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
Sprite display
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Javascript Node CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details
4 | #
5 | version: 2
6 | jobs:
7 | build:
8 | docker:
9 | # specify the version you desire here
10 | - image: circleci/node:8
11 |
12 | # Specify service dependencies here if necessary
13 | # CircleCI maintains a library of pre-built images
14 | # documented at https://circleci.com/docs/2.0/circleci-images/
15 | # - image: circleci/mongo:3.4.4
16 |
17 | branches:
18 | only:
19 | - master
20 | - next
21 |
22 | working_directory: ~/css-sprite-loader
23 |
24 | steps:
25 | - checkout
26 |
27 | # Download and cache dependencies
28 | - restore_cache:
29 | keys:
30 | - v1-dependencies-{{ checksum "package.json" }}
31 | # fallback to using the latest cache if no exact match is found
32 | - v1-dependencies-
33 |
34 | - run: npm install
35 |
36 | - save_cache:
37 | paths:
38 | - node_modules
39 | key: v1-dependencies-{{ checksum "package.json" }}
40 |
41 | # run tests!
42 | - run: npm run test
43 |
--------------------------------------------------------------------------------
/test/cases/default/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | .part {
6 | display: inline-block;
7 | width: 50%;
8 | }
9 |
10 | .source.simple {
11 | width: 128px;
12 | height: 128px;
13 | background: url('../../fixtures/images/home.png');
14 | }
15 | .sprite.simple {
16 | width: 128px;
17 | height: 128px;
18 | background: url('../../fixtures/images/home.png?sprite');
19 | }
20 |
21 | .source.query {
22 | width: 128px;
23 | height: 128px;
24 | background: url('../../fixtures/images/lollipop.png');
25 | }
26 | .sprite.query {
27 | width: 128px;
28 | height: 128px;
29 | background: url('../../fixtures/images/lollipop.png?sprite=sprite');
30 | }
31 |
32 | .source.rename {
33 | width: 128px;
34 | height: 128px;
35 | background: url('../../fixtures/images/tag.png');
36 | }
37 | .sprite.rename {
38 | width: 128px;
39 | height: 128px;
40 | background: url('../../fixtures/images/tag.png?sprite=sprite-nav');
41 | }
42 |
43 | .source.rename-2 {
44 | width: 128px;
45 | height: 128px;
46 | background: url('../../fixtures/images/html.png');
47 | }
48 | .sprite.rename-2 {
49 | width: 128px;
50 | height: 128px;
51 | background: url('../../fixtures/images/html.png?sprite=sprite-nav');
52 | }
53 |
--------------------------------------------------------------------------------
/test/cases/public-path/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | .part {
6 | display: inline-block;
7 | width: 50%;
8 | }
9 |
10 | .source.simple {
11 | width: 128px;
12 | height: 128px;
13 | background: url('../../fixtures/images/home.png');
14 | }
15 | .sprite.simple {
16 | width: 128px;
17 | height: 128px;
18 | background: url('../../fixtures/images/home.png?sprite');
19 | }
20 |
21 | .source.query {
22 | width: 128px;
23 | height: 128px;
24 | background: url('../../fixtures/images/lollipop.png');
25 | }
26 | .sprite.query {
27 | width: 128px;
28 | height: 128px;
29 | background: url('../../fixtures/images/lollipop.png?sprite=sprite');
30 | }
31 |
32 | .source.rename {
33 | width: 128px;
34 | height: 128px;
35 | background: url('../../fixtures/images/tag.png');
36 | }
37 | .sprite.rename {
38 | width: 128px;
39 | height: 128px;
40 | background: url('../../fixtures/images/tag.png?sprite=sprite-nav');
41 | }
42 |
43 | .source.rename-2 {
44 | width: 128px;
45 | height: 128px;
46 | background: url('../../fixtures/images/html.png');
47 | }
48 | .sprite.rename-2 {
49 | width: 128px;
50 | height: 128px;
51 | background: url('../../fixtures/images/html.png?sprite=sprite-nav');
52 | }
53 |
--------------------------------------------------------------------------------
/test/cases/postcss-plugins/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | .part {
6 | display: inline-block;
7 | width: 50%;
8 | }
9 |
10 | .source.simple {
11 | width: 128px;
12 | height: 128px;
13 | background: url('../../fixtures/images/home.png');
14 | }
15 | .sprite.simple {
16 | width: 128px;
17 | height: 128px;
18 | background: url('../../fixtures/images/home.png?sprite');
19 | }
20 |
21 | .source.query {
22 | width: 128px;
23 | height: 128px;
24 | background: url('../../fixtures/images/lollipop.png');
25 | }
26 | .sprite.query {
27 | width: 128px;
28 | height: 128px;
29 | background: url('../../fixtures/images/lollipop.png?sprite=sprite');
30 | }
31 |
32 | .source.rename {
33 | width: 128px;
34 | height: 128px;
35 | background: url('../../fixtures/images/tag.png');
36 | }
37 | .sprite.rename {
38 | width: 128px;
39 | height: 128px;
40 | background: url('../../fixtures/images/tag.png?sprite=sprite-nav');
41 | }
42 |
43 | .source.rename-2 {
44 | width: 128px;
45 | height: 128px;
46 | background: url('../../fixtures/images/html.png');
47 | }
48 | .sprite.rename-2 {
49 | width: 128px;
50 | height: 128px;
51 | background: url('../../fixtures/images/html.png?sprite=sprite-nav');
52 | }
53 |
--------------------------------------------------------------------------------
/test/cases/extract-text-webpack-plugin/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | .part {
6 | display: inline-block;
7 | width: 50%;
8 | }
9 |
10 | .source.simple {
11 | width: 128px;
12 | height: 128px;
13 | background: url('../../fixtures/images/home.png');
14 | }
15 | .sprite.simple {
16 | width: 128px;
17 | height: 128px;
18 | background: url('../../fixtures/images/home.png?sprite');
19 | }
20 |
21 | .source.query {
22 | width: 128px;
23 | height: 128px;
24 | background: url('../../fixtures/images/lollipop.png');
25 | }
26 | .sprite.query {
27 | width: 128px;
28 | height: 128px;
29 | background: url('../../fixtures/images/lollipop.png?sprite=sprite');
30 | }
31 |
32 | .source.rename {
33 | width: 128px;
34 | height: 128px;
35 | background: url('../../fixtures/images/tag.png');
36 | }
37 | .sprite.rename {
38 | width: 128px;
39 | height: 128px;
40 | background: url('../../fixtures/images/tag.png?sprite=sprite-nav');
41 | }
42 |
43 | .source.rename-2 {
44 | width: 128px;
45 | height: 128px;
46 | background: url('../../fixtures/images/html.png');
47 | }
48 | .sprite.rename-2 {
49 | width: 128px;
50 | height: 128px;
51 | background: url('../../fixtures/images/html.png?sprite=sprite-nav');
52 | }
53 |
--------------------------------------------------------------------------------
/test/default.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const runWebpack = require('base-css-image-loader/test/fixtures/runWebpack');
4 | const expect = require('chai').expect;
5 | const { utils } = require('base-css-image-loader');
6 |
7 | const caseName = 'default';
8 | const replaceReg = /REPLACE_BACKGROUND\([^)]*\)/g;
9 |
10 | describe('Webpack Integration test', () => {
11 | it('#test default config: ' + caseName, (done) => {
12 | runWebpack(caseName, { casesPath: path.resolve(__dirname, './cases') }, (err, data) => {
13 | if (err)
14 | return done(err);
15 |
16 | const filesContent = fs.readFileSync(path.resolve(data.outputPath, 'sprite.png'));
17 | const md5Code = utils.genMD5(filesContent);
18 | expect(md5Code).to.eql('d8defd30309a90e991feff6014911569');
19 | const filesContent2 = fs.readFileSync(path.resolve(data.outputPath, 'sprite-nav.png'));
20 | const md5Code2 = utils.genMD5(filesContent2);
21 | expect(md5Code2).to.eql('8c44ae541ba21ba1c42011335f3b0801');
22 | const cssContent = fs.readFileSync(path.resolve(data.outputPath, 'bundle.js')).toString();
23 | expect(replaceReg.test(cssContent)).to.eql(false);
24 | done();
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/test/cases/postcss-plugins/webpack.config.js:
--------------------------------------------------------------------------------
1 | const CSSSpritePlugin = require('../../../index').Plugin;
2 |
3 | const postcssPlugins = [
4 | require('postcss-px-to-viewport')({
5 | viewportWidth: 750, // (Number) The width of the viewport
6 | // viewportHeight: 1334, // (Number) The height of the viewport.
7 | unitPrecision: 3, // (Number) The decimal numbers to allow the REM units to grow to.
8 | viewportUnit: 'vw', // (String) Expected units.
9 | selectorBlackList: ['.ignore', '.hairlines'], // (Array) The selectors to ignore and leave as px.
10 | minPixelValue: 1, // (Number) Set the minimum pixel value to replace.
11 | mediaQuery: false, // (Boolean) Allow px to be converted in media queries.
12 | }),
13 | require('postcss-viewport-units')({}),
14 | ];
15 |
16 | module.exports = {
17 | entry: {
18 | bundle: './index.js',
19 | },
20 | output: {
21 | path: __dirname + '/dest',
22 | filename: '[name].js',
23 | publicPath: 'dest/',
24 | },
25 | module: {
26 | rules: [
27 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index'), {
28 | loader: 'postcss-loader',
29 | options: { plugins: postcssPlugins },
30 | }] },
31 | { test: /\.png$/, use: ['file-loader'] },
32 | ],
33 | },
34 | plugins: [new CSSSpritePlugin({
35 | plugins: postcssPlugins,
36 | })],
37 | };
38 |
--------------------------------------------------------------------------------
/test/retina.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const runWebpack = require('base-css-image-loader/test/fixtures/runWebpack');
4 | const expect = require('chai').expect;
5 | const { utils } = require('base-css-image-loader');
6 |
7 | const caseName = 'retina';
8 | const replaceReg = /REPLACE_BACKGROUND\([^)]*\)/g;
9 |
10 | describe('Webpack Integration test', () => {
11 | it('#test retina config: ' + caseName, (done) => {
12 | runWebpack(caseName, { casesPath: path.resolve(__dirname, './cases') }, (err, data) => {
13 | if (err)
14 | return done(err);
15 |
16 | const filesContent = fs.readFileSync(path.resolve(data.outputPath, 'sprite.png'));
17 | const md5Code = utils.genMD5(filesContent);
18 | expect(md5Code).to.eql('873103c642eaf6ee9d736d4703f2201d');
19 | const filesContent2 = fs.readFileSync(path.resolve(data.outputPath, 'sprite@2x.png'));
20 | const md5Code2 = utils.genMD5(filesContent2);
21 | expect(md5Code2).to.eql('51d951f98092152d8fc56bf3380577e3');
22 | const filesContent3 = fs.readFileSync(path.resolve(data.outputPath, 'sprite@3x.png'));
23 | const md5Code3 = utils.genMD5(filesContent3);
24 | expect(md5Code3).to.eql('62594d2f59ff829b82c49e9d717d7759');
25 | const filesContent4 = fs.readFileSync(path.resolve(data.outputPath, 'sprite@4x.png'));
26 | const md5Code4 = utils.genMD5(filesContent4);
27 | expect(md5Code4).to.eql('4a6a7dbace7933efe321b357d4db2fb9');
28 | const cssContent = fs.readFileSync(path.resolve(data.outputPath, 'bundle.js')).toString();
29 | expect(replaceReg.test(cssContent)).to.eql(false);
30 | done();
31 | });
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/test/dist/default.test.dev.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var fs = require('fs');
4 |
5 | var path = require('path');
6 |
7 | var expect = require('chai').expect;
8 |
9 | var _require = require('base-css-image-loader'),
10 | utils = _require.utils;
11 |
12 | var shell = require('shelljs');
13 |
14 | var execa = require('execa');
15 |
16 | var caseName = 'default';
17 | var replaceReg = /REPLACE_BACKGROUND\([^)]*\)/g;
18 | describe('Webpack Integration test', function () {
19 | var buildCLI = path.resolve(__dirname, '../node_modules/.bin/webpack');
20 | var runDir = path.join('../test/cases/' + caseName);
21 | var destDir = path.join('./cases/' + caseName + '/dest');
22 | before(function () {
23 | shell.cd(path.resolve(__dirname, runDir));
24 | });
25 | afterEach(function () {
26 | shell.rm('-rf', path.resolve(__dirname, destDir));
27 | });
28 | it('#test default config: ' + caseName, function (done) {
29 | execa(buildCLI, ['--config', './webpack.config.js']).then(function (res) {
30 | var files = fs.readdirSync(path.resolve(__dirname, destDir));
31 | expect(files).to.eql(['background_sprite.png', 'bundle.js', 'test.png']);
32 | var filesContent = fs.readFileSync(path.resolve(__dirname, destDir + '/background_sprite.png'));
33 | var md5Code = utils.md5Create(filesContent);
34 | expect(md5Code).to.eql('d158e28d383a33dd07e4b50571556e5d');
35 | var filesContent2 = fs.readFileSync(path.resolve(__dirname, destDir + '/test.png'));
36 | var md5Code2 = utils.md5Create(filesContent2);
37 | expect(md5Code2).to.eql('4812de9d8e0456fd3f178dbef18513e7');
38 | var cssContent = fs.readFileSync(path.resolve(__dirname, destDir + '/bundle.js')).toString();
39 | expect(replaceReg.test(cssContent)).to.eql(false);
40 | done();
41 | });
42 | });
43 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "css-sprite-loader",
3 | "description": "A webpack loader to convert png into sprite image",
4 | "version": "0.3.4",
5 | "author": "moujintao ",
6 | "contributors": [
7 | "Rainfore "
8 | ],
9 | "scripts": {
10 | "precommit": "node node_modules/vusion-hooks/precommit",
11 | "test": "mocha --timeout 5000 ./test/final.test.js",
12 | "test:default": "mocha --timeout 5000 ./test/default.test.js",
13 | "test:retina": "mocha --timeout 5000 ./test/retina.test.js",
14 | "eslint": "eslint ./src --fix"
15 | },
16 | "main": "./index.js",
17 | "repository": "vusion/css-sprite-loader",
18 | "bugs": {
19 | "url": "http://github.com/vusion/css-sprite-loader/issues"
20 | },
21 | "license": "MIT",
22 | "keywords": [
23 | "css",
24 | "image",
25 | "png",
26 | "sprite",
27 | "webpack",
28 | "loader"
29 | ],
30 | "tags": [
31 | "css",
32 | "image",
33 | "png",
34 | "sprite",
35 | "webpack",
36 | "loader"
37 | ],
38 | "dependencies": {
39 | "base-css-image-loader": "^0.2.7",
40 | "css-fruit": "^0.1.3",
41 | "postcss": "^6.0.23",
42 | "spritesmith": "^3.2.1"
43 | },
44 | "devDependencies": {
45 | "chai": "^4.1.2",
46 | "css-loader": "^0.28.4",
47 | "eslint": "^5.12.0",
48 | "eslint-config-vusion": "^3.0.1",
49 | "extract-text-webpack-plugin": "^3.0.0",
50 | "file-loader": "^1.1.6",
51 | "html-webpack-plugin": "^2.30.1",
52 | "husky": "^0.14.3",
53 | "image-js": "^0.21.0",
54 | "mocha": "^3.5.3",
55 | "postcss-loader": "^3.0.0",
56 | "postcss-px-to-viewport": "^0.0.3",
57 | "postcss-viewport-units": "^0.1.4",
58 | "puppeteer": "^1.7.0",
59 | "shelljs": "^0.8.4",
60 | "style-loader": "^0.18.2",
61 | "url-loader": "^0.5.9",
62 | "vusion-hooks": "^0.2.1",
63 | "webpack": "^4.46.0",
64 | "webpack-cli": "^4.9.1",
65 | "webpack-dev-server": "^2.7.1"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/example/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | .part {
6 | display: inline-block;
7 | width: 50%;
8 | font-family: Rockwell;
9 | color: #565b61;
10 | }
11 |
12 | .source, .sprite {
13 | border: 1px dashed #dfe4ec;
14 | }
15 |
16 | .source.retina2x {
17 | width: 128px;
18 | height: 128px;
19 | background: url('../test/fixtures/images/retina/angry-birds@2x.png');
20 | background-size: 100%;
21 | }
22 | .sprite.retina2x {
23 | width: 128px;
24 | height: 128px;
25 | background: url('../test/fixtures/images/retina/angry-birds@2x.png?sprite&retina@1x');
26 | }
27 |
28 | .source.retina-2 {
29 | width: 128px;
30 | height: 128px;
31 | background: url('../test/fixtures/images/retina/captain-america@2x.png') no-repeat;
32 | background-size: 80%;
33 | background-position: 30px 20px;
34 | }
35 | .sprite.retina-2 {
36 | width: 128px;
37 | height: 128px;
38 | background: url('../test/fixtures/images/retina/captain-america.png?sprite&retina') no-repeat;
39 | background-size: 80%;
40 | background-position: 30px 20px;
41 | }
42 |
43 | .source.bg-position-outside {
44 | width: 128px;
45 | height: 128px;
46 | background: url('../test/fixtures/images/tag.png') no-repeat;
47 | background-position: 30px -20px;
48 | }
49 | .sprite.bg-position-outside {
50 | width: 128px;
51 | height: 128px;
52 | background: url('../test/fixtures/images/tag.png?sprite') no-repeat;
53 | background-position: 30px -20px;
54 | }
55 |
56 | .source.bg-position-and-size-outside {
57 | width: 200px;
58 | height: 128px;
59 | background: url('../test/fixtures/images/html.png') no-repeat;
60 | background-size: 200px 100px;
61 | background-position: 30px 20px;
62 | }
63 | .sprite.bg-position-and-size-outside {
64 | width: 200px;
65 | height: 128px;
66 | background: url('../test/fixtures/images/html.png?sprite') no-repeat;
67 | background-size: 200px 100px;
68 | background-position: 30px 20px;
69 | }
70 |
--------------------------------------------------------------------------------
/test/extract-text-webpack-plugin.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const runWebpack = require('base-css-image-loader/test/fixtures/runWebpack');
4 | const expect = require('chai').expect;
5 | const { utils } = require('base-css-image-loader');
6 |
7 | const caseName = 'extract-text-webpack-plugin';
8 | const replaceReg = /REPLACE_BACKGROUND\([^)]*\)/g;
9 |
10 | describe('Webpack Integration test', () => {
11 | it('#test extract-text-webpack-plugin config: ' + caseName, (done) => {
12 | runWebpack(caseName, { casesPath: path.resolve(__dirname, './cases') }, (err, data) => {
13 | if (err)
14 | return done(err);
15 |
16 | const files = fs.readdirSync(data.outputPath);
17 | expect(files).to.eql([
18 | 'background_sprite.png',
19 | 'background_sprite@2x.png',
20 | 'bundle.js',
21 | 'index.css',
22 | 'test.png',
23 | 'test@2x.png',
24 | ]);
25 | const filesContent = fs.readFileSync(path.resolve(data.outputPath, 'background_sprite.png'));
26 | const md5Code = utils.md5Create(filesContent);
27 | expect(md5Code).to.eql('96059fa5a49046b499352e23fea86070');
28 | const filesContent2 = fs.readFileSync(path.resolve(data.outputPath, 'test.png'));
29 | const md5Code2 = utils.md5Create(filesContent2);
30 | expect(md5Code2).to.eql('e1645b7464e7a59bbc9466b7f4f1562b');
31 | const filesContent3 = fs.readFileSync(path.resolve(data.outputPath, 'background_sprite@2x.png'));
32 | const md5Code3 = utils.md5Create(filesContent3);
33 | expect(md5Code3).to.eql('96059fa5a49046b499352e23fea86070');
34 | const filesContent4 = fs.readFileSync(path.resolve(data.outputPath, 'test@2x.png'));
35 | const md5Code4 = utils.md5Create(filesContent4);
36 | expect(md5Code4).to.eql('e1645b7464e7a59bbc9466b7f4f1562b');
37 | const cssContent = fs.readFileSync(path.resolve(data.outputPath, 'index.css')).toString();
38 | expect(replaceReg.test(cssContent)).to.eql(false);
39 | done();
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/test/cases/background/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CSS Sprite Loader
6 |
7 |
8 |
9 |
10 |
Source images display
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
Sprite display
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/test/cases/retina/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | .part {
6 | display: inline-block;
7 | width: 50%;
8 | }
9 |
10 | .source.simple {
11 | width: 128px;
12 | height: 128px;
13 | background: url('../../fixtures/images/retina/angry-birds.png');
14 | background-size: 100%;
15 | }
16 | .sprite.simple {
17 | width: 128px;
18 | height: 128px;
19 | background: url('../../fixtures/images/retina/angry-birds.png?sprite');
20 | background-size: 100%;
21 | }
22 |
23 | .source.retina {
24 | width: 128px;
25 | height: 128px;
26 | background: url('../../fixtures/images/retina/angry-birds@2x.png');
27 | background-size: 100%;
28 | }
29 | .sprite.retina {
30 | width: 128px;
31 | height: 128px;
32 | background: url('../../fixtures/images/retina/angry-birds.png?sprite&retina');
33 | background-size: 100%;
34 | }
35 |
36 | .source.retina2x {
37 | width: 128px;
38 | height: 128px;
39 | background: url('../../fixtures/images/retina/angry-birds@2x.png');
40 | background-size: 100%;
41 | }
42 | .sprite.retina2x {
43 | width: 128px;
44 | height: 128px;
45 | background: url('../../fixtures/images/retina/angry-birds@2x.png?sprite&retina@1x');
46 | }
47 |
48 | .source.retina4x {
49 | width: 128px;
50 | height: 128px;
51 | background: url('../../fixtures/images/retina/angry-birds@4x.png');
52 | background-size: 100%;
53 | }
54 | .sprite.retina4x {
55 | width: 128px;
56 | height: 128px;
57 | background: url('../../fixtures/images/retina/angry-birds.png?sprite&retina&retina@3x&retina@4x');
58 | }
59 |
60 | .source.simple-2 {
61 | width: 128px;
62 | height: 128px;
63 | background: url('../../fixtures/images/retina/captain-america.png') no-repeat;
64 | background-size: 80%;
65 | }
66 | .sprite.simple-2 {
67 | width: 128px;
68 | height: 128px;
69 | background: url('../../fixtures/images/retina/captain-america.png?sprite') no-repeat;
70 | background-size: 80%;
71 | }
72 |
73 | .source.retina-2 {
74 | width: 128px;
75 | height: 128px;
76 | background: url('../../fixtures/images/retina/captain-america@2x.png') no-repeat;
77 | background-size: 80%;
78 | background-position: 30px 20px;
79 | }
80 | .sprite.retina-2 {
81 | width: 128px;
82 | height: 128px;
83 | background: url('../../fixtures/images/retina/captain-america.png?sprite&retina') no-repeat;
84 | background-size: 80%;
85 | background-position: 30px 20px;
86 | }
87 |
88 | .source.retina4x-2 {
89 | width: 128px;
90 | height: 128px;
91 | background: url('../../fixtures/images/retina/captain-america@4x.png') no-repeat;
92 | background-size: 80%;
93 | background-position: 30px 20px;
94 | }
95 | .sprite.retina4x-2 {
96 | width: 128px;
97 | height: 128px;
98 | background: url('../../fixtures/images/retina/captain-america.png?sprite&retina&retina@3x&retina@4x') no-repeat;
99 | background-size: 80%;
100 | background-position: 30px 20px;
101 | }
102 |
--------------------------------------------------------------------------------
/test/cases/image-set/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | .part {
6 | display: inline-block;
7 | width: 50%;
8 | }
9 |
10 | .source.simple {
11 | width: 128px;
12 | height: 128px;
13 | background: url('../../fixtures/images/retina/angry-birds.png');
14 | }
15 | .sprite.simple {
16 | width: 128px;
17 | height: 128px;
18 | background: url('../../fixtures/images/retina/angry-birds.png?sprite');
19 | }
20 |
21 | .source.image-set {
22 | width: 128px;
23 | height: 128px;
24 | background: image-set(url('../../fixtures/images/retina/angry-birds@2x.png') 2x);
25 | }
26 | .sprite.image-set {
27 | width: 128px;
28 | height: 128px;
29 | background: image-set(url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x);
30 | }
31 |
32 | .source.image-set-prefix {
33 | width: 128px;
34 | height: 128px;
35 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds@2x.png') 2x);
36 | }
37 | .sprite.image-set-prefix {
38 | width: 128px;
39 | height: 128px;
40 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x);
41 | }
42 |
43 | .source.image-set-fallback {
44 | width: 128px;
45 | height: 128px;
46 | background: url('../../fixtures/images/retina/angry-birds.png');
47 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png') 1x, url('../../fixtures/images/retina/angry-birds@2x.png') 2x);
48 | }
49 | .sprite.image-set-fallback {
50 | width: 128px;
51 | height: 128px;
52 | background: url('../../fixtures/images/retina/angry-birds.png?sprite');
53 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png?sprite') 1x, url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x);
54 | }
55 |
56 | .source.image-set-and-other-things {
57 | width: 100px;
58 | height: 150px;
59 | background: url('../../fixtures/images/retina/angry-birds.png');
60 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png') 1x, url('../../fixtures/images/retina/angry-birds@2x.png') 2x);
61 | background-size: 120%;
62 | background-position: 30px 20px;
63 | background-repeat: no-repeat;
64 | }
65 | .sprite.image-set-and-other-things {
66 | width: 100px;
67 | height: 150px;
68 | background: url('../../fixtures/images/retina/angry-birds.png?sprite');
69 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png?sprite') 1x, url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x);
70 | background-size: 120%;
71 | background-position: 30px 20px;
72 | }
73 |
74 | .source.image-default-resolution {
75 | width: 128px;
76 | height: 128px;
77 | background: -webkit-image-set(
78 | url('../../fixtures/images/retina/minion@3x.png') 3x,
79 | url('../../fixtures/images/retina/minion.png') 1x,
80 | url('../../fixtures/images/retina/minion@2x.png') 2x
81 | );
82 | }
83 | .sprite.image-default-resolution {
84 | width: 128px;
85 | height: 128px;
86 | background: -webkit-image-set(
87 | url('../../fixtures/images/retina/minion@3x.png?sprite') 3x
88 | url('../../fixtures/images/retina/minion.png?sprite') 1x,
89 | url('../../fixtures/images/retina/minion@2x.png?sprite') 2x,
90 | );
91 | }
92 |
93 | .source.image-set-different {
94 | width: 128px;
95 | height: 128px;
96 | background: url('../../fixtures/images/retina/captain-america.png');
97 | background: -webkit-image-set(
98 | url('../../fixtures/images/retina/captain-america.png') 1x,
99 | url('../../fixtures/images/retina/captain-america@2x.png') 2x,
100 | url('../../fixtures/images/retina/captain-america@3x.png') 3x
101 | );
102 | }
103 | .sprite.image-set-different {
104 | width: 128px;
105 | height: 128px;
106 | background: url('../../fixtures/images/retina/captain-america.png?sprite');
107 | background: -webkit-image-set(
108 | url('../../fixtures/images/retina/captain-america.png?sprite') 1x,
109 | url('../../fixtures/images/retina/captain-america@2x.png?sprite=other-sprite') 2x,
110 | url('../../fixtures/images/retina/captain-america@3x.png') 3x
111 | );
112 | }
113 |
--------------------------------------------------------------------------------
/src/Plugin.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { BasePlugin } = require('base-css-image-loader');
4 | const SpriteSmith = require('spritesmith');
5 | const postcss = require('postcss');
6 | const computeNewBackground = require('./computeNewBackground');
7 | const meta = require('./meta');
8 |
9 | class CSSSpritePlugin extends BasePlugin {
10 | constructor(options) {
11 | options = options || {};
12 | super();
13 | this.REPLACE_AFTER_OPTIMIZE_TREE = true;
14 | Object.assign(this, meta);
15 |
16 | this.options = Object.assign(this.options, {
17 | // @inherit: output: './',
18 | // @inherit: filename: '[fontName].[ext]?[hash]',
19 | // @inherit: publicPath: undefined,
20 | padding: 40,
21 | queryParam: 'sprite',
22 | defaultName: 'sprite',
23 | filter: 'query',
24 | imageSetFallback: false,
25 | plugins: [],
26 | }, options);
27 | this.data = {}; // { [group: string]: { [md5: string]: { id: string, oldBackground: Background } } }
28 | }
29 |
30 | apply(compiler) {
31 | this.plugin(compiler, 'thisCompilation', (compilation, params) => {
32 | this.plugin(compilation, 'optimizeTree', (chunks, modules, callback) => this.optimizeTree(compilation, chunks, modules, callback));
33 | });
34 | super.apply(compiler);
35 | }
36 |
37 | optimizeTree(compilation, chunks, modules, callback) {
38 | const promises = Object.keys(this.data).map((groupName) => {
39 | const group = this.data[groupName];
40 | const keys = Object.keys(group);
41 | // Make sure same cachebuster in uncertain file loaded order
42 | !this.watching && keys.sort();
43 | const files = Array.from(new Set(keys.map((key) => group[key].filePath)));
44 |
45 | return new Promise((resolve, reject) => SpriteSmith.run({
46 | src: files,
47 | algorithm: 'binary-tree',
48 | padding: this.options.padding,
49 | }, (err, result) => err ? reject(err) : resolve(result)))
50 | .then((result) => {
51 | const output = this.getOutput({
52 | name: groupName,
53 | ext: 'png',
54 | content: result.image,
55 | }, compilation);
56 |
57 | compilation.assets[output.path] = {
58 | source: () => result.image,
59 | size: () => result.image.length,
60 | };
61 |
62 | const coordinates = result.coordinates;
63 | return Promise.all(keys.map((key) => {
64 | const item = group[key];
65 | // Add new background according to result of sprite
66 | const background = computeNewBackground(
67 | item.oldBackground,
68 | output.url,
69 | item.blockSize,
70 | coordinates[item.filePath],
71 | result.properties,
72 | +item.resolution.slice(0, -1),
73 | );
74 | background.valid = true;
75 | const content = background.toString();
76 |
77 | // @TODO: Should process in postcssPlugin?
78 | return postcss(this.options.plugins).process(`background: ${content};`, {
79 | from: undefined,
80 | to: undefined,
81 | }).then((result) => {
82 | item.content = result.root.nodes[0].value;
83 | });
84 | }));
85 | });
86 | });
87 |
88 | return Promise.all(promises).then(() => callback()).catch((e) => callback(e));
89 | }
90 |
91 | /**
92 | * @override
93 | * Replace Function
94 | */
95 | REPLACER_FUNC(groupName, id) {
96 | return this.data[groupName][id].content;
97 | }
98 |
99 | /**
100 | * @override
101 | * Replace Function to escape
102 | */
103 | REPLACER_FUNC_ESCAPED(groupName, id) {
104 | return this.data[groupName][id].content;
105 | }
106 | }
107 |
108 | module.exports = CSSSpritePlugin;
109 |
--------------------------------------------------------------------------------
/test/cases/smart/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | .part {
6 | display: inline-block;
7 | width: 50%;
8 | }
9 |
10 | .source.simple {
11 | height: 200px;
12 | background: url('../../fixtures/images/retina/angry-birds.png') no-repeat;
13 | }
14 | .sprite.simple {
15 | height: 200px;
16 | background: url('../../fixtures/images/retina/angry-birds.png?sprite') no-repeat;
17 | }
18 |
19 | .source.no-width {
20 | height: 200px;
21 | background: image-set(url('../../fixtures/images/retina/angry-birds.png') 1x, url('../../fixtures/images/retina/angry-birds@2x.png') 2x) no-repeat;
22 | }
23 | .sprite.no-width {
24 | height: 200px;
25 | background: image-set(url('../../fixtures/images/retina/angry-birds.png?sprite') 1x, url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x) no-repeat;
26 | }
27 |
28 | .source.background-size {
29 | height: 200px;
30 | background: image-set(url('../../fixtures/images/retina/angry-birds.png') 1x, url('../../fixtures/images/retina/angry-birds@2x.png') 2x) no-repeat;
31 | background-size: 50%;
32 | }
33 | .sprite.background-size {
34 | height: 200px;
35 | background: image-set(url('../../fixtures/images/retina/angry-birds.png?sprite') 1x, url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x) no-repeat;
36 | background-size: 50%;
37 | }
38 |
39 | /* .source.image-set-prefix {
40 | width: 128px;
41 | height: 128px;
42 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds@2x.png') 2x);
43 | }
44 | .sprite.image-set-prefix {
45 | width: 128px;
46 | height: 128px;
47 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x);
48 | }
49 |
50 | .source.image-set-fallback {
51 | width: 128px;
52 | height: 128px;
53 | background: url('../../fixtures/images/retina/angry-birds.png');
54 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png') 1x, url('../../fixtures/images/retina/angry-birds@2x.png') 2x);
55 | }
56 | .sprite.image-set-fallback {
57 | width: 128px;
58 | height: 128px;
59 | background: url('../../fixtures/images/retina/angry-birds.png?sprite');
60 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png?sprite') 1x, url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x);
61 | }
62 |
63 | .source.image-set-and-other-things {
64 | width: 100px;
65 | height: 150px;
66 | background: url('../../fixtures/images/retina/angry-birds.png');
67 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png') 1x, url('../../fixtures/images/retina/angry-birds@2x.png') 2x);
68 | background-size: 120%;
69 | background-position: 30px 20px;
70 | background-repeat: no-repeat;
71 | }
72 | .sprite.image-set-and-other-things {
73 | width: 100px;
74 | height: 150px;
75 | background: url('../../fixtures/images/retina/angry-birds.png?sprite');
76 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png?sprite') 1x, url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x);
77 | background-size: 120%;
78 | background-position: 30px 20px;
79 | }
80 |
81 | .source.image-default-resolution {
82 | width: 128px;
83 | height: 128px;
84 | background: -webkit-image-set(
85 | url('../../fixtures/images/retina/minion@3x.png') 3x,
86 | url('../../fixtures/images/retina/minion.png') 1x,
87 | url('../../fixtures/images/retina/minion@2x.png') 2x
88 | );
89 | }
90 | .sprite.image-default-resolution {
91 | width: 128px;
92 | height: 128px;
93 | background: -webkit-image-set(
94 | url('../../fixtures/images/retina/minion@3x.png?sprite') 3x
95 | url('../../fixtures/images/retina/minion.png?sprite') 1x,
96 | url('../../fixtures/images/retina/minion@2x.png?sprite') 2x,
97 | );
98 | }
99 |
100 | .source.image-set-different {
101 | width: 128px;
102 | height: 128px;
103 | background: url('../../fixtures/images/retina/captain-america.png');
104 | background: -webkit-image-set(
105 | url('../../fixtures/images/retina/captain-america.png') 1x,
106 | url('../../fixtures/images/retina/captain-america@2x.png') 2x,
107 | url('../../fixtures/images/retina/captain-america@3x.png') 3x
108 | );
109 | }
110 | .sprite.image-set-different {
111 | width: 128px;
112 | height: 128px;
113 | background: url('../../fixtures/images/retina/captain-america.png?sprite');
114 | background: -webkit-image-set(
115 | url('../../fixtures/images/retina/captain-america.png?sprite') 1x,
116 | url('../../fixtures/images/retina/captain-america@2x.png?sprite=other-sprite') 2x,
117 | url('../../fixtures/images/retina/captain-america@3x.png') 3x
118 | );
119 | } */
120 |
--------------------------------------------------------------------------------
/test/cases/background/test.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer');
2 | const webpack = require('webpack');
3 | const webpackDevServer = require('webpack-dev-server');
4 | const path = require('path');
5 | const { Image } = require('image-js');
6 |
7 | function fetchPropFrom(meta) {
8 | const m = JSON.parse(meta);
9 | return {
10 | top: m.top,
11 | height: m.height,
12 | };
13 | }
14 |
15 | (async () => {
16 | const browser = await puppeteer.launch();
17 | const page = await browser.newPage();
18 | const server = await new Promise((res, rej) => {
19 | const compiler = webpack(require('./webpack.config.test.js'));
20 | const server = new webpackDevServer(compiler, {
21 | contentBase: __dirname + '/',
22 | compress: true,
23 | port: 8080,
24 | });
25 | server.listen(8080, 'localhost', res);
26 | res(server);
27 | });
28 | await page.goto('http://localhost:8080');
29 |
30 | page.on('console', (msg) => {
31 | for (let i = 0; i < msg.args.length; ++i)
32 | console.log(`${i}: ${msg.args[i]}`);
33 | });
34 | const metas = await page.evaluate(() => {
35 | const contrast = 'source';
36 | const experimental = 'sprite';
37 | const experiments = [
38 | 'simple',
39 | 'bg-image',
40 | 'with-color',
41 | 'with-color-outside',
42 | 'with-color-override',
43 | 'bg-size',
44 | 'bg-size-pixel',
45 | 'bg-size-height',
46 | 'bg-size-cover',
47 | 'bg-size-contain',
48 | 'bg-size-override',
49 | 'bg-position',
50 | 'bg-position-outside',
51 | 'bg-position-override',
52 | 'bg-position-and-size',
53 | 'bg-position-and-size-outside',
54 | 'without-image-set',
55 | 'image-set',
56 | 'image-set-fallback',
57 | 'image-set-and-others'];
58 | console.log(experiments, contrast);
59 | return metas = experiments.map((exp) => {
60 | const contrastSelector = `.${contrast}.${exp}`;
61 | const experimentsSelector = `.${experimental}.${exp}`;
62 | const source = document.querySelector(contrastSelector);
63 | const sprite = document.querySelector(experimentsSelector);
64 | console.log(source, sprite);
65 | if (!source || !sprite)
66 | return false;
67 |
68 | return {
69 | name: exp,
70 | source: JSON.stringify(source.getBoundingClientRect()),
71 | sprite: JSON.stringify(sprite.getBoundingClientRect()),
72 | };
73 | }).filter(Boolean);
74 | });
75 |
76 | // normalize meta
77 | const experimentsList = metas.map((meta) => ({
78 | name: meta.name,
79 | source: fetchPropFrom(meta.source),
80 | sprite: fetchPropFrom(meta.sprite),
81 | }));
82 |
83 | await page.screenshot({
84 | path: './666.png',
85 | fullPage: true,
86 | });
87 |
88 | const img = await Image.load('./666.png');
89 |
90 | const grey = img.grey();
91 | grey.save('./gray666.png');
92 | const { width } = grey;
93 |
94 | const rslt = experimentsList.map((experiment) => {
95 | const { sprite, source, name } = experiment;
96 | console.log(sprite, source, name);
97 | if (source.height === 0 || sprite.height === 0)
98 | return { code: 999, msg: '元素未渲染', errCSSBlock: name };
99 | if (sprite.height !== source.height || sprite.top !== source.top)
100 | return { code: 606, msg: '页面元素未对应', errCSSBlock: name };
101 | const sourcePart = grey.crop({ x: 0, y: source.top, width: width / 2, height: source.height });
102 | const spritePart = grey.crop({ x: width / 2, y: sprite.top, width: width / 2, height: sprite.height });
103 | const leftPartArray = sourcePart.getPixelsArray();
104 | const rigthPartArray = spritePart.getPixelsArray();
105 | if (leftPartArray.length !== rigthPartArray.length) {
106 | return {
107 | code: -1,
108 | msg: '图片大小不匹配',
109 | errCSSBlock: name,
110 | };
111 | } else {
112 | let len = leftPartArray.length - 1;
113 | while (len-- && Math.abs(leftPartArray[len][0] - rigthPartArray[len][0]) < 10) {}
114 | console.log(
115 | len,
116 | leftPartArray[len],
117 | rigthPartArray[len]);
118 | if (len === -1) {
119 | return {
120 | code: 200,
121 | msg: '校验成功',
122 | CSSBlock: name,
123 | };
124 | } else {
125 | return {
126 | code: 996,
127 | msg: `校验失败 @ ${len}`,
128 | errCSSBlock: name,
129 | };
130 | }
131 | }
132 | });
133 | console.log(rslt);
134 | await new Promise((res) => {
135 | server.close(res);
136 | });
137 | await browser.close();
138 | })();
139 |
--------------------------------------------------------------------------------
/test/cases/image-set/test.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer');
2 | const webpack = require('webpack');
3 | const WebpackDevServer = require('webpack-dev-server');
4 | const path = require('path');
5 | const { Image } = require('image-js');
6 |
7 | function fetchPropFrom(meta) {
8 | const m = JSON.parse(meta);
9 | return {
10 | top: m.top,
11 | height: m.height,
12 | };
13 | }
14 |
15 | (async () => {
16 | const browser = await puppeteer.launch();
17 | const page = await browser.newPage();
18 | const server = await new Promise((res, rej) => {
19 | const compiler = webpack(require('./webpack.config.test.js'));
20 | const server = new WebpackDevServer(compiler, {
21 | contentBase: __dirname + '/',
22 | compress: true,
23 | port: 8080,
24 | });
25 | server.listen(8080, 'localhost', res);
26 | res(server);
27 | });
28 | await page.goto('http://localhost:8080');
29 |
30 | page.on('console', (msg) => {
31 | for (let i = 0; i < msg.args.length; ++i)
32 | console.log(`${i}: ${msg.args[i]}`);
33 | });
34 | const metas = await page.evaluate(() => {
35 | const contrast = 'source';
36 | const experimental = 'sprite';
37 | const experiments = [
38 | 'simple',
39 | 'bg-image',
40 | 'with-color',
41 | 'with-color-outside',
42 | 'with-color-override',
43 | 'bg-size',
44 | 'bg-size-pixel',
45 | 'bg-size-height',
46 | 'bg-size-cover',
47 | 'bg-size-contain',
48 | 'bg-size-override',
49 | 'bg-position',
50 | 'bg-position-outside',
51 | 'bg-position-override',
52 | 'bg-position-and-size',
53 | 'bg-position-and-size-outside',
54 | 'without-image-set',
55 | 'image-set',
56 | 'image-set-fallback',
57 | 'image-set-and-others'];
58 | console.log(experiments, contrast);
59 | const metas = experiments.map((exp) => {
60 | const contrastSelector = `.${contrast}.${exp}`;
61 | const experimentsSelector = `.${experimental}.${exp}`;
62 | /* eslint-disable no-undef */
63 | const source = document.querySelector(contrastSelector);
64 | const sprite = document.querySelector(experimentsSelector);
65 | console.log(source, sprite);
66 | if (!source || !sprite)
67 | return false;
68 |
69 | return {
70 | name: exp,
71 | source: JSON.stringify(source.getBoundingClientRect()),
72 | sprite: JSON.stringify(sprite.getBoundingClientRect()),
73 | };
74 | }).filter(Boolean);
75 | return metas;
76 | });
77 |
78 | // normalize meta
79 | const experimentsList = metas.map((meta) => ({
80 | name: meta.name,
81 | source: fetchPropFrom(meta.source),
82 | sprite: fetchPropFrom(meta.sprite),
83 | }));
84 |
85 | await page.screenshot({
86 | path: './666.png',
87 | fullPage: true,
88 | });
89 |
90 | const img = await Image.load('./666.png');
91 |
92 | const grey = img.grey();
93 | grey.save('./gray666.png');
94 | const { width } = grey;
95 |
96 | const rslt = experimentsList.map((experiment) => {
97 | const { sprite, source, name } = experiment;
98 | console.log(sprite, source, name);
99 | if (source.height === 0 || sprite.height === 0)
100 | return { code: 999, msg: '元素未渲染', errCSSBlock: name };
101 | if (sprite.height !== source.height || sprite.top !== source.top)
102 | return { code: 606, msg: '页面元素未对应', errCSSBlock: name };
103 | const sourcePart = grey.crop({ x: 0, y: source.top, width: width / 2, height: source.height });
104 | const spritePart = grey.crop({ x: width / 2, y: sprite.top, width: width / 2, height: sprite.height });
105 | const leftPartArray = sourcePart.getPixelsArray();
106 | const rigthPartArray = spritePart.getPixelsArray();
107 | if (leftPartArray.length !== rigthPartArray.length) {
108 | return {
109 | code: -1,
110 | msg: '图片大小不匹配',
111 | errCSSBlock: name,
112 | };
113 | } else {
114 | let len = leftPartArray.length - 1;
115 | while (len-- && Math.abs(leftPartArray[len][0] - rigthPartArray[len][0]) < 10) {}
116 | console.log(
117 | len,
118 | leftPartArray[len],
119 | rigthPartArray[len]);
120 | if (len === -1) {
121 | return {
122 | code: 200,
123 | msg: '校验成功',
124 | CSSBlock: name,
125 | };
126 | } else {
127 | return {
128 | code: 996,
129 | msg: `校验失败 @ ${len}`,
130 | errCSSBlock: name,
131 | };
132 | }
133 | }
134 | });
135 | console.log(rslt);
136 | await new Promise((res) => {
137 | server.close(res);
138 | });
139 | await browser.close();
140 | })();
141 |
--------------------------------------------------------------------------------
/src/computeNewBackground.js:
--------------------------------------------------------------------------------
1 | const { Background, BackgroundPosition, BackgroundSize, Percentage, Length } = require('css-fruit');
2 |
3 | function checkBlockSize(blockSize) {
4 | if (!blockSize.valid)
5 | return false;
6 | if (!blockSize.width || !blockSize.height)
7 | return false;
8 | return (blockSize.width.toString() === '0' || blockSize.width.unit === 'px')
9 | && (blockSize.height.toString() === '0' || blockSize.height.unit === 'px');
10 | }
11 |
12 | function checkBackgroundPosition(position) {
13 | // top right center ...
14 | if (position.x.offset._type === 'length' && position.y.offset._type === 'length')
15 | return true;
16 | if ((position.x.offset.unit === 'px' || position.x.offset.toString() === '0')
17 | && (position.y.offset.unit === 'px' || position.y.offset.toString() === '0'))
18 | return true;
19 | throw new TypeError(`Only support px-unit in 'background-position'`);
20 | }
21 |
22 | /**
23 | * 根据原有块中的背景属性值、块的大小、原有图片本身大小、生成雪碧图的大小、分辨率要求,计算出新背景的各种属性值
24 | * 这个函数很复杂,写哭了。。😭
25 | * @param {Background} oldBackground
26 | * @param {no units} oldBlockSize
27 | * @param {no units} imageDimension
28 | * @param {no units} spriteSize
29 | * @param {number} dppx
30 | */
31 | module.exports = function computeNewBackground(oldBackground, url, oldBlockSize, imageDimension, spriteSize, dppx) {
32 | const background = new Background();
33 | background.color = oldBackground.color;
34 | background.repeat = 'no-repeat';
35 | background.image = `url('${url.replace(/'/g, "\\'")}')`;
36 | // background.clip
37 | // background.origin
38 | // background.attachment
39 | background.valid = true;
40 |
41 | /**
42 | * background-position
43 | * 检查原有的 background-position,没有的话按'0px 0px'计算
44 | * 必须用像素值,否则报错
45 | */
46 | if (oldBackground.position === undefined)
47 | oldBackground.position = new BackgroundPosition('0px 0px');
48 | else
49 | checkBackgroundPosition(oldBackground.position);
50 | background.position = new BackgroundPosition(`0px 0px`);
51 | background.position.x.offset.number = oldBackground.position.x.offset.number - imageDimension.x;
52 | background.position.y.offset.number = oldBackground.position.y.offset.number - imageDimension.y;
53 |
54 | /**
55 | * background-size
56 | * 检查原有的 background-size,没有的话按图片本身大小/分辨率来计算
57 | */
58 | let oldSize = oldBackground.size;
59 | if (String(oldSize) === 'auto')
60 | oldSize = undefined;
61 | if (!oldSize && dppx !== 1)
62 | oldSize = new BackgroundSize(imageDimension.width / dppx + 'px' + ' ' + imageDimension.height / dppx + 'px');
63 |
64 | /**
65 | * blockSize
66 | * 检查原有块的大小,没有或不明确的按图片本身大小/分辨率来计算(这是猜测,可能有偏差)
67 | */
68 | let blockSize = new BackgroundSize(oldBlockSize.width + ' ' + oldBlockSize.height);
69 | if (!checkBlockSize(blockSize))
70 | blockSize = new BackgroundSize(imageDimension.width / dppx + 'px' + ' ' + imageDimension.height / dppx + 'px');
71 |
72 | if (oldSize) { // Don't process 'auto'
73 | const spriteRadio = {
74 | x: 1,
75 | y: 1,
76 | };
77 |
78 | const blockWHRatio = blockSize.width.number / blockSize.height.number;
79 | const imageWHRatio = imageDimension.width / imageDimension.height;
80 |
81 | /**
82 | * cover or contain amounts to ...
83 | *
84 | * | | bWHRatio >= iWHRatio | bWHRatio < iWHRatio |
85 | * | ------- | -------------------- | ------------------- |
86 | * | cover | 100% auto | auto 100% |
87 | * | contain | auto 100% | 100% auto |
88 | */
89 |
90 | if ((oldSize.toString() === 'cover' && blockWHRatio >= imageWHRatio)
91 | || (oldSize.toString() === 'contain' && blockWHRatio < imageWHRatio)) {
92 | oldSize = new BackgroundSize('100% auto');
93 | }
94 | if ((oldSize.toString() === 'cover' && blockWHRatio < imageWHRatio)
95 | || (oldSize.toString() === 'contain' && blockWHRatio >= imageWHRatio)) {
96 | oldSize = new BackgroundSize('auto 100%');
97 | }
98 |
99 | // Handle case of width
100 | if (oldSize.width._type === 'percentage') {
101 | spriteRadio.x = blockSize.width.number * oldSize.width.number * 0.01 / imageDimension.width;
102 | } else if (oldSize.width._type === 'length') {
103 | if (oldSize.width.unit !== 'px')
104 | throw new Error(`Only support px-unit or percentage in 'background-size'`);
105 | spriteRadio.x = oldSize.width.number / imageDimension.width;
106 | }
107 |
108 | // Handle case of height
109 | if (oldSize.height._type === 'percentage') {
110 | spriteRadio.y = blockSize.height.number * oldSize.height.number * 0.01 / imageDimension.height;
111 | } else if (oldSize.height._type === 'length') {
112 | if (oldSize.height.unit !== 'px')
113 | throw new Error(`Only support px-unit or percentage in 'background-size'`);
114 | spriteRadio.y = oldSize.height.number / imageDimension.height;
115 | }
116 |
117 | // Handle case of auto
118 | if (oldSize.width === 'auto')
119 | spriteRadio.x = spriteRadio.y;
120 | else if (oldSize.height === 'auto')
121 | spriteRadio.y = spriteRadio.x;
122 |
123 | background.size = new BackgroundSize(
124 | (spriteSize.width * spriteRadio.x).toFixed(0) + 'px',
125 | (spriteSize.height * spriteRadio.y).toFixed(0) + 'px',
126 | );
127 |
128 | background.position.x.offset.number = oldBackground.position.x.offset.number - (imageDimension.x * spriteRadio.x).toFixed(0);
129 | background.position.y.offset.number = oldBackground.position.y.offset.number - (imageDimension.y * spriteRadio.y).toFixed(0);
130 | }
131 |
132 | return background;
133 | };
134 |
--------------------------------------------------------------------------------
/test/cases/background/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | .part {
6 | display: inline-block;
7 | width: 50%;
8 | }
9 |
10 | .source.simple {
11 | width: 128px;
12 | height: 128px;
13 | background: url('../../fixtures/images/home.png') no-repeat;
14 | }
15 | .sprite.simple {
16 | width: 128px;
17 | height: 128px;
18 | background: url('../../fixtures/images/home.png?sprite') no-repeat;
19 | }
20 |
21 | .source.background-image {
22 | width: 128px;
23 | height: 128px;
24 | background-image: url('../../fixtures/images/home.png');
25 | }
26 | .sprite.background-image {
27 | width: 128px;
28 | height: 128px;
29 | background-image: url('../../fixtures/images/home.png?sprite');
30 | }
31 |
32 | .source.with-color {
33 | width: 128px;
34 | height: 128px;
35 | background: #eee url('../../fixtures/images/home.png');
36 | }
37 | .sprite.with-color {
38 | width: 128px;
39 | height: 128px;
40 | background: #eee url('../../fixtures/images/home.png?sprite');
41 | }
42 |
43 | .source.with-color-outside {
44 | width: 128px;
45 | height: 128px;
46 | background: url('../../fixtures/images/home.png');
47 | background-color: #eee;
48 | }
49 | .sprite.with-color-outside {
50 | width: 128px;
51 | height: 128px;
52 | background: url('../../fixtures/images/home.png?sprite');
53 | background-color: #eee;
54 | }
55 |
56 | .source.with-color-override {
57 | width: 128px;
58 | height: 128px;
59 | background-color: #eee;
60 | background: url('../../fixtures/images/home.png');
61 | }
62 | .sprite.with-color-override {
63 | width: 128px;
64 | height: 128px;
65 | background-color: #eee;
66 | background: url('../../fixtures/images/home.png?sprite');
67 | }
68 |
69 | .source.bg-size {
70 | width: 100px;
71 | height: 100px;
72 | background-image: url('../../fixtures/images/lollipop.png');
73 | background-repeat: no-repeat;
74 | background-size: 100%;
75 | }
76 | .sprite.bg-size {
77 | width: 100px;
78 | height: 100px;
79 | background-image: url('../../fixtures/images/lollipop.png?sprite');
80 | background-repeat: no-repeat;
81 | background-size: 100%;
82 | }
83 |
84 | .source.bg-size-pixel {
85 | width: 100px;
86 | height: 100px;
87 | background-image: url('../../fixtures/images/lollipop.png');
88 | background-repeat: no-repeat;
89 | background-size: 80px;
90 | }
91 | .sprite.bg-size-pixel {
92 | width: 100px;
93 | height: 100px;
94 | background-image: url('../../fixtures/images/lollipop.png?sprite');
95 | background-repeat: no-repeat;
96 | background-size: 80px;
97 | }
98 |
99 | .source.bg-size-height {
100 | width: 100px;
101 | height: 150px;
102 | background-image: url('../../fixtures/images/lollipop.png');
103 | background-repeat: no-repeat;
104 | background-size: auto 120px;
105 | }
106 | .sprite.bg-size-height {
107 | width: 100px;
108 | height: 150px;
109 | background-image: url('../../fixtures/images/lollipop.png?sprite');
110 | background-repeat: no-repeat;
111 | background-size: auto 120px;
112 | }
113 |
114 | .source.bg-size-cover {
115 | width: 100px;
116 | height: 150px;
117 | background-image: url('../../fixtures/images/lollipop.png');
118 | background-repeat: no-repeat;
119 | background-size: cover;
120 | }
121 | .sprite.bg-size-cover {
122 | width: 100px;
123 | height: 150px;
124 | background-image: url('../../fixtures/images/lollipop.png?sprite');
125 | background-repeat: no-repeat;
126 | background-size: cover;
127 | }
128 |
129 | .source.bg-size-contain {
130 | width: 100px;
131 | height: 100px;
132 | background-image: url('../../fixtures/images/lollipop.png');
133 | background-repeat: no-repeat;
134 | background-size: contain;
135 | }
136 | .sprite.bg-size-contain {
137 | width: 100px;
138 | height: 100px;
139 | background-image: url('../../fixtures/images/lollipop.png?sprite');
140 | background-repeat: no-repeat;
141 | background-size: contain;
142 | }
143 |
144 | .source.bg-size-override {
145 | width: 100px;
146 | height: 150px;
147 | background-size: 100%;
148 | background: url('../../fixtures/images/lollipop.png') no-repeat;
149 | }
150 | .sprite.bg-size-override {
151 | width: 100px;
152 | height: 150px;
153 | background-size: 100%;
154 | background: url('../../fixtures/images/lollipop.png?sprite') no-repeat;
155 | }
156 |
157 | .source.bg-position {
158 | width: 128px;
159 | height: 128px;
160 | background: url('../../fixtures/images/tag.png') 30px -20px no-repeat;
161 | }
162 | .sprite.bg-position {
163 | width: 128px;
164 | height: 128px;
165 | background: url('../../fixtures/images/tag.png?sprite') 30px -20px no-repeat;
166 | }
167 |
168 | .source.bg-position-outside {
169 | width: 128px;
170 | height: 128px;
171 | background: url('../../fixtures/images/tag.png') no-repeat;
172 | background-position: 30px -20px;
173 | }
174 | .sprite.bg-position-outside {
175 | width: 128px;
176 | height: 128px;
177 | background: url('../../fixtures/images/tag.png?sprite') no-repeat;
178 | background-position: 30px -20px;
179 | }
180 |
181 | .source.bg-position-override {
182 | width: 128px;
183 | height: 128px;
184 | background-position: 30px -20px;
185 | background: url('../../fixtures/images/tag.png') no-repeat;
186 | }
187 | .sprite.bg-position-override {
188 | width: 128px;
189 | height: 128px;
190 | background-position: 30px -20px;
191 | background: url('../../fixtures/images/tag.png?sprite') no-repeat;
192 | }
193 |
194 | .source.bg-position-and-size {
195 | width: 100px;
196 | height: 150px;
197 | background: url('../../fixtures/images/html.png') 30px 20px no-repeat;
198 | background-size: 100%;
199 | }
200 | .sprite.bg-position-and-size {
201 | width: 100px;
202 | height: 150px;
203 | background: url('../../fixtures/images/html.png?sprite') 30px 20px no-repeat;
204 | background-size: 100%;
205 | }
206 |
207 | .source.bg-position-and-size-outside {
208 | width: 200px;
209 | height: 200px;
210 | background: url('../../fixtures/images/html.png') no-repeat;
211 | background-size: 200px 100px;
212 | background-position: 30px 20px;
213 | }
214 | .sprite.bg-position-and-size-outside {
215 | width: 200px;
216 | height: 200px;
217 | background: url('../../fixtures/images/html.png?sprite') no-repeat;
218 | background-size: 200px 100px;
219 | background-position: 30px 20px;
220 | }
221 |
--------------------------------------------------------------------------------
/test/cases/image-set/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CSS Sprite Loader
6 |
7 |
90 |
91 |
92 |
93 |
Source images display
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
Sprite display
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
--------------------------------------------------------------------------------
/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | # css-sprite-loader
2 |
3 | - [README in English](README.md)
4 |
5 | 这是一款可以自动将 PNG 图片合并成雪碧图的 Webpack loader。
6 |
7 | 
8 |
9 | ## 示例
10 |
11 | 给需要合并的背景图添加 sprite 后缀参数:
12 |
13 | ``` css
14 | .foo {
15 | background: url('../images/gift.png?sprite');
16 | }
17 | ```
18 |
19 | css-sprite-loader 会自动生成雪碧图:
20 |
21 | ``` css
22 | .foo {
23 | background: url(dest/sprite.png?5d40e339682970eb14baf6110a83ddde) -100px 0 no-repeat;
24 | }
25 | ```
26 |
27 | ## 特性
28 |
29 | 与别的类似的雪碧图加载器不同的是:
30 |
31 | - 通过路径参数可以很轻松地决定是否使用雪碧图。
32 | - 全面支持 CSS 的`background`属性,包括`background-position`、`background-size`等。保证处理前后效果一致。例如:
33 |
34 | ``` css
35 | .bg-position-and-size {
36 | width: 100px;
37 | height: 150px;
38 | background: url('../images/html.png?sprite') 30px 20px no-repeat;
39 | background-size: 100%;
40 | }
41 | ```
42 |
43 | 会重新自动计算位置和大小,转换成
44 |
45 | ``` css
46 | .bg-position-and-size {
47 | width: 100px;
48 | height: 150px;
49 | background: url('dest/sprite.png?dc5323f7f35c65a3d6c7f253dcc07bad') -101.25px -111.25px / 231px 231px no-repeat;
50 | }
51 | ```
52 |
53 | > **注意**:
54 | > - 使用`background-position`,必须用像素值,且位置为左上;
55 | > - 使用`background-size`时,推荐在同一个块中指定像素值的`width`和`height`属性,loader 会按这两个值计算。否则会以图片本身的大小计算(这是种猜测,可能会与原始情况有偏差)
56 |
57 | - 提供 retina 和 image-set 两种方式,用于解决高分辨率图片的问题,参见下文[retina](#retina2x-retina3x-retina4x-) 和 [image-set](#image-set)。
58 |
59 | ## 安装
60 |
61 | ``` shell
62 | npm install --save-dev css-sprite-loader
63 | ```
64 |
65 | ## 配置
66 |
67 | 除了在 Webpack 配置中添加 loader,还需要添加 Plugin。
68 |
69 | ``` javascript
70 | const CSSSpritePlugin = require('css-sprite-loader').Plugin;
71 |
72 | module.exports = {
73 | ...
74 | module: {
75 | rules: [{ test: /\.css$/, use: ['style-loader', 'css-loader', 'css-sprite-loader'] }],
76 | },
77 | plugins: [new CSSSpritePlugin()],
78 | };
79 | ```
80 |
81 | ### background 属性的选项
82 |
83 | #### sprite 参数
84 |
85 | 是否将当前的图片打入雪碧图,或指定打入哪个雪碧图。例如:
86 |
87 | ``` css
88 | .foo {
89 | background: url('../images/gift.png?sprite');
90 | }
91 |
92 | .bar {
93 | background: url('../images/light.png?sprite=sprite-nav');
94 | }
95 | ```
96 |
97 | 以上图片将会被打入两个雪碧图中:
98 |
99 | ``` css
100 | .foo {
101 | background: url('dest/sprite.png?fee16babb11468e0724c07bd3cf2f4cf');
102 | }
103 |
104 | .bar {
105 | background: url('dest/sprite-nav.png?56d33b3ab0389c5b349cec93380b7ceb');
106 | }
107 | ```
108 |
109 | #### retina@2x, retina@3x, retina@4x, ...
110 |
111 | 是否支持某种分辨率的 retina 图片。比如你的 images 目录中有以下文件:
112 |
113 | ```
114 | images/
115 | angry-birds.png
116 | angry-birds@2x.png
117 | angry-birds@4x.png
118 | ```
119 |
120 | 那么你的 CSS 可以写成如下格式:
121 |
122 | ``` css
123 | .baz {
124 | width: 128px;
125 | height: 128px;
126 | background: url('../images/retina/angry-birds.png?sprite&retina@2x&retina@4x');
127 | background-size: 100%;
128 | }
129 | ```
130 |
131 | 会转换为
132 |
133 | ``` css
134 | .baz {
135 | width: 128px;
136 | height: 128px;
137 | background: url('dest/sprite.png?369108fb0a164b04ee10def7ed6d4226') -296px 0 / 424px 424px no-repeat;
138 | }
139 |
140 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
141 | .baz {
142 | background: url('dest/sprite@2x.png?51d951f98092152d8fc56bf3380577e3') -148px 0 / 276px 128px no-repeat;
143 | }
144 | }
145 |
146 | @media (-webkit-min-device-pixel-ratio: 4), (min-resolution: 4dppx) {
147 | .baz {
148 | background: url('dest/sprite@4x.png?4a6a7dbace7933efe321b357d4db2fb9') 30px 20px / 213px 102px no-repeat;
149 | }
150 | }
151 | ```
152 |
153 | 你也可以将@2x作为默认分辨率:
154 |
155 | ```
156 | images/
157 | angry-birds@1x.png
158 | angry-birds@2x.png
159 | angry-birds@4x.png
160 | ```
161 |
162 | ``` css
163 | .baz {
164 | width: 128px;
165 | height: 128px;
166 | background: url('../images/retina/angry-birds@2x.png?sprite&retina@1x&retina@4x');
167 | background-size: 100%;
168 | }
169 | ```
170 |
171 | 会转换为
172 |
173 | ``` css
174 | .baz {
175 | width: 128px;
176 | height: 128px;
177 | background: url('dest/sprite.png?369108fb0a164b04ee10def7ed6d4226') 0 0 / 212px 212px no-repeat;
178 | }
179 |
180 | @media (-webkit-max-device-pixel-ratio: 1), (max-resolution: 1dppx) {
181 | .baz {
182 | background: url('dest/sprite@1x.png?e5cf95daa8d2c40e290009620b13fba3') 0 0 / 128px 128px no-repeat;
183 | }
184 | }
185 |
186 | @media (-webkit-min-device-pixel-ratio: 4), (min-resolution: 4dppx) {
187 | .baz {
188 | background: url('dest/sprite@4x.png?4a6a7dbace7933efe321b357d4db2fb9') 30px 20px / 213px 102px no-repeat;
189 | }
190 | }
191 | ```
192 |
193 | > **注意**:
194 | > 这里的`retina@1x`对应的原始图片路径要显式命名为`xxx@1x`,最后会被打入到`sprite@1x`当中。
195 |
196 | #### image-set function
197 |
198 | 也可以用 image-set 函数来设置不同分辨率的 retina 图片。
199 |
200 | image-set 函数这一特性目前处于[Stage 2](https://www.w3.org/TR/css-images-4/#image-set-notation),[浏览器的兼容情况](https://developer.mozilla.org/en-US/docs/Web/CSS/image-set#Browser_compatibility)在这里,但我们用 PostCSS 做了支持。
201 |
202 | > **注意**:
203 | > 要使用这个特性,在 css-sprite-loader 之前不能提前将 image-set 做降级处理。例如之前有`postcss-preset-env`,可以将它的选项设置为:
204 | > ``` js
205 | > {
206 | > features: {
207 | > 'image-set-function': false,
208 | > },
209 | > }
210 | > ```
211 |
212 | 比如你的 images 目录中有以下文件:
213 |
214 | ```
215 | images/
216 | angry-birds.png
217 | angry-birds@2x.png
218 | angry-birds@4x.png
219 | ```
220 |
221 | 那么你的 CSS 可以写成如下格式:
222 |
223 | ``` css
224 | .baz {
225 | width: 128px;
226 | height: 128px;
227 | background: image-set('../images/retina/angry-birds.png?sprite' 1x, '../images/retina/angry-birds@2x.png?sprite' 2x);
228 | background-size: 100%;
229 | }
230 | ```
231 |
232 | 会转换为
233 |
234 | ``` css
235 | .baz {
236 | width: 128px;
237 | height: 128px;
238 | background: url('dest/sprite.png?369108fb0a164b04ee10def7ed6d4226') 0 0 / 212px 212px no-repeat;
239 | }
240 |
241 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
242 | .baz {
243 | background: url('dest/sprite@2x.png?51d951f98092152d8fc56bf3380577e3') -148px 0 / 276px 128px no-repeat;
244 | }
245 | }
246 | ```
247 |
248 | 如果有需要,也可以指定高分辨率图不打包、或指定在不同的分组。
249 |
250 | ``` css
251 | .baz {
252 | width: 128px;
253 | height: 128px;
254 | background: image-set(
255 | '../images/retina/angry-birds.png?sprite' 1x,
256 | '../images/retina/angry-birds@2x.png?sprite-nav' 2x,
257 | '../images/retina/angry-birds@4x.png' 4x,
258 | );
259 | background-size: 100%;
260 | }
261 | ```
262 |
263 | 会转换为
264 |
265 | ``` css
266 | .baz {
267 | width: 128px;
268 | height: 128px;
269 | background: url('dest/sprite.png?e5cf95daa8d2c40e290009620b13fba3') 0 0 / 212px 212px no-repeat;
270 | }
271 |
272 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
273 | .baz {
274 | background: url('dest/sprite-nav@2x.png?369108fb0a164b04ee10def7ed6d4226') -148px 0 / 276px 128px no-repeat;
275 | }
276 | }
277 |
278 | @media (-webkit-min-device-pixel-ratio: 4), (min-resolution: 4dppx) {
279 | .baz {
280 | background: url('dest/angry-birds@4x?4a6a7dbace7933efe321b357d4db2fb9') no-repeat;
281 | }
282 | }
283 | ```
284 |
285 | ### loader 参数
286 |
287 | 暂无。
288 |
289 | ### plugin 参数
290 |
291 | #### defaultName
292 |
293 | 默认雪碧图分组名
294 |
295 | - Type: `string`
296 | - Default: `sprite`
297 |
298 | #### filename
299 |
300 | 用于设置生成文件名的模板,类似于 Webpack 的 output.filename。模板支持以下占位符:
301 |
302 | - `[ext]` 生成资源文件后缀
303 | - `[name]` 分组名
304 | - `[hash]` 生成文件中 svg 文件的 hash 值(默认使用16进制 md5 hash,所有文件使用 svg 的 hash,其他文件的 hash 有时会发生改变)
305 | - `[:hash::]` 生成 hash 的样式
306 | - `hashType` hash 类型,比如:`sha1`, `md5`, `sha256`, `sha512`
307 | - `digestType` 数字进制:`hex`, `base26`, `base32`, `base36`, `base49`, `base52`, `base58`, `base62`, `base64`
308 | - `length` 字符长度
309 |
310 |
311 | - Type: `string`
312 | - Default: `'[name].[ext]?[hash]'`
313 |
314 | #### output
315 |
316 | 生成的图片文件相对于 webpack 的 output 的相对路径。**必须是一个相对路径。**
317 |
318 | - Type: `string`
319 | - Default: `'./'`
320 |
321 | #### publicPath
322 |
323 | 图片在 CSS url 中的路径,与 Webpack 的 publicPath 相同,此选项用于覆盖它。
324 |
325 | - Type: `string`
326 | - Default: `''`
327 |
328 | #### padding
329 |
330 | 雪碧图中小图片之间的间距
331 |
332 | - Type: `number`
333 | - Default: `'sprite'`
334 |
335 | #### filter
336 |
337 | 如何筛选参与合并雪碧图的小图片文件,可选值:`'all'`、`'query'`、`RegExp`
338 |
339 | - `'all'`: 所有被引用的小图片都要被合并
340 | - `'query'`: 只有在路径中添加了`?sprite`后缀参数的小图片才会被合并
341 | - `RegExp`: 根据正则表达式来匹配路径
342 |
343 | - Type: `string`
344 | - Default: `'query'`
345 |
346 | #### queryParam
347 |
348 | 自定义路径中的后缀参数 key,当`filter: 'query'`才生效。
349 |
350 | - Type: `string`
351 | - Default: `'sprite'`
352 |
353 | #### imageSetFallback
354 |
355 | 是否对不需要走雪碧图流程的`image-set`也做降级处理。因为部分浏览器已经支持`-webkit-image-set`,可能不需要做降级处理。
356 |
357 | - Type: `boolean`
358 | - Default: `false`
359 |
360 |
361 | #### plugins
362 |
363 | 处理完雪碧图之后,运行 postcss 的插件列表。这些插件不会处理整个文件,只会处理与雪碧图相同的几行代码。比如使用一些单位转换的插件`require('postcss-px-to-viewport')`。
364 |
365 | - Type: `Array`
366 | - Default: `[]`
367 |
368 | ## 修改日志
369 |
370 | 参见[Releases](https://github.com/vusion/css-sprite-loader/releases)
371 |
372 | ## 贡献指南
373 |
374 | 参见[Contributing Guide](https://github.com/vusion/DOCUMENTATION/issues/4)
375 |
376 | ## 开源协议
377 |
378 | [MIT](LICENSE)
379 |
380 |
--------------------------------------------------------------------------------
/src/postcssPlugin.js:
--------------------------------------------------------------------------------
1 | const postcss = require('postcss');
2 | const { utils } = require('base-css-image-loader');
3 | const meta = require('./meta');
4 | const { default: CSSFruit, Background, URL } = require('css-fruit');
5 |
6 | CSSFruit.config({
7 | forceParsing: {
8 | url: true,
9 | 'image-set': true,
10 | length: true,
11 | percentage: true,
12 | },
13 | });
14 |
15 | function genMediaQuery(resolution, defaultResolution, selector, content) {
16 | const dppx = resolution.slice(0, -1);
17 |
18 | if (resolution > defaultResolution) {
19 | return `
20 | @media (-webkit-min-device-pixel-ratio: ${dppx}), (min-resolution: ${dppx}dppx) {
21 | ${selector} {
22 | ${content}
23 | }
24 | }
25 | `;
26 | } else if (resolution < defaultResolution) {
27 | return `
28 | @media (-webkit-max-device-pixel-ratio: ${dppx}), (max-resolution: ${dppx}dppx) {
29 | ${selector} {
30 | ${content}
31 | }
32 | }
33 | `;
34 | }
35 | }
36 |
37 | module.exports = postcss.plugin('css-sprite-parser', ({ loaderContext }) => (styles, result) => {
38 | const promises = [];
39 | const plugin = loaderContext[meta.PLUGIN_NAME];
40 | const options = plugin.options;
41 | const data = plugin.data;
42 |
43 | let imageSetFallback = options.imageSetFallback;
44 | if (imageSetFallback === true)
45 | imageSetFallback = { preserve: true };
46 |
47 | styles.walkRules((rule) => {
48 | const decls = rule.nodes.filter((node) => node.type === 'decl' && node.prop.startsWith('background'));
49 | if (!decls.length)
50 | return;
51 |
52 | /**
53 | * Core variable 0
54 | */
55 | const oldBackground = CSSFruit.absorb(decls);
56 | if (!oldBackground.valid) {
57 | rule.warn(result, 'Invalid background');
58 | return;
59 | }
60 |
61 | if (!oldBackground.image)
62 | return;
63 |
64 | // For browsers
65 | if (oldBackground.image._type === 'image-set')
66 | oldBackground.image.prefix = '-webkit-';
67 | const oldBackgroundString = oldBackground.toString();
68 |
69 | /**
70 | * Core variable 1
71 | */
72 | const ruleItem = {
73 | id: 'ID' + utils.genMD5(oldBackgroundString),
74 | defaultResolution: '1x',
75 | };
76 |
77 | /**
78 | * Core variable 2
79 | */
80 | const imageSet = [];
81 | let oldResolutions;
82 | if (oldBackground.image._type === 'url') {
83 | imageSet.push({
84 | url: oldBackground.image,
85 | src: undefined,
86 | resolution: undefined,
87 | needSprite: false,
88 | groupName: undefined,
89 | });
90 | } else if (oldBackground.image._type === 'image-set') {
91 | oldResolutions = Object.keys(oldBackground.image.values);
92 | oldResolutions.forEach((resolution) => {
93 | imageSet.push({
94 | url: oldBackground.image.values[resolution],
95 | src: undefined,
96 | resolution,
97 | needSprite: false,
98 | groupName: undefined,
99 | });
100 | });
101 | } else
102 | return; // Other type like linear-gradient
103 |
104 | // Check whether need sprite
105 | const checkWhetherNeedSprite = (url) => {
106 | if (!url.path.endsWith('.png'))
107 | return false;
108 | if (options.filter === 'query')
109 | return !!(url.query && url.query[options.queryParam]);
110 | else if (options.filter instanceof RegExp)
111 | return options.filter.test(url.path);
112 | else if (options.filter === 'all')
113 | return true;
114 | else
115 | throw new TypeError(`Unknow filter value '${options.filter}'`);
116 | };
117 |
118 | let someNeedSprite = false;
119 | imageSet.forEach((image) => {
120 | image.needSprite = checkWhetherNeedSprite(image.url);
121 | if (image.needSprite)
122 | someNeedSprite = image.needSprite;
123 | });
124 |
125 | if (!someNeedSprite && !(oldBackground.image._type === 'image-set' && imageSetFallback))
126 | return;
127 |
128 | // Fill image object, add retina image in imageSet
129 | if (oldBackground.image._type === 'url') {
130 | const image = imageSet[0];
131 | const query = image.url.query;
132 | const baseGroupName = query && typeof query[options.queryParam] === 'string' ? query[options.queryParam] : options.defaultName;
133 | image.src = image.url.path;
134 |
135 | // According to query retina, collect image set
136 | const pathRE = /(^.*?)(?:@(\d+x))?\.png$/;
137 | const paramRE = /^retina@?(\d+x)$/;
138 | const found = image.url.path.match(pathRE);
139 | if (!found)
140 | throw new Error('Error format of filePath');
141 | let [, basePath, defaultResolution] = found;
142 | if (!defaultResolution)
143 | defaultResolution = '1x';
144 | // 路径本身指示默认分辨率
145 | image.groupName = baseGroupName;
146 | image.resolution = ruleItem.defaultResolution = defaultResolution;
147 |
148 | Object.keys(query).forEach((param) => {
149 | // @compat: old version
150 | if (param === 'retina')
151 | param = 'retina@2x';
152 |
153 | const found = param.match(paramRE);
154 | if (!found)
155 | return;
156 |
157 | const resolution = found[1];
158 | const url = new URL(image.url.toString());
159 | url.path = `${basePath}@${resolution}.png`;
160 | imageSet.push({
161 | url,
162 | src: url.path,
163 | resolution,
164 | needSprite: image.needSprite,
165 | groupName: `${baseGroupName}@${resolution}`,
166 | });
167 | });
168 | } else if (oldBackground.image._type === 'image-set') {
169 | imageSet.forEach((image, index) => {
170 | const query = image.url.query;
171 | const baseGroupName = query && typeof query[options.queryParam] === 'string' ? query[options.queryParam] : options.defaultName;
172 | image.src = image.url.path;
173 | image.groupName = `${baseGroupName}@${image.resolution}`;
174 |
175 | if (index === 0) {
176 | // 第一项指示默认分辨率
177 | image.groupName = baseGroupName;
178 | ruleItem.defaultResolution = image.resolution;
179 | }
180 | });
181 | }
182 |
183 | /**
184 | * Core variable 3
185 | */
186 | const blockSize = {
187 | width: undefined,
188 | height: undefined,
189 | };
190 | // Check width & height
191 | rule.walkDecls((decl) => {
192 | if (decl.prop === 'width')
193 | blockSize.width = decl.value;
194 | else if (decl.prop === 'height')
195 | blockSize.height = decl.value;
196 | });
197 |
198 | promises.push(Promise.all(imageSet.map((image) => new Promise((resolve, reject) => {
199 | loaderContext.resolve(loaderContext.context, image.src, (err, result) => err ? reject(err) : resolve(result));
200 | }))).then((filePaths) => {
201 | // Clean decls in source
202 | decls.forEach((decl) => decl.remove());
203 |
204 | const outputs = [];
205 | filePaths.forEach((filePath, index) => {
206 | if (!filePath)
207 | throw new Error(`Cannot resolve file path '${imageSet[index].src}'`);
208 | loaderContext.addDependency(filePath);
209 |
210 | const image = imageSet[index];
211 | const groupItem = {
212 | id: ruleItem.id,
213 | groupName: image.groupName,
214 | filePath,
215 | oldBackground,
216 | blockSize,
217 | resolution: image.resolution,
218 | content: undefined, // new background cached
219 | };
220 |
221 | let content = `${meta.REPLACER_NAME}(${image.groupName}, ${groupItem.id})`;
222 | if (image.needSprite) {
223 | if (!data[image.groupName])
224 | data[image.groupName] = {};
225 | // background 的各种内容没变,id 一定不会变
226 | if (!data[image.groupName][groupItem.id])
227 | data[image.groupName][groupItem.id] = groupItem;
228 | } else {
229 | const background = new Background(oldBackgroundString);
230 | background.image = image.url;
231 | content = background.toString();
232 | }
233 |
234 | if (image.resolution === ruleItem.defaultResolution)
235 | rule.append({ prop: 'background', value: content });
236 | else {
237 | // No problem in async function
238 | outputs.push(genMediaQuery(image.resolution, ruleItem.defaultResolution, rule.selector, `background: ${content};`));
239 | }
240 | });
241 |
242 | if (oldBackground.image._type === 'image-set' && !someNeedSprite && imageSetFallback.preserve)
243 | outputs.push(`
244 | ${rule.selector} {
245 | background: ${oldBackgroundString};
246 | }
247 | `);
248 | if (outputs.length)
249 | rule.after(outputs.join(''));
250 | }));
251 | });
252 |
253 | if (promises.length) {
254 | plugin.shouldGenerate = true;
255 | loaderContext._module[meta.MODULE_MARK] = true;
256 | }
257 |
258 | return Promise.all(promises);
259 | });
260 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # css-sprite-loader
2 |
3 | - [中文说明](README.zh-CN.md)
4 |
5 | Webpack loader for creating PNG sprites.
6 |
7 | [![CircleCI][circleci-img]][circleci-url]
8 | [![NPM Version][npm-img]][npm-url]
9 | [![Dependencies][david-img]][david-url]
10 | [![NPM Download][download-img]][download-url]
11 |
12 | [circleci-img]: https://img.shields.io/circleci/project/github/vusion/css-sprite-loader.svg?style=flat-square
13 | [circleci-url]: https://circleci.com/gh/vusion/css-sprite-loader
14 | [npm-img]: http://img.shields.io/npm/v/css-sprite-loader.svg?style=flat-square
15 | [npm-url]: http://npmjs.org/package/css-sprite-loader
16 | [david-img]: http://img.shields.io/david/vusion/css-sprite-loader.svg?style=flat-square
17 | [david-url]: https://david-dm.org/vusion/css-sprite-loader
18 | [download-img]: https://img.shields.io/npm/dm/css-sprite-loader.svg?style=flat-square
19 | [download-url]: https://npmjs.org/package/css-sprite-loader
20 |
21 | 
22 |
23 | ## Example
24 |
25 | Just add a `?sprite` query after background image url:
26 |
27 | ``` css
28 | .foo {
29 | background: url('../assets/gift.png?sprite');
30 | }
31 | ```
32 |
33 | Then `css-sprite-loader` will generate a sprite image.
34 |
35 | ``` css
36 | .foo {
37 | background: url(/sprite.png?5d40e339682970eb14baf6110a83ddde) no-repeat;
38 | background-position: -100px -0px;
39 | }
40 | ```
41 |
42 | ## Features
43 |
44 | Our loader works in a way different to others:
45 |
46 | - Easy to toggle whether to use sprite or not by specifying path query.
47 | - Fully support css `background` property, includes `background-position`, `background-size` and others. Make sure there are same effect before and after handling. For example:
48 |
49 | ``` css
50 | .bg-position-and-size {
51 | width: 100px;
52 | height: 150px;
53 | background: url('../images/html.png?sprite') 30px 20px no-repeat;
54 | background-size: 100%;
55 | }
56 | ```
57 |
58 | will be newly computed position and size like this:
59 |
60 | ``` css
61 | .bg-position-and-size {
62 | width: 100px;
63 | height: 150px;
64 | background: url('dest/sprite.png?dc5323f7f35c65a3d6c7f253dcc07bad') -101.25px -111.25px / 231px 231px no-repeat;
65 | }
66 | ```
67 |
68 | > **NOTE**
69 | > - When using `background-position`, value must be pixel and position must be left and top.
70 | > - When using `background-size`, it is recommended to specify pixel `width` and `height` properties. New values of background will be computed through them. Otherwise, new values will be computed according to source image size. This may not be consistent with the original display not in some cases.
71 |
72 | - Provide two options `retina` and `image-set` to solve high resolution image problem. See below sections [retina](#retina2x-retina3x-retina4x-) and [image-set](#image-set).
73 |
74 | ## Install
75 |
76 | ``` shell
77 | npm install --save-dev css-sprite-loader
78 | ```
79 |
80 | ## Config
81 |
82 | You need add a loader and a plugin in Webpack config file.
83 |
84 | ``` javascript
85 | const CSSSpritePlugin = require('css-sprite-loader').Plugin;
86 |
87 | module.exports = {
88 | ...
89 | module: {
90 | rules: [{ test: /\.css$/, use: ['style-loader', 'css-loader', 'css-sprite-loader'] }],
91 | },
92 | plugins: [new CSSSpritePlugin()],
93 | };
94 | ```
95 |
96 | ### Options of background property
97 |
98 | #### sprite param
99 |
100 | Whether to pack this image into sprite. Or set which sprite group to pack. For example:
101 |
102 | ``` css
103 | .foo {
104 | background: url('../images/gift.png?sprite');
105 | }
106 |
107 | .bar {
108 | background: url('../images/light.png?sprite=sprite-nav');
109 | }
110 | ```
111 |
112 | images will be packed into two sprites.
113 |
114 | ``` css
115 | .foo {
116 | background: url('dest/sprite.png?fee16babb11468e0724c07bd3cf2f4cf');
117 | }
118 |
119 | .bar {
120 | background: url('dest/sprite-nav.png?56d33b3ab0389c5b349cec93380b7ceb');
121 | }
122 | ```
123 |
124 | #### retina@2x, retina@3x, retina@4x, ...
125 |
126 | Whether to use retina images in some resolution. For example, if you have a directory:
127 |
128 | ```
129 | images/
130 | angry-birds.png
131 | angry-birds@2x.png
132 | angry-birds@4x.png
133 | ```
134 |
135 | Then you can write CSS in this form
136 |
137 | ``` css
138 | .baz {
139 | width: 128px;
140 | height: 128px;
141 | background: url('../../fixtures/images/retina/angry-birds.png?sprite&retina@2x&retina@4x');
142 | background-size: 100%;
143 | }
144 | ```
145 |
146 | They will be converted to
147 |
148 | ``` css
149 | .baz {
150 | width: 128px;
151 | height: 128px;
152 | background: url('dest/sprite.png?369108fb0a164b04ee10def7ed6d4226') -296px 0 / 424px 424px no-repeat;
153 | }
154 |
155 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
156 | .baz {
157 | background: url('dest/sprite@2x.png?51d951f98092152d8fc56bf3380577e3') -148px 0 / 276px 128px no-repeat;
158 | }
159 | }
160 |
161 | @media (-webkit-min-device-pixel-ratio: 4), (min-resolution: 4dppx) {
162 | .baz {
163 | background: url('dest/sprite@4x.png?4a6a7dbace7933efe321b357d4db2fb9') 30px 20px / 213px 102px no-repeat;
164 | }
165 | }
166 | ```
167 |
168 | You can also use @2x as default resolution:
169 |
170 | ```
171 | images/
172 | angry-birds@1x.png
173 | angry-birds@2x.png
174 | angry-birds@4x.png
175 | ```
176 |
177 | ``` css
178 | .baz {
179 | width: 128px;
180 | height: 128px;
181 | background: url('../../fixtures/images/retina/angry-birds@2x.png?sprite&retina@1x&retina@4x');
182 | background-size: 100%;
183 | }
184 | ```
185 |
186 | This will be converted to
187 |
188 | ``` css
189 | .baz {
190 | width: 128px;
191 | height: 128px;
192 | background: url('dest/sprite.png?369108fb0a164b04ee10def7ed6d4226') 0 0 / 212px 212px no-repeat;
193 | }
194 |
195 | @media (-webkit-max-device-pixel-ratio: 1), (max-resolution: 1dppx) {
196 | .baz {
197 | background: url('dest/sprite@1x.png?e5cf95daa8d2c40e290009620b13fba3') 0 0 / 128px 128px no-repeat;
198 | }
199 | }
200 |
201 | @media (-webkit-min-device-pixel-ratio: 4), (min-resolution: 4dppx) {
202 | .baz {
203 | background: url('dest/sprite@4x.png?4a6a7dbace7933efe321b357d4db2fb9') 30px 20px / 213px 102px no-repeat;
204 | }
205 | }
206 | ```
207 |
208 | > **NOTE**
209 | > Here, the original image path corresponding to `retina@1x` should be explicitly named `xxx@1x`, and finally it will be packed into `sprite@1x`.
210 |
211 | #### image-set function
212 |
213 | Image-set function is another way to set different resolution images.
214 |
215 | Image-set function feature is in [Stage 2](https://www.w3.org/TR/css-images-4/#image-set-notation). Here is [Browsers Compatibility](https://developer.mozilla.org/en-US/docs/Web/CSS/image-set#Browser_compatibility). In this loader, it will be processed by PostCSS.
216 |
217 | > **NOTE**
218 | > If you want to use this feature, make sure that `image-set` won't be processed before css-sprite-loader. For example, before this loader, there is a postcss-loader and plugin of it `postcss-preset-env` willing polyfill `image-set`. You can disable it by setting options like:
219 | > ``` js
220 | > {
221 | > features: {
222 | > 'image-set-function': false,
223 | > },
224 | > }
225 | > ```
226 |
227 | For example, if you have a directory:
228 |
229 | ```
230 | images/
231 | angry-birds.png
232 | angry-birds@2x.png
233 | angry-birds@4x.png
234 | ```
235 |
236 | Then you can write CSS in this form
237 |
238 | ``` css
239 | .baz {
240 | width: 128px;
241 | height: 128px;
242 | background: image-set('../images/retina/angry-birds.png?sprite' 1x, '../images/retina/angry-birds@2x.png?sprite' 2x);
243 | background-size: 100%;
244 | }
245 | ```
246 |
247 | This will be converted to
248 |
249 | ``` css
250 | .baz {
251 | width: 128px;
252 | height: 128px;
253 | background: url('dest/sprite.png?369108fb0a164b04ee10def7ed6d4226') 0 0 / 212px 212px no-repeat;
254 | }
255 |
256 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
257 | .baz {
258 | background: url('dest/sprite@2x.png?51d951f98092152d8fc56bf3380577e3') -148px 0 / 276px 128px no-repeat;
259 | }
260 | }
261 | ```
262 |
263 | If required, you can specify an image not to be packed or packed in a different group.
264 |
265 | ``` css
266 | .baz {
267 | width: 128px;
268 | height: 128px;
269 | background: image-set(
270 | '../images/retina/angry-birds.png?sprite' 1x,
271 | '../images/retina/angry-birds@2x.png?sprite-nav' 2x,
272 | '../images/retina/angry-birds@4x.png' 4x,
273 | );
274 | background-size: 100%;
275 | }
276 | ```
277 |
278 | will be converted to
279 |
280 | ``` css
281 | .baz {
282 | width: 128px;
283 | height: 128px;
284 | background: url('dest/sprite.png?e5cf95daa8d2c40e290009620b13fba3') 0 0 / 212px 212px no-repeat;
285 | }
286 |
287 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
288 | .baz {
289 | background: url('dest/sprite-nav@2x.png?369108fb0a164b04ee10def7ed6d4226') -148px 0 / 276px 128px no-repeat;
290 | }
291 | }
292 |
293 | @media (-webkit-min-device-pixel-ratio: 4), (min-resolution: 4dppx) {
294 | .baz {
295 | background: url('dest/angry-birds@4x?4a6a7dbace7933efe321b357d4db2fb9') no-repeat;
296 | }
297 | }
298 | ```
299 |
300 | ### loader options
301 |
302 | None.
303 |
304 | ### plugin options
305 |
306 | #### defaultName
307 |
308 | Default sprite group name.
309 |
310 | - Type: `string`
311 | - Default: `'sprite'`
312 |
313 | #### filename
314 |
315 | Output filename format like output. filename of Webpack. The following tokens will be replaced:
316 |
317 | - `[ext]` the extension of the resource
318 | - `[name]` the group name
319 | - `[hash]` the hash of svg file (Buffer) (by default it's the hex digest of the md5 hash, and all file will use hash of the svg file)
320 | - `[:hash::]` optionally one can configure
321 | - other `hashType`s, i. e. `sha1`, `md5`, `sha256`, `sha512`
322 | - other `digestType`s, i. e. `hex`, `base26`, `base32`, `base36`, `base49`, `base52`, `base58`, `base62`, `base64`
323 | - and `length` the length in chars
324 |
325 |
326 | - Type: `string`
327 | - Default: `'[name].[ext]?[hash]'`
328 |
329 | #### output
330 |
331 | Output path of emitted image files, relative to webpack output path. **Must be a relative path.**
332 |
333 | - Type: `string`
334 | - Default: `'./'`
335 |
336 | #### publicPath
337 |
338 | Image public path in css url, same as webpack output.publicPath. This option is for overriding it.
339 |
340 | - Type: `string`
341 | - Default: `''`
342 |
343 | #### padding
344 |
345 | The padding between small images in sprite.
346 |
347 | - Type: `number`
348 | - Default: `20`
349 |
350 | #### filter
351 |
352 | - Type: `string`
353 | - Default: `'all'`
354 |
355 | Options: `'all'`、`'query'`、`RegExp`
356 |
357 | How to filter source image files for merging:
358 |
359 | - `'all'`: All imported images will be merged.
360 | - `'query'`: Only image path with `?sprite` query param will be merged.
361 | - `RegExp`: Only image path matched by RegExp
362 |
363 | #### queryParam
364 |
365 | Customize key of query param in svg path. Only works when `filter: 'query'`.
366 |
367 | - Type: `string`
368 | - Default: `'sprite'`
369 |
370 | #### imageSetFallback
371 |
372 | Whether to process images without sprite query in `image-set`. They may be no need to polyfill because some browsers already support `-webkit-image-set`.
373 |
374 | - Type: `boolean`
375 | - Default: `false`
376 |
377 | #### plugins
378 |
379 | Postcss plugins will be processed on related codes after creating sprite image. For example, you can use `require('postcss-px-to-viewport')` to convert units of background value.
380 |
381 | - Type: `Array`
382 | - Default: `[]`
383 |
384 | ## Changelog
385 |
386 | See [Releases](https://github.com/vusion/css-sprite-loader/releases)
387 |
388 | ## Contributing
389 |
390 | See [Contributing Guide](https://github.com/vusion/DOCUMENTATION/issues/8)
391 |
392 | ## License
393 |
394 | [MIT](LICENSE)
395 |
--------------------------------------------------------------------------------