├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── svg-superman.png ├── test ├── src │ ├── namespace.svg │ ├── square.svg │ ├── circle.svg │ ├── circle-with-gradient.svg │ └── inline-svg.html └── inline-svg.html ├── .jshintrc ├── gulpfile.js ├── package.json ├── index.js ├── README.md └── test.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: w0rm -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/dest/** 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /svg-superman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w0rm/gulp-svgstore/HEAD/svg-superman.png -------------------------------------------------------------------------------- /test/src/namespace.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": true, 3 | "boss": true, 4 | "browser": true, 5 | "node": true, 6 | "expr": true, 7 | "globals": { 8 | "define": false, 9 | "require": true, 10 | "_t": true 11 | }, 12 | "indent": 2, 13 | "laxcomma": true, 14 | "maxlen": 100, 15 | "newcap": true, 16 | "strict": false, 17 | "trailing": true, 18 | "undef": true, 19 | "unused": true, 20 | "quotmark": "single" 21 | } 22 | -------------------------------------------------------------------------------- /test/inline-svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | gulp-svgstore 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | npm-test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: ['10', '12', '14'] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | 24 | - run: | 25 | npm install 26 | npm test 27 | -------------------------------------------------------------------------------- /test/src/square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/src/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/src/circle-with-gradient.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/src/inline-svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | gulp-svgstore 4 | 14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const svgstore = require('./index') 2 | const gulp = require('gulp') 3 | const inject = require('gulp-inject') 4 | 5 | gulp.task('external', () => 6 | gulp 7 | .src('test/src/*.svg') 8 | .pipe(svgstore()) 9 | .pipe(gulp.dest('test/dest')) 10 | ) 11 | 12 | gulp.task('inline', () => { 13 | function fileContents (_, file) { 14 | return file.contents.toString('utf8') 15 | } 16 | 17 | const svgs = gulp 18 | .src('test/src/*.svg') 19 | .pipe(svgstore({ inlineSvg: true })) 20 | 21 | return gulp 22 | .src('test/src/inline-svg.html') 23 | .pipe(inject(svgs, { transform: fileContents })) 24 | .pipe(gulp.dest('test/dest')) 25 | }) 26 | 27 | gulp.task('build', gulp.series(['external', 'inline'])) 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-svgstore", 3 | "version": "9.0.0", 4 | "description": "Combine svg files into one with elements", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "scripts": { 10 | "test": "gulp build && mocha test.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/w0rm/gulp-svgstore" 15 | }, 16 | "author": { 17 | "name": "Andrey Kuzmin", 18 | "email": "unsoundscapes@gmail.com" 19 | }, 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/w0rm/gulp-svgstore/issues" 23 | }, 24 | "homepage": "https://github.com/w0rm/gulp-svgstore", 25 | "dependencies": { 26 | "cheerio": "^1.0.0-rc.10", 27 | "fancy-log": "^1.3.3", 28 | "plugin-error": "^1.0.1", 29 | "vinyl": "^2.2.1" 30 | }, 31 | "devDependencies": { 32 | "finalhandler": "^1.1.2", 33 | "gulp": "^4.0.2", 34 | "gulp-inject": "^5.0.5", 35 | "mocha": "^9.0.1", 36 | "puppeteer": "^10.0.0", 37 | "serve-static": "^1.14.1", 38 | "sinon": "^11.1.1" 39 | }, 40 | "engines": { 41 | "node": ">=10.0" 42 | }, 43 | "engineStrict": true, 44 | "keywords": [ 45 | "gulpplugin", 46 | "svg", 47 | "icon", 48 | "sprite" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const cheerio = require('cheerio') 2 | const path = require('path') 3 | const Stream = require('stream') 4 | const fancyLog = require('fancy-log') 5 | const PluginError = require('plugin-error') 6 | const Vinyl = require('vinyl') 7 | 8 | const presentationAttributes = new Set([ 9 | 'style', 'alignment-baseline', 'baseline-shift', 'clip', 'clip-path', 10 | 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 11 | 'color-profile', 'color-rendering', 'cursor', 'd', 'direction', 'display', 12 | 'dominant-baseline', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 13 | 'filter', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 14 | 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 15 | 'font-weight', 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 16 | 'image-rendering', 'kerning', 'letter-spacing', 'lighting-color', 17 | 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'overflow', 18 | 'pointer-events', 'shape-rendering', 'solid-color', 'solid-opacity', 19 | 'stop-color', 'stop-opacity', 'stroke', 'stroke-dasharray', 20 | 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 21 | 'stroke-opacity', 'stroke-width', 'text-anchor', 'text-decoration', 22 | 'text-rendering', 'transform', 'unicode-bidi', 'vector-effect', 'visibility', 23 | 'word-spacing', 'writing-mode' 24 | ]); 25 | 26 | module.exports = function (config) { 27 | 28 | config = config || {} 29 | 30 | const namespaces = {} 31 | let isEmpty = true 32 | let fileName 33 | const inlineSvg = config.inlineSvg || false 34 | const ids = {} 35 | 36 | let resultSvg = '' 37 | if (!inlineSvg) { 38 | resultSvg = 39 | '' + 40 | '' + 42 | resultSvg 43 | } 44 | 45 | const $ = cheerio.load(resultSvg, { xmlMode: true }) 46 | const $combinedSvg = $('svg') 47 | const $combinedDefs = $('defs') 48 | const stream = new Stream.Transform({ objectMode: true }) 49 | 50 | stream._transform = function transform (file, _, cb) { 51 | 52 | if (file.isStream()) { 53 | return cb(new PluginError('gulp-svgstore', 'Streams are not supported!')) 54 | } 55 | 56 | if (file.isNull()) return cb() 57 | 58 | 59 | const $svg = cheerio.load(file.contents.toString(), { xmlMode: true })('svg') 60 | 61 | if ($svg.length === 0) return cb() 62 | 63 | const idAttr = path.basename(file.relative, path.extname(file.relative)) 64 | const viewBoxAttr = $svg.attr('viewBox') 65 | const preserveAspectRatioAttr = $svg.attr('preserveAspectRatio') 66 | const $symbol = $('') 67 | 68 | if (idAttr in ids) { 69 | return cb(new PluginError('gulp-svgstore', 'File name should be unique: ' + idAttr)) 70 | } 71 | 72 | ids[idAttr] = true 73 | 74 | if (!fileName) { 75 | fileName = path.basename(file.base) 76 | if (fileName === '.' || !fileName) { 77 | fileName = 'svgstore.svg' 78 | } else { 79 | fileName = fileName.split(path.sep).shift() + '.svg' 80 | } 81 | } 82 | 83 | if (file && isEmpty) { 84 | isEmpty = false 85 | } 86 | 87 | $symbol.attr('id', idAttr) 88 | if (viewBoxAttr) { 89 | $symbol.attr('viewBox', viewBoxAttr) 90 | } 91 | if (preserveAspectRatioAttr) { 92 | $symbol.attr('preserveAspectRatio', preserveAspectRatioAttr) 93 | } 94 | 95 | const attrs = $svg[0].attribs 96 | for (let attrName in attrs) { 97 | if (attrName.match(/xmlns:.+/)) { 98 | const storedNs = namespaces[attrName] 99 | const attrNs = attrs[attrName] 100 | 101 | if (storedNs !== undefined) { 102 | if (storedNs !== attrNs) { 103 | fancyLog.info( 104 | attrName + ' namespace appeared multiple times with different value.' + 105 | ' Keeping the first one : "' + storedNs + 106 | '".\nEach namespace must be unique across files.' 107 | ) 108 | } 109 | } else { 110 | for (let nsName in namespaces) { 111 | if (namespaces[nsName] === attrNs) { 112 | fancyLog.info( 113 | 'Same namespace value under different names : ' + 114 | nsName + 115 | ' and ' + 116 | attrName + 117 | '.\nKeeping both.' 118 | ) 119 | } 120 | } 121 | namespaces[attrName] = attrNs; 122 | } 123 | } 124 | } 125 | 126 | const $defs = $svg.find('defs') 127 | if ($defs.length > 0) { 128 | $combinedDefs.append($defs.contents()) 129 | $defs.remove() 130 | } 131 | 132 | let $groupWrap = null 133 | for (let [name, value] of Object.entries($svg.attr())) { 134 | if (!presentationAttributes.has(name)) continue; 135 | if (!$groupWrap) $groupWrap = $('') 136 | $groupWrap.attr(name, value) 137 | } 138 | 139 | if ($groupWrap) { 140 | $groupWrap.append($svg.contents()) 141 | $symbol.append($groupWrap) 142 | } else { 143 | $symbol.append($svg.contents()) 144 | } 145 | $combinedSvg.append($symbol) 146 | cb() 147 | } 148 | 149 | stream._flush = function flush (cb) { 150 | if (isEmpty) return cb() 151 | if ($combinedDefs.contents().length === 0) { 152 | $combinedDefs.remove() 153 | } 154 | for (let nsName in namespaces) { 155 | $combinedSvg.attr(nsName, namespaces[nsName]) 156 | } 157 | const file = new Vinyl({ path: fileName, contents: Buffer.from($.xml()) }) 158 | this.push(file) 159 | cb() 160 | } 161 | 162 | return stream; 163 | } 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gulp-svgstore ![Build Status](https://github.com/w0rm/gulp-svgstore/actions/workflows/test.yml/badge.svg?branch=main) 2 | ============= 3 | 4 | 5 | 8 | 9 | Combine svg files into one with `` elements. 10 | Read more about this in [CSS Tricks article](http://css-tricks.com/svg-symbol-good-choice-icons/). 11 | 12 | If you need similar plugin for grunt, 13 | I encourage you to check [grunt-svgstore](https://github.com/FWeinb/grunt-svgstore). 14 | 15 | ### Options: 16 | 17 | The following options are set automatically based on file data: 18 | 19 | * `id` attribute of the `` element is set to the name of corresponding file; 20 | * result filename is the name of base directory of the first file. 21 | 22 | If your workflow is different, please use `gulp-rename` to rename sources or result. 23 | 24 | The only available option is: 25 | 26 | * inlineSvg — output only `` element without `` and `DOCTYPE` to use inline, default: `false`. 27 | 28 | 29 | ## Install 30 | 31 | ```sh 32 | npm install gulp-svgstore --save-dev 33 | ``` 34 | 35 | ## Usage 36 | 37 | The following script will combine all svg sources into single svg file with `` elements. 38 | The name of result svg is the base directory name of the first file `src.svg`. 39 | 40 | Additionally pass through [gulp-svgmin](https://github.com/ben-eb/gulp-svgmin) 41 | to minify svg and ensure unique ids. 42 | 43 | ```js 44 | const gulp = require('gulp'); 45 | const svgstore = require('gulp-svgstore'); 46 | const svgmin = require('gulp-svgmin'); 47 | const path = require('path'); 48 | 49 | gulp.task('svgstore', () => { 50 | return gulp 51 | .src('test/src/*.svg') 52 | .pipe(svgmin((file) => { 53 | const prefix = path.basename(file.relative, path.extname(file.relative)); 54 | return { 55 | plugins: [{ 56 | cleanupIDs: { 57 | prefix: prefix + '-', 58 | minify: true 59 | } 60 | }] 61 | } 62 | })) 63 | .pipe(svgstore()) 64 | .pipe(gulp.dest('test/dest')); 65 | }); 66 | ``` 67 | 68 | ### Inlining svgstore result into html body 69 | 70 | To inline combined svg into html body I suggest using [gulp-inject](https://github.com/klei/gulp-inject). 71 | The following gulp task will inject svg into 72 | 73 | In your html file (using [`sr-only` from html5-boilerplate](https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/style.css#L112) to fix the gradients): 74 | 75 | ```html 76 |
77 | 78 |
79 | ``` 80 | In your gulp tasks: 81 | 82 | ```js 83 | const gulp = require('gulp'); 84 | const svgstore = require('gulp-svgstore'); 85 | const inject = require('gulp-inject'); 86 | 87 | gulp.task('svgstore', () => { 88 | const svgs = gulp 89 | .src('test/src/*.svg') 90 | .pipe(svgstore({ inlineSvg: true })); 91 | 92 | function fileContents (filePath, file) { 93 | return file.contents.toString(); 94 | } 95 | 96 | return gulp 97 | .src('test/src/inline-svg.html') 98 | .pipe(inject(svgs, { transform: fileContents })) 99 | .pipe(gulp.dest('test/dest')); 100 | }); 101 | ``` 102 | 103 | ### Generating id attributes 104 | 105 | Id of symbol element is calculated from file name. You cannot pass files with the same name, 106 | because id should be unique. 107 | 108 | If you need to add prefix to each id, please use `gulp-rename`: 109 | 110 | ```js 111 | const gulp = require('gulp'); 112 | const rename = require('gulp-rename'); 113 | const svgstore = require('gulp-svgstore'); 114 | 115 | gulp.task('default', () => { 116 | return gulp 117 | .src('src/svg/**/*.svg', { base: 'src/svg' }) 118 | .pipe(rename({prefix: 'icon-'})) 119 | .pipe(svgstore()) 120 | .pipe(gulp.dest('dest')); 121 | }); 122 | ``` 123 | 124 | If you need to have nested directories that may have files with the same name, please 125 | use `gulp-rename`. The following example will concatenate relative path with the name of the file, 126 | e.g. `src/svg/one/two/three/circle.svg` becomes `one-two-three-circle`. 127 | 128 | 129 | ```js 130 | const gulp = require('gulp'); 131 | const path = require('path'); 132 | const rename = require('gulp-rename'); 133 | const svgstore = require('gulp-svgstore'); 134 | 135 | gulp.task('default', () => { 136 | return gulp 137 | .src('src/svg/**/*.svg', { base: 'src/svg' }) 138 | .pipe(rename((file) => { 139 | const name = file.dirname.split(path.sep); 140 | name.push(file.basename); 141 | file.basename = name.join('-'); 142 | })) 143 | .pipe(svgstore()) 144 | .pipe(gulp.dest('dest')); 145 | }); 146 | ``` 147 | 148 | ### Using svg as external file 149 | 150 | There is a problem with `` in Internet Explorer, 151 | so you should either inline everything into body with a 152 | [simple script like this](https://gist.github.com/w0rm/621a56a353f7b2a6b0db) or 153 | polyfill with [svg4everybody](https://github.com/jonathantneal/svg4everybody). 154 | 155 | ## PNG sprite fallback for unsupported browsers 156 | 157 | [gulp-svgfallback](https://github.com/w0rm/gulp-svgfallback) is a gulp plugin that generates png 158 | sprite and css file with background offsets from svg sources. Please check it and leave feedback. 159 | 160 | ## Transform svg sources or combined svg 161 | 162 | To transform either svg sources or combined svg you may pipe your files through 163 | [gulp-cheerio](https://github.com/KenPowers/gulp-cheerio). 164 | 165 | ### Transform svg sources 166 | 167 | An example below removes all fill attributes from svg sources before combining them. 168 | Please note that you have to set `xmlMode: true` to parse svgs as xml file. 169 | 170 | ```js 171 | const gulp = require('gulp'); 172 | const svgstore = require('gulp-svgstore'); 173 | const cheerio = require('gulp-cheerio'); 174 | 175 | gulp.task('svgstore', () => { 176 | return gulp 177 | .src('test/src/*.svg') 178 | .pipe(cheerio({ 179 | run: ($) => { 180 | $('[fill]').removeAttr('fill'); 181 | }, 182 | parserOptions: { xmlMode: true } 183 | })) 184 | .pipe(svgstore({ inlineSvg: true })) 185 | .pipe(gulp.dest('test/dest')); 186 | }); 187 | ``` 188 | 189 | ### Transform combined svg 190 | 191 | The following example sets `style="display:none"` on the combined svg: 192 | (beware if you use gradients and masks, display:none breaks those and just show 193 | nothing, best method is to use the [method show above](#inlining-svgstore-result-into-html-body) ) 194 | 195 | 196 | ```js 197 | const gulp = require('gulp'); 198 | const svgstore = require('gulp-svgstore'); 199 | const cheerio = require('gulp-cheerio'); 200 | 201 | gulp.task('svgstore', () => { 202 | return gulp 203 | .src('test/src/*.svg') 204 | .pipe(svgstore({ inlineSvg: true })) 205 | .pipe(cheerio({ 206 | run: ($) => { 207 | $('svg').attr('style', 'display:none'); 208 | }, 209 | parserOptions: { xmlMode: true } 210 | })) 211 | .pipe(gulp.dest('test/dest')); 212 | }); 213 | ``` 214 | 215 | ## Extracting metadata from combined svg 216 | 217 | You can extract data with cheerio. 218 | 219 | The following example extracts viewBox and id from each symbol in combined svg. 220 | 221 | ```js 222 | const gulp = require('gulp'); 223 | const Vinyl = require('vinyl'); 224 | const svgstore = require('gulp-svgstore'); 225 | const through2 = require('through2'); 226 | const cheerio = require('cheerio'); 227 | 228 | gulp.task('metadata', () => { 229 | return gulp 230 | .src('test/src/*.svg') 231 | .pipe(svgstore()) 232 | .pipe(through2.obj(function (file, encoding, cb) { 233 | const $ = cheerio.load(file.contents.toString(), {xmlMode: true}); 234 | const data = $('svg > symbol').map(() => { 235 | return { 236 | name: $(this).attr('id'), 237 | viewBox: $(this).attr('viewBox') 238 | }; 239 | }).get(); 240 | const jsonFile = new Vinyl({ 241 | path: 'metadata.json', 242 | contents: Buffer.from(JSON.stringify(data)) 243 | }); 244 | this.push(jsonFile); 245 | this.push(file); 246 | cb(); 247 | })) 248 | .pipe(gulp.dest('test/dest')); 249 | }); 250 | ``` 251 | 252 | ## Possible rendering issues with Clipping Paths in SVG 253 | 254 | If you're running into issues with SVGs not rendering correctly in some browsers (see issue #47), the issue might be that clipping paths might not have been properly intersected in the SVG file. There are currently three ways of fixing this issue: 255 | 256 | ### Correcting the Clipping Path in the SVG 257 | 258 | If you have the source file, simply converting the clipping path to a nice coded shape will fix this issue. Select the object, open up the Pathfinder panel, and click the Intersect icon. 259 | 260 | ### Editing the SVG Code 261 | 262 | If you don't have the source file or an SVG Editor (Adobe Illustrator etc.), you can manually edit the SVG code in the file. Wrapping the `` into a `` will fix this issue. Here's an example: 263 | 264 | ``` 265 | 266 | 267 | 268 | 269 | ``` 270 | 271 | Becomes: 272 | 273 | ``` 274 | 275 | 276 | 277 | 278 | ``` 279 | 280 | Or you can go further and reduce the size by removing the `` element, like this: 281 | 282 | ``` 283 | 284 | 285 | 286 | ``` 287 | 288 | ### Using gulp-cheerio to automate this 289 | 290 | Another possible solution would be to write a transformation with [gulp-cheerio](https://github.com/KenPowers/gulp-cheerio). Check this issue https://github.com/w0rm/gulp-svgstore/issues/98 for the instructions. 291 | 292 | 293 | ## Changelog 294 | 295 | * 9.0.0 296 | * transfer `` [presentation attributes](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/Presentation) to a wrapping `` element #110 297 | 298 | * 8.0.0 299 | * Update dependencies 300 | * Drop support for node < 10 301 | 302 | * 7.0.1 303 | * Include xmlns:xlink in svg definition #96 304 | 305 | * 7.0.0 306 | * Stop using deprecated `new Buffer()` api 307 | * Drop support for node 0.12 308 | 309 | * 6.1.1 310 | * Removed dependency on gulp-util to support gulp 4 311 | 312 | * 6.1.0 313 | * Copy preserveAspectRatio attribute from source svg to to symbol #76 314 | 315 | * 6.0.0 316 | * Removed cache of the cheerio object #61 317 | 318 | * 5.0.5 319 | * Correctly set namespaces of the combined svg 320 | 321 | * 5.0.4 322 | * Skip null and invalid files 323 | 324 | * 5.0.3 325 | * Updated readme with a way to ensure unique ids 326 | 327 | * 5.0.2 328 | * Updated direct dependencies 329 | 330 | * 5.0.1 331 | * Removed cheerio from devDependencies #34 332 | 333 | * 5.0.0 334 | * Removed prefix and fileName options 335 | 336 | * 4.0.3 337 | * Ensure unique file names 338 | * Improved readme with gulp-rename usage to generate id for nested directories 339 | 340 | * 4.0.1 341 | * Added cheerio to devDependencies 342 | 343 | * 4.0.0 344 | * Removed `transformSvg`, pipe files through [gulp-cheerio](https://github.com/KenPowers/gulp-cheerio) instead. 345 | * Made cheerio 0.* a peer dependency, allows to choose what version to use. 346 | * Uses `file.cheerio` if cached in gulp file object and also sets it for the combined svg. 347 | * Improved readme. 348 | 349 | * 3.0.0 350 | * Used cheerio instead of libxmljs (changes transformSvg syntax) 351 | 352 | * 2.0.0 353 | * Added check for inputs before generating SVG. 354 | 355 | * 1.0.1 356 | * Added check for missing viewBox in original svg. 357 | 358 | * 1.0.0 359 | * Initial release. 360 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after, beforeEach, afterEach */ 2 | 3 | const assert = require('assert') 4 | const cheerio = require('cheerio') 5 | const fancyLog = require('fancy-log') 6 | const finalhandler = require('finalhandler') 7 | const http = require('http') 8 | const PluginError = require('plugin-error') 9 | const puppeteer = require('puppeteer') 10 | const sandbox = require('sinon').createSandbox() 11 | const serveStatic = require('serve-static') 12 | const svgstore = require('./index') 13 | const Vinyl = require('vinyl') 14 | 15 | describe('gulp-svgstore usage test', () => { 16 | let browser 17 | let port 18 | let page 19 | 20 | const server = http.createServer((req, res) => { 21 | serveStatic('test')(req, res, finalhandler(req, res)) 22 | }) 23 | 24 | before(() => Promise.all([ 25 | puppeteer.launch() 26 | .then((b) => { browser = b }) 27 | .then(() => browser.newPage()) 28 | .then((p) => { page = p }) 29 | , new Promise((resolve) => { 30 | server.listen(() => { 31 | port = server.address().port 32 | resolve() 33 | }) 34 | }) 35 | ])) 36 | 37 | after(() => Promise.all([ 38 | browser.close() 39 | , new Promise((resolve) => { 40 | server.close() 41 | server.unref() 42 | resolve() 43 | }) 44 | ])) 45 | 46 | it('stored image should equal original svg', () => { 47 | let screenshot1 48 | 49 | return page.goto('http://localhost:' + port + '/inline-svg.html') 50 | .then(() => page.evaluate(() => document.title)) 51 | .then((title) => { 52 | assert.strictEqual(title, 'gulp-svgstore', 'Test page is not loaded') 53 | }) 54 | .then(() => page.screenshot()) 55 | .then((data) => { screenshot1 = data }) 56 | .then(() => page.goto('http://localhost:' + port + '/dest/inline-svg.html')) 57 | .then(() => page.screenshot()) 58 | .then((screenshot2) => { 59 | assert(screenshot1.toString() === screenshot2.toString(), 'Screenshots are different') 60 | }) 61 | }) 62 | }) 63 | 64 | describe('gulp-svgstore unit test', () => { 65 | beforeEach(() => { sandbox.stub(fancyLog, 'info') }) 66 | afterEach(() => { sandbox.restore() }) 67 | 68 | it('should not create empty svg file', (done) => { 69 | const stream = svgstore() 70 | let isEmpty = true 71 | 72 | stream.on('data', () => { isEmpty = false }) 73 | 74 | stream.on('end', () => { 75 | assert.ok(isEmpty, 'Created empty svg') 76 | done() 77 | }) 78 | 79 | stream.end() 80 | }) 81 | 82 | it('should correctly merge svg files', (done) => { 83 | const stream = svgstore({ inlineSvg: true }) 84 | 85 | stream.on('data', (file) => { 86 | const result = file.contents.toString() 87 | const target = 88 | '' + 89 | '' + 90 | '' + 91 | '' 92 | assert.strictEqual(result, target) 93 | done() 94 | }) 95 | 96 | stream.write(new Vinyl({ 97 | contents: Buffer.from('') 98 | , path: 'circle.svg' 99 | })) 100 | 101 | stream.write(new Vinyl({ 102 | contents: Buffer.from('') 103 | , path: 'square.svg' 104 | })) 105 | 106 | stream.end() 107 | }) 108 | 109 | it('should not include null or invalid files', (done) => { 110 | const stream = svgstore({ inlineSvg: true }) 111 | 112 | stream.on('data', (file) => { 113 | const result = file.contents.toString() 114 | const target = 115 | '' + 116 | '' + 117 | '' 118 | assert.strictEqual(result, target) 119 | done() 120 | }) 121 | 122 | stream.write(new Vinyl({ 123 | contents: Buffer.from('') 124 | , path: 'circle.svg' 125 | })) 126 | 127 | stream.write(new Vinyl({ 128 | contents: null 129 | , path: 'square.svg' 130 | })) 131 | 132 | stream.write(new Vinyl({ 133 | contents: Buffer.from('not an svg') 134 | , path: 'square.svg' 135 | })) 136 | 137 | stream.end() 138 | }) 139 | 140 | it('should merge defs to parent svg file', (done) => { 141 | const stream = svgstore({ inlineSvg: true }) 142 | 143 | stream.on('data', (file) => { 144 | const result = file.contents.toString() 145 | const target = 146 | '' + 147 | '' + 148 | '' + 149 | '' 150 | assert.strictEqual(result, target) 151 | done() 152 | }) 153 | 154 | stream.write(new Vinyl({ 155 | contents: Buffer.from( 156 | '' + 157 | '' + 158 | '' + 159 | '' 160 | ) 161 | , path: 'circle.svg' 162 | })) 163 | 164 | stream.end() 165 | }) 166 | 167 | it('should emit error if files have the same name', (done) => { 168 | const stream = svgstore() 169 | 170 | stream.on('error', (error) => { 171 | assert.ok(error instanceof PluginError) 172 | assert.strictEqual(error.message, 'File name should be unique: circle') 173 | done() 174 | }) 175 | 176 | stream.write(new Vinyl({ contents: Buffer.from(''), path: 'circle.svg' })) 177 | stream.write(new Vinyl({ contents: Buffer.from(''), path: 'circle.svg' })) 178 | 179 | stream.end() 180 | }) 181 | 182 | it('should generate result filename based on base path of the first file', (done) => { 183 | const stream = svgstore() 184 | 185 | stream.on('data', (file) => { 186 | assert.strictEqual(file.relative, 'icons.svg') 187 | done() 188 | }) 189 | 190 | stream.write(new Vinyl({ 191 | contents: Buffer.from('') 192 | , path: 'src/icons/circle.svg' 193 | , base: 'src/icons' 194 | })) 195 | 196 | stream.write(new Vinyl({ 197 | contents: Buffer.from('') 198 | , path: 'src2/icons2/square.svg' 199 | , base: 'src2/icons2' 200 | })) 201 | 202 | stream.end() 203 | }) 204 | 205 | it('should generate svgstore.svg if base path of the 1st file is dot', (done) => { 206 | const stream = svgstore() 207 | 208 | stream.on('data', (file) => { 209 | assert.strictEqual(file.relative, 'svgstore.svg') 210 | done() 211 | }) 212 | 213 | stream.write(new Vinyl({ 214 | contents: Buffer.from('') 215 | , path: 'circle.svg' 216 | , base: '.' 217 | })) 218 | 219 | stream.write(new Vinyl({ 220 | contents: Buffer.from('') 221 | , path: 'src2/icons2/square.svg' 222 | , base: 'src2' 223 | })) 224 | 225 | stream.end() 226 | }) 227 | 228 | it('should include all namespace into final svg', (done) => { 229 | const stream = svgstore() 230 | 231 | stream.on('data', (file) => { 232 | const $resultSvg = cheerio.load(file.contents.toString(), { xmlMode: true })('svg') 233 | 234 | assert.strictEqual($resultSvg.attr('xmlns'), 'http://www.w3.org/2000/svg') 235 | assert.strictEqual($resultSvg.attr('xmlns:xlink'), 'http://www.w3.org/1999/xlink') 236 | done() 237 | }) 238 | 239 | stream.write(new Vinyl({ 240 | contents: Buffer.from( 241 | '' + 242 | '' + 243 | '') 244 | , path: 'rect.svg' 245 | })) 246 | 247 | stream.write(new Vinyl({ 248 | contents: Buffer.from( 249 | '' + 251 | '' + 252 | '' + 253 | '' + 254 | '') 255 | , path: 'sandwich.svg' 256 | })) 257 | 258 | stream.end() 259 | }) 260 | 261 | it('should not include duplicate namespaces into final svg', (done) => { 262 | const stream = svgstore({ inlineSvg: true }) 263 | 264 | stream.on('data', (file) => { 265 | assert.strictEqual( 266 | '' + 267 | '', 268 | file.contents.toString() 269 | ) 270 | done() 271 | }) 272 | 273 | stream.write(new Vinyl({ 274 | contents: Buffer.from( 275 | '' 276 | ) 277 | , path: 'rect.svg' 278 | })) 279 | 280 | stream.write(new Vinyl({ 281 | contents: Buffer.from( 282 | '' 283 | ) 284 | , path: 'sandwich.svg' 285 | })) 286 | 287 | stream.end() 288 | }) 289 | 290 | it('should transfer svg presentation attributes to a wrapping g element', (done) => { 291 | const stream = svgstore({ inlineSvg: true }) 292 | const attrs = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" style="fill:#0000"'; 293 | 294 | stream.on('data', (file) => { 295 | assert.strictEqual( 296 | '' + 297 | ``, 298 | file.contents.toString() 299 | ) 300 | done() 301 | }) 302 | 303 | stream.write(new Vinyl({ 304 | contents: Buffer.from( 305 | `` + 306 | '' 307 | ) 308 | , path: 'rect.svg' 309 | })) 310 | 311 | stream.end() 312 | }) 313 | 314 | it('Warn about duplicate namespace value under different name', (done) => { 315 | const stream = svgstore() 316 | 317 | stream.on('data', () => { 318 | assert.strictEqual( 319 | 'Same namespace value under different names : xmlns:lk and xmlns:xlink.\n' + 320 | 'Keeping both.', 321 | fancyLog.info.getCall(0).args[0] 322 | ) 323 | done() 324 | }) 325 | 326 | stream.write(new Vinyl({ 327 | contents: Buffer.from( 328 | '' + 329 | '' + 330 | '' + 331 | '') 332 | , path: 'rect.svg' 333 | })) 334 | 335 | stream.write(new Vinyl({ 336 | contents: Buffer.from( 337 | '' + 339 | '' + 340 | '' + 341 | '' + 342 | '') 343 | , path: 'sandwich.svg' 344 | })) 345 | 346 | stream.end() 347 | }) 348 | 349 | it('Strong warn about duplicate namespace name with different value', (done) => { 350 | const stream = svgstore() 351 | 352 | stream.on('data', () => { 353 | assert.strictEqual( 354 | 'xmlns:xlink namespace appeared multiple times with different value. ' + 355 | 'Keeping the first one : "http://www.w3.org/1998/xlink".\n' + 356 | 'Each namespace must be unique across files.' 357 | , fancyLog.info.getCall(0).args[0] 358 | ) 359 | done() 360 | }) 361 | 362 | stream.write(new Vinyl({ 363 | contents: Buffer.from( 364 | '' + 365 | '' + 366 | '' + 367 | '') 368 | , path: 'rect.svg' 369 | })) 370 | 371 | stream.write(new Vinyl({ 372 | contents: Buffer.from( 373 | '' + 375 | '' + 376 | '' + 377 | '' + 378 | '') 379 | , path: 'sandwich.svg' 380 | })) 381 | 382 | stream.end() 383 | }) 384 | }) 385 | --------------------------------------------------------------------------------