Repository: svg/svgo
Pull request #1002 (2018-07-18 18:51:07)

Adds keepRoleAttr option for removeUnknownsAndDefaults
Fixes https://github.com/svg/svgo/issues/787

Allows for the option to keep `role` attributes by setting `keepRoleAttr: true` within `removeUnknownsAndDefaults`.

This attribute is used for accessibility reasons and should be kept under some (but not all) circumstances.


Patch:
diff --git a/README.md b/README.md
index 48f4b21..b7a6a9b 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 
 <img src="https://svg.github.io/svgo-logo.svg" width="200" height="200" alt="logo"/>
 
-## SVGO [![NPM version](https://badge.fury.io/js/svgo.svg)](https://npmjs.org/package/svgo) [![Dependency Status](https://gemnasium.com/svg/svgo.svg)](https://gemnasium.com/svg/svgo) [![Build Status](https://secure.travis-ci.org/svg/svgo.svg)](https://travis-ci.org/svg/svgo) [![Coverage Status](https://img.shields.io/coveralls/svg/svgo.svg)](https://coveralls.io/r/svg/svgo?branch=master)
+## SVGO [![NPM version](https://badge.fury.io/js/svgo.svg)](https://npmjs.org/package/svgo) [![Build Status](https://secure.travis-ci.org/svg/svgo.svg)](https://travis-ci.org/svg/svgo) [![Coverage Status](https://img.shields.io/coveralls/svg/svgo.svg)](https://coveralls.io/r/svg/svgo?branch=master)
 
 **SVG O**ptimizer is a Nodejs-based tool for optimizing SVG vector graphics files.
 ![](https://mc.yandex.ru/watch/18431326)
@@ -21,6 +21,7 @@ Today we have:
 | Plugin | Description |
 | ------ | ----------- |
 | [cleanupAttrs](https://github.com/svg/svgo/blob/master/plugins/cleanupAttrs.js) | cleanup attributes from newlines, trailing, and repeating spaces |
+| [inlineStyles](https://github.com/svg/svgo/blob/master/plugins/inlineStyles.js) | move and merge styles from `<style>` elements to element `style` attributes |
 | [removeDoctype](https://github.com/svg/svgo/blob/master/plugins/removeDoctype.js) | remove doctype declaration |
 | [removeXMLProcInst](https://github.com/svg/svgo/blob/master/plugins/removeXMLProcInst.js) | remove XML processing instructions |
 | [removeComments](https://github.com/svg/svgo/blob/master/plugins/removeComments.js) | remove comments |
diff --git a/README.ru.md b/README.ru.md
index 7a2430d..8b939d0 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -3,24 +3,25 @@
 
 <img src="https://svg.github.io/svgo-logo.svg" width="200" height="200" alt="logo"/>
 
-## SVGO [![NPM version](https://badge.fury.io/js/svgo.svg)](https://npmjs.org/package/svgo) [![Dependency Status](https://gemnasium.com/svg/svgo.svg)](https://gemnasium.com/svg/svgo) [![Build Status](https://secure.travis-ci.org/svg/svgo.svg)](https://travis-ci.org/svg/svgo) [![Coverage Status](https://img.shields.io/coveralls/svg/svgo.svg)](https://coveralls.io/r/svg/svgo?branch=master)
+## SVGO [![NPM version](https://badge.fury.io/js/svgo.svg)](https://npmjs.org/package/svgo) [![Build Status](https://secure.travis-ci.org/svg/svgo.svg)](https://travis-ci.org/svg/svgo) [![Coverage Status](https://img.shields.io/coveralls/svg/svgo.svg)](https://coveralls.io/r/svg/svgo?branch=master)
 
 **SVG** **O**ptimizer – это инструмент для оптимизации векторной графики в формате SVG, написанный на Node.js.
 ![](https://mc.yandex.ru/watch/18431326)
 
 ## Зачем?
 
-SVG-файлы, особенно – экспортированные из различных редакторов, содержат много избыточной и бесполезной информации, комментариев, скрытых элементов, неоптимальные или стандартные значения и другой мусор, удаление которого безопасно и не влияет на конечный результат отрисовки.
+SVG-файлы, особенно экспортированные из редакторов, содержат много избыточной и бесполезной информации, комментариев, скрытых элементов, неоптимальные или стандартные значения и другой мусор, удаление которого безопасно и не влияет на конечный вид изображения.
 
 ## Возможности
 
 SVGO имеет расширяемую архитектуру, в которой почти каждая оптимизация является отдельным расширением.
 
-Сегодня у нас есть:
+Что у нас есть:
 
 | Plugin | Description |
 | ------ | ----------- |
 | [cleanupAttrs](https://github.com/svg/svgo/blob/master/plugins/cleanupAttrs.js) | удаление переносов строк и лишних пробелов |
+| [inlineStyles](https://github.com/svg/svgo/blob/master/plugins/inlineStyles.js) | перенос стилей из элементов `<style>` в атрибуты `style` |
 | [removeDoctype](https://github.com/svg/svgo/blob/master/plugins/removeDoctype.js) | удаление doctype |
 | [removeXMLProcInst](https://github.com/svg/svgo/blob/master/plugins/removeXMLProcInst.js) | удаление XML-инструкций |
 | [removeComments](https://github.com/svg/svgo/blob/master/plugins/removeComments.js) | удаление комментариев |
@@ -63,7 +64,7 @@ SVGO имеет расширяемую архитектуру, в которой
 | [removeStyleElement](https://github.com/svg/svgo/blob/master/plugins/removeStyleElement.js) | удаляет элементы `<style>` (выключено по умолчанию) |
 | [removeScriptElement](https://github.com/svg/svgo/blob/master/plugins/removeScriptElement.js) | удаляет элементы `<script>` (выключено по умолчанию) |
 
-Хотите узнать, как это работает и как написать свой плагин? [Конечно же, да!](https://github.com/svg/svgo/blob/master/docs/how-it-works/ru.md).
+Хотите узнать принципы работы и как написать свой плагин? [Конечно же, да!](https://github.com/svg/svgo/blob/master/docs/how-it-works/ru.md)
 
 
 ## Как использовать
@@ -183,9 +184,11 @@ $ [sudo] npm install -g svgo
 * как таск для Mimosa – [mimosa-minify-svg](https://github.com/dbashford/mimosa-minify-svg)
 * как действие папки в OSX – [svgo-osx-folder-action](https://github.com/svg/svgo-osx-folder-action)
 * через загрузчик webpack – [image-webpack-loader](https://github.com/tcoopman/image-webpack-loader)
-* плагин для Sketch - [svgo-compressor] (https://github.com/BohemianCoding/svgo-compressor)
 * с помощью бота в Telegram – [svgo_bot](https://github.com/maksugr/svgo_bot)
 * как плагин PostCSS - [postcss-svgo](https://github.com/ben-eb/postcss-svgo)
+* как плагин для Inkscape – [inkscape-svgo](https://github.com/konsumer/inkscape-svgo)
+* как плагин для Sketch - [svgo-compressor](https://github.com/BohemianCoding/svgo-compressor)
+* в виде приложения macOS - [Image Shrinker](https://image-shrinker.com)
 
 ## Лицензия и копирайты
 
diff --git a/lib/css-tools.js b/lib/css-tools.js
index 4b1e77f..6517892 100644
--- a/lib/css-tools.js
+++ b/lib/css-tools.js
@@ -15,7 +15,7 @@ var csstree     = require('css-tree'),
 function flattenToSelectors(cssAst) {
     var selectors = [];
 
-    csstree.walkRules(cssAst, function(node) {
+    csstree.walk(cssAst, {visit: 'Rule', enter: function(node) {
         if (node.type !== 'Rule') {
             return;
         }
@@ -43,7 +43,7 @@ function flattenToSelectors(cssAst) {
 
             selectors.push(selector);
         });
-    });
+    }});
 
     return selectors;
 }
@@ -65,7 +65,7 @@ function filterByMqs(selectors, useMqs) {
         var mqStr = mqName;
         if (selector.atrule.expression &&
             selector.atrule.expression.children.first().type === 'MediaQueryList') {
-            var mqExpr = csstree.translate(selector.atrule.expression);
+            var mqExpr = csstree.generate(selector.atrule.expression);
             mqStr = [mqName, mqExpr].join(' ');
         }
 
@@ -82,7 +82,7 @@ function filterByMqs(selectors, useMqs) {
  */
 function filterByPseudos(selectors, usePseudos) {
     return selectors.filter(function(selector) {
-        var pseudoSelectorsStr = csstree.translate({
+        var pseudoSelectorsStr = csstree.generate({
             type: 'Selector',
             children: new List().fromArray(selector.pseudos.map(function(pseudo) {
                 return pseudo.item.data;
@@ -165,7 +165,7 @@ function sortSelectors(selectors) {
  */
 function csstreeToStyleDeclaration(declaration) {
     var propertyName = declaration.property,
-        propertyValue = csstree.translate(declaration.value),
+        propertyValue = csstree.generate(declaration.value),
         propertyPriority = (declaration.important ? 'important' : '');
     return {
         name: propertyName,
diff --git a/lib/svgo.js b/lib/svgo.js
index e448c05..9f9881c 100644
--- a/lib/svgo.js
+++ b/lib/svgo.js
@@ -17,7 +17,7 @@ var CONFIG = require('./svgo/config.js'),
     encodeSVGDatauri = require('./svgo/tools.js').encodeSVGDatauri,
     JS2SVG = require('./svgo/js2svg.js');
 
-var SVGO = module.exports = function(config) {
+var SVGO = function(config) {
     this.config = CONFIG(config);
 };
 
@@ -45,6 +45,9 @@ SVGO.prototype.optimize = function(svgstr, info) {
                     if (config.datauri) {
                         svgjs.data = encodeSVGDatauri(svgjs.data, config.datauri);
                     }
+                    if(info.path) {
+                        svgjs.path = info.path;
+                    }
                     resolve(svgjs);
                 }
             };
@@ -77,3 +80,7 @@ SVGO.prototype._optimizeOnce = function(svgstr, info, callback) {
 SVGO.prototype.createContentItem = function(data) {
     return new JSAPI(data);
 };
+
+module.exports = SVGO;
+// Offer ES module interop compatibility.
+module.exports.default = SVGO;
diff --git a/lib/svgo/config.js b/lib/svgo/config.js
index 786c37a..2f4abdc 100644
--- a/lib/svgo/config.js
+++ b/lib/svgo/config.js
@@ -27,7 +27,7 @@ module.exports = function(config) {
         }
     } else {
         defaults = Object.assign({}, yaml.safeLoad(FS.readFileSync(__dirname + '/../../.svgo.yml', 'utf8')));
-        defaults.plugins = preparePluginsArray(defaults.plugins);
+        defaults.plugins = preparePluginsArray(defaults.plugins || []);
         defaults = extendConfig(defaults, config);
     }
 
diff --git a/lib/svgo/css-class-list.js b/lib/svgo/css-class-list.js
index 28e5816..8401a9f 100644
--- a/lib/svgo/css-class-list.js
+++ b/lib/svgo/css-class-list.js
@@ -13,6 +13,28 @@ var CSSClassList = function(node) {
     //this.classValue = null;
 };
 
+/**
+ * Performs a deep clone of this object.
+ *
+ * @param parentNode the parentNode to assign to the cloned result
+ */
+CSSClassList.prototype.clone = function(parentNode) {
+    var node = this;
+    var nodeData = {};
+
+    Object.keys(node).forEach(function(key) {
+        if (key !== 'parentNode') {
+            nodeData[key] = node[key];
+        }
+    });
+
+    // Deep-clone node data.
+    nodeData = JSON.parse(JSON.stringify(nodeData));
+
+    var clone = new CSSClassList(parentNode);
+   Object.assign(clone, nodeData);
+    return clone;
+};
 
 CSSClassList.prototype.hasClass = function() {
     this.classAttr = { // empty class attr
diff --git a/lib/svgo/css-style-declaration.js b/lib/svgo/css-style-declaration.js
index 5b2300b..fb6d781 100644
--- a/lib/svgo/css-style-declaration.js
+++ b/lib/svgo/css-style-declaration.js
@@ -16,6 +16,28 @@ var CSSStyleDeclaration = function(node) {
     this.parseError = false;
 };
 
+/**
+ * Performs a deep clone of this object.
+ *
+ * @param parentNode the parentNode to assign to the cloned result
+ */
+CSSStyleDeclaration.prototype.clone = function(parentNode) {
+    var node = this;
+    var nodeData = {};
+
+    Object.keys(node).forEach(function(key) {
+        if (key !== 'parentNode') {
+            nodeData[key] = node[key];
+        }
+    });
+
+    // Deep-clone node data.
+    nodeData = JSON.parse(JSON.stringify(nodeData));
+
+    var clone = new CSSStyleDeclaration(parentNode);
+    Object.assign(clone, nodeData);
+    return clone;
+};
 
 CSSStyleDeclaration.prototype.hasStyle = function() {
     this.addStyleHandler();
@@ -105,8 +127,14 @@ CSSStyleDeclaration.prototype._loadCssText = function() {
 
     var self = this;
     declarations.children.each(function(declaration) {
-        var styleDeclaration = csstools.csstreeToStyleDeclaration(declaration);
-        self.setProperty(styleDeclaration.name, styleDeclaration.value, styleDeclaration.priority);
+        try {
+          var styleDeclaration = csstools.csstreeToStyleDeclaration(declaration);
+          self.setProperty(styleDeclaration.name, styleDeclaration.value, styleDeclaration.priority);
+        } catch(styleError) {
+            if(styleError.message !== 'Unknown node type: undefined') {
+                self.parseError = styleError;
+            }
+        }
     });
 };
 
diff --git a/lib/svgo/jsAPI.js b/lib/svgo/jsAPI.js
index b4d4d1b..4cf958c 100644
--- a/lib/svgo/jsAPI.js
+++ b/lib/svgo/jsAPI.js
@@ -28,12 +28,12 @@ JSAPI.prototype.clone = function() {
     var nodeData = {};
 
     Object.keys(node).forEach(function(key) {
-        if (key !== 'content') {
+        if (key !== 'class' && key !== 'style' && key !== 'content') {
             nodeData[key] = node[key];
         }
     });
 
-    // Deep-clone node data
+    // Deep-clone node data.
     nodeData = JSON.parse(JSON.stringify(nodeData));
 
     // parentNode gets set to a proper object by the parent clone,
@@ -41,6 +41,12 @@ JSAPI.prototype.clone = function() {
     // in the constructor.
     var clonedNode = new JSAPI(nodeData, !!node.parentNode);
 
+    if (node.class) {
+        clonedNode.class = node.class.clone(clonedNode);
+    }
+    if (node.style) {
+        clonedNode.style = node.style.clone(clonedNode);
+    }
     if (node.content) {
         clonedNode.content = node.content.map(function(childNode) {
             var clonedChild = childNode.clone();
@@ -243,7 +249,10 @@ JSAPI.prototype.renameElem = function(name) {
 
     if (!arguments.length) return false;
 
-    if (Array.isArray(name)) name.forEach(this.removeAttr, this);
+    if (Array.isArray(name)) {
+        name.forEach(this.removeAttr, this);
+        return false;
+    }
 
     if (!this.hasAttr(name)) return false;
 
diff --git a/package.json b/package.json
index 8b689f6..676171f 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "colors": "~1.1.2",
     "css-select": "~1.3.0-rc0",
     "css-select-base-adapter": "~0.1.0",
-    "css-tree": "1.0.0-alpha25",
+    "css-tree": "1.0.0-alpha.28",
     "css-url-regex": "^1.1.0",
     "csso": "^3.5.0",
     "js-yaml": "~3.10.0",
diff --git a/plugins/_path.js b/plugins/_path.js
index 5222d35..47fe69d 100644
--- a/plugins/_path.js
+++ b/plugins/_path.js
@@ -208,16 +208,22 @@ exports.applyTransforms = function(elem, path, params) {
         if (scale !== 1) {
             var strokeWidth = elem.computedAttr('stroke-width') || defaultStrokeWidth;
 
-            if (elem.hasAttr('stroke-width')) {
-                elem.attrs['stroke-width'].value = elem.attrs['stroke-width'].value.trim()
-                    .replace(regNumericValues, function(num) { return removeLeadingZero(num * scale) });
-            } else {
-                elem.addAttr({
-                    name: 'stroke-width',
-                    prefix: '',
-                    local: 'stroke-width',
-                    value: strokeWidth.replace(regNumericValues, function(num) { return removeLeadingZero(num * scale) })
-                });
+            if (!elem.hasAttr('vector-effect') || elem.attr('vector-effect').value !== 'non-scaling-stroke') {
+                if (elem.hasAttr('stroke-width')) {
+                    elem.attrs['stroke-width'].value = elem.attrs['stroke-width'].value.trim()
+                        .replace(regNumericValues, function(num) {
+                            return removeLeadingZero(num * scale);
+                        });
+                } else {
+                    elem.addAttr({
+                        name: 'stroke-width',
+                        prefix: '',
+                        local: 'stroke-width',
+                        value: strokeWidth.replace(regNumericValues, function(num) {
+                            return removeLeadingZero(num * scale);
+                        })
+                    });
+                }
             }
         }
     } else if (id) { // Stroke and stroke-width can be redefined with <use>
diff --git a/plugins/cleanupNumericValues.js b/plugins/cleanupNumericValues.js
index e91fdf2..f7c8a1e 100644
--- a/plugins/cleanupNumericValues.js
+++ b/plugins/cleanupNumericValues.js
@@ -48,6 +48,9 @@ exports.fn = function(item, params) {
         }
 
         item.eachAttr(function(attr) {
+            // The `version` attribute is a text string and cannot be rounded
+            if (attr.name === 'version') { return }
+            
             var match = attr.value.match(regNumericValues);
 
             // if attribute value matches regNumericValues
diff --git a/plugins/collapseGroups.js b/plugins/collapseGroups.js
index 6e38651..f1cb7c1 100644
--- a/plugins/collapseGroups.js
+++ b/plugins/collapseGroups.js
@@ -50,7 +50,7 @@ exports.fn = function(item) {
                 if (g.hasAttr() && g.content.length === 1) {
                     var inner = g.content[0];
 
-                    if (inner.isElem() && !inner.hasAttr('id') &&
+                    if (inner.isElem() && !inner.hasAttr('id') && !g.hasAttr('filter') &&
                         !(g.hasAttr('class') && inner.hasAttr('class')) && (
                             !g.hasAttr('clip-path') && !g.hasAttr('mask') ||
                             inner.isElem('g') && !g.hasAttr('transform') && !inner.hasAttr('transform')
diff --git a/plugins/convertPathData.js b/plugins/convertPathData.js
index fe8e475..789bb30 100644
--- a/plugins/convertPathData.js
+++ b/plugins/convertPathData.js
@@ -22,7 +22,8 @@ exports.params = {
     collapseRepeated: true,
     utilizeAbsolute: true,
     leadingZero: true,
-    negativeExtraSpace: true
+    negativeExtraSpace: true,
+    forceAbsolutePath: false
 };
 
 var pathElems = require('./_collections.js').pathElems,
@@ -671,11 +672,12 @@ function convertToMixed(path, params) {
         var absoluteDataStr = cleanupOutData(adata, params),
             relativeDataStr = cleanupOutData(data, params);
 
-        // Convert to absolute coordinates if it's shorter.
+        // Convert to absolute coordinates if it's shorter or forceAbsolutePath is true.
         // v-20 -> V0
         // Don't convert if it fits following previous instruction.
         // l20 30-10-50 instead of l20 30L20 30
         if (
+            params.forceAbsolutePath || (
             absoluteDataStr.length < relativeDataStr.length &&
             !(
                 params.negativeExtraSpace &&
@@ -683,7 +685,7 @@ function convertToMixed(path, params) {
                 prev.instruction.charCodeAt(0) > 96 &&
                 absoluteDataStr.length == relativeDataStr.length - 1 &&
                 (data[0] < 0 || /^0\./.test(data[0]) && prev.data[prev.data.length - 1] % 1)
-            )
+            ))
         ) {
             item.instruction = instruction.toUpperCase();
             item.data = adata;
diff --git a/plugins/inlineStyles.js b/plugins/inlineStyles.js
index 8573649..e4f65e0 100644
--- a/plugins/inlineStyles.js
+++ b/plugins/inlineStyles.js
@@ -103,7 +103,7 @@ exports.fn = function(document, opts) {
 
     // match selectors
     for (selector of sortedSelectors) {
-        var selectorStr = csstree.translate(selector.item.data),
+        var selectorStr = csstree.generate(selector.item.data),
             selectedEls = null;
 
         try {
@@ -143,7 +143,7 @@ exports.fn = function(document, opts) {
             }
 
             // merge declarations
-            csstree.walkDeclarations(selector.rule, function(styleCsstreeDeclaration) {
+            csstree.walk(selector.rule, {visit: 'Declaration', enter: function(styleCsstreeDeclaration) {
 
                 // existing inline styles have higher priority
                 // no inline styles, external styles,                                    external styles used
@@ -155,7 +155,7 @@ exports.fn = function(document, opts) {
                     return;
                 }
                 selectedEl.style.setProperty(styleDeclaration.name, styleDeclaration.value, styleDeclaration.priority);
-            });
+            }});
         }
 
         if (opts.removeMatchedSelectors && selector.selectedEls !== null && selector.selectedEls.length > 0) {
@@ -202,7 +202,7 @@ exports.fn = function(document, opts) {
 
     // clean up now empty elements
     for (var style of styles) {
-        csstree.walkRules(style.cssAst, function(node, item, list) {
+        csstree.walk(style.cssAst, {visit: 'Rule', enter: function(node, item, list) {
             // clean up <style/> atrules without any rulesets left
             if (node.type === 'Atrule' &&
                 // only Atrules containing rulesets
@@ -217,7 +217,7 @@ exports.fn = function(document, opts) {
                 node.prelude.children.isEmpty()) {
                 list.remove(item);
             }
-        });
+        }});
 
 
         if (style.cssAst.children.isEmpty()) {
@@ -237,7 +237,7 @@ exports.fn = function(document, opts) {
 
 
         // update existing, left over <style>s
-        cssTools.setCssStr(style.styleEl, csstree.translate(style.cssAst));
+        cssTools.setCssStr(style.styleEl, csstree.generate(style.cssAst));
     }
 
 
diff --git a/plugins/prefixIds.js b/plugins/prefixIds.js
index 7d2b8c8..906a79b 100644
--- a/plugins/prefixIds.js
+++ b/plugins/prefixIds.js
@@ -194,7 +194,7 @@ exports.fn = function(node, opts, extra) {
         });
 
         // update <style>s
-        node.content[0].text = csstree.translate(cssAst);
+        node.content[0].text = csstree.generate(cssAst);
         return node;
     }
 
diff --git a/plugins/removeUnknownsAndDefaults.js b/plugins/removeUnknownsAndDefaults.js
index adfd487..a2c006a 100644
--- a/plugins/removeUnknownsAndDefaults.js
+++ b/plugins/removeUnknownsAndDefaults.js
@@ -12,7 +12,8 @@ exports.params = {
     defaultAttrs: true,
     uselessOverrides: true,
     keepDataAttrs: true,
-    keepAriaAttrs: true
+    keepAriaAttrs: true,
+    keepRoleAttr: false
 };
 
 var collections = require('./_collections'),
@@ -107,7 +108,8 @@ exports.fn = function(item, params) {
                     attr.name !== 'xmlns' &&
                     (attr.prefix === 'xml' || !attr.prefix) &&
                     (!params.keepDataAttrs || attr.name.indexOf('data-') != 0) &&
-                    (!params.keepAriaAttrs || attr.name.indexOf('aria-') != 0)
+                    (!params.keepAriaAttrs || attr.name.indexOf('aria-') != 0) &&
+                    (!params.keepRoleAttr || attr.name != 'role')
                 ) {
                     if (
                         // unknown attrs


Test patch:
diff --git a/test/jsapi/_index.js b/test/jsapi/_index.js
index 77cf2e7..e4d2cc9 100644
--- a/test/jsapi/_index.js
+++ b/test/jsapi/_index.js
@@ -37,4 +37,8 @@ describe('svgo object', function() {
         item.should.be.empty;
     });
 
+    it('should have ES module interop default property', function() {
+        SVGO.should.equal(SVGO.default);
+    });
+
 });
diff --git a/test/plugins/collapseGroups.16.svg b/test/plugins/collapseGroups.16.svg
new file mode 100644
index 0000000..38944f9
--- /dev/null
+++ b/test/plugins/collapseGroups.16.svg
@@ -0,0 +1,15 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+    <g filter="url(#...)">
+        <g>
+            <path d="..."/>
+        </g>
+    </g>
+</svg>
+
+@@@
+
+<svg xmlns="http://www.w3.org/2000/svg">
+    <g filter="url(#...)">
+        <path d="..."/>
+    </g>
+</svg>
diff --git a/test/plugins/convertPathData.19.svg b/test/plugins/convertPathData.19.svg
new file mode 100644
index 0000000..c57c8e2
--- /dev/null
+++ b/test/plugins/convertPathData.19.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
+    <path transform="translate(250, 250) scale(1.5, 1.5) translate(-250, -250)" fill="#7ED321" stroke="#000" stroke-width="15" vector-effect="non-scaling-stroke" d="M125 125h250v250h-250v-250z"/>
+</svg>
+
+@@@
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
+    <path fill="#7ED321" stroke="#000" stroke-width="15" vector-effect="non-scaling-stroke" d="M62.5 62.5h375v375h-375v-375z"/>
+</svg>
diff --git a/test/plugins/inlineStyles.03.svg b/test/plugins/inlineStyles.03.svg
index 32f5542..f0a98c4 100644
--- a/test/plugins/inlineStyles.03.svg
+++ b/test/plugins/inlineStyles.03.svg
@@ -18,7 +18,7 @@
 <svg xmlns="http://www.w3.org/2000/svg" id="dark" viewBox="0 0 258.12 225.88">
 <!--for https://github.com/svg/svgo/pull/592#issuecomment-266327016-->
     <style>
-        .cls-8{cls-7-and-8: 1}
+        .cls-8{cls-7-and-8:1}
     </style>
     <path style="cls-7-and-8:1;only-cls-7:1"/>
 </svg>
diff --git a/test/plugins/inlineStyles.13.svg b/test/plugins/inlineStyles.13.svg
index 5c7b396..6bb9b8b 100644
--- a/test/plugins/inlineStyles.13.svg
+++ b/test/plugins/inlineStyles.13.svg
@@ -61,7 +61,7 @@
 <svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81.285 81.285">
     <defs>
         <style>
-            @charset &apos;UTF-8&apos;;@namespace svg url(http://www.w3.org/2000/svg);@import url(&apos;https://fonts.googleapis.com/css?family=Roboto&apos;);@font-face{font-family: SomeFont;src: local(&quot;Some Font&quot;), local(&quot;SomeFont&quot;), url(SomeFont.ttf);font-weight: bold}@viewport{zoom: 0.8;min-zoom: 0.4;max-zoom: 0.9}@keyframes identifier{0%{top: 0}50%{top: 30px;left: 20px}50%{top: 10px}100%{top: 0}}@page :first{margin: 1in}@supports (display: flex){.module{display: flex}}@document url(&apos;http://example.com/test.html&apos;){rect{stroke: red}}
+            @charset &apos;UTF-8&apos;;@namespace svg url(http://www.w3.org/2000/svg);@import url(&apos;https://fonts.googleapis.com/css?family=Roboto&apos;);@font-face{font-family:SomeFont;src:local(&quot;Some Font&quot;), local(&quot;SomeFont&quot;), url(SomeFont.ttf);font-weight:bold}@viewport{zoom:0.8;min-zoom:0.4;max-zoom:0.9}@keyframes identifier{0%{top:0}50%{top:30px;left:20px}50%{top:10px}100%{top:0}}@page :first{margin:1in}@supports (display:flex){.module{display:flex}}@document url(&apos;http://example.com/test.html&apos;){rect{stroke:red}}
         </style>
     </defs>
     <rect width="100" height="100" style="fill:blue"/>
diff --git a/test/plugins/inlineStyles.14.svg b/test/plugins/inlineStyles.14.svg
index b89d3b3..4dd4a0f 100644
--- a/test/plugins/inlineStyles.14.svg
+++ b/test/plugins/inlineStyles.14.svg
@@ -19,7 +19,7 @@
 <svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81.285 81.285">
     <defs>
         <style>
-            @media only screen and (min-device-width:320px) and (max-device-width:480px) and (-webkit-min-device-pixel-ratio:2){.blue{fill: blue}}
+            @media only screen and (min-device-width:320px) and (max-device-width:480px) and (-webkit-min-device-pixel-ratio:2){.blue{fill:blue}}
         </style>
     </defs>
     <rect width="100" height="100" class="blue"/>
diff --git a/test/plugins/inlineStyles.15.svg b/test/plugins/inlineStyles.15.svg
index eb9dabf..50618df 100644
--- a/test/plugins/inlineStyles.15.svg
+++ b/test/plugins/inlineStyles.15.svg
@@ -58,7 +58,7 @@
 <svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <defs xmlns="http://www.w3.org/1999/xhtml">
         <style type="text/css">
-            html /deep/ [layout][horizontal],html /deep/ [layout][vertical]{display: flex}html /deep/ [layout][horizontal][inline],html /deep/ [layout][vertical][inline]{display: inline-flex}html /deep/ [layout][horizontal]{flex-direction: row}html /deep/ [layout][horizontal][reverse]{flex-direction: row-reverse}html /deep/ [layout][vertical]{flex-direction: column}html /deep/ [layout][vertical][reverse]{flex-direction: column-reverse}html /deep/ [layout][wrap]{flex-wrap: wrap}html /deep/ [layout][wrap-reverse]{flex-wrap: wrap-reverse}html /deep/ [flex]{flex: 1 1 0px}html /deep/ [flex][auto]{flex: 1 1 auto}html /deep/ [flex][none]{flex: 0 0 auto}html /deep/ [flex][one]{flex: 1 1 0px}html /deep/ [flex][two]{flex: 2 1 0px}html /deep/ [flex][three]{flex: 3 1 0px}html /deep/ [flex][four]{flex: 4 1 0px}html /deep/ [flex][five]{flex: 5 1 0px}html /deep/ [flex][six]{flex: 6 1 0px}html /deep/ [flex][seven]{flex: 7 1 0px}html /deep/ [flex][eight]{flex: 8 1 0px}html /deep/ [flex][nine]{flex: 9 1 0px}html /deep/ [flex][ten]{flex: 10 1 0px}html /deep/ [flex][eleven]{flex: 11 1 0px}html /deep/ [flex][twelve]{flex: 12 1 0px}html /deep/ [layout][start]{align-items: flex-start}html /deep/ [layout][center]{align-items: center}html /deep/ [layout][end]{align-items: flex-end}html /deep/ [layout][start-justified]{justify-content: flex-start}html /deep/ [layout][center-justified]{justify-content: center}html /deep/ [layout][end-justified]{justify-content: flex-end}html /deep/ [layout][around-justified]{justify-content: space-around}html /deep/ [layout][justified]{justify-content: space-between}html /deep/ [self-start]{align-self: flex-start}html /deep/ [self-center]{align-self: center}html /deep/ [self-end]{align-self: flex-end}html /deep/ [self-stretch]{align-self: stretch}html /deep/ [block]{display: block}html /deep/ [hidden]{display: none !important}html /deep/ [relative]{position: relative}html /deep/ [fit]{position: absolute;top: 0px;right: 0px;bottom: 0px;left: 0px}body[fullbleed]{margin: 0px;height: 100vh}html /deep/ [segment],html /deep/ segment{display: block;position: relative;box-sizing: border-box;margin: 1em 0.5em;padding: 1em;-webkit-box-shadow: rgba(0, 0, 0, 0.0980392) 0px 0px 0px 1px;box-shadow: rgba(0, 0, 0, 0.0980392) 0px 0px 0px 1px;border-top-left-radius: 5px;border-top-right-radius: 5px;border-bottom-right-radius: 5px;border-bottom-left-radius: 5px;background-color: white}html /deep/ core-icon{display: inline-block;vertical-align: middle;background-repeat: no-repeat}html /deep/ core-icon[size=&quot;&quot;]{position: relative}
+            html /deep/ [layout][horizontal],html /deep/ [layout][vertical]{display:flex}html /deep/ [layout][horizontal][inline],html /deep/ [layout][vertical][inline]{display:inline-flex}html /deep/ [layout][horizontal]{flex-direction:row}html /deep/ [layout][horizontal][reverse]{flex-direction:row-reverse}html /deep/ [layout][vertical]{flex-direction:column}html /deep/ [layout][vertical][reverse]{flex-direction:column-reverse}html /deep/ [layout][wrap]{flex-wrap:wrap}html /deep/ [layout][wrap-reverse]{flex-wrap:wrap-reverse}html /deep/ [flex]{flex:1 1 0px}html /deep/ [flex][auto]{flex:1 1 auto}html /deep/ [flex][none]{flex:0 0 auto}html /deep/ [flex][one]{flex:1 1 0px}html /deep/ [flex][two]{flex:2 1 0px}html /deep/ [flex][three]{flex:3 1 0px}html /deep/ [flex][four]{flex:4 1 0px}html /deep/ [flex][five]{flex:5 1 0px}html /deep/ [flex][six]{flex:6 1 0px}html /deep/ [flex][seven]{flex:7 1 0px}html /deep/ [flex][eight]{flex:8 1 0px}html /deep/ [flex][nine]{flex:9 1 0px}html /deep/ [flex][ten]{flex:10 1 0px}html /deep/ [flex][eleven]{flex:11 1 0px}html /deep/ [flex][twelve]{flex:12 1 0px}html /deep/ [layout][start]{align-items:flex-start}html /deep/ [layout][center]{align-items:center}html /deep/ [layout][end]{align-items:flex-end}html /deep/ [layout][start-justified]{justify-content:flex-start}html /deep/ [layout][center-justified]{justify-content:center}html /deep/ [layout][end-justified]{justify-content:flex-end}html /deep/ [layout][around-justified]{justify-content:space-around}html /deep/ [layout][justified]{justify-content:space-between}html /deep/ [self-start]{align-self:flex-start}html /deep/ [self-center]{align-self:center}html /deep/ [self-end]{align-self:flex-end}html /deep/ [self-stretch]{align-self:stretch}html /deep/ [block]{display:block}html /deep/ [hidden]{display:none!important}html /deep/ [relative]{position:relative}html /deep/ [fit]{position:absolute;top:0px;right:0px;bottom:0px;left:0px}body[fullbleed]{margin:0px;height:100vh}html /deep/ [segment],html /deep/ segment{display:block;position:relative;box-sizing:border-box;margin:1em 0.5em;padding:1em;-webkit-box-shadow:rgba(0, 0, 0, 0.0980392) 0px 0px 0px 1px;box-shadow:rgba(0, 0, 0, 0.0980392) 0px 0px 0px 1px;border-top-left-radius:5px;border-top-right-radius:5px;border-bottom-right-radius:5px;border-bottom-left-radius:5px;background-color:white}html /deep/ core-icon{display:inline-block;vertical-align:middle;background-repeat:no-repeat}html /deep/ core-icon[size=&quot;&quot;]{position:relative}
         </style>
     </defs>
     <g id="airplanemode-on">
diff --git a/test/plugins/inlineStyles.19.svg b/test/plugins/inlineStyles.19.svg
index 2d7e429..cb847f9 100644
--- a/test/plugins/inlineStyles.19.svg
+++ b/test/plugins/inlineStyles.19.svg
@@ -36,7 +36,7 @@
 <svg id="icon_time" data-name="icon time" xmlns="http://www.w3.org/2000/svg" width="51" height="51" viewBox="0 0 51 51">
     <defs>
         <style>
-            .cls-2,.cls-3{fill: #f5f5f5;stroke: gray}.cls-2{stroke-width: 1px}.cls-2{fill-rule: evenodd}.cls-3{stroke-width: 2px}
+            .cls-2,.cls-3{fill:#f5f5f5;stroke:gray}.cls-2{stroke-width:1px}.cls-2{fill-rule:evenodd}.cls-3{stroke-width:2px}
         </style>
     </defs>
     <circle cx="25.5" cy="25.5" r="25" style="stroke-width:1px;fill:#f5f5f5;stroke:gray"/>
diff --git a/test/plugins/removeUnknownsAndDefaults.13.svg b/test/plugins/removeUnknownsAndDefaults.13.svg
new file mode 100644
index 0000000..33e7eb5
--- /dev/null
+++ b/test/plugins/removeUnknownsAndDefaults.13.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" role="img">
+    <g/>
+</svg>
+
+@@@
+
+<svg xmlns="http://www.w3.org/2000/svg">
+    <g/>
+</svg>
diff --git a/test/plugins/removeUnknownsAndDefaults.14.svg b/test/plugins/removeUnknownsAndDefaults.14.svg
new file mode 100644
index 0000000..a90c229
--- /dev/null
+++ b/test/plugins/removeUnknownsAndDefaults.14.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" role="img">
+    <g/>
+</svg>
+
+@@@
+
+<svg xmlns="http://www.w3.org/2000/svg" role="img">
+    <g/>
+</svg>
+
+@@@
+
+{ "keepRoleAttr": true }
diff --git a/test/svg2js/_index.js b/test/svg2js/_index.js
index 4a791bc..7528d34 100644
--- a/test/svg2js/_index.js
+++ b/test/svg2js/_index.js
@@ -3,6 +3,15 @@
 var SHOULD = require('should'),
     FS = require('fs'),
     PATH = require('path'),
+    JSAPI = require(process.env.COVERAGE ?
+                     '../../lib-cov/svgo/jsAPI' :
+                     '../../lib/svgo/jsAPI'),
+    CSSClassList = require(process.env.COVERAGE ?
+                     '../../lib-cov/svgo/css-class-list' :
+                     '../../lib/svgo/css-class-list'),
+    CSSStyleDeclaration = require(process.env.COVERAGE ?
+                     '../../lib-cov/svgo/css-style-declaration' :
+                     '../../lib/svgo/css-style-declaration'),
     SVG2JS = require(process.env.COVERAGE ?
                      '../../lib-cov/svgo/svg2js' :
                      '../../lib/svgo/svg2js');
@@ -179,6 +188,26 @@ describe('svg2js', function() {
 
         describe('API', function() {
 
+            describe('clone()', function() {
+
+                it('svg should have property "clone"', function() {
+                    return root.content[3].should.have.property('clone');
+                });
+
+                it('svg.clone() should be an instance of JSAPI', function() {
+                    return root.content[3].clone().should.be.instanceOf(JSAPI);
+                });
+
+                it('root.content[3].content[0].clone() has a valid style property', function() {
+                    return root.content[3].content[0].clone().style.should.be.instanceof(CSSStyleDeclaration);
+                });
+
+                it('root.content[3].content[2].clone() has a valid class property', function() {
+                    return root.content[3].content[2].clone().class.should.be.instanceof(CSSClassList);
+                });
+
+            });
+
             describe('isElem()', function() {
 
                 it('svg should have property "isElem"', function() {
diff --git a/test/svg2js/test.svg b/test/svg2js/test.svg
index 4069162..f8e741d 100644
--- a/test/svg2js/test.svg
+++ b/test/svg2js/test.svg
@@ -15,5 +15,5 @@
             <text>test</text>
         </g>
     </g>
-    <g style="color: black"></g>
+    <g style="color: black" class="unknown-class"></g>
 </svg>