diff --git a/CHANGELOG.md b/CHANGELOG.md index 676329f..558aa03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +# Change Log + +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + +# [0.8.0](https://github.com/sairion/svg-inline-loader/compare/0.6.1...v0.8.0) (2017-07-16) + + +### Bug Fixes + +* add missing idPrefix as default value to config, close [#36](https://github.com/sairion/svg-inline-loader/issues/36) ([ba7738d](https://github.com/sairion/svg-inline-loader/commit/ba7738d)) +* corrupted css properties in style tag ([2d28c42](https://github.com/sairion/svg-inline-loader/commit/2d28c42)) +* don't transform webpack2 query objects ([9373e3e](https://github.com/sairion/svg-inline-loader/commit/9373e3e)) +* multiple classes in class string fix ([2024e06](https://github.com/sairion/svg-inline-loader/commit/2024e06)) + + +### Features + +* warnTags and warnTagAttrs ([ada00d7](https://github.com/sairion/svg-inline-loader/commit/ada00d7)) + + + # Changelog ## 0.6.1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8c11fc7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index cf45c35..5285eb5 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,39 @@ -**NOTICE [2016-06-26]: I'm not using or developing this lib anymore. Therefore I'm closing issues (which I cannot handle anymore), but you can send PR to improve or fix problems you facing with this lib.** - -# SVG Inline Loader for Webpack - -This Webpack loader inlines SVG as module. If you use Adobe suite or Sketch to export SVGs, you will get auto-generated, unneeded crusts. This loader removes it for you, too. +[![npm][npm]][npm-url] +[![deps][deps]][deps-url] +[![test][test]][test-url] +[![coverage][cover]][cover-url] +[![chat][chat]][chat-url] + +
+ + + + +

SVG Inline Loader for Webpack

+

This Webpack loader inlines SVG as module. If you use Adobe suite or Sketch to export SVGs, you will get auto-generated, unneeded crusts. This loader removes it for you, too.

+

+ +

Install

+ +```bash +npm install svg-inline-loader --save-dev +``` -## Config +

Configuration

Simply add configuration object to `module.loaders` like this. ```javascript { test: /\.svg$/, - loader: 'svg-inline' + loader: 'svg-inline-loader' } ``` -warning: You should configure this loader only once via `module.loaders` or `require('!...')`. See [#15](https://github.com/sairion/svg-inline-loader/issues/15) for detail. +warning: You should configure this loader only once via `module.loaders` or `require('!...')`. See [#15](https://github.com/webpack-contrib/svg-inline-loader/issues/15) for detail. -### query options +

Query Options

#### `removeTags: boolean` @@ -31,6 +47,12 @@ warning: this won't work unless you specify `removeTags: true` default: `removingTags: ['title', 'desc', 'defs', 'style']` +#### `warnTags: [...string]` + +warns about tags, ex: ['desc', 'defs', 'style'] + +default: `warnTags: []` + #### `removeSVGTagAttrs: boolean` Removes `width` and `height` attributes from ``. @@ -43,22 +65,34 @@ Removes attributes from inside the ``. default: `removingTagAttrs: []` +#### `warnTagAttrs: [...string]` + +Warns to console about attributes from inside the ``. + +default: `warnTagAttrs: []` #### `classPrefix: boolean || string` Adds a prefix to class names to avoid collision across svg files. default: `classPrefix: false` -##### Example Usage +#### `idPrefix: boolean || string` + +Adds a prefix to ids to avoid collision across svg files. + +default: `idPrefix: false` + +

Example Usage

+ ```js // Using default hashed prefix (__[hash:base64:7]__) -var logoTwo = require('svg-inline?classPrefix!./logo_two.svg'); +var logoTwo = require('svg-inline-loader?classPrefix!./logo_two.svg'); // Using custom string -var logoOne = require('svg-inline?classPrefix=my-prefix-!./logo_one.svg'); +var logoOne = require('svg-inline-loader?classPrefix=my-prefix-!./logo_one.svg'); // Using custom string and hash -var logoThree = require('svg-inline?classPrefix=__prefix-[sha512:hash:hex:5]__!./logo_three.svg'); +var logoThree = require('svg-inline-loader?classPrefix=__prefix-[sha512:hash:hex:5]__!./logo_three.svg'); ``` See [loader-utils](https://github.com/webpack/loader-utils#interpolatename) for hash options. @@ -66,12 +100,54 @@ Preferred usage is via a `module.loaders`: ```js { test: /\.svg$/, - loader: 'svg-inline?classPrefix' + loader: 'svg-inline-loader?classPrefix' } ``` -## Notes - -- `` React Component is **DEPRECATED**, use `svg-inline-react` package instead. -- Known problems: - - currently inlining SVG in css is unable. See #22 +

Maintainers

+ + + + + + + + + + +
+ +
+ Juho Vepsäläinen +
+ +
+ Joshua Wiens +
+ +
+ Kees Kluskens +
+ +
+ Sean Larkin +
+ +[npm]: https://img.shields.io/npm/v/svg-inline-loader.svg +[npm-url]: https://npmjs.com/package/svg-inline-loader + +[deps]: https://david-dm.org/webpack-contrib/svg-inline-loader.svg +[deps-url]: https://david-dm.org/webpack-contrib/svg-inline-loader + +[chat]: https://img.shields.io/badge/gitter-webpack%2Fwebpack-brightgreen.svg +[chat-url]: https://gitter.im/webpack/webpack + +[test]: https://travis-ci.org/webpack-contrib/svg-inline-loader.svg?branch=master +[test-url]: https://travis-ci.org/webpack-contrib/svg-inline-loader + +[cover]: https://codecov.io/gh/webpack-contrib/svg-inline-loader/branch/master/graph/badge.svg +[cover-url]: https://codecov.io/gh/webpack-contrib/svg-inline-loader diff --git a/config.js b/config.js index 9c65dcf..5dde54f 100644 --- a/config.js +++ b/config.js @@ -9,4 +9,7 @@ module.exports = { ], removingTagAttrs: [], classPrefix: false, + idPrefix: false, + warnTags: [], + warnTagAttrs: [] }; diff --git a/index.js b/index.js index 45b8242..584d278 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ var simpleHTMLTokenizer = require('simple-html-tokenizer'); var tokenize = simpleHTMLTokenizer.tokenize; var generate = simpleHTMLTokenizer.generate; var loaderUtils = require('loader-utils'); +var assign = require('object-assign'); var conditions = require('./lib/conditions'); var transformer = require('./lib/transformer'); @@ -16,15 +17,26 @@ var regexSequences = [ // SVG XML -> HTML5 [/\<([A-Za-z]+)([^\>]*)\/\>/g, "<$1$2>"], // convert self-closing XML SVG nodes to explicitly closed HTML5 SVG nodes [/\s+/g, " "], // replace whitespace sequences with a single space - [/\> \<"], // remove whitespace between tags + [/\> \<"] // remove whitespace between tags ]; function getExtractedSVG(svgStr, query) { + var config; // interpolate hashes in classPrefix - if(!!query && !!query.classPrefix) { - const name = query.classPrefix === true ? '__[hash:base64:7]__' : query.classPrefix; - query.classPrefix = loaderUtils.interpolateName({}, name, {content: svgStr}); + if(!!query) { + config = assign({}, query); + + if (!!config.classPrefix) { + const name = config.classPrefix === true ? '__[hash:base64:7]__' : config.classPrefix; + config.classPrefix = loaderUtils.interpolateName({}, name, { content: svgStr }); + } + + if (!!config.idPrefix) { + const id_name = config.idPrefix === true ? '__[hash:base64:7]__' : config.idPrefix; + config.idPrefix = loaderUtils.interpolateName({}, id_name, { content: svgStr }); + } } + // Clean-up XML crusts like comments and doctype, etc. var tokens; var cleanedUp = regexSequences.reduce(function (prev, regexSequence) { @@ -41,7 +53,7 @@ function getExtractedSVG(svgStr, query) { } // If the token is start-tag, then remove width and height attributes. - return generate(transformer.runTransform(tokens, query)); + return generate(transformer.runTransform(tokens, config)); } function SVGInlineLoader(content) { diff --git a/karma.conf.js b/karma.conf.js index 352f746..e830121 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -6,8 +6,8 @@ var webpackConf = { loaders: [ { test: /\.json$/, - loaders: ['json'], - }, + loaders: ['json'] + } ] }, entry: [ @@ -15,7 +15,7 @@ var webpackConf = { ], resolve: { extensions: ["", ".js", ".jsx"], - }, + } }; module.exports = function(config) { @@ -23,11 +23,11 @@ module.exports = function(config) { basePath: '.', frameworks: ['mocha'], files: [ - './tests/**/*.js', + './tests/**/*.js' ], exclude: [], preprocessors: { - './tests/**/*.js': ['webpack'], + './tests/**/*.js': ['webpack'] }, reporters: ['spec'], port: 9876, @@ -39,7 +39,7 @@ module.exports = function(config) { 'karma-spec-reporter', 'karma-mocha', 'karma-chrome-launcher', - 'karma-webpack', + 'karma-webpack' ], webpack: webpackConf, autoWatch: true, diff --git a/lib/conditions.js b/lib/conditions.js index 8f049fa..9157319 100644 --- a/lib/conditions.js +++ b/lib/conditions.js @@ -26,11 +26,18 @@ function createHasNoAttributes(attributes) { } } +function createHasAttributes(attributes) { + return function hasAttributes(attributeToken) { + return attributes.indexOf(attributeToken[0]) > -1; + } +} + module.exports = { isSVGToken: isSVGToken, isStyleToken: isStyleToken, isFilledObject: isFilledObject, hasNoWidthHeight: hasNoWidthHeight, createHasNoAttributes: createHasNoAttributes, + createHasAttributes: createHasAttributes, isStartTag: isStartTag }; diff --git a/lib/transformer.js b/lib/transformer.js index 71bb26a..cef721d 100644 --- a/lib/transformer.js +++ b/lib/transformer.js @@ -18,13 +18,32 @@ function createRemoveTagAttrs(removingTagAttrs) { tag.attributes = tag.attributes.filter(hasNoAttributes); } return tag; - } + }; +} +function createWarnTagAttrs(warnTagAttrs) { + warnTagAttrs = warnTagAttrs || []; + var hasNoAttributes = conditions.createHasAttributes(warnTagAttrs); + return function warnTagAttrs(tag) { + if (conditions.isStartTag(tag)) { + var attrs=tag.attributes.filter(hasNoAttributes); + if(attrs.length > 0) { + var attrList=[]; + for(var i=0;i -1; } - +function isWarningTag(warningTags, tag) { + return warningTags.indexOf(tag.tagName) > -1; +} // FIXME: Due to limtation of parser, we need to implement our // very own little state machine to express tree structure @@ -43,9 +62,19 @@ function createRemoveTags(removingTags) { // Reached the end tag of a removingTag removingTag = null; } - } + }; } +function createWarnTags(warningTags) { + warningTags = warningTags || []; + + return function warnTags(tag) { + if (conditions.isStartTag(tag) && isWarningTag(warningTags, tag)) { + console.warn('svg-inline-loader: forbidden tag ' + tag.tagName); + } + return tag; + }; +} function getAttributeIndex (tag, attr) { if( tag.attributes !== undefined && tag.attributes.length > 0 ) { for(var i = 0; i < tag.attributes.length; i++) { @@ -58,7 +87,8 @@ function getAttributeIndex (tag, attr) { } function createClassPrefix(classPrefix) { - var re = /\.[\w\-]+/g; + //http://stackoverflow.com/questions/12391760/regex-match-css-class-name-from-single-string-containing-multiple-classes + var re = /\.(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)(?![^\{]*\})/g; var inStyleTag = false; return function prefixClasses(tag) { @@ -83,11 +113,55 @@ function createClassPrefix(classPrefix) { else { var classIdx = getAttributeIndex(tag,'class'); if(classIdx >= 0) { - tag.attributes[classIdx][1] = classPrefix + tag.attributes[classIdx][1]; + //Prefix classes when multiple classes are present + var classes = tag.attributes[classIdx][1]; + var prefixedClassString = ""; + + classes = classes.replace(/[ ]+/,' '); + classes = classes.split(' '); + classes.forEach(function(classI){ + prefixedClassString += classPrefix + classI + ' '; + }); + + tag.attributes[classIdx][1] = prefixedClassString; } } return tag; - } + }; +} + +function createIdPrefix(idPrefix) { + var url_pattern = /^url\(#.+\)$/i; + return function prefixIds(tag) { + var idIdx = getAttributeIndex(tag, 'id'); + if (idIdx !== -1) { + // prefix id definitions + tag.attributes[idIdx][1] = idPrefix + tag.attributes[idIdx][1]; + } + + if (tag.tagName == 'use') { + // replace references via + var hrefIdx = getAttributeIndex(tag, 'xlink:href'); + if (hrefIdx !== -1) { + tag.attributes[hrefIdx][1] = '#' + idPrefix + tag.attributes[hrefIdx][1].substring(1); + + } + } + if (tag.attributes && tag.attributes.length > 0) { + // replace instances of url(#foo) in attributes + tag.attributes.forEach(function (attr) { + if (attr[1].match(url_pattern)) { + attr[1] = attr[1].replace(url_pattern, function (match) { + var id = match.substring(5, match.length -1); + return "url(#" + idPrefix + id + ")"; + }); + } + + }); + } + + return tag; + }; } function runTransform(tokens, configOverride) { @@ -95,8 +169,11 @@ function runTransform(tokens, configOverride) { var config = conditions.isFilledObject(configOverride) ? assign({}, defaultConfig, configOverride) : defaultConfig; if (config.classPrefix !== false) transformations.push(createClassPrefix(config.classPrefix)); + if (config.idPrefix !== false) transformations.push(createIdPrefix(config.idPrefix)); if (config.removeSVGTagAttrs === true) transformations.push(removeSVGTagAttrs); + if (config.warnTags.length > 0) transformations.push(createWarnTags(config.warnTags)); if (config.removeTags === true) transformations.push(createRemoveTags(config.removingTags)); + if (config.warnTagAttrs.length > 0) transformations.push(createWarnTagAttrs(config.warnTagAttrs)); if (config.removingTagAttrs.length > 0) transformations.push(createRemoveTagAttrs(config.removingTagAttrs)); transformations.forEach(function (transformation) { diff --git a/package.json b/package.json index 35e23f0..d6771dc 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,14 @@ { "name": "svg-inline-loader", - "version": "0.6.1", + "version": "0.8.0", "description": "Cleans up and inlines your SVG files into Webpack module.", - "main": "index.js", - "scripts": { - "test": "karma start" - }, "author": "Jaeho Lee ", "license": "MIT", - "repository": { - "type": "git", - "url": "git@github.com:sairion/svg-inline-loader.git" - }, - "keywords": [ - "svg", - "webpack", - "react", - "loader" - ], - "bugs": { - "url": "https://github.com/sairion/svg-inline-loader/issues" + "main": "index.js", + "scripts": { + "test": "karma start", + "release": "standard-version" }, - "homepage": "https://github.com/sairion/svg-inline-loader", "dependencies": { "loader-utils": "^0.2.11", "object-assign": "^4.0.1", @@ -29,6 +16,7 @@ }, "devDependencies": { "chai": "^3.0.0", + "chai-spies": "^0.7.1", "json-loader": "^0.5.4", "karma": "^1.0.0", "karma-chrome-launcher": "^1.0.1", @@ -39,6 +27,21 @@ "mocha": "^2.5.3", "node-libs-browser": "^1.0.0", "raw-loader": "^0.5.1", + "standard-version": "^4.2.0", "webpack": "^1.13.1" - } + }, + "repository": { + "type": "git", + "url": "git@github.com:sairion/svg-inline-loader.git" + }, + "bugs": { + "url": "https://github.com/sairion/svg-inline-loader/issues" + }, + "homepage": "https://github.com/sairion/svg-inline-loader", + "keywords": [ + "svg", + "webpack", + "react", + "loader" + ] } diff --git a/tests/fixtures/with-ids.svg b/tests/fixtures/with-ids.svg new file mode 100644 index 0000000..822b61c --- /dev/null +++ b/tests/fixtures/with-ids.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tests/svg-inline-loader.test.js b/tests/svg-inline-loader.test.js index 36a9e1e..676440c 100644 --- a/tests/svg-inline-loader.test.js +++ b/tests/svg-inline-loader.test.js @@ -2,7 +2,12 @@ var simpleHTMLTokenizer = require('simple-html-tokenizer'); var tokenize = simpleHTMLTokenizer.tokenize; var SVGInlineLoader = require('../index'); -var assert = require('chai').assert; +var chai = require('chai'); +var assert = chai.assert; +var expect = chai.expect; +var spies = require('chai-spies'); +chai.use(spies); +var createSpy = chai.spy; var _ = require('lodash'); var svgWithRect = require('raw!./fixtures/xml-rect.svg'); @@ -47,6 +52,18 @@ describe('getExtractedSVG()', function(){ assert.isTrue( processedStyleInsertedSVG.match(/class="test\.prefix-/g).length === 1 ); }); + it('should apply prefixes to ids', function () { + var svgWithStyle = require('raw!./fixtures/with-ids.svg'); + var processedStyleInsertedSVG = SVGInlineLoader.getExtractedSVG(svgWithStyle, { idPrefix: 'test.prefix-' }); + + + assert.isTrue( processedStyleInsertedSVG.match(/test\.prefix-foo/g).length === 3 ); + // // replaces xlink:href= + assert.isTrue( processedStyleInsertedSVG.match(/xlink:href=/g).length === 1 ); + // // replaces url(#foo) + assert.isTrue( processedStyleInsertedSVG.match(/url\(#test\.prefix-foo\)/g).length === 1 ); + }); + it('should be able to specify tags to be removed by `removingTags` option', function () { var svgRemovingTags = require('raw!./fixtures/removing-tags.svg'); var tobeRemoved = require('./fixtures/removing-tags-to-be-removed.json'); @@ -105,4 +122,34 @@ describe('getExtractedSVG()', function(){ } }); }); + it('should be able to warn about tagsAttrs to be removed listed in `warnTagAttrs` option via console.log', function () { + var svg = require('raw!./fixtures/with-ids.svg'); + var tobeWarned = ['id']; + var oldConsoleWarn = console.warn; + var warnings=[]; + console.warn=createSpy(function (str) { + warnings.push(str); + }); + var processedSVG = SVGInlineLoader.getExtractedSVG(svg, { warnTagAttrs: tobeWarned }); + var reTokenizedSVG = tokenize(processedSVG); + expect(console.warn).to.have.been.called.with('svg-inline-loader: tag path has forbidden attrs: id'); + console.warn = oldConsoleWarn; // reset console back + }); + + it('should be able to specify tags to be warned about by `warnTags` option', function () { + var svg = require('raw!./fixtures/removing-tags.svg'); + var tobeWarnedAbout = ['title', 'desc', 'defs', 'style', 'image']; + var oldConsoleWarn = console.warn; + var warnings=[]; + console.warn=createSpy(function (str) { + warnings.push(str); + }); + var processedStyleInsertedSVG = SVGInlineLoader.getExtractedSVG(svg, { warnTags: tobeWarnedAbout }); + var reTokenizedStyleInsertedSVG = tokenize(processedStyleInsertedSVG); + + expect(console.warn).to.have.been.called(); + expect(console.warn).to.have.been.called.min(3); + expect(console.warn).to.have.been.called.with('svg-inline-loader: forbidden tag style'); + console.warn = oldConsoleWarn; // reset console back + }); });