Repository: svg/svgo
Pull request #924 (2018-03-19 02:27:17)

Upgrade css-tree from alpha25 to alpha.28
API changed walkFoo(ast, fun) to walk(ast,{visit: 'Rule',enter: fun)
csstree.translate renamed to csstree.generate
csstree.generate throws an error on invalid style where translate silently ignored it - update style processing to catch and ignore instead
Updated tests - csstree now omits more whitespace

Should convertStyleToAttrs.0.2.svg actually pass like it currently does - dropping some of the most invalid properties or should I update the code to treat invalid styles as parse errors/some other behavior?


Patch:
diff --git a/README.md b/README.md
index ab99c08..b7a6a9b 100644
--- a/README.md
+++ b/README.md
@@ -3,14 +3,14 @@
 
 <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)
 
 ## Why?
 
-SVG files, especially exported from various editors, usually contain a lot of redundant and useless information such as editor metadata, comments, hidden elements, default or non-optimal values and other stuff that can be safely removed or converted without affecting SVG rendering result.
+SVG files, especially those exported from various editors, usually contain a lot of redundant and useless information. This can include editor metadata, comments, hidden elements, default or non-optimal values and other stuff that can be safely removed or converted without affecting the SVG rendering result.
 
 ## What it can do
 
@@ -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 |
@@ -186,6 +187,8 @@ Arguments:
 * as a Telegram Bot – [svgo_bot](https://github.com/maksugr/svgo_bot)
 * as a PostCSS plugin – [postcss-svgo](https://github.com/ben-eb/postcss-svgo)
 * as an Inkscape plugin – [inkscape-svgo](https://github.com/konsumer/inkscape-svgo)
+* as a Sketch plugin - [svgo-compressor](https://github.com/BohemianCoding/svgo-compressor)
+* as macOS app - [Image Shrinker](https://image-shrinker.com)
 
 ## License and Copyright
 
diff --git a/README.ru.md b/README.ru.md
index 26f1499..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)
 
 
 ## Как использовать
@@ -185,6 +186,9 @@ $ [sudo] npm install -g svgo
 * через загрузчик webpack – [image-webpack-loader](https://github.com/tcoopman/image-webpack-loader)
 * с помощью бота в 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/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/lib/svgo/tools.js b/lib/svgo/tools.js
index 7dd33c0..fb52c9d 100644
--- a/lib/svgo/tools.js
+++ b/lib/svgo/tools.js
@@ -15,9 +15,12 @@ exports.encodeSVGDatauri = function(str, type) {
     if (!type || type === 'base64') {
 
         prefix += ';base64,';
-
-        str = prefix + new Buffer(str).toString('base64');
-
+        if (Buffer.from) {
+            str = prefix + Buffer.from(str).toString('base64');
+        } else {
+            str = prefix + new Buffer(str).toString('base64');
+        }
+        
     // URI encoded
     } else if (type === 'enc') {
 
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/_collections.js b/plugins/_collections.js
index 3348dcb..fab9287 100644
--- a/plugins/_collections.js
+++ b/plugins/_collections.js
@@ -2291,7 +2291,8 @@ exports.editorNamespaces = [
     'http://ns.adobe.com/Flows/1.0/',
     'http://ns.adobe.com/ImageReplacement/1.0/',
     'http://ns.adobe.com/GenericCustomNamespace/1.0/',
-    'http://ns.adobe.com/XPath/1.0/'
+    'http://ns.adobe.com/XPath/1.0/',
+    'http://schemas.microsoft.com/visio/2003/SVGExtensions/'
 ];
 
 // http://www.w3.org/TR/SVG/linking.html#processingIRI
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/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 fd7b0ea..906a79b 100644
--- a/plugins/prefixIds.js
+++ b/plugins/prefixIds.js
@@ -61,13 +61,13 @@ var prefixId = function(val) {
 
 // attr.value helper methods
 
-// prefixes a normal attribute value
-var addPrefixToAttr = function(attr) {
+// prefixes a class attribute value
+var addPrefixToClassAttr = function(attr) {
     if (!attrNotEmpty(attr)) {
         return;
     }
 
-    attr.value = addPrefix(attr.value);
+    attr.value = attr.value.split(/\s+/).map(addPrefix).join(' ');
 };
 
 // prefixes an ID attribute value
@@ -76,6 +76,15 @@ var addPrefixToIdAttr = function(attr) {
         return;
     }
 
+    attr.value = addPrefix(attr.value);
+};
+
+// prefixes a href attribute value
+var addPrefixToHrefAttr = function(attr) {
+    if (!attrNotEmpty(attr)) {
+        return;
+    }
+
     var idPrefixed = prefixId(attr.value);
     if (!idPrefixed) {
         return;
@@ -185,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;
     }
 
@@ -197,14 +206,16 @@ exports.fn = function(node, opts, extra) {
     }
 
     // ID
-    addPrefixToAttr(node.attrs.id);
+    addPrefixToIdAttr(node.attrs.id);
+
     // Class
-    addPrefixToAttr(node.attrs.class);
+    addPrefixToClassAttr(node.attrs.class);
 
     // href
-    addPrefixToIdAttr(node.attrs.href);
+    addPrefixToHrefAttr(node.attrs.href);
+
     // (xlink:)href (deprecated, must be still supported)
-    addPrefixToIdAttr(node.attrs['xlink:href']);
+    addPrefixToHrefAttr(node.attrs['xlink:href']);
 
     // referenceable properties
     for (var referencesProp of referencesProps) {
diff --git a/plugins/removeHiddenElems.js b/plugins/removeHiddenElems.js
index 231a2bd..7379125 100644
--- a/plugins/removeHiddenElems.js
+++ b/plugins/removeHiddenElems.js
@@ -7,6 +7,7 @@ exports.active = true;
 exports.description = 'removes hidden elements (zero sized, with absent attributes)';
 
 exports.params = {
+    isHidden: true,
     displayNone: true,
     opacity0: true,
     circleR0: true,
@@ -44,9 +45,15 @@ var regValidPath = /M\s*(?:[-+]?(?:\d*\.\d+|\d+(?:\.|(?!\.)))([eE][-+]?\d+)?(?!\
  *
  * @author Kir Belevich
  */
-exports.fn = function(item, params) {
+exports.fn = function (item, params) {
 
     if (item.elem) {
+        // Removes hidden elements
+        // https://www.w3schools.com/cssref/pr_class_visibility.asp
+        if (
+            params.isHidden &&
+            item.hasAttr('visibility', 'hidden')
+        ) return false;
 
         // display="none"
         //
diff --git a/plugins/sortAttrs.js b/plugins/sortAttrs.js
index 221a4bf..cba5c1d 100644
--- a/plugins/sortAttrs.js
+++ b/plugins/sortAttrs.js
@@ -30,7 +30,8 @@ exports.fn = function(item, params) {
 
 	var attrs = [],
 		sorted = {},
-		orderlen = params.order.length + 1;
+		orderlen = params.order.length + 1,
+		xmlnsOrder = params.xmlnsOrder || 'front';
 
 	if (item.elem) {
 
@@ -41,10 +42,12 @@ exports.fn = function(item, params) {
 		attrs.sort(function(a, b) {
 			if (a.prefix != b.prefix) {
 				// xmlns attributes implicitly have the prefix xmlns
-				if (a.prefix == 'xmlns')
-					return -1;
-				if (b.prefix == 'xmlns')
-					return 1;
+				if (xmlnsOrder == 'front') {
+                    if (a.prefix == 'xmlns')
+                        return -1;
+                    if (b.prefix == 'xmlns')
+                        return 1;
+                }
 				return a.prefix < b.prefix ? -1 : 1;
 			}
 


Test patch:
diff --git a/examples/test.js b/examples/test.js
index b6fc31a..e4f4fa4 100644
--- a/examples/test.js
+++ b/examples/test.js
@@ -34,7 +34,7 @@ var FS = require('fs'),
         },{
           removeViewBox: false,
         },{
-          cleanUpEnableBackground: true,
+          cleanupEnableBackground: true,
         },{
           convertStyleToAttrs: true,
         },{
@@ -69,8 +69,6 @@ var FS = require('fs'),
           convertShapeToPath: true,
         },{
           sortAttrs: true,
-        },{
-          transformsWithOnePath: false,
         },{
           removeDimensions: true,
         },{
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/prefixIds.01.svg b/test/plugins/prefixIds.01.svg
index ea23b2f..e99c2ea 100644
--- a/test/plugins/prefixIds.01.svg
+++ b/test/plugins/prefixIds.01.svg
@@ -1,6 +1,6 @@
 <svg width="120" height="120" xmlns="http://www.w3.org/2000/svg">
     <style>
-        .test2 {
+        .test {
             color: blue;
         }
         #test {
@@ -16,7 +16,7 @@
 
 <svg width="120" height="120" xmlns="http://www.w3.org/2000/svg">
     <style>
-        .prefixIds_01_svg__test2{color:blue}#prefixIds_01_svg__test{color:red}
+        .prefixIds_01_svg__test{color:blue}#prefixIds_01_svg__test{color:red}
     </style>
     <rect class="prefixIds_01_svg__test" x="10" y="10" width="100" height="100"/>
     <rect class="" id="prefixIds_01_svg__test" x="10" y="10" width="100" height="100"/>
diff --git a/test/plugins/prefixIds.06.svg b/test/plugins/prefixIds.06.svg
new file mode 100644
index 0000000..9bd4f76
--- /dev/null
+++ b/test/plugins/prefixIds.06.svg
@@ -0,0 +1,38 @@
+<svg width="120" height="120" xmlns="http://www.w3.org/2000/svg">
+    <style>
+        .test {
+            color: blue;
+        }
+        .test2 {
+            color: green;
+        }
+        #test {
+            color: red;
+        }
+        .test3 .test4 {
+            color: black;
+        }
+        .test5.test6 {
+            color: brown;
+        }
+        .test5.test6 #test7 {
+            color: yellow;
+        }
+    </style>
+    <rect class="test" x="10" y="10" width="100" height="100"/>
+    <rect class="test test2" x="10" y="10" width="100" height="100"/>
+    <rect class="test  test2" x="10" y="10" width="100" height="100"/>
+    <rect class="" id="test" x="10" y="10" width="100" height="100"/>
+</svg>
+
+@@@
+
+<svg width="120" height="120" xmlns="http://www.w3.org/2000/svg">
+    <style>
+        .prefixIds_06_svg__test{color:blue}.prefixIds_06_svg__test2{color:green}#prefixIds_06_svg__test{color:red}.prefixIds_06_svg__test3 .prefixIds_06_svg__test4{color:black}.prefixIds_06_svg__test5.prefixIds_06_svg__test6{color:brown}.prefixIds_06_svg__test5.prefixIds_06_svg__test6 #prefixIds_06_svg__test7{color:yellow}
+    </style>
+    <rect class="prefixIds_06_svg__test" x="10" y="10" width="100" height="100"/>
+    <rect class="prefixIds_06_svg__test prefixIds_06_svg__test2" x="10" y="10" width="100" height="100"/>
+    <rect class="prefixIds_06_svg__test prefixIds_06_svg__test2" x="10" y="10" width="100" height="100"/>
+    <rect class="" id="prefixIds_06_svg__test" x="10" y="10" width="100" height="100"/>
+</svg>
diff --git a/test/plugins/sortAttrs.02.svg b/test/plugins/sortAttrs.02.svg
new file mode 100644
index 0000000..3237551
--- /dev/null
+++ b/test/plugins/sortAttrs.02.svg
@@ -0,0 +1,15 @@
+<svg foo="bar" xmlns="http://www.w3.org/2000/svg" height="10" baz="quux" width="10" hello="world">
+    <rect x="0" y="0" width="100" height="100" stroke-width="1" stroke-linejoin="round" fill="red" stroke="orange" xmlns="http://www.w3.org/2000/svg"/>
+    test
+</svg>
+
+@@@
+
+<svg width="10" height="10" baz="quux" foo="bar" hello="world" xmlns="http://www.w3.org/2000/svg">
+    <rect width="100" height="100" x="0" y="0" fill="red" stroke="orange" stroke-linejoin="round" stroke-width="1" xmlns="http://www.w3.org/2000/svg"/>
+    test
+</svg>
+
+@@@
+
+{ "xmlnsOrder": "alphabetical" }
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>