├── test ├── sprite-empty.svg ├── paths.css ├── sprite-module │ ├── package.json │ ├── module-sprite-camera.svg │ └── module-sprite.svg ├── sprite-camera.svg ├── sprite.svg ├── paths.expect.css ├── package.css ├── basic.css ├── package.expect.css ├── basic.svgo.expect.css ├── basic.expect.css └── basic.base64.expect.css ├── .travis.yml ├── .gitignore ├── .editorconfig ├── .rollup.js ├── .tape.js ├── src ├── lib │ ├── element-by-id.js │ ├── element-clone.js │ ├── element-as-data-uri-svg.js │ ├── read-closest-svg.md │ ├── transpile-styles.js │ ├── transpile-decl.js │ └── read-closest-svg.js └── index.js ├── CONTRIBUTING.md ├── package.json ├── CHANGELOG.md ├── README.md ├── INSTALL.md └── LICENSE.md /test/sprite-empty.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/paths.css: -------------------------------------------------------------------------------- 1 | test-sprite { 2 | background-image: url("module-sprite-camera.svg"); 3 | } 4 | -------------------------------------------------------------------------------- /test/sprite-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sprite-module", 3 | "version": "1.0.0", 4 | "media": "module-sprite.svg", 5 | "private": true 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://docs.travis-ci.com/user/travis-lint 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - 6 7 | 8 | install: 9 | - npm install --ignore-scripts 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | *.log* 4 | *.result.css 5 | .* 6 | !.editorconfig 7 | !.gitignore 8 | !.rollup.js 9 | !.tape.js 10 | !.travis.yml 11 | /index.* 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [*.{json,md,yml}] 14 | indent_size = 2 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /.rollup.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | 3 | export default { 4 | input: 'src/index.js', 5 | output: [ 6 | { file: 'index.js', format: 'cjs', sourcemap: true }, 7 | { file: 'index.mjs', format: 'esm', sourcemap: true } 8 | ], 9 | plugins: [ 10 | babel({ 11 | presets: [ 12 | ['@babel/env', { modules: false, targets: { node: 6 } }] 13 | ] 14 | }) 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /test/sprite-camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.tape.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'basic': { 3 | message: 'supports basic usage' 4 | }, 5 | 'basic:base64': { 6 | message: 'supports { "utf8": false } usage', 7 | options: { 8 | utf8: false 9 | } 10 | }, 11 | 'basic:svgo': { 12 | message: 'supports { "svgo": true } usage', 13 | options: { 14 | svgo: true 15 | } 16 | }, 17 | 'package': { 18 | message: 'supports package usage' 19 | }, 20 | 'paths': { 21 | message: 'supports { "paths" } usage', 22 | options: { 23 | dirs: 'test/sprite-module' 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /test/sprite-module/module-sprite-camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/sprite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/paths.expect.css: -------------------------------------------------------------------------------- 1 | test-sprite { 2 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' style='fill: black'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/element-by-id.js: -------------------------------------------------------------------------------- 1 | /* Element by ID 2 | /* ========================================================================== */ 3 | 4 | export default function elementById(element, id) { 5 | // conditionally return the matching element 6 | if (element.attr && element.attr.id === id) { 7 | return element; 8 | } else if (element.children) { 9 | // otherwise, return matching child elements 10 | let index = -1; 11 | let child; 12 | 13 | while (child = element.children[++index]) { 14 | child = elementById(child, id); 15 | 16 | if (child) { 17 | return child; 18 | } 19 | } 20 | } 21 | 22 | // return undefined if no matching elements are find 23 | return undefined; 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/element-clone.js: -------------------------------------------------------------------------------- 1 | /* Clone from element 2 | /* ========================================================================== */ 3 | 4 | export default function elementClone(element) { 5 | // element clone 6 | const clone = {}; 7 | 8 | // for each key in the element 9 | for (let key in element) { 10 | if (element[key] instanceof Array) { 11 | // conditionally clone the child array 12 | clone[key] = element[key].map(elementClone); 13 | } else if (typeof element[key] === 'object') { 14 | // otherwise, conditionally clone the child object 15 | clone[key] = elementClone(element[key]); 16 | } else { 17 | // otherwise, copy the child 18 | clone[key] = element[key]; 19 | } 20 | } 21 | 22 | // return the element clone 23 | return clone; 24 | } 25 | -------------------------------------------------------------------------------- /test/package.css: -------------------------------------------------------------------------------- 1 | test-sprite-module-with-fragment { 2 | background-image: url(sprite-module#camera); 3 | } 4 | 5 | test-sprite-module-without-fragment { 6 | background-image: url(sprite-module); 7 | } 8 | 9 | test-sprite-module-with-fragment-and-param { 10 | background-image: url("sprite-module#camera" param(--fill blue)); 11 | } 12 | 13 | test-sprite-module-file-with-param { 14 | background-image: url("sprite-module/module-sprite-camera.svg" param(--fill blue)); 15 | } 16 | 17 | test-sprite-module-file-with-fragment-and-param { 18 | background-image: url("sprite-module/module-sprite.svg#camera" param(--fill blue)); 19 | } 20 | 21 | test-sprite-module-with-invalid-fragment-1 { 22 | background-image: url(sprite-module#none); 23 | } 24 | 25 | test-sprite-module-with-invalid-fragment-2 { 26 | background-image: url(sprite-module#); 27 | } 28 | -------------------------------------------------------------------------------- /test/sprite-module/module-sprite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/basic.css: -------------------------------------------------------------------------------- 1 | test-sprite { 2 | background-image: url("sprite-camera.svg"); 3 | } 4 | 5 | test-sprite-with-id { 6 | background-image: url("sprite.svg#camera"); 7 | } 8 | 9 | test-sprite-with-id-without-quotes { 10 | background-image: url(sprite.svg#camera); 11 | } 12 | 13 | test-sprite-with-param { 14 | background-image: url("sprite-camera.svg" param(--color blue)); 15 | } 16 | 17 | test-sprite-with-hex-param { 18 | background-image: url("sprite-camera.svg" param(--color #00F)); 19 | } 20 | 21 | test-sprite-with-rgb-param { 22 | background-image: url("sprite-camera.svg" param(--color rgb(0, 0, 255))); 23 | } 24 | 25 | test-sprite-with-rgba-param { 26 | background-image: url("sprite-camera.svg" param(--color rgba(0, 0, 255, 0.5))); 27 | } 28 | 29 | test-sprite-with-hsl-param { 30 | background-image: url("sprite-camera.svg" param(--color hsl(240, 100%, 50%))); 31 | } 32 | 33 | test-sprite-with-hsla-param { 34 | background-image: url("sprite-camera.svg" param(--color hsla(240, 100%, 50%, 0.5))); 35 | } 36 | 37 | test-sprite-with-id-with-param { 38 | background-image: url("sprite.svg#camera" param(--color blue)); 39 | } 40 | 41 | test-empty-sprite { 42 | background-image: url("sprite-empty.svg"); 43 | } 44 | 45 | test-empty-sprite-with-id { 46 | background-image: url("sprite-empty.svg#camera"); 47 | } 48 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* Tooling 2 | /* ========================================================================== */ 3 | 4 | // external tooling 5 | import postcss from 'postcss'; 6 | 7 | // internal tooling 8 | import transpileDecl from './lib/transpile-decl'; 9 | 10 | /* Inline SVGs 11 | /* ========================================================================== */ 12 | 13 | export default postcss.plugin('postcss-svg-fragments', opts => (css, result) => { 14 | // svg promises array 15 | const promises = []; 16 | 17 | // plugin options 18 | const normalizedOpts = { 19 | // additional directories to search for SVGs 20 | dirs: 'dirs' in Object(opts) ? [].concat(opts.dirs) : [], 21 | // whether to encode as utf-8 22 | utf8: 'utf8' in Object(opts) ? Boolean(opts.utf8) : true, 23 | // whether and how to compress with svgo 24 | svgo: 'svgo' in Object(opts) ? Object(opts.svgo) : false 25 | }; 26 | 27 | // cache of file content and json content promises 28 | const cache = {}; 29 | 30 | // for each declaration in the stylesheet 31 | css.walkDecls(decl => { 32 | // if the declaration contains a url() 33 | if (containsUrlFunction(decl)) { 34 | // transpile declaration parts 35 | transpileDecl(result, promises, decl, normalizedOpts, cache); 36 | } 37 | }); 38 | 39 | // return chained svg promises array 40 | return Promise.all(promises); 41 | }); 42 | 43 | /* Inline Tooling 44 | /* ========================================================================== */ 45 | 46 | // whether the declaration contains a url() 47 | function containsUrlFunction(decl) { 48 | return /(^|\s)url\(.+\)(\s|$)/.test(decl.value); 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/element-as-data-uri-svg.js: -------------------------------------------------------------------------------- 1 | /* Tooling 2 | /* ========================================================================== */ 3 | 4 | // external tooling 5 | import Svgo from 'svgo'; 6 | 7 | /* Element as a data URI SVG 8 | /* ========================================================================== */ 9 | 10 | export default function elementAsDataURISvg(element, document, opts) { 11 | // rebuild element as 12 | element.name = 'svg'; 13 | 14 | delete element.attr.id; 15 | 16 | element.attr.viewBox = element.attr.viewBox || document.attr.viewBox; 17 | 18 | element.attr.xmlns = 'http://www.w3.org/2000/svg'; 19 | 20 | const xml = element.toString({ 21 | compressed: true 22 | }); 23 | 24 | // promise data URI 25 | return (opts.svgo 26 | ? new Svgo(opts.svgo).optimize(xml) 27 | : Promise.resolve({ data: xml })) 28 | .then(result => `data:image/svg+xml;${opts.utf8 ? `charset=utf-8,${encodeUTF8(result.data)}` : `base64,${Buffer.from(result.data).toString('base64')}`}`); 29 | } 30 | 31 | /* Inline Tooling 32 | /* ========================================================================== */ 33 | 34 | // return a UTF-8-encoded string 35 | function encodeUTF8(string) { 36 | // encode as UTF-8 37 | return encodeURIComponent( 38 | string.replace( 39 | // collapse whitespace 40 | /[\n\r\s\t]+/g, ' ' 41 | ).replace( 42 | // remove comments 43 | /<\!--([\W\w]*(?=-->))-->/g, '' 44 | ).replace( 45 | // pre-encode ampersands 46 | /&/g, '%26' 47 | ) 48 | ).replace( 49 | // escape commas 50 | /'/g, '\\\'' 51 | ).replace( 52 | // un-encode compatible characters 53 | /%20/g, ' ' 54 | ).replace( 55 | /%22/g, '\'' 56 | ).replace( 57 | /%2F/g, '/' 58 | ).replace( 59 | /%3A/g, ':' 60 | ).replace( 61 | /%3D/g, '=' 62 | ).replace( 63 | // encode additional incompatible characters 64 | /\(/g, '%28' 65 | ).replace( 66 | /\)/g, '%29' 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to PostCSS SVG 2 | 3 | You want to help? You rock! Now, take a moment to be sure your contributions 4 | make sense to everyone else. 5 | 6 | ## Reporting Issues 7 | 8 | Found a problem? Want a new feature? 9 | 10 | - See if your issue or idea has [already been reported]. 11 | - Provide a [reduced test case] or a [live example]. 12 | 13 | Remember, a bug is a _demonstrable problem_ caused by _our_ code. 14 | 15 | ## Submitting Pull Requests 16 | 17 | Pull requests are the greatest contributions, so be sure they are focused in 18 | scope and avoid unrelated commits. 19 | 20 | 1. To begin; [fork this project], clone your fork, and add our upstream. 21 | ```bash 22 | # Clone your fork of the repo into the current directory 23 | git clone git@github.com:YOUR_USER/postcss-svg.git 24 | 25 | # Navigate to the newly cloned directory 26 | cd postcss-svg 27 | 28 | # Assign the original repo to a remote called "upstream" 29 | git remote add upstream git@github.com:jonathantneal/postcss-svg.git 30 | 31 | # Install the tools necessary for testing 32 | npm install 33 | ``` 34 | 35 | 2. Create a branch for your feature or fix: 36 | ```bash 37 | # Move into a new branch for your feature 38 | git checkout -b feature/thing 39 | ``` 40 | ```bash 41 | # Move into a new branch for your fix 42 | git checkout -b fix/something 43 | ``` 44 | 45 | 3. If your code follows our practices, then push your feature branch: 46 | ```bash 47 | # Test current code 48 | npm test 49 | ``` 50 | ```bash 51 | # Push the branch for your new feature 52 | git push origin feature/thing 53 | ``` 54 | ```bash 55 | # Or, push the branch for your update 56 | git push origin update/something 57 | ``` 58 | 59 | That’s it! Now [open a pull request] with a clear title and description. 60 | 61 | [already been reported]: issues 62 | [fork this project]: fork 63 | [live example]: https://codepen.io/pen 64 | [open a pull request]: https://help.github.com/articles/using-pull-requests/ 65 | [reduced test case]: https://css-tricks.com/reduced-test-cases/ 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-svg", 3 | "version": "3.0.0", 4 | "description": "Inline SVGs in CSS. Supports SVG Fragments, SVG Parameters.", 5 | "author": "Jonathan Neal ", 6 | "contributors": [ 7 | "Pavel Gorlov ", 8 | "Jonathan Neal " 9 | ], 10 | "license": "CC0-1.0", 11 | "repository": "jonathantneal/postcss-svg", 12 | "homepage": "https://github.com/jonathantneal/postcss-svg#readme", 13 | "bugs": "https://github.com/jonathantneal/postcss-svg/issues", 14 | "main": "index.js", 15 | "module": "index.mjs", 16 | "files": [ 17 | "index.js", 18 | "index.js.map", 19 | "index.mjs", 20 | "index.mjs.map" 21 | ], 22 | "scripts": { 23 | "prepublish": "npm test", 24 | "pretest": "rollup -c .rollup.js --silent", 25 | "test": "npm run test:js && npm run test:tape", 26 | "test:js": "eslint src/*.js --cache --ignore-path .gitignore --quiet", 27 | "test:tape": "postcss-tape" 28 | }, 29 | "engines": { 30 | "node": ">=6.0.0" 31 | }, 32 | "dependencies": { 33 | "postcss": "^7.0.6", 34 | "postcss-values-parser": "^2.0.0", 35 | "svgo": "^1.1.1", 36 | "xmldoc": "^1.1.2" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.1.6", 40 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 41 | "@babel/preset-env": "^7.1.6", 42 | "babel-eslint": "^10.0.1", 43 | "eslint": "^5.9.0", 44 | "eslint-config-dev": "^2.0.0", 45 | "postcss-tape": "^3.0.0", 46 | "pre-commit": "^1.2.2", 47 | "rollup": "^0.67.3", 48 | "rollup-plugin-babel": "^4.0.3" 49 | }, 50 | "eslintConfig": { 51 | "extends": "dev", 52 | "parser": "babel-eslint" 53 | }, 54 | "keywords": [ 55 | "postcss", 56 | "css", 57 | "postcss-plugin", 58 | "svgs", 59 | "scalable", 60 | "vector", 61 | "graphic", 62 | "image", 63 | "img", 64 | "picture", 65 | "pic", 66 | "fragment", 67 | "identifier", 68 | "id", 69 | "hash", 70 | "url", 71 | "inline", 72 | "utf8", 73 | "base64", 74 | "custom", 75 | "properties", 76 | "property" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes to PostCSS SVG 2 | 3 | ### 3.0.0 (November 29, 2018) 4 | 5 | - Support Node 6+ 6 | - Support PostCSS 7+ 7 | 8 | ### 2.4.0 (December 5, 2017) 9 | 10 | - Conditionally watch SVG files for changes 11 | 12 | ### 2.3.0 (November 2, 2017) 13 | 14 | - Update SVGO to 1.x 15 | - Improve SVG file resolution 16 | - Improve SVG file caching 17 | 18 | ### 2.2.0 (October 9, 2017) 19 | 20 | - Added configurable SVGO functionality 21 | 22 | ### 2.1.0 (June 23, 2017) 23 | 24 | - Merge pull request: 25 | - Support rgb/rgba and hsl/hsla colors in parameters 26 | https://github.com/Pavliko/postcss-svg/pull/40 27 | - Remove unused files 28 | - Update dependencies; don’t specify minor, patch requirements 29 | 30 | ### 2.0.0 (May 15, 2017) 31 | 32 | - Refactored plugin: 33 | - Functionality from PostCSS SVG Fragments 34 | 35 | ### 1.0.6 (May 7, 2016) 36 | 37 | - Merge pull request: 38 | - added charset=utf8 and set default image sizes to 100% https://github.com/Pavliko/postcss-svg/pull/32 39 | 40 | ### 1.0.5 (April 19, 2016) 41 | 42 | - Merge pull requests: 43 | - Fix init cache position https://github.com/Pavliko/postcss-svg/pull/30 44 | - Pin dependencies https://github.com/Pavliko/postcss-svg/pull/26 45 | - fix deprecated func https://github.com/Pavliko/postcss-svg/pull/20 46 | - pass strip:false to the template engine (really, this time!) https://github.com/Pavliko/postcss-svg/pull/16 47 | 48 | ### 1.0.4 (November 9, 2015) 49 | 50 | - PostCSS 5 compatibility (https://github.com/Pavliko/postcss-svg/issues/13) 51 | 52 | ### 1.0.3 (November 8, 2015) 53 | 54 | - Fix multiple svg in one decl (https://github.com/Pavliko/postcss-svg/issues/19) 55 | 56 | ### 1.0.2 (November 8, 2015) 57 | 58 | - Update to PostCSS 5 59 | 60 | ### 1.0.1 (July 31, 2015) 61 | 62 | - Fix some issues: 63 | - [Raise a error on unknown file](https://github.com/Pavliko/postcss-svg/issues/7) 64 | - [Absolute paths](https://github.com/Pavliko/postcss-svg/issues/8) 65 | - [SVG names with single quotes do not get replaced](https://github.com/Pavliko/postcss-svg/issues/10) 66 | 67 | ### 1.0.0 (July 10, 2015) 68 | 69 | - Initial release from [postcss-svg](https://github.com/Pavliko/postcss-svg) 70 | -------------------------------------------------------------------------------- /src/lib/read-closest-svg.md: -------------------------------------------------------------------------------- 1 | # SVG Resolve Algorithm 2 | 3 | The following high-level algorithm is used to resolve the location and contents 4 | of an SVG found within `url(id)` from `cwd`: 5 | 6 | 1. if `id` starts with [root](#root), set `cwd` as the [root](#root) 7 | 2. [resolve as a file](#resolve-as-a-file) using `cwd/id` as `file` 8 | 3. otherwise, [resolve as a directory](#resolve-as-a-directory) using `cwd/id` 9 | as `dir` 10 | 4. otherwise, if `id` does not start with [root](#root) or 11 | [relative](#relative), [resolve as a module](#resolve-as-a-module) using 12 | `cwd` and `id` 13 | 5. otherwise, reject as `id not found` 14 | 15 | ## Resolve as a File 16 | 17 | 1. if `file` is a file, resolve the contents of `file` 18 | 2. otherwise, if `file.svg` is a file, resolve the contents of `file.svg` 19 | 20 | ## Resolve as a Directory 21 | 22 | 1. if `dir/package.json` is a file, set `pkg` as the JSON contents of 23 | `dir/package.json` 24 | 1. if `pkg` contains a `media` field 25 | 1. resolve the contents of `dir/pkg.media` 26 | 2. otherwise, if `pkg` contains a `main` field 27 | 1. resolve the contents of `dir/pkg.main` 28 | 3. otherwise, if `id/index.svg` is a file 29 | 1. resolve the contents of `dir/index.svg` 30 | 31 | ## Resolve as a Module 32 | 33 | 1. for each `dir` in [module dirs](#module-dirs) using `cwd`: 34 | 1. [resolve as a file](#resolve-as-a-file) using `dir/id` as `file` 35 | 2. otherwise, [resolve as a directory](#resolve-as-a-directory) using 36 | `dir/id` as `dir` 37 | 38 | --- 39 | 40 | ## Module Dirs 41 | 42 | 1. set `segments` as `cwd` split by the [separator](#separator) 43 | 2. set `count` as the length of `segments` 44 | 3. set `dirs` as an empty list 45 | 4. while `count` is greater than `0`: 46 | 1. if `segments[count]` is not `node_modules` 47 | 1. push a new item to `dirs` as the [separator](#separator)-joined 48 | `segments[0 - count]` and `node_modules` 49 | 2. set `count` as `count` minus `1` 50 | 5. return `dirs` 51 | 52 | --- 53 | 54 | ## Terms 55 | 56 | ### Root 57 | 58 | The filesystem root; like the starting `/` in `/Users/janedoe/path/to/file` 59 | 60 | ### Relative 61 | 62 | A filesystem relative path; like the starting `./` or `../` in `../path/to/file` 63 | 64 | ### Separator 65 | 66 | The symbol used to define hierarchy in a filesystem; like any `/` 67 | -------------------------------------------------------------------------------- /src/lib/transpile-styles.js: -------------------------------------------------------------------------------- 1 | /* Tooling 2 | /* ========================================================================== */ 3 | 4 | // external tooling 5 | import parser from 'postcss-values-parser'; 6 | import postcss from 'postcss'; 7 | 8 | /* Transpile element styles with params 9 | /* ========================================================================== */ 10 | 11 | export default function transpileStyles(element, params) { 12 | if (hasStyleAttr(element)) { 13 | // conditionally update the style attribute 14 | element.attr.style = updatedStyleAttr(element.attr.style, params); 15 | } 16 | 17 | if (element.children) { 18 | // conditionally walk the child elements 19 | let index = -1; 20 | let child; 21 | 22 | while (child = element.children[++index]) { 23 | transpileStyles(child, params); 24 | } 25 | } 26 | } 27 | 28 | /* Inline Tooling 29 | /* ========================================================================== */ 30 | 31 | function hasStyleAttr(element) { 32 | return element.attr && element.attr.style; 33 | } 34 | 35 | function updatedStyleAttr(style, params) { 36 | // parse the style attribute 37 | const styleAST = postcss.parse(style); 38 | 39 | // walk the declarations within the style attribute 40 | styleAST.walkDecls(decl => { 41 | const declAST = parser(decl.value).parse(); 42 | 43 | // update the declaration with all transpiled var()s 44 | declAST.walk(node => { 45 | // conditionally update the var() 46 | if (isVarFuntion(node)) { 47 | transpileVar(node, params); 48 | } 49 | }); 50 | 51 | decl.value = declAST.toString(); 52 | }); 53 | 54 | // return the updated style attribute 55 | return styleAST.toString(); 56 | } 57 | 58 | // whether the node is a var() 59 | function isVarFuntion(node) { 60 | return node.type === 'func' && node.value === 'var' && Object(node.nodes).length && /^--/.test(node.nodes[1].value); 61 | } 62 | 63 | // transpile var() 64 | function transpileVar(node, params) { 65 | // css variable 66 | const cssvar = node.nodes[1].value; 67 | 68 | // css variable backup value 69 | const backup = node.nodes[3]; 70 | 71 | if (cssvar in params) { 72 | // conditionally transpile the css var() function into the matched param 73 | node.replaceWith( 74 | parser.word({ value: params[cssvar] }) 75 | ); 76 | } else if (backup) { 77 | // conditionally transpile the css var() function into the backup value 78 | node.replaceWith(backup); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/package.expect.css: -------------------------------------------------------------------------------- 1 | test-sprite-module-with-fragment { 2 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 512 512' style='fill: black' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 3 | } 4 | 5 | test-sprite-module-without-fragment { 6 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Csymbol id='camera' viewBox='0 0 512 512' style='fill: black'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/symbol%3E%3Csymbol id='pencil' style='fill: black'%3E%3Cpath d='M432 0c44.182 0 80 35.817 80 80 0 18.01-5.955 34.629-16 48l-32 32-112-112 32-32c13.371-10.045 29.989-16 48-16zm-400 368l-32 144 144-32 296-296-112-112-296 296zm325.789-186.211l-224 224-27.578-27.578 224-224 27.578 27.578z'/%3E%3C/symbol%3E%3C/svg%3E"); 7 | } 8 | 9 | test-sprite-module-with-fragment-and-param { 10 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 512 512' style='fill:blue' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 11 | } 12 | 13 | test-sprite-module-file-with-param { 14 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' style='fill: black'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 15 | } 16 | 17 | test-sprite-module-file-with-fragment-and-param { 18 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 512 512' style='fill:blue' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 19 | } 20 | 21 | test-sprite-module-with-invalid-fragment-1 { 22 | background-image: url(sprite-module#none); 23 | } 24 | 25 | test-sprite-module-with-invalid-fragment-2 { 26 | background-image: url(sprite-module#); 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS SVG [PostCSS][postcss] 2 | 3 | [![NPM Version][npm-img]][npm-url] 4 | [![Build Status][cli-img]][cli-url] 5 | [![Support Chat][git-img]][git-url] 6 | 7 | [PostCSS SVG] lets you inline SVGs in CSS. 8 | 9 | ```pcss 10 | .icon--square { 11 | content: url("shared-sprites#square" param(--color blue)); 12 | } 13 | 14 | /* becomes */ 15 | 16 | .icon--square { 17 | content: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect style='fill:blue' width='100%25' height='100%25'/%3E%3C/svg%3E"); 18 | } 19 | ``` 20 | 21 | [SVG Fragments] let you reference elements within an SVG. [SVG Parameters] let 22 | you push compiled CSS variables into your SVGs. 23 | 24 | ```svg 25 | 26 | 27 | 28 | 29 | 30 | ``` 31 | 32 | [Modules] let you reference the `media` or `main` fields from a `package.json`: 33 | 34 | ```json 35 | { 36 | "name": "shared-sprites", 37 | "media": "sprites.svg" 38 | } 39 | ``` 40 | 41 | The location of an SVG is intelligently resolved using the 42 | [SVG Resolve Algorithm]. 43 | 44 | ## Usage 45 | 46 | Add [PostCSS SVG] to your project: 47 | 48 | ```bash 49 | npm install postcss-svg --save-dev 50 | ``` 51 | 52 | Use [PostCSS SVG] to process your CSS: 53 | 54 | ```js 55 | const postcssSVG = require('postcss-svg'); 56 | 57 | postcssSVG.process(YOUR_CSS /*, processOptions, pluginOptions */); 58 | ``` 59 | 60 | Or use it as a [PostCSS] plugin: 61 | 62 | ```js 63 | const postcss = require('postcss'); 64 | const postcssSVG = require('postcss-svg'); 65 | 66 | postcss([ 67 | postcssSVG(/* pluginOptions */) 68 | ]).process(YOUR_CSS /*, processOptions */); 69 | ``` 70 | 71 | [PostCSS SVG] runs in all Node environments, with special instructions for: 72 | 73 | | [Node](INSTALL.md#node) | [PostCSS CLI](INSTALL.md#postcss-cli) | [Webpack](INSTALL.md#webpack) | [Create React App](INSTALL.md#create-react-app) | [Gulp](INSTALL.md#gulp) | [Grunt](INSTALL.md#grunt) | 74 | | --- | --- | --- | --- | --- | --- | 75 | 76 | ## Options 77 | 78 | ### dirs 79 | 80 | The `dirs` option specifies additional directories used to locate SVGs. 81 | 82 | ```js 83 | postcssSVG({ 84 | dirs: ['some-folder', 'another-folder'] /* Just a string will work, too */ 85 | }) 86 | ``` 87 | 88 | The `utf8` option determines whether the SVG is UTF-8 encoded or base64 encoded. 89 | 90 | ```js 91 | postcssSVG({ 92 | utf8: false /* Whether to use utf-8 or base64 encoding. Default is true. */ 93 | }) 94 | ``` 95 | 96 | The `svgo` option determines whether and how [svgo] compression is used. 97 | 98 | ```js 99 | postcssSVG({ 100 | svgo: { plugins: [{ cleanupAttrs: true }] } /* Whether and how to use svgo compression. Default is false. */ 101 | }) 102 | ``` 103 | 104 | [cli-img]: https://img.shields.io/travis/jonathantneal/postcss-svg.svg 105 | [cli-url]: https://travis-ci.org/jonathantneal/postcss-svg 106 | [git-img]: https://img.shields.io/badge/support-chat-blue.svg 107 | [git-url]: https://gitter.im/postcss/postcss 108 | [npm-img]: https://img.shields.io/npm/v/postcss-svg.svg 109 | [npm-url]: https://www.npmjs.com/package/postcss-svg 110 | 111 | [PostCSS]: https://github.com/postcss/postcss 112 | [PostCSS SVG]: https://github.com/jonathantneal/postcss-svg 113 | [Modules]: https://nodejs.org/api/modules.html#modules_modules 114 | [SVG Fragments]: https://css-tricks.com/svg-fragment-identifiers-work/ 115 | [SVG Parameters]: https://tabatkins.github.io/specs/svg-params/ 116 | [SVG Resolve Algorithm]: lib/read-closest-svg.md 117 | [svgo]: https://github.com/svg/svgo 118 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installing PostCSS SVG 2 | 3 | [PostCSS SVG] runs in all Node environments, with special instructions for: 4 | 5 | | [Node](#node) | [PostCSS CLI](#postcss-cli) | [Webpack](#webpack) | [Create React App](#create-react-app) | [Gulp](#gulp) | [Grunt](#grunt) | 6 | | --- | --- | --- | --- | --- | --- | 7 | 8 | ## Node 9 | 10 | Add [PostCSS SVG] to your project: 11 | 12 | ```bash 13 | npm install postcss-svg --save-dev 14 | ``` 15 | 16 | Use [PostCSS SVG] to process your CSS: 17 | 18 | ```js 19 | const postcssSVG = require('postcss-svg'); 20 | 21 | postcssSVG.process(YOUR_CSS /*, processOptions, pluginOptions */); 22 | ``` 23 | 24 | Or use it as a [PostCSS] plugin: 25 | 26 | ```js 27 | const postcss = require('postcss'); 28 | const postcssSVG = require('postcss-svg'); 29 | 30 | postcss([ 31 | postcssSVG(/* pluginOptions */) 32 | ]).process(YOUR_CSS /*, processOptions */); 33 | ``` 34 | 35 | ## PostCSS CLI 36 | 37 | Add [PostCSS CLI] to your project: 38 | 39 | ```bash 40 | npm install postcss-cli --save-dev 41 | ``` 42 | 43 | Use [PostCSS SVG] in your `postcss.config.js` configuration file: 44 | 45 | ```js 46 | const postcssSVG = require('postcss-svg'); 47 | 48 | module.exports = { 49 | plugins: [ 50 | postcssSVG(/* pluginOptions */) 51 | ] 52 | } 53 | ``` 54 | 55 | ## Webpack 56 | 57 | Add [PostCSS Loader] to your project: 58 | 59 | ```bash 60 | npm install postcss-loader --save-dev 61 | ``` 62 | 63 | Use [PostCSS SVG] in your Webpack configuration: 64 | 65 | ```js 66 | const postcssSVG = require('postcss-svg'); 67 | 68 | module.exports = { 69 | module: { 70 | rules: [ 71 | { 72 | test: /\.css$/, 73 | use: [ 74 | 'style-loader', 75 | { loader: 'css-loader', options: { importLoaders: 1 } }, 76 | { loader: 'postcss-loader', options: { 77 | ident: 'postcss', 78 | plugins: () => [ 79 | postcssSVG(/* pluginOptions */) 80 | ] 81 | } } 82 | ] 83 | } 84 | ] 85 | } 86 | } 87 | ``` 88 | 89 | ## Create React App 90 | 91 | Add [React App Rewired] and [React App Rewire PostCSS] to your project: 92 | 93 | ```bash 94 | npm install react-app-rewired react-app-rewire-postcss --save-dev 95 | ``` 96 | 97 | Use [React App Rewire PostCSS] and [PostCSS SVG] in your 98 | `config-overrides.js` file: 99 | 100 | ```js 101 | const reactAppRewirePostcss = require('react-app-rewire-postcss'); 102 | const postcssSVG = require('postcss-svg'); 103 | 104 | module.exports = config => reactAppRewirePostcss(config, { 105 | plugins: () => [ 106 | postcssSVG(/* pluginOptions */) 107 | ] 108 | }); 109 | ``` 110 | 111 | ## Gulp 112 | 113 | Add [Gulp PostCSS] to your project: 114 | 115 | ```bash 116 | npm install gulp-postcss --save-dev 117 | ``` 118 | 119 | Use [PostCSS SVG] in your Gulpfile: 120 | 121 | ```js 122 | const postcss = require('gulp-postcss'); 123 | const postcssSVG = require('postcss-svg'); 124 | 125 | gulp.task('css', () => gulp.src('./src/*.css').pipe( 126 | postcss([ 127 | postcssSVG(/* pluginOptions */) 128 | ]) 129 | ).pipe( 130 | gulp.dest('.') 131 | )); 132 | ``` 133 | 134 | ## Grunt 135 | 136 | Add [Grunt PostCSS] to your project: 137 | 138 | ```bash 139 | npm install grunt-postcss --save-dev 140 | ``` 141 | 142 | Use [PostCSS SVG] in your Gruntfile: 143 | 144 | ```js 145 | const postcssSVG = require('postcss-svg'); 146 | 147 | grunt.loadNpmTasks('grunt-postcss'); 148 | 149 | grunt.initConfig({ 150 | postcss: { 151 | options: { 152 | use: [ 153 | postcssSVG(/* pluginOptions */) 154 | ] 155 | }, 156 | dist: { 157 | src: '*.css' 158 | } 159 | } 160 | }); 161 | ``` 162 | 163 | [Gulp PostCSS]: https://github.com/postcss/gulp-postcss 164 | [Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss 165 | [PostCSS]: https://github.com/postcss/postcss 166 | [PostCSS CLI]: https://github.com/postcss/postcss-cli 167 | [PostCSS Loader]: https://github.com/postcss/postcss-loader 168 | [PostCSS SVG]: https://github.com/jonathantneal/postcss-svg 169 | [React App Rewire PostCSS]: https://github.com/csstools/react-app-rewire-postcss 170 | [React App Rewired]: https://github.com/timarney/react-app-rewired 171 | -------------------------------------------------------------------------------- /src/lib/transpile-decl.js: -------------------------------------------------------------------------------- 1 | /* Tooling 2 | /* ========================================================================== */ 3 | 4 | // native tooling 5 | import path from 'path'; 6 | 7 | // external tooling 8 | import parser from 'postcss-values-parser'; 9 | 10 | // internal tooling 11 | import elementClone from './element-clone'; 12 | import elementById from './element-by-id'; 13 | import elementAsDURISVG from './element-as-data-uri-svg'; 14 | import readClosestSVG from './read-closest-svg'; 15 | import transpileStyles from './transpile-styles'; 16 | 17 | /* Transpile declarations 18 | /* ========================================================================== */ 19 | 20 | export default function transpileDecl(result, promises, decl, opts, cache) { // eslint-disable-line max-params 21 | // path to the current working file and directory by declaration 22 | const declWF = path.resolve(decl.source && decl.source.input && decl.source.input.file ? decl.source.input.file : result.root.source && result.root.source.input && result.root.source.input.file ? result.root.source.input.file : path.join(process.cwd(), 'index.css')); 23 | const declWD = path.dirname(declWF); 24 | 25 | // list of files to watch 26 | const files = {}; 27 | 28 | // walk each node of the declaration 29 | const declAST = parser(decl.value).parse(); 30 | 31 | declAST.walk(node => { 32 | // if the node is a url containing an svg fragment 33 | if (isExternalURLFunction(node)) { 34 | // of url() 35 | const urlNode = node.nodes[1]; 36 | 37 | // split by fragment identifier symbol (#) 38 | const urlParts = urlNode.value.split('#'); 39 | 40 | // src 41 | const src = urlParts[0]; 42 | 43 | // fragment identifier 44 | const id = urlParts.slice(1).join('#'); 45 | 46 | // whether the has a fragment identifier 47 | const hasID = urlParts.length > 1; 48 | 49 | // param()s 50 | const params = paramsFromNodes( 51 | node.nodes.slice(2, -1) 52 | ); 53 | 54 | node.nodes.slice(2, -1).forEach(childNode => { 55 | childNode.remove(); 56 | }); 57 | 58 | promises.push( 59 | readClosestSVG(src, [declWD].concat(opts.dirs), cache).then(svgResult => { 60 | const file = svgResult.file; 61 | const document = svgResult.document; 62 | 63 | // conditionally watch svgs for changes 64 | if (!files[file]) { 65 | files[file] = result.messages.push({ 66 | type: 'dependency', 67 | file, 68 | parent: declWF 69 | }); 70 | } 71 | 72 | // document cache 73 | const ids = document.ids = document.ids || {}; 74 | 75 | // conditionally update the document cache 76 | if (hasID && !ids[id]) { 77 | ids[id] = elementById(document, id); 78 | } 79 | 80 | // element fragment or document 81 | const element = hasID ? ids[id] : document; 82 | 83 | // if the element exists 84 | if (element) { 85 | // clone of the element 86 | const clone = elementClone(element); 87 | 88 | // update the clone styles using the params 89 | transpileStyles(clone, params); 90 | 91 | // promise updated and declaration 92 | return elementAsDURISVG(clone, document, opts).then(xml => { 93 | // update 94 | urlNode.value = xml; 95 | 96 | // conditionally quote 97 | if (opts.utf8) { 98 | urlNode.replaceWith( 99 | parser.string({ 100 | value: urlNode.value, 101 | quoted: true, 102 | raws: Object.assign(urlNode.raws, { quote: '"' }) 103 | }) 104 | ); 105 | } 106 | 107 | // update declaration 108 | decl.value = String(declAST); 109 | }); 110 | } 111 | }).catch(error => { 112 | result.warn(error, node); 113 | }) 114 | ); 115 | } 116 | }); 117 | } 118 | 119 | /* Inline Tooling 120 | /* ========================================================================== */ 121 | 122 | // whether the node if a function() 123 | function isExternalURLFunction(node) { 124 | return node.type === 'func' && node.value === 'url' && Object(node.nodes).length && (/^(?!data:)/).test(node.nodes[1].value); 125 | } 126 | 127 | // params from nodes 128 | function paramsFromNodes(nodes) { 129 | // valid params as an object 130 | const params = {}; 131 | 132 | // for each node 133 | nodes.forEach(node => { 134 | // conditionally add the valid param 135 | if (isFilledParam(node)) { 136 | params[node.nodes[1].value] = String(node.nodes[2]).trim(); 137 | } 138 | }); 139 | 140 | // return valid params as an object 141 | return params; 142 | } 143 | 144 | // whether the node is a filled param() 145 | function isFilledParam(node) { 146 | return node.type === 'func' && node.value === 'param' && node.nodes.length === 4 && node.nodes[1].type === 'word'; 147 | } 148 | -------------------------------------------------------------------------------- /src/lib/read-closest-svg.js: -------------------------------------------------------------------------------- 1 | /* Tooling 2 | /* ========================================================================== */ 3 | 4 | // native tooling 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | 8 | // external tooling 9 | import { XmlDocument } from 'xmldoc'; 10 | 11 | /* Promise the XML tree of the closest svg 12 | /* ========================================================================== */ 13 | 14 | export default function readClosestSVG(id, wds, cache) { 15 | return wds.reduce( 16 | // for each working directory 17 | (promise, wd) => promise.catch(() => { 18 | // set cwd as the current working directory 19 | let cwd = wd; 20 | 21 | // if id starts with root 22 | if (starts_with_root(id)) { 23 | // set cwd as the root 24 | cwd = ''; 25 | } 26 | 27 | // resolve as a file using cwd/id as file 28 | return resolveAsFile(path.join(cwd, id), cache) 29 | // otherwise, resolve as a directory using cwd/id as dir 30 | .catch(() => resolve_as_directory(path.join(cwd, id), cache)) 31 | // otherwise, if id does not start with root or relative 32 | .catch(() => !starts_with_root_or_relative(id) 33 | // resolve as a module using cwd and id 34 | ? resolve_as_module(cwd, id, cache) 35 | : Promise.reject() 36 | ) 37 | // otherwise, reject as id not found 38 | .catch(() => Promise.reject(`${id} not found`)); 39 | }), 40 | Promise.reject() 41 | ).then( 42 | // resolve xml contents 43 | (result) => ({ 44 | file: result.file, 45 | document: new XmlDocument(result.contents) 46 | }) 47 | ); 48 | } 49 | 50 | function resolveAsFile(file, cache) { 51 | // if file is a file, resolve the contents of file 52 | return file_contents(file, cache) 53 | // otherwise, if file.svg is a file, resolve the contents of file.svg 54 | .catch(() => file_contents(`${file}.svg`, cache)); 55 | } 56 | 57 | function resolve_as_directory(dir, cache) { 58 | // if dir/package.json is a file, set pkg as the JSON contents of dir/package.json 59 | return json_contents(dir, cache).then( 60 | // if pkg contains a media field 61 | pkg => 'media' in pkg 62 | // resolve the contents of dir/pkg.media 63 | ? file_contents(path.join(dir, pkg.media), cache) 64 | // otherwise, if pkg contains a main field 65 | : 'main' in pkg 66 | // resolve the contents of dir/pkg.main 67 | ? file_contents(path.join(dir, pkg.main), cache) 68 | // otherwise, if dir/index.svg is a file, resolve the contents of dir/index.svg 69 | : file_contents(path.join(dir, 'index.svg'), cache) 70 | ); 71 | } 72 | 73 | function resolve_as_module(cwd, id, cache) { 74 | return node_modules_dirs(cwd).reduce( 75 | // for each dir in module dirs using cwd: 76 | (promise, dir) => promise.catch( 77 | // resolve as a file using dir/id as file 78 | () => resolveAsFile(path.join(dir, id), cache) 79 | // otherwise, resolve as a directory using dir/id as dir 80 | .catch(() => resolve_as_directory(path.join(dir, id), cache)) 81 | ), 82 | Promise.reject() 83 | ); 84 | } 85 | 86 | function node_modules_dirs(cwd) { 87 | // set segments as cwd split by the separator 88 | const segments = cwd.split(path.sep); 89 | 90 | // set count as the length of segments 91 | let count = segments.length; 92 | 93 | // set dirs as an empty array 94 | const dirs = []; 95 | 96 | // while count is greater than 0: 97 | while (count > 0) { 98 | // if segments[count] is not node_modules 99 | if (segments[count] !== 'node_modules') { 100 | // push a new item to dirs as the separator-joined segments[0 - count] and node_modules 101 | dirs.push( 102 | path.join(segments.slice(0, count).join('/') || '/', 'node_modules') 103 | ); 104 | } 105 | 106 | // set count as count minus 1 107 | --count; 108 | } 109 | 110 | return dirs; 111 | } 112 | 113 | function file_contents(file, cache) { 114 | // if file is a file, resolve the contents of file 115 | cache[file] = cache[file] || new Promise( 116 | (resolvePromise, rejectPromise) => fs.readFile( 117 | file, 118 | 'utf8', 119 | (error, contents) => error ? rejectPromise(error) : resolvePromise({ 120 | file, 121 | contents 122 | }) 123 | ) 124 | ); 125 | 126 | return cache[file]; 127 | } 128 | 129 | function json_contents(dir, cache) { 130 | // path of dir/package.json 131 | const pkg = path.join(dir, 'package.json'); 132 | 133 | // resolve the JSON contents of dir/package.json 134 | cache[pkg] = cache[pkg] || new Promise( 135 | (resolvePromise, rejectPromise) => fs.readFile( 136 | pkg, 137 | 'utf8', 138 | (error, contents) => error ? rejectPromise(error) : resolvePromise(JSON.parse(contents)) 139 | ) 140 | ); 141 | 142 | return cache[pkg]; 143 | } 144 | 145 | function starts_with_root(id) { 146 | return /^\//.test(id); 147 | } 148 | 149 | function starts_with_root_or_relative(id) { 150 | return /^\.{0,2}\//.test(id); 151 | } 152 | -------------------------------------------------------------------------------- /test/basic.svgo.expect.css: -------------------------------------------------------------------------------- 1 | test-sprite { 2 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176H368c-8-32-16-64-48-64H192c-32 0-40 32-48 64H32c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32V160c0-17.6-14.4-32-32-32zM256 446c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 3 | } 4 | 5 | test-sprite-with-id { 6 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176H368c-8-32-16-64-48-64H192c-32 0-40 32-48 64H32c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32V160c0-17.6-14.4-32-32-32zM256 446c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 7 | } 8 | 9 | test-sprite-with-id-without-quotes { 10 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176H368c-8-32-16-64-48-64H192c-32 0-40 32-48 64H32c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32V160c0-17.6-14.4-32-32-32zM256 446c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 11 | } 12 | 13 | test-sprite-with-param { 14 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' fill='%2300f'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176H368c-8-32-16-64-48-64H192c-32 0-40 32-48 64H32c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32V160c0-17.6-14.4-32-32-32zM256 446c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 15 | } 16 | 17 | test-sprite-with-hex-param { 18 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' fill='%2300f'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176H368c-8-32-16-64-48-64H192c-32 0-40 32-48 64H32c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32V160c0-17.6-14.4-32-32-32zM256 446c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 19 | } 20 | 21 | test-sprite-with-rgb-param { 22 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' fill='%2300f'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176H368c-8-32-16-64-48-64H192c-32 0-40 32-48 64H32c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32V160c0-17.6-14.4-32-32-32zM256 446c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 23 | } 24 | 25 | test-sprite-with-rgba-param { 26 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' fill='rgba%280%2C0%2C255%2C.5%29'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176H368c-8-32-16-64-48-64H192c-32 0-40 32-48 64H32c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32V160c0-17.6-14.4-32-32-32zM256 446c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 27 | } 28 | 29 | test-sprite-with-hsl-param { 30 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' fill='%2300f'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176H368c-8-32-16-64-48-64H192c-32 0-40 32-48 64H32c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32V160c0-17.6-14.4-32-32-32zM256 446c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 31 | } 32 | 33 | test-sprite-with-hsla-param { 34 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' fill='rgba%280%2C0%2C255%2C.5%29'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176H368c-8-32-16-64-48-64H192c-32 0-40 32-48 64H32c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32V160c0-17.6-14.4-32-32-32zM256 446c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 35 | } 36 | 37 | test-sprite-with-id-with-param { 38 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg' fill='%2300f'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176H368c-8-32-16-64-48-64H192c-32 0-40 32-48 64H32c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32V160c0-17.6-14.4-32-32-32zM256 446c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 39 | } 40 | 41 | test-empty-sprite { 42 | background-image: url("sprite-empty.svg"); 43 | } 44 | 45 | test-empty-sprite-with-id { 46 | background-image: url("sprite-empty.svg#camera"); 47 | } 48 | -------------------------------------------------------------------------------- /test/basic.expect.css: -------------------------------------------------------------------------------- 1 | test-sprite { 2 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' style='fill: black'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 3 | } 4 | 5 | test-sprite-with-id { 6 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg style='fill: black' viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 7 | } 8 | 9 | test-sprite-with-id-without-quotes { 10 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg style='fill: black' viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 11 | } 12 | 13 | test-sprite-with-param { 14 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' style='fill: blue'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 15 | } 16 | 17 | test-sprite-with-hex-param { 18 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' style='fill: %2300F'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 19 | } 20 | 21 | test-sprite-with-rgb-param { 22 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' style='fill: rgb%280%2C 0%2C 255%29'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 23 | } 24 | 25 | test-sprite-with-rgba-param { 26 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' style='fill: rgba%280%2C 0%2C 255%2C 0.5%29'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 27 | } 28 | 29 | test-sprite-with-hsl-param { 30 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' style='fill: hsl%28240%2C 100%25%2C 50%25%29'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 31 | } 32 | 33 | test-sprite-with-hsla-param { 34 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' style='fill: hsla%28240%2C 100%25%2C 50%25%2C 0.5%29'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 35 | } 36 | 37 | test-sprite-with-id-with-param { 38 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg style='fill: blue' viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zm328-176h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zm-224 318c-78.425 0-142-63.574-142-142 0-78.425 63.575-142 142-142 78.426 0 142 63.575 142 142 0 78.426-63.573 142-142 142zm224-222h-64v-32h64v32z'/%3E%3C/svg%3E"); 39 | } 40 | 41 | test-empty-sprite { 42 | background-image: url("sprite-empty.svg"); 43 | } 44 | 45 | test-empty-sprite-with-id { 46 | background-image: url("sprite-empty.svg#camera"); 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # CC0 1.0 Universal 2 | 3 | ## Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an “owner”) of an original work of 8 | authorship and/or a database (each, a “Work”). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific works 12 | (“Commons”) that the public can reliably and without fear of later claims of 13 | infringement build upon, modify, incorporate in other works, reuse and 14 | redistribute as freely as possible in any form whatsoever and for any purposes, 15 | including without limitation commercial purposes. These owners may contribute 16 | to the Commons to promote the ideal of a free culture and the further 17 | production of creative, cultural and scientific works, or to gain reputation or 18 | greater distribution for their Work in part through the use and efforts of 19 | others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation of 22 | additional consideration or compensation, the person associating CC0 with a 23 | Work (the “Affirmer”), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and 25 | publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights (“Copyright and 31 | Related Rights”). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 1. the right to reproduce, adapt, distribute, perform, display, communicate, 34 | and translate a Work; 35 | 2. moral rights retained by the original author(s) and/or performer(s); 36 | 3. publicity and privacy rights pertaining to a person’s image or likeness 37 | depicted in a Work; 38 | 4. rights protecting against unfair competition in regards to a Work, 39 | subject to the limitations in paragraph 4(i), below; 40 | 5. rights protecting the extraction, dissemination, use and reuse of data in 41 | a Work; 42 | 6. database rights (such as those arising under Directive 96/9/EC of the 43 | European Parliament and of the Council of 11 March 1996 on the legal 44 | protection of databases, and under any national implementation thereof, 45 | including any amended or successor version of such directive); and 46 | 7. other similar, equivalent or corresponding rights throughout the world 47 | based on applicable law or treaty, and any national implementations 48 | thereof. 49 | 50 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 51 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 52 | unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright 53 | and Related Rights and associated claims and causes of action, whether now 54 | known or unknown (including existing as well as future claims and causes of 55 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 56 | duration provided by applicable law or treaty (including future time 57 | extensions), (iii) in any current or future medium and for any number of 58 | copies, and (iv) for any purpose whatsoever, including without limitation 59 | commercial, advertising or promotional purposes (the “Waiver”). Affirmer 60 | makes the Waiver for the benefit of each member of the public at large and 61 | to the detriment of Affirmer’s heirs and successors, fully intending that 62 | such Waiver shall not be subject to revocation, rescission, cancellation, 63 | termination, or any other legal or equitable action to disrupt the quiet 64 | enjoyment of the Work by the public as contemplated by Affirmer’s express 65 | Statement of Purpose. 66 | 67 | 3. Public License Fallback. Should any part of the Waiver for any reason be 68 | judged legally invalid or ineffective under applicable law, then the Waiver 69 | shall be preserved to the maximum extent permitted taking into account 70 | Affirmer’s express Statement of Purpose. In addition, to the extent the 71 | Waiver is so judged Affirmer hereby grants to each affected person a 72 | royalty-free, non transferable, non sublicensable, non exclusive, 73 | irrevocable and unconditional license to exercise Affirmer’s Copyright and 74 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 75 | maximum duration provided by applicable law or treaty (including future time 76 | extensions), (iii) in any current or future medium and for any number of 77 | copies, and (iv) for any purpose whatsoever, including without limitation 78 | commercial, advertising or promotional purposes (the “License”). The License 79 | shall be deemed effective as of the date CC0 was applied by Affirmer to the 80 | Work. Should any part of the License for any reason be judged legally 81 | invalid or ineffective under applicable law, such partial invalidity or 82 | ineffectiveness shall not invalidate the remainder of the License, and in 83 | such case Affirmer hereby affirms that he or she will not (i) exercise any 84 | of his or her remaining Copyright and Related Rights in the Work or (ii) 85 | assert any associated claims and causes of action with respect to the Work, 86 | in either case contrary to Affirmer’s express Statement of Purpose. 87 | 88 | 4. Limitations and Disclaimers. 89 | 1. No trademark or patent rights held by Affirmer are waived, abandoned, 90 | surrendered, licensed or otherwise affected by this document. 91 | 2. Affirmer offers the Work as-is and makes no representations or warranties 92 | of any kind concerning the Work, express, implied, statutory or 93 | otherwise, including without limitation warranties of title, 94 | merchantability, fitness for a particular purpose, non infringement, or 95 | the absence of latent or other defects, accuracy, or the present or 96 | absence of errors, whether or not discoverable, all to the greatest 97 | extent permissible under applicable law. 98 | 3. Affirmer disclaims responsibility for clearing rights of other persons 99 | that may apply to the Work or any use thereof, including without 100 | limitation any person’s Copyright and Related Rights in the Work. 101 | Further, Affirmer disclaims responsibility for obtaining any necessary 102 | consents, permissions or other rights required for any use of the Work. 103 | 4. Affirmer understands and acknowledges that Creative Commons is not a 104 | party to this document and has no duty or obligation with respect to this 105 | CC0 or use of the Work. 106 | 107 | For more information, please see 108 | http://creativecommons.org/publicdomain/zero/1.0/. 109 | -------------------------------------------------------------------------------- /test/basic.base64.expect.css: -------------------------------------------------------------------------------- 1 | test-sprite { 2 | background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZmlsbDogIGJsYWNrIj48cGF0aCBkPSJNMTUyIDMwNGMwIDU3LjQzOCA0Ni41NjIgMTA0IDEwNCAxMDRzMTA0LTQ2LjU2MiAxMDQtMTA0LTQ2LjU2Mi0xMDQtMTA0LTEwNC0xMDQgNDYuNTYyLTEwNCAxMDR6bTMyOC0xNzZoLTExMmMtOC0zMi0xNi02NC00OC02NGgtMTI4Yy0zMiAwLTQwIDMyLTQ4IDY0aC0xMTJjLTE3LjYgMC0zMiAxNC40LTMyIDMydjI4OGMwIDE3LjYgMTQuNCAzMiAzMiAzMmg0NDhjMTcuNiAwIDMyLTE0LjQgMzItMzJ2LTI4OGMwLTE3LjYtMTQuNC0zMi0zMi0zMnptLTIyNCAzMThjLTc4LjQyNSAwLTE0Mi02My41NzQtMTQyLTE0MiAwLTc4LjQyNSA2My41NzUtMTQyIDE0Mi0xNDIgNzguNDI2IDAgMTQyIDYzLjU3NSAxNDIgMTQyIDAgNzguNDI2LTYzLjU3MyAxNDItMTQyIDE0MnptMjI0LTIyMmgtNjR2LTMyaDY0djMyeiIvPjwvc3ZnPg=="); 3 | } 4 | 5 | test-sprite-with-id { 6 | background-image: url("data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZmlsbDogIGJsYWNrIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTUyIDMwNGMwIDU3LjQzOCA0Ni41NjIgMTA0IDEwNCAxMDRzMTA0LTQ2LjU2MiAxMDQtMTA0LTQ2LjU2Mi0xMDQtMTA0LTEwNC0xMDQgNDYuNTYyLTEwNCAxMDR6bTMyOC0xNzZoLTExMmMtOC0zMi0xNi02NC00OC02NGgtMTI4Yy0zMiAwLTQwIDMyLTQ4IDY0aC0xMTJjLTE3LjYgMC0zMiAxNC40LTMyIDMydjI4OGMwIDE3LjYgMTQuNCAzMiAzMiAzMmg0NDhjMTcuNiAwIDMyLTE0LjQgMzItMzJ2LTI4OGMwLTE3LjYtMTQuNC0zMi0zMi0zMnptLTIyNCAzMThjLTc4LjQyNSAwLTE0Mi02My41NzQtMTQyLTE0MiAwLTc4LjQyNSA2My41NzUtMTQyIDE0Mi0xNDIgNzguNDI2IDAgMTQyIDYzLjU3NSAxNDIgMTQyIDAgNzguNDI2LTYzLjU3MyAxNDItMTQyIDE0MnptMjI0LTIyMmgtNjR2LTMyaDY0djMyeiIvPjwvc3ZnPg=="); 7 | } 8 | 9 | test-sprite-with-id-without-quotes { 10 | background-image: url(data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZmlsbDogIGJsYWNrIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTUyIDMwNGMwIDU3LjQzOCA0Ni41NjIgMTA0IDEwNCAxMDRzMTA0LTQ2LjU2MiAxMDQtMTA0LTQ2LjU2Mi0xMDQtMTA0LTEwNC0xMDQgNDYuNTYyLTEwNCAxMDR6bTMyOC0xNzZoLTExMmMtOC0zMi0xNi02NC00OC02NGgtMTI4Yy0zMiAwLTQwIDMyLTQ4IDY0aC0xMTJjLTE3LjYgMC0zMiAxNC40LTMyIDMydjI4OGMwIDE3LjYgMTQuNCAzMiAzMiAzMmg0NDhjMTcuNiAwIDMyLTE0LjQgMzItMzJ2LTI4OGMwLTE3LjYtMTQuNC0zMi0zMi0zMnptLTIyNCAzMThjLTc4LjQyNSAwLTE0Mi02My41NzQtMTQyLTE0MiAwLTc4LjQyNSA2My41NzUtMTQyIDE0Mi0xNDIgNzguNDI2IDAgMTQyIDYzLjU3NSAxNDIgMTQyIDAgNzguNDI2LTYzLjU3MyAxNDItMTQyIDE0MnptMjI0LTIyMmgtNjR2LTMyaDY0djMyeiIvPjwvc3ZnPg==); 11 | } 12 | 13 | test-sprite-with-param { 14 | background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZmlsbDogYmx1ZSI+PHBhdGggZD0iTTE1MiAzMDRjMCA1Ny40MzggNDYuNTYyIDEwNCAxMDQgMTA0czEwNC00Ni41NjIgMTA0LTEwNC00Ni41NjItMTA0LTEwNC0xMDQtMTA0IDQ2LjU2Mi0xMDQgMTA0em0zMjgtMTc2aC0xMTJjLTgtMzItMTYtNjQtNDgtNjRoLTEyOGMtMzIgMC00MCAzMi00OCA2NGgtMTEyYy0xNy42IDAtMzIgMTQuNC0zMiAzMnYyODhjMCAxNy42IDE0LjQgMzIgMzIgMzJoNDQ4YzE3LjYgMCAzMi0xNC40IDMyLTMydi0yODhjMC0xNy42LTE0LjQtMzItMzItMzJ6bS0yMjQgMzE4Yy03OC40MjUgMC0xNDItNjMuNTc0LTE0Mi0xNDIgMC03OC40MjUgNjMuNTc1LTE0MiAxNDItMTQyIDc4LjQyNiAwIDE0MiA2My41NzUgMTQyIDE0MiAwIDc4LjQyNi02My41NzMgMTQyLTE0MiAxNDJ6bTIyNC0yMjJoLTY0di0zMmg2NHYzMnoiLz48L3N2Zz4="); 15 | } 16 | 17 | test-sprite-with-hex-param { 18 | background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZmlsbDogIzAwRiI+PHBhdGggZD0iTTE1MiAzMDRjMCA1Ny40MzggNDYuNTYyIDEwNCAxMDQgMTA0czEwNC00Ni41NjIgMTA0LTEwNC00Ni41NjItMTA0LTEwNC0xMDQtMTA0IDQ2LjU2Mi0xMDQgMTA0em0zMjgtMTc2aC0xMTJjLTgtMzItMTYtNjQtNDgtNjRoLTEyOGMtMzIgMC00MCAzMi00OCA2NGgtMTEyYy0xNy42IDAtMzIgMTQuNC0zMiAzMnYyODhjMCAxNy42IDE0LjQgMzIgMzIgMzJoNDQ4YzE3LjYgMCAzMi0xNC40IDMyLTMydi0yODhjMC0xNy42LTE0LjQtMzItMzItMzJ6bS0yMjQgMzE4Yy03OC40MjUgMC0xNDItNjMuNTc0LTE0Mi0xNDIgMC03OC40MjUgNjMuNTc1LTE0MiAxNDItMTQyIDc4LjQyNiAwIDE0MiA2My41NzUgMTQyIDE0MiAwIDc4LjQyNi02My41NzMgMTQyLTE0MiAxNDJ6bTIyNC0yMjJoLTY0di0zMmg2NHYzMnoiLz48L3N2Zz4="); 19 | } 20 | 21 | test-sprite-with-rgb-param { 22 | background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZmlsbDogcmdiKDAsIDAsIDI1NSkiPjxwYXRoIGQ9Ik0xNTIgMzA0YzAgNTcuNDM4IDQ2LjU2MiAxMDQgMTA0IDEwNHMxMDQtNDYuNTYyIDEwNC0xMDQtNDYuNTYyLTEwNC0xMDQtMTA0LTEwNCA0Ni41NjItMTA0IDEwNHptMzI4LTE3NmgtMTEyYy04LTMyLTE2LTY0LTQ4LTY0aC0xMjhjLTMyIDAtNDAgMzItNDggNjRoLTExMmMtMTcuNiAwLTMyIDE0LjQtMzIgMzJ2Mjg4YzAgMTcuNiAxNC40IDMyIDMyIDMyaDQ0OGMxNy42IDAgMzItMTQuNCAzMi0zMnYtMjg4YzAtMTcuNi0xNC40LTMyLTMyLTMyem0tMjI0IDMxOGMtNzguNDI1IDAtMTQyLTYzLjU3NC0xNDItMTQyIDAtNzguNDI1IDYzLjU3NS0xNDIgMTQyLTE0MiA3OC40MjYgMCAxNDIgNjMuNTc1IDE0MiAxNDIgMCA3OC40MjYtNjMuNTczIDE0Mi0xNDIgMTQyem0yMjQtMjIyaC02NHYtMzJoNjR2MzJ6Ii8+PC9zdmc+"); 23 | } 24 | 25 | test-sprite-with-rgba-param { 26 | background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZmlsbDogcmdiYSgwLCAwLCAyNTUsIDAuNSkiPjxwYXRoIGQ9Ik0xNTIgMzA0YzAgNTcuNDM4IDQ2LjU2MiAxMDQgMTA0IDEwNHMxMDQtNDYuNTYyIDEwNC0xMDQtNDYuNTYyLTEwNC0xMDQtMTA0LTEwNCA0Ni41NjItMTA0IDEwNHptMzI4LTE3NmgtMTEyYy04LTMyLTE2LTY0LTQ4LTY0aC0xMjhjLTMyIDAtNDAgMzItNDggNjRoLTExMmMtMTcuNiAwLTMyIDE0LjQtMzIgMzJ2Mjg4YzAgMTcuNiAxNC40IDMyIDMyIDMyaDQ0OGMxNy42IDAgMzItMTQuNCAzMi0zMnYtMjg4YzAtMTcuNi0xNC40LTMyLTMyLTMyem0tMjI0IDMxOGMtNzguNDI1IDAtMTQyLTYzLjU3NC0xNDItMTQyIDAtNzguNDI1IDYzLjU3NS0xNDIgMTQyLTE0MiA3OC40MjYgMCAxNDIgNjMuNTc1IDE0MiAxNDIgMCA3OC40MjYtNjMuNTczIDE0Mi0xNDIgMTQyem0yMjQtMjIyaC02NHYtMzJoNjR2MzJ6Ii8+PC9zdmc+"); 27 | } 28 | 29 | test-sprite-with-hsl-param { 30 | background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZmlsbDogaHNsKDI0MCwgMTAwJSwgNTAlKSI+PHBhdGggZD0iTTE1MiAzMDRjMCA1Ny40MzggNDYuNTYyIDEwNCAxMDQgMTA0czEwNC00Ni41NjIgMTA0LTEwNC00Ni41NjItMTA0LTEwNC0xMDQtMTA0IDQ2LjU2Mi0xMDQgMTA0em0zMjgtMTc2aC0xMTJjLTgtMzItMTYtNjQtNDgtNjRoLTEyOGMtMzIgMC00MCAzMi00OCA2NGgtMTEyYy0xNy42IDAtMzIgMTQuNC0zMiAzMnYyODhjMCAxNy42IDE0LjQgMzIgMzIgMzJoNDQ4YzE3LjYgMCAzMi0xNC40IDMyLTMydi0yODhjMC0xNy42LTE0LjQtMzItMzItMzJ6bS0yMjQgMzE4Yy03OC40MjUgMC0xNDItNjMuNTc0LTE0Mi0xNDIgMC03OC40MjUgNjMuNTc1LTE0MiAxNDItMTQyIDc4LjQyNiAwIDE0MiA2My41NzUgMTQyIDE0MiAwIDc4LjQyNi02My41NzMgMTQyLTE0MiAxNDJ6bTIyNC0yMjJoLTY0di0zMmg2NHYzMnoiLz48L3N2Zz4="); 31 | } 32 | 33 | test-sprite-with-hsla-param { 34 | background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZmlsbDogaHNsYSgyNDAsIDEwMCUsIDUwJSwgMC41KSI+PHBhdGggZD0iTTE1MiAzMDRjMCA1Ny40MzggNDYuNTYyIDEwNCAxMDQgMTA0czEwNC00Ni41NjIgMTA0LTEwNC00Ni41NjItMTA0LTEwNC0xMDQtMTA0IDQ2LjU2Mi0xMDQgMTA0em0zMjgtMTc2aC0xMTJjLTgtMzItMTYtNjQtNDgtNjRoLTEyOGMtMzIgMC00MCAzMi00OCA2NGgtMTEyYy0xNy42IDAtMzIgMTQuNC0zMiAzMnYyODhjMCAxNy42IDE0LjQgMzIgMzIgMzJoNDQ4YzE3LjYgMCAzMi0xNC40IDMyLTMydi0yODhjMC0xNy42LTE0LjQtMzItMzItMzJ6bS0yMjQgMzE4Yy03OC40MjUgMC0xNDItNjMuNTc0LTE0Mi0xNDIgMC03OC40MjUgNjMuNTc1LTE0MiAxNDItMTQyIDc4LjQyNiAwIDE0MiA2My41NzUgMTQyIDE0MiAwIDc4LjQyNi02My41NzMgMTQyLTE0MiAxNDJ6bTIyNC0yMjJoLTY0di0zMmg2NHYzMnoiLz48L3N2Zz4="); 35 | } 36 | 37 | test-sprite-with-id-with-param { 38 | background-image: url("data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZmlsbDogYmx1ZSIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE1MiAzMDRjMCA1Ny40MzggNDYuNTYyIDEwNCAxMDQgMTA0czEwNC00Ni41NjIgMTA0LTEwNC00Ni41NjItMTA0LTEwNC0xMDQtMTA0IDQ2LjU2Mi0xMDQgMTA0em0zMjgtMTc2aC0xMTJjLTgtMzItMTYtNjQtNDgtNjRoLTEyOGMtMzIgMC00MCAzMi00OCA2NGgtMTEyYy0xNy42IDAtMzIgMTQuNC0zMiAzMnYyODhjMCAxNy42IDE0LjQgMzIgMzIgMzJoNDQ4YzE3LjYgMCAzMi0xNC40IDMyLTMydi0yODhjMC0xNy42LTE0LjQtMzItMzItMzJ6bS0yMjQgMzE4Yy03OC40MjUgMC0xNDItNjMuNTc0LTE0Mi0xNDIgMC03OC40MjUgNjMuNTc1LTE0MiAxNDItMTQyIDc4LjQyNiAwIDE0MiA2My41NzUgMTQyIDE0MiAwIDc4LjQyNi02My41NzMgMTQyLTE0MiAxNDJ6bTIyNC0yMjJoLTY0di0zMmg2NHYzMnoiLz48L3N2Zz4="); 39 | } 40 | 41 | test-empty-sprite { 42 | background-image: url("sprite-empty.svg"); 43 | } 44 | 45 | test-empty-sprite-with-id { 46 | background-image: url("sprite-empty.svg#camera"); 47 | } 48 | --------------------------------------------------------------------------------