gulp-stacksvg 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +88 -74
  2. package/index.js +69 -139
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,14 +1,11 @@
1
1
  # gulp-stacksvg
2
2
 
3
- <img align="right" width="130" height="175" title="SVG Superman" src="https://raw.githubusercontent.com/firefoxic/gulp-stacksvg/master/svg-superman.png">
4
-
5
3
  [![Test Status][test-image]][test-url]
6
4
  [![License: MIT][license-image]][license-url]
7
5
  [![NPM version][npm-image]][npm-url]
8
6
  [![Vulnerabilities count][vulnerabilities-image]][vulnerabilities-url]
9
7
 
10
- Combine svg files into one with stack method.
11
- Read more about this in [the Simurai article](https://simurai.com/blog/2012/04/02/svg-stacks).
8
+ Combine svg icon files into one with stack method.
12
9
 
13
10
  ## Installation
14
11
 
@@ -16,14 +13,6 @@ Read more about this in [the Simurai article](https://simurai.com/blog/2012/04/0
16
13
  npm install gulp-stacksvg --save-dev
17
14
  ```
18
15
 
19
- ### Avalable options
20
-
21
- | Option | Description | Default |
22
- |-------------|--------------------------------------------------------------------------------------|-------------|
23
- | `output` | Sets the stack file name. Accepts values ​​both with and without the `.svg` extension. | `stack.svg` |
24
- | `separator` | Replaces the directory separator for the `id` attribute. | `_` |
25
- | `spacer` | Joins space-separated words for the `id` attribute. | `-` |
26
-
27
16
  ## Usage
28
17
 
29
18
  The following script will combine all svg sources into single svg file with stack method.
@@ -36,100 +25,125 @@ const { src, dest } = gulp
36
25
 
37
26
  function makeStack () {
38
27
  return src(`./src/icons/**/*.svg`)
39
- .pipe(stacksvg())
28
+ .pipe(stacksvg({ output: `sprite` }))
40
29
  .pipe(dest(`./dest/icons`))
41
30
  }
42
31
  ```
43
32
 
33
+ ### Avalable options
34
+
35
+ | Option | Description | Default |
36
+ |-------------|--------------------------------------------------------------------------------------|-------------|
37
+ | `output` | Sets the stack file name. Accepts values ​both with and without the `.svg` extension. | `stack.svg` |
38
+ | `separator` | Replaces the directory separator for the `id` attribute. | `_` |
39
+ | `spacer` | Joins space-separated words for the `id` attribute. | `-` |
40
+
44
41
  ### Inlining stacksvg result into markup
45
42
 
46
43
  You just don't have to want it.
47
44
 
48
- ### Editing id attributes
45
+ ## Why a stack?
49
46
 
50
- If you need to add prefix to each id, please use [gulp-rename](https://github.com/hparra/gulp-rename):
47
+ Unlike all other methods for assembling a sprite, the stack does not limit us in choosing how to insert a vector into a page. Take a look at [the results](https://demos.frontend-design.ru/sprite/src/) of different ways to display fragments of different types of sprites.
51
48
 
52
- ```js
53
- import { stacksvg } from "gulp-stacksvg"
54
- import rename from "gulp-rename"
55
- import gulp from "gulp"
49
+ We can use the stack in all four possible ways:
56
50
 
57
- const { src, dest } = gulp
51
+ - in markup:
52
+ - in `src` of `img` tag — static,
53
+ - in the `href` of the `use` tag — with the possibility of repainting,
54
+ - in styles:
55
+ - in `url()` properties `background` — static,
56
+ - in `url()` properties `mask` — with the possibility of repainting.
58
57
 
59
- function makeStack () {
60
- return src(`./src/icons/**/*.svg`, { base: `src/icons` })
61
- .pipe(rename({ prefix: `icon-` }))
62
- .pipe(stacksvg())
63
- .pipe(dest(`./dest/icons`))
64
- }
65
- ```
58
+ [Demo page](https://firefoxic.github.io/gulp-stacksvg/test/) to prove it.
66
59
 
67
- ### Transform svg sources or combined svg
60
+ ## Stack under the hood
68
61
 
69
- To transform either svg sources or combined svg you may pipe your files through [gulp-cheerio](https://github.com/KenPowers/gulp-cheerio).
62
+ This method was first mentioned in a Simurai [article](https://simurai.com/blog/2012/04/02/svg-stacks) on April 2, 2012. But even it uses unnecessarily complex code transformations.
70
63
 
71
- An example below removes all fill attributes from svg sources before combining them.
72
- Please note that you have to set `xmlMode: true` to parse svgs as xml file.
64
+ This can be done much easier. In general, the stack is arranged almost like a symbol sprite, but without changing the icon tag (it remain the `svg` tag, as in the original icon files) and with the addition of a tiny bit of style.
73
65
 
74
- ```js
75
- import { stacksvg } from "gulp-stacksvg"
76
- import cheerio from "gulp-cheerio"
77
- import gulp from "gulp"
66
+ ```xml
67
+ <?xml version="1.0" encoding="UTF-8"?>
68
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
78
69
 
79
- const { src, dest } = gulp
70
+ <style>
71
+ :root { visibility: hidden }
72
+ :target { visibility: visible }
73
+ </style>
74
+ ```
80
75
 
81
- function makeStack () {
82
- return src(`./src/icons/**/*.svg`)
83
- .pipe(cheerio({
84
- run: ($) => {
85
- $(`[fill]`).removeAttr(`fill`)
86
- },
87
- parserOptions: { xmlMode: true }
88
- }))
89
- .pipe(stacksvg())
90
- .pipe(dest(`./dest/icons`))
91
- }
76
+ <img align="left" width="90" height="90" title="sun" src="https://raw.githubusercontent.com/firefoxic/gulp-stacksvg/main/test/stack.svg#sun-alpha">
77
+
78
+ ```xml
79
+ <svg id="sun" viewBox="0 0 24 24">
80
+ <!-- Inner code of sun icon -->
81
+ </svg>
92
82
  ```
93
83
 
94
- ## Possible rendering issues with Clipping Paths in SVG
84
+ <img align="left" width="90" height="90" title="heart" src="https://raw.githubusercontent.com/firefoxic/gulp-stacksvg/main/test/stack.svg#heart-red">
95
85
 
96
- 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:
86
+ ```xml
87
+ <svg id="heart" viewBox="0 0 24 24">
88
+ <!-- Inner code of heart icon -->
89
+ </svg>
90
+ ```
91
+
92
+ <img align="left" width="90" height="90" title="thumbup" src="https://raw.githubusercontent.com/firefoxic/gulp-stacksvg/main/test/stack.svg#thumbup-alpha">
93
+
94
+ ```xml
95
+ <svg id="thumbup" viewBox="0 0 24 24">
96
+ <!-- Inner code of thumbup icon -->
97
+ </svg>
98
+ ```
97
99
 
98
- ### Correcting the Clipping Path in the SVG
100
+ ```xml
101
+ </svg>
102
+ ```
99
103
 
100
- 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.
104
+ The magic is in the stack inner style:
101
105
 
102
- ### Editing the SVG Code
106
+ - `:root { visibility: hidden }` — hides the entire contents of the stack,
107
+ - `:target { visibility: visible }` — shows only the fragment that is requested by its link.
103
108
 
104
- 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 `<clipPath>` into a `<defs>` will fix this issue. Here's an example:
109
+ And now the icons from the external sprite are available in the styles <img width="16" height="16" title="heart" src="https://raw.githubusercontent.com/firefoxic/gulp-stacksvg/main/test/stack.svg#heart-red" alt="heart">
105
110
 
106
- ```diff
107
- <defs>
108
- <path d="M28.4 30.5l5.3 5c0-.1 7-6.9 7-6.9l-4-6.8-8.3 8.7z" id="a"/>
109
- + <clipPath id="b">
110
- + <use overflow="visible" href="#a"/>
111
- + </clipPath>
112
- </defs>
113
- -<clipPath id="b">
114
- - <use overflow="visible" xlink:href="#a"/>
115
- -</clipPath>
111
+ ```html
112
+ <button class="button button--icon_heart" type="button">
113
+ <span class="visually-hidden">Add to favorites</span>
114
+ </button>
116
115
  ```
117
116
 
118
- Or you can go further and reduce the size by removing the `<use>` element, like this:
117
+ ```css
118
+ .button {
119
+ display: inline-flex;
120
+ align-items: center;
121
+ gap: 0.5em;
122
+ }
119
123
 
120
- ```diff
121
- <defs>
122
- - <path d="M28.4 30.5l5.3 5c0-.1 7-6.9 7-6.9l-4-6.8-8.3 8.7z" id="a"/>
123
- <clipPath id="b">
124
- - <use overflow="visible" href="#a"/>
125
- + <path d="M28.4 30.5l5.3 5c0-.1 7-6.9 7-6.9l-4-6.8-8.3 8.7z"/>
126
- </clipPath>
127
- </defs>
124
+ .button--icon_heart {
125
+ --icon: url("../icons/stack.svg#heart");
126
+ }
127
+
128
+ .button:hover {
129
+ --fill: red;
130
+ }
131
+
132
+ .button::before {
133
+ content: "";
134
+ width: 1em;
135
+ height: 1em;
136
+ /* icon shape */
137
+ mask: var(--icon) no-repeat center / contain;
138
+ /* icon color */
139
+ background: var(--fill, orangered);
140
+ }
128
141
  ```
129
142
 
130
- ### Using gulp-cheerio to automate this
143
+ > ⚠️ Note:
144
+ > We still need the [autoprefixer](https://github.com/postcss/autoprefixer) for the mask property.
131
145
 
132
- Another possible solution would be to write a transformation with [gulp-cheerio](https://github.com/KenPowers/gulp-cheerio). Check this issue <https://github.com/firefoxic/gulp-stacksvg/issues/98> for the instructions.
146
+ For an icon inserted via `mask`, simply change the `background`. Moreover, unlike `use`, you can draw anything in the background under the mask, for example, a gradient.
133
147
 
134
148
  [test-url]: https://github.com/firefoxic/gulp-stacksvg/actions
135
149
  [test-image]: https://github.com/firefoxic/gulp-stacksvg/actions/workflows/test.yml/badge.svg?branch=main
package/index.js CHANGED
@@ -1,152 +1,92 @@
1
- import { load } from "cheerio"
1
+ import { parse } from "node-html-parser"
2
2
  import { basename, extname, sep } from "path"
3
3
  import { Transform } from "stream"
4
4
  import fancyLog from "fancy-log"
5
5
  import PluginError from "plugin-error"
6
6
  import Vinyl from "vinyl"
7
7
 
8
- const presentationAttributes = new Set([
9
- `alignment-baseline`,
10
- `baseline-shift`,
11
- `clip-path`,
12
- `clip-rule`,
13
- `clip`,
14
- `color-interpolation-filters`,
15
- `color-interpolation`,
16
- `color-profile`,
17
- `color-rendering`,
18
- `color`,
19
- `cursor`,
20
- `d`,
21
- `direction`,
22
- `display`,
23
- `dominant-baseline`,
8
+ const excessAttrs = [
24
9
  `enable-background`,
25
- `fill-opacity`,
26
- `fill-rule`,
27
- `fill`,
28
- `filter`,
29
- `flood-color`,
30
- `flood-opacity`,
31
- `font-family`,
32
- `font-size-adjust`,
33
- `font-size`,
34
- `font-stretch`,
35
- `font-style`,
36
- `font-variant`,
37
- `font-weight`,
38
- `glyph-orientation-horizontal`,
39
- `glyph-orientation-vertical`,
40
- `image-rendering`,
41
- `kerning`,
42
- `letter-spacing`,
43
- `lighting-color`,
44
- `marker-end`,
45
- `marker-mid`,
46
- `marker-start`,
47
- `mask`,
48
- `opacity`,
49
- `overflow`,
50
- `pointer-events`,
51
- `shape-rendering`,
52
- `solid-color`,
53
- `solid-opacity`,
54
- `stop-color`,
55
- `stop-opacity`,
56
- `stroke-dasharray`,
57
- `stroke-dashoffset`,
58
- `stroke-linecap`,
59
- `stroke-linejoin`,
60
- `stroke-miterlimit`,
61
- `stroke-opacity`,
62
- `stroke-width`,
63
- `stroke`,
64
- `style`,
65
- `text-anchor`,
66
- `text-decoration`,
67
- `text-rendering`,
68
- `transform`,
69
- `unicode-bidi`,
70
- `vector-effect`,
71
- `visibility`,
72
- `word-spacing`,
73
- `writing-mode`
74
- ])
75
-
76
- export function stacksvg (options) {
77
-
78
- options = options || {}
10
+ `height`,
11
+ `version`,
12
+ `width`,
13
+ `x`,
14
+ `xml:space`,
15
+ `y`
16
+ ]
79
17
 
80
- const ids = {}
81
- const namespaces = {}
82
- const separator = options.separator ?? `_`
83
- const spacer = options.spacer ?? `-`
18
+ export function stacksvg ({ output = `stack.svg`, separator = `_`, spacer = `-` } = {}) {
84
19
 
85
- let resultSvg = `<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style>:root{visibility:hidden}:target{visibility:visible}</style><defs/></svg>`
86
20
  let isEmpty = true
87
- let fileName = options.output || `stack.svg`
88
-
89
- fileName = fileName.endsWith(`.svg`) ? fileName : `${fileName}.svg`
90
-
91
- const $ = load(resultSvg, { xmlMode: true })
92
- const $combinedSvg = $(`svg`)
93
- const $combinedDefs = $(`defs`)
21
+ const ids = {}
22
+ const namespaces = {}
23
+ const stack = parse(`<svg xmlns="http://www.w3.org/2000/svg"><style>:root{visibility:hidden}:target{visibility:visible}</style></svg>`)
24
+ const rootSvg = stack.querySelector(`svg`)
94
25
  const stream = new Transform({ objectMode: true })
95
26
 
96
- stream._transform = function transform (file, _, cb) {
27
+ function transform (file, _, cb) {
97
28
 
98
29
  if (file.isStream()) {
99
30
  return cb(new PluginError(`gulp-stacksvg`, `Streams are not supported!`))
100
31
  }
101
32
 
102
- if (file.isNull()) {return cb()}
103
-
33
+ if (file.isNull() || !parse(file.contents.toString()).querySelector(`svg`)) {
34
+ return cb()
35
+ }
104
36
 
105
- const $svg = load(file.contents.toString(), { xmlMode: true })(`svg`)
37
+ const icon = parse(file.contents.toString()).querySelector(`svg`)
106
38
 
107
- if ($svg.length === 0) {return cb()}
39
+ isEmpty = false
108
40
 
109
- const idAttr = basename(
41
+ const iconId = basename(
110
42
  file.relative.split(sep).join(separator).replace(/\s/g, spacer),
111
43
  extname(file.relative)
112
44
  )
113
- const viewBoxAttr = $svg.attr(`viewBox`)
114
- const widthAttr = $svg.attr(`width`)
115
- const heightAttr = $svg.attr(`height`)
116
- const preserveAspectRatioAttr = $svg.attr(`preserveAspectRatio`)
117
- const $icon = $(`<svg/>`)
118
-
119
- if (idAttr in ids) {
120
- return cb(new PluginError(`gulp-stacksvg`, `File name should be unique: ${idAttr}`))
45
+
46
+ if (iconId in ids) {
47
+ return cb(new PluginError(`gulp-stacksvg`, `File name should be unique: ${iconId}`))
121
48
  }
122
49
 
123
- ids[idAttr] = true
50
+ ids[iconId] = true
51
+ icon.setAttribute(`id`, iconId)
124
52
 
125
- if (file && isEmpty) {
126
- isEmpty = false
127
- }
53
+ const viewBoxAttr = icon.getAttribute(`viewBox`)
54
+ const widthAttr = icon.getAttribute(`width`)?.replace(/[^0-9]/g, ``)
55
+ const heightAttr = icon.getAttribute(`height`)?.replace(/[^0-9]/g, ``)
128
56
 
129
- $icon.attr(`id`, idAttr)
130
- if (viewBoxAttr) {
131
- $icon.attr(`viewBox`, viewBoxAttr)
132
- } else if (widthAttr && heightAttr) {
133
- $icon.attr(`viewBox`, `0 0 ${widthAttr.replace(/[^0-9]/g,``)} ${heightAttr.replace(/[^0-9]/g,``)}`)
57
+ if (!viewBoxAttr && widthAttr && heightAttr) {
58
+ icon.setAttribute(`viewBox`, `0 0 ${widthAttr} ${heightAttr}`)
134
59
  }
135
- if (preserveAspectRatioAttr) {
136
- $icon.attr(`preserveAspectRatio`, preserveAspectRatioAttr)
60
+
61
+ excessAttrs.forEach((attr) => icon.removeAttribute(attr))
62
+ icon.querySelectorAll(`[id]`).forEach(changeInnerId)
63
+
64
+ function changeInnerId (targetElem, suffix) {
65
+ let oldId = targetElem.id
66
+ let newId = `${iconId}_${suffix}`
67
+ targetElem.setAttribute(`id`, newId)
68
+ icon.querySelectorAll(`*`).forEach(updateUsingId)
69
+
70
+ function updateUsingId (elem) {
71
+ if (~elem.rawAttrs.search(`#${oldId}`)) {
72
+ for (let attr in elem._attrs) {
73
+ let attrValue = elem._attrs[attr].replace(`#${oldId}`, `#${newId}`)
74
+ elem.setAttribute(attr, attrValue)
75
+ }
76
+ }
77
+ }
137
78
  }
138
79
 
139
- const attrs = $svg[0].attribs
80
+ const attrs = icon._attrs
81
+
140
82
  for (let attrName in attrs) {
141
- if (attrName.match(/xmlns:.+/)) {
83
+ if (attrName.startsWith(`xmlns`)) {
142
84
  const storedNs = namespaces[attrName]
143
85
  const attrNs = attrs[attrName]
144
86
 
145
87
  if (storedNs !== undefined) {
146
88
  if (storedNs !== attrNs) {
147
- fancyLog.info(
148
- `${attrName} namespace appeared multiple times with different value. Keeping the first one : "${storedNs}".\nEach namespace must be unique across files.`
149
- )
89
+ fancyLog.info(`${attrName} namespace appeared multiple times with different value. Keeping the first one : "${storedNs}".\nEach namespace must be unique across files.`)
150
90
  }
151
91
  } else {
152
92
  for (let nsName in namespaces) {
@@ -156,44 +96,34 @@ export function stacksvg (options) {
156
96
  }
157
97
  namespaces[attrName] = attrNs
158
98
  }
159
- }
160
- }
161
-
162
- const $defs = $svg.find(`defs`)
163
- if ($defs.length > 0) {
164
- $combinedDefs.append($defs.contents())
165
- $defs.remove()
166
- }
167
99
 
168
- let $groupWrap = null
169
- for (let [name, value] of Object.entries($svg.attr())) {
170
- if (!presentationAttributes.has(name)) {continue}
171
- if (!$groupWrap) {$groupWrap = $(`<g/>`)}
172
- $groupWrap.attr(name, value)
100
+ icon.removeAttribute(attrName)
101
+ }
173
102
  }
174
103
 
175
- if ($groupWrap) {
176
- $groupWrap.append($svg.contents())
177
- $icon.append($groupWrap)
178
- } else {
179
- $icon.append($svg.contents())
180
- }
181
- $combinedSvg.append($icon)
104
+ rootSvg.appendChild(icon)
182
105
  cb()
183
106
  }
184
107
 
185
- stream._flush = function flush (cb) {
186
- if (isEmpty) {return cb()}
187
- if ($combinedDefs.contents().length === 0) {
188
- $combinedDefs.remove()
108
+ function flush (cb) {
109
+ if (isEmpty) {
110
+ return cb()
189
111
  }
112
+
190
113
  for (let nsName in namespaces) {
191
- $combinedSvg.attr(nsName, namespaces[nsName])
114
+ rootSvg.setAttribute(nsName, namespaces[nsName])
192
115
  }
193
- const file = new Vinyl({ path: fileName, contents: Buffer.from($.xml()) })
116
+
117
+ output = output.endsWith(`.svg`) ? output : `${output}.svg`
118
+
119
+ const file = new Vinyl({ path: output, contents: Buffer.from(stack.toString()) })
120
+
194
121
  this.push(file)
195
122
  cb()
196
123
  }
197
124
 
125
+ stream._transform = transform
126
+ stream._flush = flush
127
+
198
128
  return stream
199
129
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gulp-stacksvg",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "description": "Combine svg files into one with stack method",
6
6
  "main": "index.js",
@@ -9,7 +9,7 @@
9
9
  ],
10
10
  "scripts": {
11
11
  "lint": "eslint ./",
12
- "test": "gulp build && mocha",
12
+ "test": "gulp test && mocha",
13
13
  "pretest": "npm run lint",
14
14
  "preversion": "npm test",
15
15
  "postversion": "npm publish",
@@ -29,8 +29,8 @@
29
29
  },
30
30
  "homepage": "https://github.com/firefoxic/gulp-stacksvg",
31
31
  "dependencies": {
32
- "cheerio": "^1.0.0-rc.12",
33
32
  "fancy-log": "^2.0.0",
33
+ "node-html-parser": "^5.4.2",
34
34
  "plugin-error": "^2.0.0",
35
35
  "vinyl": "^2.2.1"
36
36
  },