diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..e7e4dbd --- /dev/null +++ b/.babelrc @@ -0,0 +1,35 @@ +{ + "presets": [ + [ + "env", + { + "useBuiltIns": true, + "targets": { + "node": "4.3" + }, + "exclude": [ + "transform-async-to-generator", + "transform-regenerator" + ] + } + ] + ], + "plugins": [ + [ + "transform-object-rest-spread", + { + "useBuiltIns": true + } + ] + ], + "env": { + "test": { + "presets": [ + "env" + ], + "plugins": [ + "transform-object-rest-spread" + ] + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..28e1806 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# editorconfig.org + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..b2d59d1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +/node_modules +/dist \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..3f5ed69 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,18 @@ +{ + "extends": "webpack", + "rules": { + "no-underscore-dangle": [1, { "allowAfterThis": true }], + "no-plusplus": 1, + "consistent-return": 1, + "no-multi-assign": 1, + "no-param-reassign": 1, + "prefer-destructuring": 1, + "no-nested-ternary": 1, + "prefer-rest-params": 1, + "import/no-unresolved": 1, + "import/extensions": 1, + "no-useless-escape": 1, + "no-undefined": 1, + "no-cond-assign": 1 + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c5aa452 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +yarn.lock -diff +* text=auto +bin/* eol=lf +package-lock.json -diff \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..4601e18 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,7 @@ + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..cadb1bf --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ + diff --git a/.gitignore b/.gitignore index 96fa2a2..afba0cd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,19 @@ npm-debug.log # IDE .idea *.iml + +logs +*.log +npm-debug.log* +yarn-debug.log* +.eslintcache +/coverage +/dist +/local +/reports +/node_modules +.DS_Store +Thumbs.db +.vscode +*.sublime-project +*.sublime-workspace \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..bcccf74 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,48 @@ +sudo: false +dist: trusty +language: node_js +branches: + only: + - master +jobs: + fast_finish: true + allow_failures: + - env: WEBPACK_VERSION=canary + include: + - &test-latest + stage: Webpack latest + node_js: 6 + env: WEBPACK_VERSION=latest JOB_PART=test + script: npm run travis:$JOB_PART + - <<: *test-latest + node_js: 4.3 + env: WEBPACK_VERSION=latest JOB_PART=test + script: npm run travis:$JOB_PART + - <<: *test-latest + node_js: 8 + env: WEBPACK_VERSION=latest JOB_PART=lint + script: npm run travis:$JOB_PART + - <<: *test-latest + node_js: 8 + env: WEBPACK_VERSION=latest JOB_PART=coverage + script: npm run travis:$JOB_PART + after_success: 'bash <(curl -s https://codecov.io/bash)' + - stage: Webpack canary + before_script: npm i --no-save git://github.com/webpack/webpack.git#master + script: npm run travis:$JOB_PART + node_js: 8 + env: WEBPACK_VERSION=canary JOB_PART=test +before_install: + - 'if [[ `npm -v` != 5* ]]; then npm i -g npm@^5.0.0; fi' + - nvm --version + - node --version + - npm --version +before_script: + - |- + if [ "$WEBPACK_VERSION" ]; then + npm i --no-save webpack@$WEBPACK_VERSION + fi +script: + - 'npm run travis:$JOB_PART' +after_success: + - 'bash <(curl -s https://codecov.io/bash)' diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..aa4f18a --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,28 @@ +branches: + only: + - master +init: + - git config --global core.autocrlf input +environment: + matrix: + - nodejs_version: '8' + webpack_version: latest + job_part: test + - nodejs_version: '6' + webpack_version: latest + job_part: test + - nodejs_version: '4.3' + webpack_version: latest + job_part: test +build: 'off' +matrix: + fast_finish: true +install: + - ps: Install-Product node $env:nodejs_version x64 + - npm install +before_test: + - cmd: npm install webpack@%webpack_version% +test_script: + - node --version + - npm --version + - cmd: npm run appveyor:%job_part% diff --git a/config.js b/config.js deleted file mode 100644 index 5dde54f..0000000 --- a/config.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - removeSVGTagAttrs: true, - removeTags: false, - removingTags: [ - 'title', - 'desc', - 'defs', - 'style' - ], - removingTagAttrs: [], - classPrefix: false, - idPrefix: false, - warnTags: [], - warnTagAttrs: [] -}; diff --git a/index.js b/index.js deleted file mode 100644 index 584d278..0000000 --- a/index.js +++ /dev/null @@ -1,72 +0,0 @@ -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'); - -// TODO: find better parser/tokenizer -var regexSequences = [ - // Remove XML stuffs and comments - [/<\?xml[\s\S]*?>/gi, ""], - [//gi, ""], - [//gi, ""], - - // 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 -]; - -function getExtractedSVG(svgStr, query) { - var config; - // interpolate hashes in classPrefix - 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) { - return ''.replace.apply(prev, regexSequence); - }, svgStr).trim(); - - // Tokenize and filter attributes using `simpleHTMLTokenizer.tokenize(source)`. - try { - tokens = tokenize(cleanedUp); - } catch (e) { - // If tokenization has failed, return earlier with cleaned-up string - console.warn('svg-inline-loader: Tokenization has failed, please check SVG is correct.'); - return cleanedUp; - } - - // If the token is start-tag, then remove width and height attributes. - return generate(transformer.runTransform(tokens, config)); -} - -function SVGInlineLoader(content) { - this.cacheable && this.cacheable(); - this.value = content; - // Configuration - var query = loaderUtils.parseQuery(this.query); - - return "module.exports = " + JSON.stringify(getExtractedSVG(content, query)); -} - -SVGInlineLoader.getExtractedSVG = getExtractedSVG; -SVGInlineLoader.conditions = conditions; -SVGInlineLoader.regexSequences = regexSequences; - -module.exports = SVGInlineLoader; diff --git a/karma.conf.js b/karma.conf.js deleted file mode 100644 index e830121..0000000 --- a/karma.conf.js +++ /dev/null @@ -1,55 +0,0 @@ -var webpackConf = { - cache: true, - debug: true, - devtool: 'inline-source-map', - module: { - loaders: [ - { - test: /\.json$/, - loaders: ['json'] - } - ] - }, - entry: [ - './index.js' - ], - resolve: { - extensions: ["", ".js", ".jsx"], - } -}; - -module.exports = function(config) { - var conf = { - basePath: '.', - frameworks: ['mocha'], - files: [ - './tests/**/*.js' - ], - exclude: [], - preprocessors: { - './tests/**/*.js': ['webpack'] - }, - reporters: ['spec'], - port: 9876, - colors: true, - logLevel: config.LOG_DEBUG, - browsers: ['Chrome'], - browserNoActivityTimeout: 100000, - plugins: [ - 'karma-spec-reporter', - 'karma-mocha', - 'karma-chrome-launcher', - 'karma-webpack' - ], - webpack: webpackConf, - autoWatch: true, - singleRun: false - }; - - if (process.env.NODE_ENV === 'CI') { - conf.autoWatch = false; - conf.singleRun = true; - } - - config.set(conf); -}; diff --git a/lib/component.jsx b/lib/component.jsx deleted file mode 100644 index 398639b..0000000 --- a/lib/component.jsx +++ /dev/null @@ -1,36 +0,0 @@ -var React = require('react'); -var assign = require('object-assign'); - - -// DEPRECATED. Please use `svg-inline-react` package. -// -// -// -console.warn('` React Component is DEPRECATED -> Use `svg-inline-react` package instead.'); - -var IconSVG = React.createClass({ - getDefaultProps: function getDefaultProps() { - return { - elementName: 'i', - defaultClassName: 'icon-svg' - }; - }, - propTypes: { - src: React.PropTypes.string.isRequired, - elementName: React.PropTypes.string - }, - render: function render() { - var props = assign({}, this.props, - { - src: null, - dangerouslySetInnerHTML: { __html: this.props.src }, - className: (typeof this.props.className === 'string') ? this.props.className + ' ' + this.props.defaultClassName : - this.props.defaultClassName - } - ); - - return React.createElement(this.props.elementName, props); - } -}); - -module.exports = IconSVG; diff --git a/lib/conditions.js b/lib/conditions.js deleted file mode 100644 index 9157319..0000000 --- a/lib/conditions.js +++ /dev/null @@ -1,43 +0,0 @@ -function isStartTag (tag) { - return tag !== undefined && tag.type === 'StartTag'; -} - -function isSVGToken (tag) { - return isStartTag(tag) && tag.tagName === 'svg'; -} - -function isStyleToken (tag) { - return isStartTag(tag) && tag.tagName === 'style'; -} - -function isFilledObject (obj) { - return obj != null && - typeof obj === 'object' && - Object.keys(obj).length !== 0; -} - -function hasNoWidthHeight(attributeToken) { - return attributeToken[0] !== 'width' && attributeToken[0] !== 'height'; -} - -function createHasNoAttributes(attributes) { - return function hasNoAttributes(attributeToken) { - return attributes.indexOf(attributeToken[0]) === -1; - } -} - -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 deleted file mode 100644 index cef721d..0000000 --- a/lib/transformer.js +++ /dev/null @@ -1,191 +0,0 @@ -var assign = require('object-assign'); - -var conditions = require('./conditions'); -var defaultConfig = require('../config'); - -function removeSVGTagAttrs(tag) { - if (conditions.isSVGToken(tag)) { - tag.attributes = tag.attributes.filter(conditions.hasNoWidthHeight); - } - return tag; -} - -function createRemoveTagAttrs(removingTagAttrs) { - removingTagAttrs = removingTagAttrs || []; - var hasNoAttributes = conditions.createHasNoAttributes(removingTagAttrs); - return function removeTagAttrs(tag) { - if (conditions.isStartTag(tag)) { - 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 - -function createRemoveTags(removingTags) { - removingTags = removingTags || []; - var removingTag = null; - - return function removeTags(tag) { - if (removingTag == null) { - if (isRemovingTag(removingTags, tag)) { - removingTag = tag.tagName; - } else { - return tag; - } - } else if (tag.tagName === removingTag && tag.type === 'EndTag') { - // 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++) { - if(tag.attributes[i][0] === attr) { - return i; - } - } - } - return -1; -} - -function createClassPrefix(classPrefix) { - //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) { - if( inStyleTag ) { - var string = tag.chars; - // push matches to an array so we can operate in reverse - var match; - var matches = []; - while(match = re.exec(string)) matches.push(match); - // update the string in reverse so our matches indices don't get off - for(var i = matches.length-1; i>=0; i--) { - string = string.substring(0,matches[i].index+1) + - classPrefix + - string.substring(matches[i].index+1); - } - tag.chars = string; - inStyleTag = false; - } - else if (conditions.isStyleToken(tag)) { - inStyleTag = true; - } - else { - var classIdx = getAttributeIndex(tag,'class'); - if(classIdx >= 0) { - //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) { - var transformations = []; - 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) { - tokens = tokens.map(transformation); - }); - - return tokens.filter(function (nonNull) { return nonNull; }); -} - -module.exports = { - removeSVGTagAttrs: removeSVGTagAttrs, - createRemoveTags: createRemoveTags, - createClassPrefix: createClassPrefix, - runTransform: runTransform -}; diff --git a/package.json b/package.json index d6771dc..ffe37b9 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,68 @@ { "name": "svg-inline-loader", - "version": "0.8.0", + "version": "0.7.1", "description": "Cleans up and inlines your SVG files into Webpack module.", "author": "Jaeho Lee ", "license": "MIT", - "main": "index.js", + "main": "dist/cjs.js", + "files": [ + "dist" + ], "scripts": { - "test": "karma start", - "release": "standard-version" + "test": "jest", + "test:integration": "karma start", + "webpack-defaults": "webpack-defaults", + "start": "npm run build -- -w", + "build": "cross-env NODE_ENV=production babel src -d dist --ignore 'src/**/*.test.js'", + "clean:dist": "del-cli dist", + "lint": "eslint --cache src test", + "lint-staged": "lint-staged", + "prebuild": "npm run clean", + "prepublish": "npm run build", + "release": "standard-version", + "security": "nsp check", + "serve:dev": "nodemon $2 --exec babel-node", + "test:watch": "jest --watch", + "test:coverage": "jest --collectCoverageFrom='src/**/*.js' --coverage", + "travis:coverage": "npm run test:coverage -- --runInBand", + "travis:lint": "npm run lint && npm run security", + "travis:test": "npm run test -- --runInBand", + "appveyor:test": "npm run test", + "clean": "del-cli dist" }, "dependencies": { - "loader-utils": "^0.2.11", - "object-assign": "^4.0.1", + "loader-utils": "^1.1.0", "simple-html-tokenizer": "^0.1.1" }, "devDependencies": { - "chai": "^3.0.0", - "chai-spies": "^0.7.1", + "babel-cli": "^6.24.0", + "babel-jest": "^20.0.3", + "babel-plugin-transform-object-rest-spread": "^6.23.0", + "babel-polyfill": "^6.23.0", + "babel-preset-env": "^1.6.0", + "cross-env": "^5.0.1", + "del-cli": "^1.1.0", + "eslint": "^3.18.0", + "eslint-config-webpack": "^1.2.0", + "eslint-plugin-import": "^2.7.0", + "jest": "^20.0.4", "json-loader": "^0.5.4", - "karma": "^1.0.0", - "karma-chrome-launcher": "^1.0.1", - "karma-mocha": "^1.0.1", - "karma-spec-reporter": "0.0.26", - "karma-webpack": "^1.5.1", + "lint-staged": "^3.4.0", "lodash": "^4.6.1", - "mocha": "^2.5.3", "node-libs-browser": "^1.0.0", + "nodemon": "^1.11.0", + "nsp": "^2.6.3", + "pre-commit": "^1.2.2", "raw-loader": "^0.5.1", "standard-version": "^4.2.0", - "webpack": "^1.13.1" + "webpack": "^3.2.0", + "webpack-defaults": "^1.5.0" + }, + "peerDependencies": { + "webpack": "^2.0.0 || ^3.0.0" + }, + "engines": { + "node": ">= 4.3 < 5.0.0 || >= 5.10" }, "repository": { "type": "git", @@ -43,5 +77,12 @@ "webpack", "react", "loader" - ] + ], + "pre-commit": "lint-staged", + "lint-staged": { + "*.js": [ + "eslint --fix", + "git add" + ] + } } diff --git a/src/cjs.js b/src/cjs.js new file mode 100644 index 0000000..82657ce --- /dev/null +++ b/src/cjs.js @@ -0,0 +1 @@ +module.exports = require('./index').default; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..4b16b11 --- /dev/null +++ b/src/index.js @@ -0,0 +1,72 @@ +import simpleHTMLTokenizer from 'simple-html-tokenizer'; +import loaderUtils from 'loader-utils'; +import conditions from './lib/conditions'; +import transformer from './lib/transformer'; + +const tokenize = simpleHTMLTokenizer.tokenize; +const generate = simpleHTMLTokenizer.generate; + +// TODO: find better parser/tokenizer +const regexSequences = [ + // Remove XML stuffs and comments + [/<\?xml[\s\S]*?>/gi, ''], + [//gi, ''], + [//gi, ''], + + // SVG XML -> HTML5 + // convert self-closing XML SVG nodes to explicitly closed HTML5 SVG nodes + [/\<([A-Za-z]+)([^\>]*)\/\>/g, '<$1$2>'], + // replace whitespace sequences with a single space + [/\s+/g, ' '], + // remove whitespace between tags + [/\> \<'], +]; + +function getExtractedSVG(svgStr, query) { + let config; + // interpolate hashes in classPrefix + if (!query) { + config = Object.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 idName = config.idPrefix === true ? '__[hash:base64:7]__' : config.idPrefix; + config.idPrefix = loaderUtils.interpolateName({}, idName, { content: svgStr }); + } + } + + // Clean-up XML crusts like comments and doctype, etc. + let tokens; + const cleanedUp = regexSequences.reduce((prev, regexSequence) => ''.replace.apply(prev, regexSequence), svgStr).trim(); + + // Tokenize and filter attributes using `simpleHTMLTokenizer.tokenize(source)`. + try { + tokens = tokenize(cleanedUp); + } catch (e) { + // If tokenization has failed, return earlier with cleaned-up string + console.warn('svg-inline-loader: Tokenization has failed, please check SVG is correct.'); + return cleanedUp; + } + + // If the token is start-tag, then remove width and height attributes. + return generate(transformer.runTransform(tokens, config)); +} + +function SVGInlineLoader(content) { + this.cacheable && this.cacheable(); // eslint-disable-line no-unused-expressions + this.value = content; + // Configuration + const query = loaderUtils.getOptions(this) || {}; + + return `module.exports = ${JSON.stringify(getExtractedSVG(content, query))}`; +} + +SVGInlineLoader.getExtractedSVG = getExtractedSVG; +SVGInlineLoader.conditions = conditions; +SVGInlineLoader.regexSequences = regexSequences; + +export default SVGInlineLoader; diff --git a/src/lib/conditions.js b/src/lib/conditions.js new file mode 100644 index 0000000..297c818 --- /dev/null +++ b/src/lib/conditions.js @@ -0,0 +1,43 @@ +function isStartTag(tag) { + return tag !== undefined && tag.type === 'StartTag'; +} + +function isSVGToken(tag) { + return isStartTag(tag) && tag.tagName === 'svg'; +} + +function isStyleToken(tag) { + return isStartTag(tag) && tag.tagName === 'style'; +} + +function isFilledObject(obj) { + return obj != null && + typeof obj === 'object' && + Object.keys(obj).length !== 0; +} + +function hasNoWidthHeight(attributeToken) { + return attributeToken[0] !== 'width' && attributeToken[0] !== 'height'; +} + +function createHasNoAttributes(attributes) { + return function hasNoAttributes(attributeToken) { + return !attributes.includes(attributeToken[0]); + }; +} + +function createHasAttributes(attributes) { + return function hasAttributes(attributeToken) { + return attributes.includes(attributeToken[0]); + }; +} + +export default { + isSVGToken, + isStyleToken, + isFilledObject, + hasNoWidthHeight, + createHasNoAttributes, + createHasAttributes, + isStartTag, +}; diff --git a/src/lib/config.js b/src/lib/config.js new file mode 100644 index 0000000..52b5529 --- /dev/null +++ b/src/lib/config.js @@ -0,0 +1,15 @@ +export default { + removeSVGTagAttrs: true, + removeTags: false, + removingTags: [ + 'title', + 'desc', + 'defs', + 'style', + ], + removingTagAttrs: [], + classPrefix: false, + idPrefix: false, + warnTags: [], + warnTagAttrs: [], +}; diff --git a/src/lib/transformer.js b/src/lib/transformer.js new file mode 100644 index 0000000..6989f84 --- /dev/null +++ b/src/lib/transformer.js @@ -0,0 +1,181 @@ +import defaultConfig from './config'; +import conditions from './conditions'; + +function removeSVGTagAttrs(tag) { + if (conditions.isSVGToken(tag)) { + tag.attributes = tag.attributes.filter(conditions.hasNoWidthHeight); + } + return tag; +} + +function createRemoveTagAttrs(removingTagAttrs = []) { + const hasNoAttributes = conditions.createHasNoAttributes(removingTagAttrs); + return function removeTagAttrs(tag) { + if (conditions.isStartTag(tag)) { + tag.attributes = tag.attributes.filter(hasNoAttributes); + } + return tag; + }; +} +function createWarnTagAttrs(warnTagAttrs = []) { + const hasNoAttributes = conditions.createHasAttributes(warnTagAttrs); + return function warnTagAttrs(tag) { + if (conditions.isStartTag(tag)) { + const attrs = tag.attributes.filter(hasNoAttributes); + if (attrs.length > 0) { + const attrList = []; + + for (const attr of attrs) { + attrList.push(attr[0]); + } + + console.warn(`svg-inline-loader: tag ${tag.tagName} has forbidden attrs: ${attrList.join(', ')}`); + } + } + return tag; + }; +} +function isRemovingTag(removingTags, tag) { + return removingTags.includes(tag.tagName); +} +function isWarningTag(warningTags, tag) { + return warningTags.includes(tag.tagName); +} +// FIXME: Due to limtation of parser, we need to implement our +// very own little state machine to express tree structure + +function createRemoveTags(removingTags = []) { + let removingTag = null; + + return function removeTags(tag) { + if (removingTag == null) { + if (isRemovingTag(removingTags, tag)) { + removingTag = tag.tagName; + } else { + return tag; + } + } else if (tag.tagName === removingTag && tag.type === 'EndTag') { + // Reached the end tag of a removingTag + removingTag = null; + } + }; +} + +function createWarnTags(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 (let i = 0; i < tag.attributes.length; i++) { + if (tag.attributes[i][0] === attr) { + return i; + } + } + } + return -1; +} + +function createClassPrefix(classPrefix) { + // http://stackoverflow.com/questions/12391760/regex-match-css-class-name-from-single-string-containing-multiple-classes + const re = /\.(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)(?![^\{]*\})/g; + let inStyleTag = false; + + return function prefixClasses(tag) { + if (inStyleTag) { + let string = tag.chars; + // push matches to an array so we can operate in reverse + let match; + const matches = []; + while (match = re.exec(string)) matches.push(match); + // update the string in reverse so our matches indices don't get off + for (let i = matches.length - 1; i >= 0; i--) { + string = string.substring(0, matches[i].index + 1) + + classPrefix + + string.substring(matches[i].index + 1); + } + tag.chars = string; + inStyleTag = false; + } else if (conditions.isStyleToken(tag)) { + inStyleTag = true; + } else { + const classIdx = getAttributeIndex(tag, 'class'); + if (classIdx >= 0) { + // Prefix classes when multiple classes are present + let classes = tag.attributes[classIdx][1]; + let prefixedClassString = ''; + + classes = classes.replace(/[ ]+/, ' '); + classes = classes.split(' '); + classes.forEach((classI) => { + prefixedClassString += `${classPrefix + classI} `; + }); + + tag.attributes[classIdx][1] = prefixedClassString; + } + } + return tag; + }; +} + +function createIdPrefix(idPrefix) { + const urlPattern = /^url\(#.+\)$/i; + return function prefixIds(tag) { + const 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 + const 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((attr) => { + if (attr[1].match(urlPattern)) { + attr[1] = attr[1].replace(urlPattern, (match) => { + const id = match.substring(5, match.length - 1); + return `url(#${idPrefix}${id})`; + }); + } + }); + } + + return tag; + }; +} + +function runTransform(tokens, configOverride) { + const transformations = []; + const config = conditions.isFilledObject(configOverride) ? Object.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((transformation) => { + tokens = tokens.map(transformation); + }); + + return tokens.filter(nonNull => nonNull); +} + +export default { + removeSVGTagAttrs, + createRemoveTags, + createClassPrefix, + runTransform, +}; diff --git a/tests/fixtures/removing-attrs-to-be-removed.json b/test/fixtures/removing-attrs-to-be-removed.json similarity index 100% rename from tests/fixtures/removing-attrs-to-be-removed.json rename to test/fixtures/removing-attrs-to-be-removed.json diff --git a/tests/fixtures/removing-tags-to-be-remain.json b/test/fixtures/removing-tags-to-be-remain.json similarity index 100% rename from tests/fixtures/removing-tags-to-be-remain.json rename to test/fixtures/removing-tags-to-be-remain.json diff --git a/tests/fixtures/removing-tags-to-be-removed.json b/test/fixtures/removing-tags-to-be-removed.json similarity index 100% rename from tests/fixtures/removing-tags-to-be-removed.json rename to test/fixtures/removing-tags-to-be-removed.json diff --git a/tests/fixtures/removing-tags.svg b/test/fixtures/removing-tags.svg similarity index 99% rename from tests/fixtures/removing-tags.svg rename to test/fixtures/removing-tags.svg index 89f4d30..00313cf 100644 --- a/tests/fixtures/removing-tags.svg +++ b/test/fixtures/removing-tags.svg @@ -1,6 +1,6 @@ - diff --git a/tests/fixtures/style-inserted.svg b/test/fixtures/style-inserted.svg similarity index 100% rename from tests/fixtures/style-inserted.svg rename to test/fixtures/style-inserted.svg diff --git a/tests/fixtures/with-ids.svg b/test/fixtures/with-ids.svg similarity index 100% rename from tests/fixtures/with-ids.svg rename to test/fixtures/with-ids.svg diff --git a/tests/fixtures/xml-rect.svg b/test/fixtures/xml-rect.svg similarity index 100% rename from tests/fixtures/xml-rect.svg rename to test/fixtures/xml-rect.svg diff --git a/test/svg-inline-loader.test.js b/test/svg-inline-loader.test.js new file mode 100644 index 0000000..266e7e4 --- /dev/null +++ b/test/svg-inline-loader.test.js @@ -0,0 +1,147 @@ +import simpleHTMLTokenizer from 'simple-html-tokenizer'; +import _ from 'lodash'; +import rawLoader from 'raw-loader'; +import SVGInlineLoader from '../src/index'; + +const tokenize = simpleHTMLTokenizer.tokenize; + +describe('getExtractedSVG()', () => { + const svgWithRect = rawLoader('./fixtures/xml-rect.svg'); + const processedSVG = SVGInlineLoader.getExtractedSVG(svgWithRect); + const reTokenized = tokenize(processedSVG); + + test('should remove width and height from element', () => { + reTokenized.forEach((tag) => { + if (SVGInlineLoader.conditions.isSVGToken(tag)) { + tag.attributes.forEach((attributeToken) => { + expect(SVGInlineLoader.conditions.hasNoWidthHeight(attributeToken)).toBeTruthy(); + }); + } + }); + }); + + test('should remove xml declaration', () => { + expect(reTokenized[0].tagName === 'xml').toBeFalsy(); + }); + + test('should remove `` and its children if `removeTags` option is on', () => { + const svgWithStyle = rawLoader('./fixtures/style-inserted.svg'); + const processedStyleInsertedSVG = SVGInlineLoader.getExtractedSVG(svgWithStyle, { removeTags: true }); + const reTokenizedStyleInsertedSVG = tokenize(processedStyleInsertedSVG); + + reTokenizedStyleInsertedSVG.forEach((tag) => { + expect(tag.tagName !== 'style' && tag.tagName !== 'defs').toBeTruthy(); + }); + }); + + // test('should apply prefixes to class names', () => { + // const svgWithStyle = rawLoader('./fixtures/style-inserted.svg'); + // const processedStyleInsertedSVG = SVGInlineLoader.getExtractedSVG(svgWithStyle, { classPrefix: 'test.prefix-' }); + // console.log(processedStyleInsertedSVG); + // // Are all 10 classes prefixed in