Repository: svg/svgo (js)

Issue:
ES module compatible export
It would be useful to make svgo compatible with ES module interop tools, so that downstream libraries that depend on svgo can also provide an ES module distribution.

I think the simplest approach here would be to add a `default` property on the module which references the `SVGO` constructor.

Pull request:
feat: offer ES module interop default export (#934)
Resolves #934 

Patch:
diff --git a/README.md b/README.md
index 53bea3e..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 |
@@ -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.js b/lib/svgo.js
index e448c05..318a8ec 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);
 };
 
@@ -77,3 +77,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/_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/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/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/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>