diff --git a/README.md b/README.md index db13226..4fd2e59 100644 --- a/README.md +++ b/README.md @@ -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 @@ -30,7 +38,6 @@ are acted on by the loader version of `svg-react`. Installation ------------ - ~~~ % npm install --save-dev svg-react-loader ~~~ @@ -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. @@ -121,6 +132,7 @@ module: { loader: 'svg-react-loader', query: { classIdPrefix: '[name]-[hash:8]__', + uniqueIdPrefix: true, filters: [ function (value) { // ... @@ -204,6 +216,7 @@ Report an Issue * [Bugs](http://github.com/jhamlet/svg-react-loader/issues) * Contact the author: +* For issues with the generation of unique ID prefixes, please contact License diff --git a/lib/loader.js b/lib/loader.js index 1333e5e..fd2262d 100644 --- a/lib/loader.js +++ b/lib/loader.js @@ -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(); @@ -72,6 +73,8 @@ module.exports = function svgReactLoader (source) { lutils.interpolatename(context, classIdPrefix) : classIdPrefix; + options.uniqueIdPrefix = uniqueIdPrefix === true ? displayName + '__' : ''; + if (params.filters) { filters = filters. diff --git a/lib/options.js b/lib/options.js index 0e15cf7..9623eac 100644 --- a/lib/options.js +++ b/lib/options.js @@ -8,6 +8,7 @@ var DEFAULTS = { 'for': 'htmlFor' }, classIdPrefix: false, + uniqueIdPrefix: true, raw: false, xmlnsTest: /^xmlns(Xlink)?$/ }; @@ -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)); diff --git a/lib/sanitize/filters/unique-svg-ids.js b/lib/sanitize/filters/unique-svg-ids.js new file mode 100644 index 0000000..964eb53 --- /dev/null +++ b/lib/sanitize/filters/unique-svg-ids.js @@ -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); + } + }; +}; diff --git a/package.json b/package.json index 7f01cdd..4322d31 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/integration/test.js b/test/integration/test.js index 0cdf9c8..24f1598 100644 --- a/test/integration/test.js +++ b/test/integration/test.js @@ -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'); @@ -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", @@ -142,4 +144,41 @@ describe('svg-react-loader', () => { be. true; }); + + it('clippath.svg', () => { + const wrapper = mount(); + + 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(); + + const expectedProps = { + href: "#Use__myCircle", + x: "20", + fill: "white", + stroke: "blue" + } + + getPropsMinusChildren(wrapper.find('use')). + should. + eql(expectedProps); + }); }); diff --git a/test/samples/clippath.svg b/test/samples/clippath.svg new file mode 100644 index 0000000..eb5999e --- /dev/null +++ b/test/samples/clippath.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/samples/use.svg b/test/samples/use.svg new file mode 100644 index 0000000..a41e800 --- /dev/null +++ b/test/samples/use.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/test/unit/sanitize/filters/unique-id-prefix.js b/test/unit/sanitize/filters/unique-id-prefix.js new file mode 100644 index 0000000..b371761 --- /dev/null +++ b/test/unit/sanitize/filters/unique-id-prefix.js @@ -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' + }); + }); +});