@tomasvn/svgstore 3.1.2 → 3.1.4
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.
- package/.github/workflows/publish.yml +21 -0
- package/CHANGELOG.md +14 -1
- package/docs/examples/make.js +4 -1
- package/package.json +1 -1
- package/src/svgstore.js +105 -51
- package/src/utils/safe-clean-defs.js +10 -0
- package/test/svgstore.js +65 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Publish Package to npmjs
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
# Setup .npmrc file to publish to npm
|
|
14
|
+
- uses: actions/setup-node@v4
|
|
15
|
+
with:
|
|
16
|
+
node-version: '20.x'
|
|
17
|
+
registry-url: 'https://registry.npmjs.org'
|
|
18
|
+
- run: npm ci
|
|
19
|
+
- run: npm publish --provenance --access public
|
|
20
|
+
env:
|
|
21
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# 3.1.4
|
|
4
|
+
|
|
5
|
+
- Closes [#36][https://github.com/svgstore/svgstore/issues/36]
|
|
6
|
+
- Fix failing test
|
|
7
|
+
|
|
3
8
|
# 3.1.0
|
|
9
|
+
|
|
4
10
|
- implements [PR #33](https://github.com/svgstore/svgstore/pull/33) from original work
|
|
5
11
|
|
|
6
12
|
# 3.1.1
|
|
13
|
+
|
|
7
14
|
- parsing of `linearGradient` elements
|
|
8
15
|
|
|
9
16
|
# 3.1.2
|
|
10
|
-
|
|
17
|
+
|
|
18
|
+
- update readme file
|
|
19
|
+
|
|
20
|
+
# 3.1.3
|
|
21
|
+
|
|
22
|
+
- add safe clean `def` util
|
|
23
|
+
- passing `opts`, `cleanDefs: true` would break `linearGradient`
|
package/docs/examples/make.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
var svgstore = require('../../src/svgstore');
|
|
4
4
|
var fs = require('fs');
|
|
5
5
|
|
|
6
|
-
var sprites = svgstore({
|
|
6
|
+
var sprites = svgstore({
|
|
7
|
+
fragmentIdentifier: (id) => `view-${id}`,
|
|
8
|
+
processGradient: true,
|
|
9
|
+
})
|
|
7
10
|
.add('unicorn', fs.readFileSync('./assets/unicorn.svg', 'utf8'))
|
|
8
11
|
.add('rainbow', fs.readFileSync('./assets/rainbow.svg', 'utf8'))
|
|
9
12
|
.add('gradient', fs.readFileSync('./assets/gradient.svg', 'utf8'));
|
package/package.json
CHANGED
package/src/svgstore.js
CHANGED
|
@@ -6,6 +6,7 @@ var removeAttributes = require('./utils/remove-attributes');
|
|
|
6
6
|
var setAttributes = require('./utils/set-attributes');
|
|
7
7
|
var svgToSymbol = require('./utils/svg-to-symbol');
|
|
8
8
|
var getSvgRect = require('./utils/get-svg-rect');
|
|
9
|
+
var safeCleanDefs = require("./utils/safe-clean-defs");
|
|
9
10
|
|
|
10
11
|
var SELECTOR_SVG = 'svg';
|
|
11
12
|
var SELECTOR_DEFS = 'defs';
|
|
@@ -15,7 +16,19 @@ var TEMPLATE_DOCTYPE =
|
|
|
15
16
|
'<?xml version="1.0" encoding="UTF-8"?>' +
|
|
16
17
|
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" ' +
|
|
17
18
|
'"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';
|
|
18
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Default options used by svgstore.
|
|
21
|
+
* @typedef {Object} SvgstoreOptions
|
|
22
|
+
* @property {boolean} cleanDefs - Whether to clean <defs> elements.
|
|
23
|
+
* @property {boolean} cleanSymbols - Whether to clean <symbol> elements.
|
|
24
|
+
* @property {boolean} inline - Whether to output inline SVG.
|
|
25
|
+
* @property {boolean|Object} svgAttrs - Attributes to set on the root <svg>.
|
|
26
|
+
* @property {boolean|Object} symbolAttrs - Attributes to set on <symbol> elements.
|
|
27
|
+
* @property {boolean} copyAttrs - Whether to copy attributes from original SVG.
|
|
28
|
+
* @property {boolean} renameDefs - Whether to rename IDs inside <defs>.
|
|
29
|
+
* @property {function|null} fragmentIdentifier - Function to generate fragment identifiers.
|
|
30
|
+
* @property {boolean} processGradient - Whether to process gradients or not
|
|
31
|
+
*/
|
|
19
32
|
var DEFAULT_OPTIONS = {
|
|
20
33
|
cleanDefs: false,
|
|
21
34
|
cleanSymbols: false,
|
|
@@ -25,9 +38,17 @@ var DEFAULT_OPTIONS = {
|
|
|
25
38
|
copyAttrs: false,
|
|
26
39
|
renameDefs: false,
|
|
27
40
|
fragmentIdentifier: null,
|
|
41
|
+
processGradient: false,
|
|
28
42
|
};
|
|
29
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Creates an SVG store to combine multiple SVG files into one.
|
|
46
|
+
*
|
|
47
|
+
* @param {Partial<SvgstoreOptions>} [options] - Configuration options to override defaults.
|
|
48
|
+
* @returns {Object} An object with methods to add SVGs and output the combined SVG.
|
|
49
|
+
*/
|
|
30
50
|
function svgstore(options) {
|
|
51
|
+
/** @type {SvgstoreOptions} */
|
|
31
52
|
var svgstoreOptions = Object.assign({}, DEFAULT_OPTIONS, options);
|
|
32
53
|
|
|
33
54
|
// <svg>
|
|
@@ -40,6 +61,14 @@ function svgstore(options) {
|
|
|
40
61
|
return {
|
|
41
62
|
element: parent,
|
|
42
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Adds an SVG file to the store.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} id - Unique identifier for the SVG symbol.
|
|
68
|
+
* @param {string} file - SVG file content or path to load.
|
|
69
|
+
* @param {Partial<SvgstoreOptions>} [options] - Options to override defaults for this addition.
|
|
70
|
+
* @returns {Object} The svgstore instance for chaining.
|
|
71
|
+
*/
|
|
43
72
|
add: function (id, file, options) {
|
|
44
73
|
var child = loadXml(file);
|
|
45
74
|
var addOptions = Object.assign({}, svgstoreOptions, options);
|
|
@@ -47,38 +76,56 @@ function svgstore(options) {
|
|
|
47
76
|
// <defs>
|
|
48
77
|
var childDefs = child(SELECTOR_DEFS);
|
|
49
78
|
|
|
50
|
-
removeAttributes(childDefs, addOptions.cleanDefs);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
linearGradients.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
removeAttributes(childDefs, safeCleanDefs(addOptions.cleanDefs));
|
|
80
|
+
|
|
81
|
+
if (addOptions.processGradient) {
|
|
82
|
+
var linearGradients = childDefs.find('linearGradient');
|
|
83
|
+
if (linearGradients.length) {
|
|
84
|
+
linearGradients.each(function (i, elem) {
|
|
85
|
+
var $elem = child(elem);
|
|
86
|
+
var oldId = $elem.attr('id');
|
|
87
|
+
if (!oldId) return;
|
|
88
|
+
|
|
89
|
+
var newId = id + '__' + oldId;
|
|
90
|
+
$elem.attr('id', newId);
|
|
91
|
+
|
|
92
|
+
// Update references in fill, stroke, filter, style attributes
|
|
93
|
+
child('[fill], [stroke], [filter], [style]').each(
|
|
94
|
+
function (i, refElem) {
|
|
95
|
+
var $refElem = child(refElem);
|
|
96
|
+
['fill', 'stroke', 'filter', 'style'].forEach(
|
|
97
|
+
function (attr) {
|
|
98
|
+
var val = $refElem.attr(attr);
|
|
99
|
+
if (
|
|
100
|
+
val &&
|
|
101
|
+
val.includes('url(#' + oldId + ')')
|
|
102
|
+
) {
|
|
103
|
+
var newVal = val.replace(
|
|
104
|
+
new RegExp(
|
|
105
|
+
'url\\(#' + oldId + '\\)',
|
|
106
|
+
'g'
|
|
107
|
+
),
|
|
108
|
+
'url(#' + newId + ')'
|
|
109
|
+
);
|
|
110
|
+
$refElem.attr(attr, newVal);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
76
113
|
);
|
|
77
|
-
$refElem.attr(attr, newVal);
|
|
78
114
|
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Update <use> elements referencing the old gradient ID
|
|
118
|
+
child('use').each(function (i, useElem) {
|
|
119
|
+
var $useElem = child(useElem);
|
|
120
|
+
['xlink:href', 'href'].forEach(function (prop) {
|
|
121
|
+
var val = $useElem.attr(prop);
|
|
122
|
+
if (val === '#' + oldId) {
|
|
123
|
+
$useElem.attr(prop, '#' + newId);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
79
126
|
});
|
|
80
127
|
});
|
|
81
|
-
}
|
|
128
|
+
}
|
|
82
129
|
}
|
|
83
130
|
|
|
84
131
|
/* rename defs ids (existing original logic) */
|
|
@@ -87,32 +134,33 @@ function svgstore(options) {
|
|
|
87
134
|
var elem = child(_elem);
|
|
88
135
|
var oldDefId = elem.attr('id');
|
|
89
136
|
var newDefId = id + '_' + oldDefId;
|
|
137
|
+
var oldHref = '#' + oldDefId;
|
|
138
|
+
var newHref = '#' + newDefId;
|
|
139
|
+
var oldUrl = 'url(' + oldHref + ')';
|
|
140
|
+
var newUrl = 'url(' + newHref + ')';
|
|
90
141
|
elem.attr('id', newDefId);
|
|
91
142
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
var
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
143
|
+
child('*').each(function (i, refElem) {
|
|
144
|
+
var ref = child(refElem);
|
|
145
|
+
var attrs = refElem.attribs || {};
|
|
146
|
+
Object.keys(attrs).forEach(function (attr) {
|
|
147
|
+
var originalValue = attrs[attr];
|
|
148
|
+
var value = originalValue;
|
|
149
|
+
if (typeof value !== 'string') return;
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
(attr === 'href' || attr === 'xlink:href') &&
|
|
153
|
+
value === oldHref
|
|
154
|
+
) {
|
|
155
|
+
value = newHref;
|
|
102
156
|
}
|
|
103
|
-
}
|
|
104
|
-
if (!foundProperty) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
child(use).attr(foundProperty, '#' + newDefId);
|
|
108
|
-
});
|
|
109
157
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
158
|
+
if (value.indexOf(oldUrl) !== -1) {
|
|
159
|
+
value = value.split(oldUrl).join(newUrl);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (value !== originalValue) ref.attr(attr, value);
|
|
163
|
+
});
|
|
116
164
|
});
|
|
117
165
|
});
|
|
118
166
|
}
|
|
@@ -148,6 +196,12 @@ function svgstore(options) {
|
|
|
148
196
|
return this;
|
|
149
197
|
},
|
|
150
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Converts the stored SVG elements to a string.
|
|
201
|
+
*
|
|
202
|
+
* @param {Partial<SvgstoreOptions>} [options] - Options to override defaults for output.
|
|
203
|
+
* @returns {string} The combined SVG as a string.
|
|
204
|
+
*/
|
|
151
205
|
toString: function (options) {
|
|
152
206
|
// Create a clone so we don't modify the parent document.
|
|
153
207
|
var clone = loadXml(parent.xml());
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
function safeCleanDefsAttrs(attrs) {
|
|
2
|
+
if (attrs === true) {
|
|
3
|
+
return ['style'];
|
|
4
|
+
}
|
|
5
|
+
if (!Array.isArray(attrs)) return [];
|
|
6
|
+
// Exclude 'style' to preserve gradient stops and fills
|
|
7
|
+
return attrs.filter((attr) => attr !== 'style' && attr !== 'id');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = safeCleanDefsAttrs;
|
package/test/svgstore.js
CHANGED
|
@@ -231,6 +231,71 @@ test('should rename defs id', async (t) => {
|
|
|
231
231
|
t.strictEqual(store.toString(), expected);
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
+
test('should update all references to renamed defs ids', async (t) => {
|
|
235
|
+
const svg =
|
|
236
|
+
'<svg><defs>' +
|
|
237
|
+
'<linearGradient id="paint0"/>' +
|
|
238
|
+
'<linearGradient id="paint1" href="#paint0"/>' +
|
|
239
|
+
'<clipPath id="clip0"/>' +
|
|
240
|
+
'<mask id="mask0"/>' +
|
|
241
|
+
'<filter id="filter0"/>' +
|
|
242
|
+
'<marker id="marker0"/>' +
|
|
243
|
+
'</defs>' +
|
|
244
|
+
'<g clip-path="url(#clip0)" mask="url(#mask0)" filter="url(#filter0)" ' +
|
|
245
|
+
'marker-start="url(#marker0)" marker-mid="url(#marker0)" ' +
|
|
246
|
+
'marker-end="url(#marker0)">' +
|
|
247
|
+
'<path fill="url(#paint0)" stroke="url(#paint0)" ' +
|
|
248
|
+
'style="fill:url(#paint0);stroke:url(#paint0)"/>' +
|
|
249
|
+
'</g></svg>';
|
|
250
|
+
const output = svgstore({ inline: true, renameDefs: true })
|
|
251
|
+
.add('icon', svg)
|
|
252
|
+
.toString();
|
|
253
|
+
|
|
254
|
+
[
|
|
255
|
+
'href="#icon_paint0"',
|
|
256
|
+
'clip-path="url(#icon_clip0)"',
|
|
257
|
+
'mask="url(#icon_mask0)"',
|
|
258
|
+
'filter="url(#icon_filter0)"',
|
|
259
|
+
'marker-start="url(#icon_marker0)"',
|
|
260
|
+
'marker-mid="url(#icon_marker0)"',
|
|
261
|
+
'marker-end="url(#icon_marker0)"',
|
|
262
|
+
'fill="url(#icon_paint0)"',
|
|
263
|
+
'stroke="url(#icon_paint0)"',
|
|
264
|
+
'style="fill:url(#icon_paint0);stroke:url(#icon_paint0)"',
|
|
265
|
+
].forEach((reference) => {
|
|
266
|
+
t.ok(output.includes(reference), `expected output to include ${reference}`);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('should match the renameDefs issue reproduction', async (t) => {
|
|
271
|
+
const icon =
|
|
272
|
+
'<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">' +
|
|
273
|
+
'<g clip-path="url(#clip0)"><path d="M0 12h24" stroke="url(#paint0)" stroke-width="4"/></g>' +
|
|
274
|
+
'<defs>' +
|
|
275
|
+
'<linearGradient id="paint0" x2="24" gradientUnits="userSpaceOnUse">' +
|
|
276
|
+
'<stop stop-color="red"/><stop offset="1" stop-color="blue"/>' +
|
|
277
|
+
'</linearGradient>' +
|
|
278
|
+
'<clipPath id="clip0"><rect width="12" height="24"/></clipPath>' +
|
|
279
|
+
'</defs></svg>';
|
|
280
|
+
|
|
281
|
+
const output = svgstore({ renameDefs: true })
|
|
282
|
+
.add('icon', icon)
|
|
283
|
+
.toString({ inline: true });
|
|
284
|
+
|
|
285
|
+
const expected =
|
|
286
|
+
'<svg><defs>' +
|
|
287
|
+
'<linearGradient id="icon_paint0" x2="24" gradientUnits="userSpaceOnUse">' +
|
|
288
|
+
'<stop stop-color="red"/><stop offset="1" stop-color="blue"/>' +
|
|
289
|
+
'</linearGradient>' +
|
|
290
|
+
'<clipPath id="icon_clip0"><rect width="12" height="24"/></clipPath>' +
|
|
291
|
+
'</defs>' +
|
|
292
|
+
'<symbol id="icon" viewBox="0 0 24 24">' +
|
|
293
|
+
'<g clip-path="url(#icon_clip0)"><path d="M0 12h24" stroke="url(#icon_paint0)" stroke-width="4"/></g>' +
|
|
294
|
+
'</symbol></svg>';
|
|
295
|
+
|
|
296
|
+
t.strictEqual(output, expected);
|
|
297
|
+
});
|
|
298
|
+
|
|
234
299
|
test('should add <view/> and <use/> to enable SVG Fragment Identifiers', async (t) => {
|
|
235
300
|
const store = svgstore({ fragmentIdentifier: (id) => `view-${id}` })
|
|
236
301
|
.add('foo', doctype + FIXTURE_SVGS.foo.replace('<svg', svgNs.slice(0, -1)))
|