Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unique Prefix ID from svg filename to prevent svg id collision #3

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: <[email protected]>
* For issues with the generation of unique ID prefixes, please contact <[email protected]>


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;

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) {
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
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', () => {
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'
});
});
});