diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3927b4b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +*.sublime-* +*~ +*.lock +*.DS_Store +*.swp +*.min.svg diff --git a/.svgo b/.svgo new file mode 100644 index 00000000..e0adf108 --- /dev/null +++ b/.svgo @@ -0,0 +1,119 @@ +{ + "saxXMLParser": { + "strict": true, + "options": { + "trim": true, + "normalize": true, + "lowercase": true, + "xmlns": true, + "position": false + } + }, + "plugins": { + "directPass": [ + { + "name": "removeDoctype", + "active": true + },{ + "name": "removeXMLProcInst", + "active": true + },{ + "name": "removeComments", + "active": true + },{ + "name": "removeMetadata", + "active": true + },{ + "name": "removeEditorsNSData", + "active": true + },{ + "name": "convertStyleToAttrs", + "active": true + },{ + "name": "cleanupAttrs", + "active": true, + "params": { + "newlines": true, + "trim": true, + "spaces": true + } + },{ + "name": "removeEmptyAttrs", + "active": true + },{ + "name": "removeDefaultPx", + "active": true + },{ + "name": "removeHiddenElems", + "active": true, + "params": { + "displayNone": true, + "opacity0": true, + "circleR0": true, + "ellipseRX0": true, + "ellipseRY0": true, + "rectWidth0": true, + "rectHeight0": true, + "patternWidth0": true, + "patternHeight0": true, + "imageWidth0": true, + "imageHeight0": true, + "pathEmptyD": true, + "polylineEmptyPoints": true, + "polygonEmptyPoints": true + } + },{ + "name": "removeEmptyText", + "active": true, + "params": { + "text": true, + "tspan": true, + "tref": true + } + },{ + "name": "removeSVGAttrs", + "active": true, + "params": { + "id": true, + "version": true, + "x0": true, + "y0": true, + "xmlspace": true + } + },{ + "name": "convertColors", + "active": true, + "params": { + "names2hex": true, + "rgb2hex": true, + "shorthex": true + } + } + ], + "reversePass": [ + { + "name": "removeEmptyContainers", + "active": true, + "params": { + "a": true, + "defs": true, + "glyph": true, + "g": true, + "marker": true, + "mask": true, + "missing-glyph": true, + "pattern": true, + "svg": true, + "switch": true, + "symbol": true + } + },{ + "name": "moveElemsAttrsToGroup", + "active": true + },{ + "name": "collapseGroups", + "active": true + } + ] + } +} diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 00000000..04ac29be --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,7 @@ +test: + @./node_modules/.bin/mocha --reporter min + +test-v: + @./node_modules/.bin/mocha --reporter spec + +.PHONY: test test-v \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..bea9ce7d --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2012 Kir Belevich + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 4fdac487..aa2a08ab 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,74 @@ -svgo -==== \ No newline at end of file +``` +o-o o o o--o o-o + \ \ / | | | | +o-o o o--o o-o + | + o--o +``` + +## SVGO + +**SVG** **O**ptimizer is a Nodejs-based tool for optimizing SVG vector graphics files. + +## Why? + +SVG files, especially exported from various editors, usually contains a lot of redundant and useless information such as editor metadata, comments, hidden elements and other stuff that can be safely removed without affecting SVG rendering result. + +## What it can do + +SVGO has a plugin-based architecture, so almost every optimization is a separate plugin. + +Today we have: + +* [ [>](svgo/tree/master/plugins/cleanupAttrs.js) ] cleanup attributes from newlines, trailing and repeating spaces +* [ [>](svgo/tree/master/plugins/removeDoctype.js) ] remove doctype declaration +* [ [>](svgo/tree/master/plugins/removeXMLProcInst.js) ] remove XML processing instructions +* [ [>](svgo/tree/master/plugins/removeComments.js) ] remove comments +* [ [>](svgo/tree/master/plugins/removeMetadata.js) ] remove metadata +* [ [>](svgo/tree/master/plugins/removeEditorsNSData.js) ] remove editors namespaces, elements and attributes +* [ [>](svgo/tree/master/plugins/removeEmptyAttrs.js) ] remove empty attributes +* [ [>](svgo/tree/master/plugins/removeDefaultPx.js) ] remove default "px" unit +* [ [>](svgo/tree/master/plugins/removeHiddenElems.js) ] remove a lot of hidden elements +* [ [>](svgo/tree/master/plugins/removeEmptyText.js) ] remove empty Text elements +* [ [>](svgo/tree/master/plugins/removeEmptyContainers.js) ] remove empty Container elements +* [ [>](svgo/tree/master/plugins/convertStyleToAttrs.js) ] convert styles into attributes +* [ [>](svgo/tree/master/plugins/convertColors.js) ] convert colors +* [ [>](svgo/tree/master/plugins/moveElemsAttrsToGroup.js) ] move elements attributes to the existing group wrapper +* [ [>](svgo/tree/master/plugins/collapseGroups.js) ] collapse groups + +But it's not only about rude removing, SVG has a strict [specification](http://www.w3.org/TR/SVG/expanded-toc.html) with a lot of opportunities for optimizations, default values, geometry hacking and more. + +How-to instructions and plugins API docs will coming ASAP. + + +## How to use + +``` +npm install -g svgo +``` + +``` +Usage: + svgo [OPTIONS] [ARGS] + + +Options: + -h, --help : Help + -v, --version : Version + -c CONFIG, --config=CONFIG : Local config + -i INPUT, --input=INPUT : Input file (default: stdin) + -o OUTPUT, --output=OUTPUT : Output file (default: stdout) +``` + +``` +svgo -i myTestFile.svg -o myTestFile.min.svg +``` + +## TODO +It's only the very first public alpha :) + +1. documentation! +2. phantomjs-based server-side SVG rendering "before vs after" tests +3. more unit tests +4. more plugins +5. … \ No newline at end of file diff --git a/bin/svgo b/bin/svgo new file mode 100755 index 00000000..c46d5f4f --- /dev/null +++ b/bin/svgo @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require('../lib/coa').run(); \ No newline at end of file diff --git a/lib/coa.js b/lib/coa.js new file mode 100644 index 00000000..896e67f9 --- /dev/null +++ b/lib/coa.js @@ -0,0 +1,60 @@ +var Q = require('q'), + info = JSON.parse(require('fs').readFileSync(__dirname + '/../package.json')); + +module.exports = require('coa').Cmd() + .helpful() + .name(info.name) + .title(info.description) + .opt() + .name('version').title('Version') + .short('v').long('version') + .only() + .flag() + .act(function(opts) { + return info.version; + }) + .end() + .opt() + .name('config').title('Local config') + .short('c').long('config') + .end() + .opt() + .name('input').title('Input file (default: stdin)') + .short('i').long('input') + .input() + .end() + .opt() + .name('output').title('Output file (default: stdout)') + .short('o').long('output') + .output() + .end() + .act(function(options) { + + var input = [], + deferred = Q.defer(), + SVGO = require('./svgo'); + + options.input + .on('data', function(chunk) { + input.push(chunk); + }) + .once('end', function() { + deferred.resolve(input.join()); + }) + .resume(); + + return deferred.promise + .then(function(svg) { + return SVGO(svg, options); + }) + .then(function(svgmin) { + var output = options.output; + + output.write(svgmin); + + output === process.stdout ? + output.write('\n') : + output.end(); + }); + + }); diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 00000000..528467a6 --- /dev/null +++ b/lib/config.js @@ -0,0 +1,52 @@ +var QFS = require('q-fs'), + PATH = require('path'), + extend = require('./tools').extend; + +module.exports = function(options) { + + var defaultConfigPath = PATH.resolve(__dirname, '../.svgo'); + + if (!options || !options.config) return readConfig(defaultConfigPath); + + return readConfig(defaultConfigPath) + .then(function(defaultConfig) { + + if (typeof options.config === 'string') { + + var localConfigPath = PATH.resolve(process.cwd, options.config); + + return QFS.exists(localConfigPath) + .then(function(exists) { + + if (!exists) return defaultConfig; + + return readConfig(localConfigPath) + .then(function(localConfig) { + return extend(true, defaultConfig, localConfig); + }); + + }); + + } else if (Object.prototype.toString.call(options.config) === '[object Object]') { + + return extend(true, defaultConfig, options.config); + + } else { + + // TODO: ... + throw new Error('...'); + + } + + }); + +}; + +function readConfig(path) { + + return QFS.read(path) + .then(function(data) { + return JSON.parse(data); + }); + +}; diff --git a/lib/js2svg.js b/lib/js2svg.js new file mode 100644 index 00000000..7d1e8af2 --- /dev/null +++ b/lib/js2svg.js @@ -0,0 +1,229 @@ +var INHERIT = require('inherit'); + +/** + * Convert SVG-as-JS object to SVG (XML) string. + * + * @param {Object} data svg-as-js data object + * @param {Class} [converter] custom converter class + * @return {Converter} Converter instance + */ +module.exports = function(data, converter) { + + return new (converter || Converter)(data).run(data); + +}; + +/** + * @class Converter + */ +var Converter = exports.Converter = INHERIT( +/** + * @lends Nodes.prototype + */ + { + + /** + * @constructs + * @private + */ + __constructor: function() { + + /** + * Converter options. + * + * @type {Object} + */ + this.options = { + doctypeStart: '', + + procInstStart: '', + + tagOpenStart: '<', + tagOpenEnd: '>', + tagCloseStart: '', + + tagShortStart: '<', + tagShortEnd: '/>', + + attrStart: '="', + attrEnd: '"', + + commentStart: '', + + cdataStart: '' + }; + + }, + + /** + * Start conversion. + * + * @param {Object} svg-as-js data object + * @return {String} + */ + run: function(data) { + + var svg = ''; + + if (data.content) { + + data.content.forEach(function(item) { + + if (item.isElem()) { + svg += this.createElem(item); + } else if (item.isText()) { + svg += item.text; + } else if (item.isDoctype()) { + svg += this.createDoctype(item.doctype); + } else if (item.isProcInst()) { + svg += this.createProcInst(item.processinginstruction); + } else if (item.isComment()) { + svg += this.createComment(item.comment); + } else if (item.isCDATA()) { + svg += this.createCDATA(item.cdata); + } + + }, this); + + } + + return svg; + + }, + + /** + * Create doctype tag. + * + * @param {String} doctype doctype body string + * @return {String} + */ + createDoctype: function(doctype) { + + return this.options.doctypeStart + + doctype + + this.options.doctypeEnd; + + }, + + /** + * Create XML Processing Instruction tag. + * + * @param {Object} instruction instruction object + * @return {String} + */ + createProcInst: function(instruction) { + + return this.options.procInstStart + + instruction.name + + ' ' + + instruction.body + + this.options.procInstEnd; + + }, + + /** + * Create comment tag. + * + * @param {String} comment comment body + * @return {String} + */ + createComment: function(comment) { + + return this.options.commentStart + + comment + + this.options.commentEnd; + + }, + + /** + * Create CDATA section. + * + * @param {String} cdata CDATA body + * @return {String} + */ + createCDATA: function(cdata) { + + return this.options.cdataStart + + cdata + + this.options.cdataEnd; + + }, + + /** + * Create element tag. + * + * @param {Object} data element object + * @return {String} + */ + createElem: function(data) { + + // empty element and short tag + if (data.isEmpty()) { + + return this.options.tagShortStart + + data.elem + + this.createAttrs(data) + + this.options.tagShortEnd; + + // non-empty element + } else { + + return this.options.tagOpenStart + + data.elem + + this.createAttrs(data) + + this.options.tagOpenEnd + + this.run(data) + + this.options.tagCloseStart + + data.elem + + this.options.tagCloseEnd; + + } + + }, + + /** + * Create element attributes. + * + * @param {Object} elem attributes object + * @return {String} + */ + createAttrs: function(elem) { + + var self = this, + attrs = ''; + + if (elem.hasAttr()) { + + elem.eachAttr(function(attr) { + + attrs += ' ' + + attr.name + + self.options.attrStart + + attr.value + + self.options.attrEnd; + + }); + + } + + return attrs; + + } + + } +); + +/* +var MyConv = INHERIT(Converter, { + + __constructor: function(options) { + this.__base(); + } + +}); +*/ diff --git a/lib/jsAPI.js b/lib/jsAPI.js new file mode 100644 index 00000000..28bbc02d --- /dev/null +++ b/lib/jsAPI.js @@ -0,0 +1,249 @@ +var INHERIT = require('inherit'), + extend = require('./tools').extend; + +/** + * @class SVG-as-JS Nodes API. + */ +exports.Nodes = INHERIT( + /** + * @lends Nodes.prototype + */ + { + + /** + * @constructs + * @private + */ + __constructor: function(data) { + + extend(this, data); + + }, + + /** + * Remove item. + */ + remove: function() { + + delete this; + + }, + + /** + * Determine if item is an element + * (any, with a specific name or in a names array). + * + * @param {String|Array} [param] element name or names arrays + * @return {Boolean} + */ + isElem: function(param) { + + if (!param) return !!this.elem; + + if (Array.isArray(param)) return !!this.elem && (param.indexOf(this.elem) > -1); + + return !!this.elem && this.elem === param; + + }, + + /** + * Determine if item is a doctype. + * + * @return {Boolean} + */ + isDoctype: function() { + + return !!this.doctype; + + }, + + /** + * Determine if item is a XML Processing Instruction. + * + * @return {Boolean} + */ + isProcInst: function() { + + return !!this.processinginstruction; + + }, + + /** + * Determine if item is a CDATA block. + * + * @return {Boolean} + */ + isCDATA: function() { + + return !!this.cdata; + + }, + + /** + * Determine if item is a comment node. + * + * @return {Boolean} + */ + isComment: function() { + + return !!this.comment; + + }, + + /** + * Determine if item is a text node. + * + * @return {Boolean} + */ + isText: function() { + + return !!this.text; + + }, + + /** + * Determine if element is empty. + * + * @return {Boolean} + */ + isEmpty: function() { + + return !this.content || !this.content.length; + + }, + + /** + * Iterates over all attributes. + * + * @param {Function} callback + * @return {Boolean} false if the are no any attributes + */ + eachAttr: function(callback) { + + if (!this.hasAttr()) return false; + + Object.getOwnPropertyNames(this.attrs).forEach(function(name) { + callback(this.attrs[name]); + }, this); + + }, + + /** + * Determine if element has an attribute + * (any, or by name or by name + value). + * + * @param {String|Object} [name] attribute name or object + * @param {String} [val] attribute value (will be toString()'ed) + * @return {Boolean} + */ + hasAttr: function(attr, val) { + + if (!this.attrs || !Object.keys(this.attrs).length) return false; + + if (!arguments.length) return !!this.attrs; + + if (typeof attr === 'object') { + val = attr.value; + attr = attr.name; + } + + if (val !== undefined) return !!this.attrs[attr] && this.attrs[attr].value === val.toString(); + + return !!this.attrs[attr]; + + }, + + /** + * Get a specific attribute from an element + * (by name or name + value). + * + * @param {String} [name] attribute name + * @param {String} [val] attribute value (will be toString()'ed) + * @return {Object} + */ + attr: function(name, val) { + + if (!this.hasAttr() || !arguments.length) return undefined; + + if (val !== undefined) return this.hasAttr(name, val) && this.attrs[name]; + + return this.hasAttr(name) && this.attrs[name]; + + }, + + /** + * Remove a specific attribute. + * + * @param {String|Object} attr attribute name or object + * @param {String} [val] attribute value + * @return {Boolean} + */ + removeAttr: function(attr, val) { + + if (!this.hasAttr(attr)) return false; + + if (!arguments.length) { + delete this.attrs; + return true; + } + + if (typeof attr === 'object') { + val = attr.value; + attr = attr.name; + } + + if (val && this.attrs[attr].value !== val) return false; + + delete this.attrs[attr]; + + if (!Object.keys(this.attrs).length) delete this.attrs; + + return true; + + }, + + /** + * Add attribute. + * + * @param {Object} attr attribute object + */ + addAttr: function(attr, val) { + + this.attrs = this.attrs || {}; + + this.attrs[attr.name] = attr; + + }, + + /** + * Determine if item's content has an element(s) + * (any or by name or by names array). + * + * @param {String|Array} [name] element name or names array + * @return {Boolean} + */ + hasElem: function(name) { + + return this.content.some(function(item) { + return name ? item.isElem(name) : item.isElem(); + }); + + }, + + /** + * Determine if item's content has only elements + * (any or by name or by names array). + * + * @param {String|Array} [name] element name or names array + * @return {Boolean} + */ + hasAllElems: function(name) { + + return this.content.every(function(item) { + return name ? item.isElem(name) : item.isElem(); + }); + + } + + } +); diff --git a/lib/plugins.js b/lib/plugins.js new file mode 100644 index 00000000..31ff7cd9 --- /dev/null +++ b/lib/plugins.js @@ -0,0 +1,84 @@ +var INHERIT = require('inherit'); + +module.exports = function(json, plugins, pluginsEngine) { + + var engine = new (pluginsEngine || PluginsEngine)(); + + json = engine.pass(json, plugins.directPass); + json = engine.pass(json, plugins.reversePass, true); + + return json; + +}; + +var PluginsEngine = exports.PluginsEngine = INHERIT({ + + _makePluginsList: function(arr) { + + return arr.map(function(plugin) { + plugin.fn = require('../plugins/' + plugin.name)[plugin.name]; + + return plugin; + }); + + }, + + pass: function(json, plugins, reverse) { + + plugins = this._makePluginsList(plugins); + + function monkeys(data) { + + data.content = data.content.filter(function(item) { + + if (reverse && item.content) { + monkeys.call(this, item); + } + + var filter = true; + + plugins.forEach(function(plugin) { + if (plugin.active && plugin.fn(item, plugin.params) === false) { + filter = false; + } + }); + + if (!reverse && item.content) { + monkeys.call(this, item); + } + + return filter; + + }, this); + + return data; + + }; + + return monkeys.call(this, json); + + }, + + full: function() { + + this.fullList.forEach(function(plugin) { + if (plugin.active) { + json = plugin.fn(json, plugin.params); + } + }); + + return json; + + } + +}); + +/* +var MyPluginsEngine = INHERIT(PluginsEngine, { + + __constructor: function(options) { + this.__base(); + } + +}); +*/ diff --git a/lib/svg2js.js b/lib/svg2js.js new file mode 100644 index 00000000..cb10dbe2 --- /dev/null +++ b/lib/svg2js.js @@ -0,0 +1,126 @@ +var Q = require('q'), + SAX = require('sax'), + TOOLS = require('./tools'), + JSAPI = require('./jsAPI'); + +/** + * Convert SVG (XML) string to SVG-as-JS object. + * + * @param {String} svg SVG (XML) string + * @param {Object} config sax xml parser config + * @return {Object} + */ +module.exports = function(svg, config) { + + config = config || { + strict: true, + options: { + trim: true, + normalize: true, + lowercase: true, + xmlns: true, + position: false + } + }; + + var deferred = Q.defer(), + sax = SAX.parser(config.strict, config.options), + root = {}, + current = root, + stack = []; + + function pushToContent(content) { + + content = new JSAPI.Nodes(content); + + (current.content = current.content || []).push(content); + + return content; + + }; + + sax.ondoctype = function(doctype) { + + pushToContent({ + doctype: doctype + }); + + }; + + sax.onprocessinginstruction = function(data) { + + pushToContent({ + processinginstruction: data + }); + + }; + + sax.oncomment = function(comment) { + + pushToContent({ + comment: comment + }); + + }; + + sax.oncdata = function(cdata) { + + pushToContent({ + cdata: cdata + }); + + }; + + sax.onopentag = function(data) { + + var elem = { + elem: data.name, + prefix: data.prefix, + local: data.local + }; + + if (Object.keys(data.attributes).length) { + elem.attrs = {}; + + Object.getOwnPropertyNames(data.attributes).forEach(function(name) { + elem.attrs[name] = { + name: name, + value: data.attributes[name].value, + prefix: data.attributes[name].prefix, + local: data.attributes[name].local + }; + }); + } + + elem = pushToContent(elem); + current = elem; + stack.push(elem); + + }; + + sax.ontext = function(text) { + + pushToContent({ + text: text + }); + + }; + + sax.onclosetag = function() { + + stack.pop(); + current = stack[stack.length - 1]; + + }; + + sax.onend = function() { + + deferred.resolve(root); + + }; + + sax.write(svg).close(); + + return deferred.promise; + +}; diff --git a/lib/svgo.js b/lib/svgo.js new file mode 100644 index 00000000..4d7e7150 --- /dev/null +++ b/lib/svgo.js @@ -0,0 +1,20 @@ +var CONFIG = require('./config'), + SVG2JS = require('./svg2js'), + PLUGINS = require('./plugins'), + JS2SVG = require('./js2svg'); + +module.exports = function(svg, options) { + + return CONFIG(options) + .then(function(config) { + + return SVG2JS(svg, config.saxXMLParser) + .then(function(json) { + + return JS2SVG(PLUGINS(json, config.plugins)); + + }); + + }); + +}; diff --git a/lib/tools.js b/lib/tools.js new file mode 100644 index 00000000..55756100 --- /dev/null +++ b/lib/tools.js @@ -0,0 +1,118 @@ +/** + * Adopted from jquery's extend method. Under the terms of MIT License. + * + * http://code.jquery.com/jquery-1.4.2.js + * + * Modified by mscdex to use Array.isArray instead of the custom isArray method + */ + var extend = exports.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy; + + // Handle a deep copy situation + if (typeof target === 'boolean') { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if (typeof target !== 'object' && !typeof target === 'function') + target = {}; + + var isPlainObject = function(obj) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if (!obj || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.setInterval) + return false; + + var has_own_constructor = hasOwnProperty.call(obj, 'constructor'); + var has_is_property_of_method = hasOwnProperty.call(obj.constructor.prototype, 'isPrototypeOf'); + // Not own constructor property must be Object + if (obj.constructor && !has_own_constructor && !has_is_property_of_method) + return false; + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key, last_key; + for (key in obj) + last_key = key; + + return typeof last_key === 'undefined' || hasOwnProperty.call(obj, last_key); + }; + + + for (; i < length; i++) { + // Only deal with non-null/undefined values + if ((options = arguments[i]) !== null) { + // Extend the base object + for (name in options) { + if (!hasOwnProperty.call(options, name)) + continue; + src = target[name]; + copy = options[name]; + + // Prevent never-ending loop + if (target === copy) + continue; + + // Recurse if we're merging object literal values or arrays + if (deep && copy && (isPlainObject(copy) || Array.isArray(copy))) { + var clone = src && (isPlainObject(src) || Array.isArray(src)) ? src : Array.isArray(copy) ? [] : {}; + + // Never move original objects, clone them + target[name] = extend(deep, clone, copy); + + // Don't bring in undefined values + } else if (typeof copy !== 'undefined') + target[name] = copy; + } + } + } + + // Return the modified object + return target; +}; + +exports.inspect = require('eyes').inspector({ maxLength: 99999 }); + +exports.flatten = function(array) { + var result = [], + that = arguments.callee; + + array.forEach(function(item) { + Array.prototype.push.apply( + result, + Array.isArray(item) ? that(item) : [item] + ); + }); + + return result; +}; + +exports.intersectAttrs = function(a, b) { + var c = {}; + + Object.getOwnPropertyNames(a).forEach(function(n) { + if ( + b.hasOwnProperty(n) && + a[n].name === b[n].name && + a[n].value === b[n].value && + a[n].prefix === b[n].prefix && + a[n].local === b[n].local + ) { + c[n] = a[n]; + } + }); + + return c; +}; + +exports.intersectArrays = function(a, b) { + return a.filter(function(n) { + return b.indexOf(n) > -1; + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 00000000..5a984b85 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "svgo", + "version": "0.0.1", + "description": "SVG Optimizer", + "keywords": ["svgo", "svg", "optimize", "minify"], + "homepage": " ", + "bugs": { + "url": "https://github.com/deepsweet/svgo/issues", + "email": "kir@soulshine.in" + }, + "author": "Kir Belevich (https://github.com/deepsweet)", + "contributors": [ + ], + "repository": { + "type": "git", + "url": "git://github.com/deepsweet/svgo.git" + }, + "main": "./lib/svgo.js", + "directories": { + "bin": "./bin", + "lib": "./lib" + }, + "bin": { + "svgo": "./bin/svgo" + }, + "scripts": { + "test": "./node_modules/.bin/mocha --reporter min", + "test-v": "./node_modules/.bin/mocha --reporter spec" + }, + "dependencies": { + "inherit": "*", + "q": "0.8", + "q-fs": "", + "coa": "0.3.x", + "sax": "0.4.x", + "mocha": "1.4.*", + "should": "", + "eyes": "" + }, + "engines": { + "node": ">=0.6.0" + } +} diff --git a/plugins/_collections.js b/plugins/_collections.js new file mode 100644 index 00000000..645ae48b --- /dev/null +++ b/plugins/_collections.js @@ -0,0 +1,640 @@ +// http://wiki.inkscape.org/wiki/index.php/Inkscape-specific_XML_attributes +exports.editorNamespaces = [ + 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', + 'http://www.inkscape.org/namespaces/inkscape', + 'http://ns.adobe.com/AdobeIllustrator/10.0/', + 'http://ns.adobe.com/Graphs/1.0/', + 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', + 'http://ns.adobe.com/Variables/1.0/', + 'http://ns.adobe.com/SaveForWeb/1.0/', + 'http://ns.adobe.com/Extensibility/1.0/', + '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://www.w3.org/TR/SVG/styling.html#SVGStylingProperties +exports.stylingProps = [ + 'font', + 'font-family', + 'font-size', + 'font-size-adjust', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-weight',, + 'direction', + 'letter-spacing', + 'text-decoration', + 'unicode-bidi', + 'word-spacing', + 'clip', + 'color', + 'cursor', + 'display', + 'overflow', + 'visibility', + 'clip-path', + 'clip-rule', + 'mask', + 'opacity', + 'enable-background', + 'filter', + 'flood-color', + 'flood-opacity', + 'lighting-color', + 'stop-color', + 'stop-opacity', + 'pointer-events', + 'color-interpolation', + 'color-interpolation-filters', + 'color-profile', + 'color-rendering', + 'fill', + 'fill-opacity', + 'fill-rule', + 'image-rendering', + 'marker', + 'marker-end', + 'marker-mid', + 'marker-start', + 'shape-rendering', + 'stroke', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke-width', + 'text-rendering', + 'alignment-baseline', + 'baseline-shift', + 'dominant-baseline', + 'glyph-orientation-horizontal', + 'glyph-orientation-vertical', + 'kerning', + 'text-anchor', + 'writing-mode' +]; + +exports.defaultValues = { + // http://www.w3.org/TR/SVG/attindex.html#RegularAttributes + // http://www.w3.org/TR/SVG/attindex.html#PresentationAttributes + 'accumulate': { 'val': 'none', 'inh': true }, + 'additive': { 'val': 'replace', 'inh': true }, + 'alphabetic': { 'val': '0', 'inh': true }, + 'amplitude': { 'val': '1', 'inh': false }, + 'arabic-form': { 'val': 'initial', 'inh': true }, + + // http://www.w3.org/TR/SVG/propidx.html + 'baseline-shift': { 'val': 'baseline', 'inh': false }, + 'clip': { 'val': 'auto', 'inh': false }, + 'clip-path': { 'val': 'none', 'inh': false }, + 'color-interpolation': { 'val': 'sRGB', 'inh': true }, + 'color-interpolation-filters': { 'val': 'linearRGB', 'inh': true }, + 'color-profile': { 'val': 'auto', 'inh': true }, + 'color-rendering': { 'val': 'auto', 'inh': true }, + 'cursor': { 'val': 'auto', 'inh': true }, + 'direction': { 'val': 'ltr', 'inh': true }, + 'display': { 'val': 'inline', 'inh': false }, + 'dominant-baseline': { 'val': 'auto', 'inh': false }, + 'enable-background': { 'val': 'accumulate', 'inh': false }, + 'fill': { 'val': ['black', '#000000', '#000'], 'inh': true }, + 'fill-opacity': { 'val': '1', 'inh': true }, + 'filter': { 'val': 'none', 'inh': false }, + 'flood-color': { 'val': ['black', '#000000', '#000'], 'inh': false }, + 'flood-opacity': { 'val': '1', 'inh': false }, + 'font-size': { 'val': 'medium', 'inh': true }, + 'font-size-adjust': { 'val': 'none', 'inh': true }, + 'font-stretch': { 'val': 'normal', 'inh': true }, + 'font-style': { 'val': 'normal', 'inh': true }, + 'font-variant': { 'val': 'normal', 'inh': true }, + 'font-weight': { 'val': 'normal', 'inh': true }, + 'glyph-orientation-horizontal': { 'val': '0deg', 'inh': true }, + 'glyph-orientation-vertical': { 'val': 'auto', 'inh': true }, + 'image-rendering': { 'val': 'auto', 'inh': true }, + 'kerning': { 'val': 'auto', 'inh': true }, + 'letter-spacing': { 'val': 'normal', 'inh': true }, + 'lighting-color': { 'val': ['white', '#ffffff', '#fff'], 'inh': false }, + 'marker-end': { 'val': 'none', 'inh': true }, + 'marker-mid': { 'val': 'none', 'inh': true }, + 'marker-start': { 'val': 'none', 'inh': true }, + 'mask': { 'val': 'none', 'inh': false }, + 'opacity': { 'val': '1', 'inh': false }, + 'pointer-events': { 'val': 'visiblePainted', 'inh': true }, + 'shape-rendering': { 'val': 'auto', 'inh': true }, + 'stop-color': { 'val': ['black', '#000000', '#000'], 'inh': false }, + 'stop-opacity': { 'val': '1', 'inh': false }, + 'stroke': { 'val': 'none', 'inh': true }, + 'stroke-dasharray': { 'val': 'none', 'inh': true }, + 'stroke-dashoffset': { 'val': '0', 'inh': true }, + 'stroke-linecap': { 'val': 'butt', 'inh': true }, + 'stroke-linejoin': { 'val': 'miter', 'inh': true }, + 'stroke-miterlimit': { 'val': '4', 'inh': true }, + 'stroke-opacity': { 'val': '1', 'inh': true }, + 'stroke-width': { 'val': '1', 'inh': true }, + 'text-anchor': { 'val': 'start', 'inh': true }, + 'text-decoration': { 'val': 'none', 'inh': false }, + 'text-rendering': { 'val': 'auto', 'inh': true }, + 'unicode-bidi': { 'val': 'normal', 'inh': false }, + 'visibility': { 'val': 'visible', 'inh': true }, + 'word-spacing': { 'val': 'normal', 'inh': true }, + 'writing-mode': { 'val': 'lr-tb', 'inh': true } +}; + +// http://www.w3.org/TR/SVG/intro.html#Definitions +var elems = exports.elems = { + 'animation': ['animate', 'animateColor', 'animateMotion', 'animateTransform', 'set'], + 'descriptive': ['desc', 'metadata', 'title'], + 'shape': ['circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect'], + 'structural': ['defs', 'g', 'svg', 'symbol', 'use'], + 'gradient': ['linearGradient', 'radialGradient'], + // http://www.w3.org/TR/SVG/intro.html#TermContainerElement + 'container': ['a', 'defs', 'glyph', 'g', 'marker', 'mask', 'missing-glyph', 'pattern', 'svg', 'switch', 'symbol'] +}; + +// http://www.w3.org/TR/SVG/intro.html#Definitions +var attrs = exports.attrs = { + 'animationAddition': ['additive', 'accumulate'], + 'animationAttributeTarget': ['attributeType', 'attributeName'], + 'animationEvent': ['onbegin', 'onend', 'onrepeat', 'onload'], + 'animationTiming': ['begin', 'dur', 'end', 'min', 'max', 'restart', 'repeatCount', 'repeatDur', 'fill'], + 'animationValue': ['calcMode', 'values', 'keyTimes', 'keySplines', 'from', 'to', 'by'], + 'conditionalProcessing': ['requiredFeatures', 'requiredExtensions', 'systemLanguage'], + 'core': ['id', 'xml:base', 'xml:lang', 'xml:space'], + 'graphicalEvent': ['onfocusin', 'onfocusout', 'onactivate', 'onclick', 'onmousedown', 'onmouseup', 'onmouseover', 'onmousemove', 'onmouseout', 'onload'], + 'presentation': ['alignment-baseline', 'baseline-shift', 'clip', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cursor', 'direction', 'display', 'dominant-baseline', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 'image-rendering', 'kerning', 'letter-spacing', 'lighting-color', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'overflow', 'pointer-events', 'shape-rendering', 'stop-color', 'stop-opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-anchor', 'text-decoration', 'text-rendering', 'unicode-bidi', 'visibility', 'word-spacing', 'writing-mode'], + 'xlink': ['xlink:href', 'xlink:show', 'xlink:actuate', 'xlink:type', 'xlink:role', 'xlink:arcrole', 'xlink:title'] +}; + +// http://www.w3.org/TR/SVG/eltindex.html +exports.possibles = { + 'a': { + 'attrs': [attrs.conditionalProcessing, attrs.core, attrs.graphicalEvent, attrs.presentation, attrs.xlink, 'class', 'style', 'externalResourcesRequired', 'transform', 'target'], + 'content': [elems.animation, elems.descriptive, elems.shape, elems.structural, elems.gradient, 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', 'font', 'font-face', 'foreignObject', 'image', 'marker', 'mask', 'pattern', 'script', 'style', 'switch', 'text', 'view'] + }, + 'altGlyph': { + 'attrs': [attrs.conditionalProcessing, attrs.core, attrs.graphicalEvent, attrs.presentation, attrs.xlink, 'class', 'style', 'externalResourcesRequired', 'x', 'y', 'dx', 'dy', 'glyphRef', 'format', 'rotate'], + 'content': true + }, + 'altGlyphDef': { + 'attrs': [attrs.core], + 'content': ['glyphRef'] + }, + 'altGlyphItem': { + 'attrs': [attrs.core], + 'content': ['glyphRef', 'altGlyphItem'] + }, + 'animate': { + 'attrs': [attrs.conditionalProcessing, attrs.core, attrs.animationAddition, attrs.animationAttributeTarget, attrs.animationEvent, attrs.animationTiming, attrs.animationValue, attrs.presentation, attrs.xlink, 'externalResourcesRequired'], + 'content': [elems.descriptive] + }, + 'animateColor': { + 'attrs': [], + 'content': [] + }, + 'animateMotion': { + 'attrs': [], + 'content': [] + }, + 'animateTransform': { + 'attrs': [], + 'content': [] + }, + 'circle': { + 'attrs': [], + 'content': [] + }, + 'clipPath': { + 'attrs': [], + 'content': [] + }, + 'color-profile': { + 'attrs': [], + 'content': [] + }, + 'cursor': { + 'attrs': [], + 'content': [] + }, + 'defs': { + 'attrs': [], + 'content': [] + }, + 'desc': { + 'attrs': [], + 'content': [] + }, + 'ellipse': { + 'attrs': [], + 'content': [] + }, + 'feBlend': { + 'attrs': [], + 'content': [] + }, + 'feColorMatrix': { + 'attrs': [], + 'content': [] + }, + 'feComponentTransfer': { + 'attrs': [], + 'content': [] + }, + 'feComposite': { + 'attrs': [], + 'content': [] + }, + 'feConvolveMatrix': { + 'attrs': [], + 'content': [] + }, + 'feDiffuseLighting': { + 'attrs': [], + 'content': [] + }, + 'feDisplacementMap': { + 'attrs': [], + 'content': [] + }, + 'feDistantLight': { + 'attrs': [], + 'content': [] + }, + 'feFlood': { + 'attrs': [], + 'content': [] + }, + 'feFuncA': { + 'attrs': [], + 'content': [] + }, + 'feFuncB': { + 'attrs': [], + 'content': [] + }, + 'feFuncG': { + 'attrs': [], + 'content': [] + }, + 'feFuncR': { + 'attrs': [], + 'content': [] + }, + 'feGaussianBlur': { + 'attrs': [], + 'content': [] + }, + 'feImage': { + 'attrs': [], + 'content': [] + }, + 'feMerge': { + 'attrs': [], + 'content': [] + }, + 'feMergeNode': { + 'attrs': [], + 'content': [] + }, + 'feMorphology': { + 'attrs': [], + 'content': [] + }, + 'feOffset': { + 'attrs': [], + 'content': [] + }, + 'fePointLight': { + 'attrs': [], + 'content': [] + }, + 'feSpecularLighting': { + 'attrs': [], + 'content': [] + }, + 'feSpotLight': { + 'attrs': [], + 'content': [] + }, + 'feTile': { + 'attrs': [], + 'content': [] + }, + 'feTurbulence': { + 'attrs': [], + 'content': [] + }, + 'filter': { + 'attrs': [], + 'content': [] + }, + 'font': { + 'attrs': [], + 'content': [] + }, + 'font-face': { + 'attrs': [], + 'content': [] + }, + 'font-face-format': { + 'attrs': [], + 'content': [] + }, + 'font-face-name': { + 'attrs': [], + 'content': [] + }, + 'font-face-src': { + 'attrs': [], + 'content': [] + }, + 'font-face-uri': { + 'attrs': [], + 'content': [] + }, + 'foreignObject': { + 'attrs': [], + 'content': [] + }, + 'g': { + 'attrs': [], + 'content': [] + }, + 'glyph': { + 'attrs': [], + 'content': [] + }, + 'glyphRef': { + 'attrs': [], + 'content': [] + }, + 'hkern': { + 'attrs': [], + 'content': [] + }, + 'image': { + 'attrs': [], + 'content': [] + }, + 'line': { + 'attrs': [], + 'content': [] + }, + 'linearGradient': { + 'attrs': [], + 'content': [] + }, + 'marker': { + 'attrs': [], + 'content': [] + }, + 'mask': { + 'attrs': [], + 'content': [] + }, + 'metadata': { + 'attrs': [], + 'content': [] + }, + 'missing-glyph': { + 'attrs': [], + 'content': [] + }, + 'mpath': { + 'attrs': [], + 'content': [] + }, + 'path': { + 'attrs': [], + 'content': [] + }, + 'pattern': { + 'attrs': [], + 'content': [] + }, + 'polygon': { + 'attrs': [], + 'content': [] + }, + 'polyline': { + 'attrs': [], + 'content': [] + }, + 'radialGradient': { + 'attrs': [], + 'content': [] + }, + 'rect': { + 'attrs': [], + 'content': [] + }, + 'script': { + 'attrs': [], + 'content': [] + }, + 'set': { + 'attrs': [], + 'content': [] + }, + 'stop': { + 'attrs': [], + 'content': [] + }, + 'style': { + 'attrs': [], + 'content': [] + }, + 'svg': { + 'attrs': [], + 'content': [] + }, + 'switch': { + 'attrs': [], + 'content': [] + }, + 'symbol': { + 'attrs': [], + 'content': [] + }, + 'text': { + 'attrs': [], + 'content': [] + }, + 'textPath': { + 'attrs': [], + 'content': [] + }, + 'title': { + 'attrs': [], + 'content': [] + }, + 'tref': { + 'attrs': [], + 'content': [] + }, + 'tspan': { + 'attrs': [], + 'content': [] + }, + 'use': { + 'attrs': [], + 'content': [] + }, + 'view': { + 'attrs': [], + 'content': [] + }, + 'vkern': [] +}; + +// http://www.w3.org/TR/SVG/single-page.html#types-ColorKeywords +exports.colorsNames = { + 'aliceblue': '#f0f8ff', + 'antiquewhite': '#faebd7', + 'aqua': '#00ffff', + 'aquamarine': '#7fffd4', + 'azure': '#f0ffff', + 'beige': '#f5f5dc', + 'bisque': '#ffe4c4', + 'black': '#000000', + 'blanchedalmond': '#ffebcd', + 'blue': '#0000ff', + 'blueviolet': '#8a2be2', + 'brown': '#a52a2a', + 'burlywood': '#deb887', + 'cadetblue': '#5f9ea0', + 'chartreuse': '#7fff00', + 'chocolate': '#d2691e', + 'coral': '#ff7f50', + 'cornflowerblue': '#6495ed', + 'cornsilk': '#fff8dc', + 'crimson': '#dc143c', + 'cyan': '#00ffff', + 'darkblue': '#00008b', + 'darkcyan': '#008b8b', + 'darkgoldenrod': '#b8860b', + 'darkgray': '#a9a9a9', + 'darkgreen': '#006400', + 'darkkhaki': '#bdb76b', + 'darkmagenta': '#8b008b', + 'darkolivegreen': '#556b2f', + 'darkorange': '#ff8c00', + 'darkorchid': '#9932cc', + 'darkred': '#8b0000', + 'darksalmon': '#e9967a', + 'darkseagreen': '#8fbc8f', + 'darkslateblue': '#483d8b', + 'darkslategray': '#2f4f4f', + 'darkturquoise': '#00ced1', + 'darkviolet': '#9400d3', + 'deeppink': '#ff1493', + 'deepskyblue': '#00bfff', + 'dimgray': '#696969', + 'dodgerblue': '#1e90ff', + 'firebrick': '#b22222', + 'floralwhite': '#fffaf0', + 'forestgreen': '#228b22', + 'fuchsia': '#ff00ff', + 'gainsboro': '#dcdcdc', + 'ghostwhite': '#f8f8ff', + 'gold': '#ffd700', + 'goldenrod': '#daa520', + 'gray': '#808080', + 'green': '#008000', + 'greenyellow': '#adff2f', + 'honeydew': '#f0fff0', + 'hotpink': '#ff69b4', + 'indianred': '#cd5c5c', + 'indigo': '#4b0082', + 'ivory': '#fffff0', + 'khaki': '#f0e68c', + 'lavender': '#e6e6fa', + 'lavenderblush': '#fff0f5', + 'lawngreen': '#7cfc00', + 'lemonchiffon': '#fffacd', + 'lightblue': '#add8e6', + 'lightcoral': '#f08080', + 'lightcyan': '#e0ffff', + 'lightgoldenrodyellow': '#fafad2', + 'lightgreen': '#90ee90', + 'lightgrey': '#d3d3d3', + 'lightpink': '#ffb6c1', + 'lightsalmon': '#ffa07a', + 'lightseagreen': '#20b2aa', + 'lightskyblue': '#87cefa', + 'lightslategray': '#778899', + 'lightsteelblue': '#b0c4de', + 'lightyellow': '#ffffe0', + 'lime': '#00ff00', + 'limegreen': '#32cd32', + 'linen': '#faf0e6', + 'magenta': '#ff00ff', + 'maroon': '#800000', + 'mediumaquamarine': '#66cdaa', + 'mediumblue': '#0000cd', + 'mediumorchid': '#ba55d3', + 'mediumpurple': '#9370db', + 'mediumseagreen': '#3cb371', + 'mediumslateblue': '#7b68ee', + 'mediumspringgreen': '#00fa9a', + 'mediumturquoise': '#48d1cc', + 'mediumvioletred': '#c71585', + 'midnightblue': '#191970', + 'mintcream': '#f5fffa', + 'mistyrose': '#ffe4e1', + 'moccasin': '#ffe4b5', + 'navajowhite': '#ffdead', + 'navy': '#000080', + 'oldlace': '#fdf5e6', + 'olive': '#808000', + 'olivedrab': '#6b8e23', + 'orange': '#ffa500', + 'orangered': '#ff4500', + 'orchid': '#da70d6', + 'palegoldenrod': '#eee8aa', + 'palegreen': '#98fb98', + 'paleturquoise': '#afeeee', + 'palevioletred': '#db7093', + 'papayawhip': '#ffefd5', + 'peachpuff': '#ffdab9', + 'peru': '#cd853f', + 'pink': '#ffc0cb', + 'plum': '#dda0dd', + 'powderblue': '#b0e0e6', + 'purple': '#800080', + 'red': '#ff0000', + 'rosybrown': '#bc8f8f', + 'royalblue': '#4169e1', + 'saddlebrown': '#8b4513', + 'salmon': '#fa8072', + 'sandybrown': '#f4a460', + 'seagreen': '#2e8b57', + 'seashell': '#fff5ee', + 'sienna': '#a0522d', + 'silver': '#c0c0c0', + 'skyblue': '#87ceeb', + 'slateblue': '#6a5acd', + 'slategray': '#708090', + 'snow': '#fffafa', + 'springgreen': '#00ff7f', + 'steelblue': '#4682b4', + 'tan': '#d2b48c', + 'teal': '#008080', + 'thistle': '#d8bfd8', + 'tomato': '#ff6347', + 'turquoise': '#40e0d0', + 'violet': '#ee82ee', + 'wheat': '#f5deb3', + 'white': '#ffffff', + 'whitesmoke': '#f5f5f5', + 'yellow': '#ffff00', + 'yellowgreen': '#9acd32' +}; + +// http://www.w3.org/TR/SVG/single-page.html#types-DataTypeColor +exports.colorsProps = [ + 'color', 'fill', 'stroke', 'stop-color', 'flood-color', 'lighting-color' +]; diff --git a/plugins/cleanupAttrs.js b/plugins/cleanupAttrs.js new file mode 100644 index 00000000..34887c25 --- /dev/null +++ b/plugins/cleanupAttrs.js @@ -0,0 +1,35 @@ +var regNewlines = /\n/g, + regSpaces = /\s{2,}/g; + +/** + * Cleanup attributes values from newlines, trailing and repeating spaces. + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.cleanupAttrs = function(item, params) { + + if (item.isElem() && item.hasAttr()) { + + item.eachAttr(function(attr) { + + if (params.newlines) { + attr.value = attr.value.replace(regNewlines, ''); + } + + if (params.trim) { + attr.value = attr.value.trim(); + } + + if (params.spaces) { + attr.value = attr.value.replace(regSpaces, ''); + } + + }); + + } + +}; diff --git a/plugins/collapseGroups.js b/plugins/collapseGroups.js new file mode 100644 index 00000000..da4fe0a6 --- /dev/null +++ b/plugins/collapseGroups.js @@ -0,0 +1,62 @@ +var flatten = require('../lib/tools').flatten; + +/* + * Collapse useless groups. + * + * @example + * + * + * + * + * + * ⬇ + * + * + * + * + * + * ⬇ + * + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.collapseGroups = function(item, params) { + + // non-empty elements + if (item.isElem() && !item.isEmpty()) { + + item.content.forEach(function(g, i) { + + // non-empty groups + if (g.isElem('g') && !g.isEmpty()) { + + // move group attibutes to the single content element + if (g.hasAttr() && g.content.length === 1) { + var inner = g.content[0]; + + if (inner.isElem()) { + g.eachAttr(function(attr) { + if (!inner.hasAttr(attr.name)) { + inner.addAttr(attr); + } + g.removeAttr(attr); + }); + } + } + + // collapse groups without attributes + if (!g.hasAttr()) { + item.content.splice(i, 1, g.content); + item.content = flatten(item.content); + } + } + + }); + + } + +}; diff --git a/plugins/convertColors.js b/plugins/convertColors.js new file mode 100644 index 00000000..bf3c2f6c --- /dev/null +++ b/plugins/convertColors.js @@ -0,0 +1,88 @@ +var collections = require('./_collections'), + regRGB = /^rgb\((\d+%?),\s*(\d+%?),\s*(\d+%?)\)$/, + regHEX = /^\#(([a-fA-F0-9])\2){3}$/; + +/** + * Convert [r, g, b] to #rrggbb. + * + * @see https://gist.github.com/983535 + * + * @example + * rgb2hex([255, 255, 255]) // '#ffffff' + * + * @param {Array} rgb [r, g, b] + * @return {String} #rrggbb + * + * @author Jed Schmidt + */ +function rgb2hex(rgb) { + return "#" + ((256 + rgb[0] << 8 | rgb[1]) << 8 | rgb[2]).toString(16).slice(1); +}; + +/** + * Convert different colors formats in element attributes to hex. + * + * @see http://www.w3.org/TR/SVG/types.html#DataTypeColor + * @see http://www.w3.org/TR/SVG/single-page.html#types-ColorKeywords + * + * @example + * Convert color name keyword to long hex: + * fuchsia ➡ #ff00ff + * + * Convert rgb() to long hex: + * rgb(255, 0, 255) ➡ #ff00ff + * rgb(50%, 100, 100%) ➡ #7f64ff + * + * Convert long hex to short hex: + * #aabbcc ➡ #abc + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.convertColors = function(item, params) { + + if (item.isElem() && item.hasAttr()) { + + item.eachAttr(function(attr) { + + if (collections.colorsProps.indexOf(attr.name) > -1) { + + var val = attr.value.toLowerCase(), + tmp = val, + match; + + // Convert color name keyword to long hex + if (params.names2hex && val in collections.colorsNames) { + val = collections.colorsNames[val]; + } + + // Convert rgb() to long hex + if (params.rgb2hex && (match = val.match(regRGB))) { + match = match.slice(1, 4).map(function(m) { + if (m.indexOf('%') > -1) { + m = Math.round(parseFloat(m) * 2.55); + } + + return +m; + }); + + val = rgb2hex(match); + } + + // Convert long hex to short hex + if (params.shorthex && (match = val.match(regHEX))) { + val = '#' + match[0][1] + match[0][3] + match[0][5]; + } + + if (tmp !== val) attr.value = val; + + } + + }); + + } + +}; diff --git a/plugins/convertStyleToAttrs.js b/plugins/convertStyleToAttrs.js new file mode 100644 index 00000000..2dc4b21d --- /dev/null +++ b/plugins/convertStyleToAttrs.js @@ -0,0 +1,70 @@ +var extend = require('../lib/tools').extend, + stylingProps = require('./_collections').stylingProps; + +/** + * Convert style in attributes. + * + * @example + * + * ⬇ + * + * + * @example + * + * ⬇ + * + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.convertStyleToAttrs = function(item, params) { + + if (item.isElem() && item.hasAttr('style')) { + // ['opacity: 1', 'color: #000'] + var styles = item.attr('style').value.split(';').filter(function(style) { + return style; + }), + attrs = {}; + + if (styles.length) { + + styles = styles.filter(function(style) { + if (style) { + // ['opacity', 1] + style = style.split(':'); + + var prop = style[0].trim(), + val = style[1].replace(/^[\'\"](.+)[\'\"]$/, "$1").trim(); + + if (stylingProps.indexOf(prop) > -1) { + + attrs[prop] = { + name: prop, + value: val, + local: prop, + prefix: '' + }; + + return false; + } + } + + return true; + }); + + extend(item.attrs, attrs); + + if (styles.length) { + item.attr('style').value = styles.join(';'); + } else { + item.removeAttr('style'); + } + + } + + } + +}; diff --git a/plugins/moveElemsAttrsToGroup.js b/plugins/moveElemsAttrsToGroup.js new file mode 100644 index 00000000..ac223585 --- /dev/null +++ b/plugins/moveElemsAttrsToGroup.js @@ -0,0 +1,57 @@ +var intersectAttrs = require('../lib/tools').intersectAttrs; + +/** + * Collapse content's intersected attributes to the existing group wrapper. + * + * @example + * + * + * text + * + * + * + * ⬇ + * + * + * text + * + * + * + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.moveElemsAttrsToGroup = function(item, params) { + + if (item.isElem('g') && !item.isEmpty() && item.content.length > 1) { + + var intersection = {}, + every = item.content.every(function(g) { + if (g.isElem() && g.hasAttr()) { + if (!Object.keys(intersection).length) { + intersection = g.attrs; + } else { + intersection = intersectAttrs(intersection, g.attrs) + } + + return true; + } + }); + + if (every && Object.keys(intersection).length) { + + item.content.forEach(function(g) { + for (var name in intersection) { + g.removeAttr(name); + item.addAttr(intersection[name]); + } + }); + + } + + } + +}; diff --git a/plugins/removeComments.js b/plugins/removeComments.js new file mode 100644 index 00000000..1a048743 --- /dev/null +++ b/plugins/removeComments.js @@ -0,0 +1,18 @@ +/** + * Remove comments. + * + * @example + * + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.removeComments = function(item, params) { + + return !item.isComment(); + +}; diff --git a/plugins/removeDefaultPx.js b/plugins/removeDefaultPx.js new file mode 100644 index 00000000..ebff2cdc --- /dev/null +++ b/plugins/removeDefaultPx.js @@ -0,0 +1,26 @@ +var regValPx = /^(\d+)px$/; + +/** + * Remove default "px" unit from attributes values. + * + * "One px unit is defined to be equal to one user unit. + * Thus, a length of 5px is the same as a length of 5" + * http://www.w3.org/TR/SVG/coords.html#Units + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.removeDefaultPx = function(item, params) { + + if (item.isElem() && item.hasAttr()) { + + item.eachAttr(function(attr) { + attr.value = attr.value.replace(regValPx, "$1"); + }); + + } + +}; diff --git a/plugins/removeDoctype.js b/plugins/removeDoctype.js new file mode 100644 index 00000000..b4bc9bb9 --- /dev/null +++ b/plugins/removeDoctype.js @@ -0,0 +1,25 @@ +/** + * Remove DOCTYPE declaration. + * + * "Unfortunately the SVG DTDs are a source of so many + * issues that the SVG WG has decided not to write one + * for the upcoming SVG 1.2 standard. In fact SVG WG + * members are even telling people not to use a DOCTYPE + * declaration in SVG 1.0 and 1.1 documents" + * https://jwatt.org/svg/authoring/#doctype-declaration + * + * @example + * + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.removeDoctype = function(item, params) { + + return !item.isDoctype(); + +}; diff --git a/plugins/removeEditorsNSData.js b/plugins/removeEditorsNSData.js new file mode 100644 index 00000000..df4b54ca --- /dev/null +++ b/plugins/removeEditorsNSData.js @@ -0,0 +1,49 @@ +var editorNamespaces = require('./_collections').editorNamespaces, + prefixes = []; + +/** + * Remove editors namespaces, elements and attributes. + * + * @example + * + * + * + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.removeEditorsNSData = function(item, params) { + + if (item.isElem() && item.hasAttr()) { + + if (item.isElem('svg')) { + + item.eachAttr(function(attr) { + if (attr.prefix === 'xmlns' && editorNamespaces.indexOf(attr.value) > -1) { + prefixes.push(attr.local); + + // + item.removeAttr(attr.name); + } + }); + + } + + // + if (prefixes.indexOf(item.prefix) > -1) { + return false; + } + + // <* sodipodi:*=""> + item.eachAttr(function(attr) { + if (prefixes.indexOf(attr.prefix) > -1) { + item.removeAttr(attr.name); + } + }); + + } + +}; diff --git a/plugins/removeEmptyAttrs.js b/plugins/removeEmptyAttrs.js new file mode 100644 index 00000000..52cc8f54 --- /dev/null +++ b/plugins/removeEmptyAttrs.js @@ -0,0 +1,22 @@ +/** + * Remove attributes with empty values. + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.removeEmptyAttrs = function(item, params) { + + if (item.isElem() && item.hasAttr()) { + + item.eachAttr(function(attr) { + if (attr.value === '') { + item.removeAttr(attr.name); + } + }); + + } + +}; diff --git a/plugins/removeEmptyContainers.js b/plugins/removeEmptyContainers.js new file mode 100644 index 00000000..8f49611f --- /dev/null +++ b/plugins/removeEmptyContainers.js @@ -0,0 +1,24 @@ +var container = require('./_collections').elems.container; + +/** + * Remove empty containers. + * + * @see http://www.w3.org/TR/SVG/intro.html#TermContainerElement + * + * @example + * + * + * @example + * + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.removeEmptyContainers = function(item, params) { + + return !(item.isElem(container) && item.isEmpty()); + +}; diff --git a/plugins/removeEmptyText.js b/plugins/removeEmptyText.js new file mode 100644 index 00000000..a05de73b --- /dev/null +++ b/plugins/removeEmptyText.js @@ -0,0 +1,45 @@ +/** + * Remove empty Text elements. + * + * @see http://www.w3.org/TR/SVG/text.html + * + * @example + * Remove empty text element: + * + * + * Remove empty tspan element: + * + * + * Remove tref with empty xlink:href attribute: + * + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.removeEmptyText = function(item, params) { + + // Remove empty text element + if ( + params.text && + item.isElem('text') && + item.isEmpty() + ) return false; + + // Remove empty tspan element + if ( + params.tspan && + item.isElem('tspan') && + item.isEmpty() + ) return false; + + // Remove tref with empty xlink:href attribute + if ( + params.tref && + item.isElem('tref') && + !item.hasAttr('xlink:href') + ) return false; + +}; diff --git a/plugins/removeHiddenElems.js b/plugins/removeHiddenElems.js new file mode 100644 index 00000000..f1860f91 --- /dev/null +++ b/plugins/removeHiddenElems.js @@ -0,0 +1,186 @@ +/** + * Remove hidden elements with disabled rendering: + * - display="none" + * - opacity="0" + * - circle with zero radius + * - ellipse with zero x-axis or y-axis radius + * - rectangle with zero width or height + * - pattern with zero width or height + * - image with zero width or height + * - path with empty data + * - polyline with empty points + * - polygon with empty points + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.removeHiddenElems = function(item, params) { + + if (item.isElem()) { + + // display="none" + // + // http://www.w3.org/TR/SVG/painting.html#DisplayProperty + // "A value of display: none indicates that the given element + // and its children shall not be rendered directly" + if ( + params.displayNone && + item.hasAttr('display', 'none') + ) return false; + + // opacity="0" + // + // http://www.w3.org/TR/SVG/masking.html#ObjectAndGroupOpacityProperties + if ( + params.opacity0 && + item.hasAttr('opacity', '0') + ) return false; + + // Circles with zero radius + // + // http://www.w3.org/TR/SVG/shapes.html#CircleElementRAttribute + // "A value of zero disables rendering of the element" + // + // + if ( + params.circleR0 && + item.isElem('circle') && + item.hasAttr('r', '0') + ) return false; + + // Ellipse with zero x-axis radius + // + // http://www.w3.org/TR/SVG/shapes.html#EllipseElementRXAttribute + // "A value of zero disables rendering of the element" + // + // + if ( + params.ellipseRX0 && + item.isElem('ellipse') && + item.hasAttr('rx', '0') + ) return false; + + // Ellipse with zero y-axis radius + // + // http://www.w3.org/TR/SVG/shapes.html#EllipseElementRYAttribute + // "A value of zero disables rendering of the element" + // + // + if ( + params.ellipseRY0 && + item.isElem('ellipse') && + item.hasAttr('ry', '0') + ) return false; + + // Rectangle with zero width + // + // http://www.w3.org/TR/SVG/shapes.html#RectElementWidthAttribute + // "A value of zero disables rendering of the element" + // + // + if ( + params.rectWidth0 && + item.isElem('rect') && + item.hasAttr('width', '0') + ) return false; + + // Rectangle with zero height + // + // http://www.w3.org/TR/SVG/shapes.html#RectElementHeightAttribute + // "A value of zero disables rendering of the element" + // + // + if ( + params.rectHeight0 && + params.rectWidth0 && + item.isElem('rect') && + item.hasAttr('height', '0') + ) return false; + + // Pattern with zero width + // + // http://www.w3.org/TR/SVG/pservers.html#PatternElementWidthAttribute + // "A value of zero disables rendering of the element (i.e., no paint is applied)" + // + // + if ( + params.patternWidth0 && + item.isElem('pattern') && + item.hasAttr('width', '0') + ) return false; + + // Pattern with zero height + // + // http://www.w3.org/TR/SVG/pservers.html#PatternElementHeightAttribute + // "A value of zero disables rendering of the element (i.e., no paint is applied)" + // + // + if ( + params.patternHeight0 && + item.isElem('pattern') && + item.hasAttr('height', '0') + ) return false; + + // Image with zero width + // + // http://www.w3.org/TR/SVG/struct.html#ImageElementWidthAttribute + // "A value of zero disables rendering of the element" + // + // + if ( + params.imageWidth0 && + item.isElem('image') && + item.hasAttr('width', '0') + ) return false; + + // Image with zero height + // + // http://www.w3.org/TR/SVG/struct.html#ImageElementHeightAttribute + // "A value of zero disables rendering of the element" + // + // + if ( + params.imageHeight0 && + item.isElem('image') && + item.hasAttr('height', '0') + ) return false; + + // Path with empty data + // + // http://www.w3.org/TR/SVG/paths.html#DAttribute + // + // + if ( + params.pathEmptyD && + item.isElem('path') && + !item.hasAttr('d') + ) return false; + + // Polyline with empty points + // + // http://www.w3.org/TR/SVG/shapes.html#PolylineElementPointsAttribute + // + // + if ( + params.polylineEmptyPoints && + item.isElem('polyline') && + !item.hasAttr('points') + ) return false; + + // Polygon with empty points + // + // http://www.w3.org/TR/SVG/shapes.html#PolygonElementPointsAttribute + // + // + if ( + params.polygonEmptyPoints && + item.isElem('polygon') && + !item.hasAttr('points') + ) return false; + + } + +}; diff --git a/plugins/removeMetadata.js b/plugins/removeMetadata.js new file mode 100644 index 00000000..0eea9bc2 --- /dev/null +++ b/plugins/removeMetadata.js @@ -0,0 +1,16 @@ +/** + * Remove . + * + * http://www.w3.org/TR/SVG/metadata.html + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.removeMetadata = function(item, params) { + + return !item.isElem('metadata'); + +}; diff --git a/plugins/removeSVGAttrs.js b/plugins/removeSVGAttrs.js new file mode 100644 index 00000000..66e86882 --- /dev/null +++ b/plugins/removeSVGAttrs.js @@ -0,0 +1,48 @@ +/** + * Remove some useless svg element attributes. + * + * @see http://www.w3.org/TR/SVG/struct.html#SVGElement + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.removeSVGAttrs = function(item, params) { + + if (item.isElem('svg') && item.hasAttr()) { + + /** + * Remove id attribute. + * + * @see http://www.w3.org/TR/SVG/struct.html#IDAttribute + * + * @example + * + */ + if (params.id) item.removeAttr('id'); + + /** + * Remove version attribute. + * + * @see http://www.w3.org/TR/SVG/struct.html#SVGElementVersionAttribute + * + * @example + * + */ + if (params.version) item.removeAttr('version'); + + /** + * Remove xnl:space attribute. + * + * @see http://www.w3.org/TR/SVG/struct.html#XMLSpaceAttribute + * + * @example + * + */ + if (params.xmlspace) item.removeAttr('xml:space'); + + } + +}; diff --git a/plugins/removeXMLProcInst.js b/plugins/removeXMLProcInst.js new file mode 100644 index 00000000..f9fff33a --- /dev/null +++ b/plugins/removeXMLProcInst.js @@ -0,0 +1,17 @@ +/** + * Remove XML Processing Instruction. + * + * @example + * + * + * @param {Object} item current iteration item + * @param {Object} params plugin params + * @return {Boolean} if false, item will be filtered out + * + * @author Kir Belevich + */ +exports.removeXMLProcInst = function(item, params) { + + return !item.isProcInst(); + +}; diff --git a/test/config.cfg b/test/config.cfg new file mode 100644 index 00000000..c8a461dd --- /dev/null +++ b/test/config.cfg @@ -0,0 +1,13 @@ +{ + "plugins": { + "directPass": [ + { + "name": "removeDoctype", + "active": false + },{ + "name": "myTestPlugin", + "active": true + } + ] + } +} \ No newline at end of file diff --git a/test/config.js b/test/config.js new file mode 100644 index 00000000..dd00d335 --- /dev/null +++ b/test/config.js @@ -0,0 +1,113 @@ +var should = require('should'), + config = require('../lib/config'); + +describe('config', function() { + + describe('default config', function() { + + var result; + + before(function(done) { + config().then(function(data) { + result = data; + done(); + }); + }); + + it('result should exists', function() { + result.should.exist; + }); + + it('result should be an instance of Object', function() { + result.should.be.an.instanceOf(Object); + }); + + it('result should have property "saxXMLParser" with instance of Object', function() { + result.should.have.property('saxXMLParser').with.instanceOf(Object); + }); + + it('result should have property "plugins" with instance of Object', function() { + result.should.have.property('plugins').with.instanceOf(Object); + }); + + it('plugins should have property "directPass" with instance of Array', function() { + result.plugins.should.have.property('directPass').with.instanceOf(Array); + }); + + it('directPass should include "removeDoctype" plugin with default params', function() { + result.plugins.directPass.should.includeEql({ + name: 'removeDoctype', + active: true + }); + }); + + }); + + describe('extending default config with object', function() { + + var myConfig = { + config: { + plugins: { + directPass: [ + { name: 'removeDoctype', active: false }, + { name: 'myTestPlugin', active: true } + ] + } + } + }, + result; + + before(function(done) { + config(myConfig).then(function(data) { + result = data; + done(); + }); + }); + + it('directPass should include extended "removeDoctype" plugin', function() { + result.plugins.directPass.should.includeEql({ + name: 'removeDoctype', + active: false + }); + }); + + it('directPass should include new "myTestPlugin" plugin', function() { + result.plugins.directPass.should.includeEql({ + name: 'myTestPlugin', + active: true + }); + }); + + }); + + describe('extending default config with file', function() { + + var myConfig = { + config: './test/config.cfg' + }, + result; + + before(function(done) { + config(myConfig).then(function(data) { + result = data; + done(); + }); + }); + + it('directPass should include extended "removeDoctype" plugin', function() { + result.plugins.directPass.should.includeEql({ + name: 'removeDoctype', + active: false + }); + }); + + it('directPass should include new "myTestPlugin" plugin', function() { + result.plugins.directPass.should.includeEql({ + name: 'myTestPlugin', + active: true + }); + }); + + }); + +}); diff --git a/test/svg2js.js b/test/svg2js.js new file mode 100644 index 00000000..ec99b091 --- /dev/null +++ b/test/svg2js.js @@ -0,0 +1,180 @@ +var should = require('should'), + svg2js = require('../lib/svg2js'); + +describe('svg2json\n\n | \n | 123\n | \n', function() { + + var svg = '', + result; + + before(function(done) { + svg2js(svg).then(function(data) { + result = data; + done(); + }); + }); + + describe('parser', function() { + + it('result should exist', function() { + result.should.exist; + }); + + it('result should be an instance of Object', function() { + result.content.should.be.an.instanceOf(Object); + }); + + it('result should have property "content" with instance of Array', function() { + result.should.have.property('content').with.instanceOf(Array); + }); + + it('content should have length 1', function() { + result.content.should.have.length(1); + }); + + it('content[0] should have property "elem" with value "svg"', function() { + result.content[0].should.have.property('elem', 'svg'); + }); + + it('svg should have properties "prefix" and "local', function() { + result.content[0].should.have.property('prefix'); + result.content[0].should.have.property('local'); + }); + + }); + + describe('attributes', function() { + + it('svg should have property "attrs" with instance of Object', function() { + result.content[0].should.have.property('attrs').with.instanceOf(Object); + }); + + it('svg.attrs should have property "xmlns" with instance of Object', function() { + result.content[0].attrs.should.have.property('xmlns').with.instanceOf(Object); + }); + + it('svg.attrs.xmlns should have properties "name", "value", "prefix", "local" and "uri"', function() { + result.content[0].attrs.xmlns.should.have.property('name'); + result.content[0].attrs.xmlns.should.have.property('prefix'); + result.content[0].attrs.xmlns.should.have.property('local'); + }); + + it('svg.attrs.xmlns.name shoud be equal "xmlns"', function() { + result.content[0].attrs.xmlns.name.should.equal('xmlns'); + }); + + it('svg.attrs.xmlns.value shoud be equal "http://www.w3.org/2000/svg"', function() { + result.content[0].attrs.xmlns.value.should.equal('http://www.w3.org/2000/svg'); + }); + + it('svg.attrs.xmlns.prefix shoud be equal "xmlns"', function() { + result.content[0].attrs.xmlns.prefix.should.equal('xmlns'); + }); + + it('svg.attrs.xmlns.local shoud be empty ""', function() { + result.content[0].attrs.xmlns.local.should.be.empty; + }); + + }); + + describe('content', function() { + + it('svg should have property "content" with instance of Array', function() { + result.content[0].should.have.property('content').with.instanceOf(Array); + }); + + it('svg.content should have length 1', function() { + result.content[0].content.should.have.length(1); + }); + + }); + + describe('API', function() { + + describe('isElem()', function() { + + it('svg should have property "isElem" with instance of Function', function() { + result.content[0].should.have.property('isElem').with.instanceOf(Function); + }); + + it('svg.sNode() should be true', function() { + result.content[0].isElem().should.be.true; + }); + + it('svg.isElem("svg") should be true', function() { + result.content[0].isElem('svg').should.be.true; + }); + + it('svg.isElem("trololo") should be false', function() { + result.content[0].isElem('123').should.be.false; + }); + + it('svg.isElem(["svg", "trololo"]) should be true', function() { + result.content[0].isElem(['svg', 'trololo']).should.be.true; + }); + + }); + + describe('hasAttr()', function() { + + it('svg should have property "hasAttr" with instance of Function', function() { + result.content[0].should.have.property('hasAttr').with.instanceOf(Function); + }); + + it('svg.hasAttr() should be true', function() { + result.content[0].hasAttr().should.be.true; + }); + + it('svg.hasAttr("xmlns") should be true', function() { + result.content[0].hasAttr('xmlns').should.be.true; + }); + + it('svg.hasAttr("xmlns", "http://www.w3.org/2000/svg") should be true', function() { + result.content[0].hasAttr('xmlns', 'http://www.w3.org/2000/svg').should.be.true; + }); + + it('svg.hasAttr("xmlns", "trololo") should be false', function() { + result.content[0].hasAttr('xmlns', 'trololo').should.be.false; + }); + + it('svg.hasAttr("trololo") should be false', function() { + result.content[0].hasAttr('trololo').should.be.false; + }); + + it('svg.hasAttr("trololo", "ololo") should be false', function() { + result.content[0].hasAttr('trololo', 'ololo').should.be.false; + }); + + it('svg.g.hasAttr() should be false', function() { + result.content[0].content[0].hasAttr().should.be.false; + }); + + it('svg.g.hasAttr("trololo") should be false', function() { + result.content[0].content[0].hasAttr('trololo').should.be.false; + }); + + it('svg.g.hasAttr("trololo", "ololo") should be false', function() { + result.content[0].content[0].hasAttr('trololo', 'ololo').should.be.false; + }); + + }); + + describe('isEmpty()', function() { + + it('svg should have property "isEmpty" with instance of Function', function() { + result.content[0].should.have.property('isEmpty').with.instanceOf(Function); + }); + + it('svg.isEmpty() should be false', function() { + result.content[0].isEmpty().should.be.false; + }); + + it('svg.g.isEmpty() should be true', function() { + result.content[0].content[0].isEmpty().should.be.true; + }); + + }); + + + }); + +});