Skip to content
This repository was archived by the owner on Dec 11, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ other loaders before `svg-react` to alter/update/remove nodes before reaching
In addition, the new [filters](#filters) API allows for additional ways to
modify the generated SVG Component. This allows `svg-react` to also be used as a
pre-loader (with `filters` and `raw=true` params) for modifying SVGs before they
are acted on by the loader version of `svg-react`.
are acted on by the loader version of `svg-react`.

There is a filter which creates 'unique' IDs and mask, fill, and xlink:href
references to those IDs by prefixing the SVG filename. This solves a common problem
encountered when loading multiple SVGs onto the same page: if the IDs within the different
SVGs are the same, there will be ID collisions which will cause a variety of issues with
the rendering of the SVG components. Although there are plugins available for [SVGO](https://github.com/svg/svgo)
designed to solve this problem, the solution implemented here provides another way to
avoid ID collision issues on SVGs.

### Notes

Expand All @@ -30,7 +38,6 @@ are acted on by the loader version of `svg-react`.

Installation
------------

~~~
% npm install --save-dev svg-react-loader
~~~
Expand Down Expand Up @@ -93,6 +100,10 @@ the resource will override those given for the loader.
blocks, or within `className` properties, with. If indicated without a string,
the file's basename will be used as a prefix.

* `uniqueIdPrefix`: When set to `true` will prefix the filename to the IDs and
references within the SVG, solving the problem of ID collision when multiple
SVGs are used on the same page.

* `raw`: If set to `true` will output the parsed object tree repesenting the SVG
as a JSON string. Otherwise, returns a string of JavaScript that represents
the component's module.
Expand Down Expand Up @@ -121,6 +132,7 @@ module: {
loader: 'svg-react-loader',
query: {
classIdPrefix: '[name]-[hash:8]__',
uniqueIdPrefix: true,
filters: [
function (value) {
// ...
Expand Down Expand Up @@ -204,6 +216,7 @@ Report an Issue

* [Bugs](http://github.com/jhamlet/svg-react-loader/issues)
* Contact the author: <jerry@hamletink.com>
* For issues with the generation of unique ID prefixes, please contact <wfbrinkert@gmail.com>


License
Expand Down
3 changes: 3 additions & 0 deletions lib/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ module.exports = function svgReactLoader (source) {
var raw = params.raw;
var xmlnsTest = params.xmlnsTest;
var classIdPrefix = params.classIdPrefix || false;
var uniqueIdPrefix = params.uniqueIdPrefix || false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to use the value, after interpolation, of classIdPrefix as a default?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(interpolation will be fixed as soon as #107 is merged, btw)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if that would work as intended.

Using the same prefix would across all instances and files would collide too, no?

It might be more relevant to allow tokenizing within a default string, a la: [name]-[hash]-


context.cacheable();

Expand Down Expand Up @@ -72,6 +73,8 @@ module.exports = function svgReactLoader (source) {
lutils.interpolatename(context, classIdPrefix) :
classIdPrefix;

options.uniqueIdPrefix = uniqueIdPrefix === true ? displayName + '__' : '';

if (params.filters) {
filters =
filters.
Expand Down
8 changes: 8 additions & 0 deletions lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var DEFAULTS = {
'for': 'htmlFor'
},
classIdPrefix: false,
uniqueIdPrefix: true,
raw: false,
xmlnsTest: /^xmlns(Xlink)?$/
};
Expand Down Expand Up @@ -38,6 +39,13 @@ module.exports = function (opts) {
}));
}

if (options.uniqueIdPrefix) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This option sounds and feels like it should default to true

any reasons for not making it default to being on?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I would agree with that. Super busy today, but if I can carve out ten minutes I can update that.

filters.
push(require('./sanitize/filters/unique-svg-ids')({
prefix: options.uniqueIdPrefix
}));
}

if (options.root) {
filters.
push(require('./sanitize/filters/custom-root')(options.root));
Expand Down
68 changes: 68 additions & 0 deletions lib/sanitize/filters/unique-svg-ids.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
var R = require('ramda');
var css = require('css');

// This should be changed, because this isn't going to help
// anyone in the case of a failure to get filename for prefix.
var DEFAULTS = {
prefix: 'filename-prefix__',
};

module.exports = function configureUniquePrefixId (opts) {
var options = R.merge(DEFAULTS, opts || {});
var cache = options.cache = {};
var selectorRegex = /(url\(#)((\w|-)*)(\))/gmi;

// Find the ID reference in items such as: "url(#a)" and return "a"
_getMatches = (field, val) => {
var str = val.toString();
var matches = selectorRegex.exec(str);
selectorRegex.lastIndex = 0;
// clean up and get rid of the quotes
return opts.prefix + matches[2].replace('\"', "");
}

return function createUniquePrefixId (value) {
// Find all the xlink:href props with local references and update
if (value.xlinkHref && value.xlinkHref.toString().startsWith("#")){
var newValue = "#" + opts.prefix + value.xlinkHref.toString().replace("#", "");
value.xlinkHref = newValue;
this.update(value);
}

// Find all href props and update
if (value.href && value.href.toString().startsWith("#")){
var newValue = "#" + opts.prefix + value.href.toString().replace("#", "");
value.href = newValue;
this.update(value);
}

// Find all IDs and update with filename prefix
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of thoughts:

  1. Are these the only attributes that need to be sanitized for the id prefix? I can think of (at least) a couple more: clip-path, use, ?...

  2. There is a bit of repetition going on here for some of the attributes... you might be able to break this down to an array of objects with a test property, and then a operator function property which takes the node reference and other parameters it may need (prefixId, etc...).

By abstracting the update away into separate functions, and operating over a list of definitions, it becomes trivial to handle new cases (just add them to the list).

Bonus Points: Allow for end users to configure their loader with additional tests (exporting the utility operator functions too).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for looking this over and providing the feedback. I will endeavor to get back around to this and resolve as soon as I am able.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree about abstracting items into separate functions. As the old saying goes, "I would have written a shorter letter if I'd had more time". I was able to carve out a few minutes to add a couple of tests and continue the not-abstract, brute force methods for some expanded cases.

Perhaps I will have more time in the future to revisit, refactor, and make sure that all cases of ID reference are handled

Copy link
Copy Markdown

@anagami anagami Mar 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi!
if i try insert svg with some custom filter

<svg xmlns="http://www.w3.org/2000/svg" width="16" height="22" viewBox="0 0 16 22">
    <defs>
        <filter id="a" width="200%" height="200%" x="-50%" y="-50%" filterUnits="objectBoundingBox">
            ....
        </filter>
    </defs>
    <g fill="none" fill-rule="evenodd" filter="url(#a)" transform="matrix(-1 0 0 1 19 -3)">
        ...
    </g>
</svg>

loader convert only <filter id="test__a", but reference to filter - original url <g filter="url(#a)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SilverFox70 created pull request to resolve this issue SilverFox70#5

if (value.id){
var newValue = opts.prefix + value.id;
value.id = newValue;
this.update(value);
}

// Find all fill props and update with filename prefix
if (value.fill && value.fill.toString().startsWith("url")){
var newValue = "url(#" + _getMatches('fill', value.fill) + ")";
value.fill = newValue;
this.update(value);
}

// Find all mask props and update with filename prefix
if (value.mask && value.mask.toString().startsWith("url")){
var newValue = _getMatches('mask', value.mask);
var newValue = "url(#" + _getMatches('fill', value.mask) + ")";
value.mask = newValue;
this.update(value);
}

// Find all clipPath props and update with filename prefix
if (value.clipPath && value.clipPath.toString().startsWith("url")){
var newValue = "url(#" + _getMatches('clipPath', value.clipPath) + ")";
value.clipPath = newValue;
this.update(value);
}
};
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"bugs": {
"url": "https://github.com/jhamlet/svg-react-loader/issues"
},
"homepage": "https://github.com/jhamlet/svg-react-loader#readme",
"homepage": "https://github.com/SilverFox70/svg-react-loader#readme",
"dependencies": {
"css": "2.2.1",
"loader-utils": "1.1.0",
Expand Down
43 changes: 41 additions & 2 deletions test/integration/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import React, { Component } from 'react';
import { mount } from 'enzyme';

import SimpleSvg from '../../lib/loader.js?name=SimpleSvg!../samples/simple.svg';
import StylesSvg from '../../lib/loader.js?classIdPrefix!../samples/styles.svg';
import StylesSvg from '../../lib/loader.js?classIdPrefix&uniqueIdPrefix=true!../samples/styles.svg';
import TextSvg from '../../lib/loader.js!../samples/text.svg';
import ObjectSvg from '../../lib/loader.js!../samples/object.json';
import ClipPathSvg from '../../lib/loader.js?uniqueIdPrefix=true!../samples/clippath.svg';
import UseSvg from "../../lib/loader.js?uniqueIdPrefix=true!../samples/use.svg";

require('should');

Expand Down Expand Up @@ -61,7 +63,7 @@ describe('svg-react-loader', () => {

const expectedProps = {
version: "1.1",
id: "Layer_1",
id: "Styles__Layer_1",
width: "50px",
height: "50px",
x: "0px",
Expand Down Expand Up @@ -142,4 +144,41 @@ describe('svg-react-loader', () => {
be.
true;
});

it('clippath.svg', () => {
const wrapper = mount(<ClipPathSvg />);

const expectedClipPathProps = {
id: "Clippath__myClip"
}

const expectedUseProps = {
clipPath: "url(#Clippath__myClip)",
xlinkHref: "#Clippath__heart",
fill: "red"
}

getPropsMinusChildren(wrapper.find('clipPath')).
should.
eql(expectedClipPathProps);

getPropsMinusChildren(wrapper.find('use')).
should.
eql(expectedUseProps);
});

it('use.svg', () => {
const wrapper = mount(<UseSvg />);

const expectedProps = {
href: "#Use__myCircle",
x: "20",
fill: "white",
stroke: "blue"
}

getPropsMinusChildren(wrapper.find('use')).
should.
eql(expectedProps);
});
});
19 changes: 19 additions & 0 deletions test/samples/clippath.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions test/samples/use.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 80 additions & 0 deletions test/unit/sanitize/filters/unique-id-prefix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*globals describe, it*/
require('should');

describe('svg-react-loader/lib/sanitize/filters/unique-svg-ids', () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can also include a simple test in test/integration/test.js on an actual svg file.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

const traverse = require('traverse');
const prefixFilename =
require('../../../../lib/sanitize/filters/unique-svg-ids')({prefix: 'svgFilename__'});

it('should work on a simple tree', () => {
const tree = {
id: 'a',
fill: 'url(#a)',
mask: 'url(#a)',
xlinkHref: '#a'
};

var result = traverse.map(tree, prefixFilename);

result.
should.
eql({
id: 'svgFilename__a',
fill: 'url(#svgFilename__a)',
mask: 'url(#svgFilename__a)',
xlinkHref: '#svgFilename__a'
});
});

it('should work on a more complex tree', () => {
const tree = {
id: 'a',
fill: 'url(#a)',
mask: 'url(#a)',
xlinkHref: '#a',
props: {
id: 'b',
fill: 'url(#b)',
mask: 'url(#b)',
xlinkHref: '#b'
}
};

const result = traverse.map(tree, prefixFilename);

result.
should.
eql({
id: 'svgFilename__a',
fill: 'url(#svgFilename__a)',
mask: 'url(#svgFilename__a)',
xlinkHref: '#svgFilename__a',
props: {
id: 'svgFilename__b',
fill: 'url(#svgFilename__b)',
mask: 'url(#svgFilename__b)',
xlinkHref: '#svgFilename__b'
}
});
});

it('should not update values for fill, mask, or xlink:href if they are not references to IDs', () => {
const tree = {
id: 'c',
fill: '#fafafa',
mask: '#ae4d19',
xlinkHref: 'http://www.w3.org/1999/xlink'
};

const result = traverse.map(tree, prefixFilename);

result.
should.
eql({
id: 'svgFilename__c',
fill: '#fafafa',
mask: '#ae4d19',
xlinkHref: 'http://www.w3.org/1999/xlink'
});
});
});