1
0
mirror of https://github.com/svg/svgo.git synced 2025-07-28 09:22:00 +03:00
This commit is contained in:
deepsweet
2012-09-27 14:06:28 +03:00
parent b1f3a62809
commit 13af2ed95e
35 changed files with 2947 additions and 2 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules/
*.sublime-*
*~
*.lock
*.DS_Store
*.swp
*.min.svg

119
.svgo Normal file
View File

@ -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
}
]
}
}

7
GNUmakefile Normal file
View File

@ -0,0 +1,7 @@
test:
@./node_modules/.bin/mocha --reporter min
test-v:
@./node_modules/.bin/mocha --reporter spec
.PHONY: test test-v

22
LICENSE Normal file
View File

@ -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.

View File

@ -1,2 +1,74 @@
svgo ```
==== 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. …

3
bin/svgo Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
require('../lib/coa').run();

60
lib/coa.js Normal file
View File

@ -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();
});
});

52
lib/config.js Normal file
View File

@ -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);
});
};

229
lib/js2svg.js Normal file
View File

@ -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: '<!DOCTYPE',
doctypeEnd: '>',
procInstStart: '<?',
procInstEnd: '?>',
tagOpenStart: '<',
tagOpenEnd: '>',
tagCloseStart: '</',
tagCloseEnd: '>',
tagShortStart: '<',
tagShortEnd: '/>',
attrStart: '="',
attrEnd: '"',
commentStart: '<!--',
commentEnd: '-->',
cdataStart: '<![CDATA[',
cdataEnd: ']]>'
};
},
/**
* 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();
}
});
*/

249
lib/jsAPI.js Normal file
View File

@ -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();
});
}
}
);

84
lib/plugins.js Normal file
View File

@ -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();
}
});
*/

126
lib/svg2js.js Normal file
View File

@ -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;
};

20
lib/svgo.js Normal file
View File

@ -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));
});
});
};

118
lib/tools.js Normal file
View File

@ -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;
});
};

43
package.json Normal file
View File

@ -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 <kir@soulshine.in> (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"
}
}

640
plugins/_collections.js Normal file
View File

@ -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'
];

35
plugins/cleanupAttrs.js Normal file
View File

@ -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, '');
}
});
}
};

62
plugins/collapseGroups.js Normal file
View File

@ -0,0 +1,62 @@
var flatten = require('../lib/tools').flatten;
/*
* Collapse useless groups.
*
* @example
* <g>
* <g attr1="val1">
* <path d="..."/>
* </g>
* </g>
* ⬇
* <g>
* <g>
* <path attr1="val1" d="..."/>
* </g>
* </g>
* ⬇
* <path d="..."/>
*
* @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);
}
}
});
}
};

88
plugins/convertColors.js Normal file
View File

@ -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;
}
});
}
};

View File

@ -0,0 +1,70 @@
var extend = require('../lib/tools').extend,
stylingProps = require('./_collections').stylingProps;
/**
* Convert style in attributes.
*
* @example
* <g style="fill:#000; color: #fff;">
* ⬇
* <g fill="#000" color="#fff">
*
* @example
* <g style="fill:#000; color: #fff; -webkit-blah: blah">
* ⬇
* <g fill="#000" color="#fff" slyle="-webkit-blah: blah">
*
* @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');
}
}
}
};

View File

@ -0,0 +1,57 @@
var intersectAttrs = require('../lib/tools').intersectAttrs;
/**
* Collapse content's intersected attributes to the existing group wrapper.
*
* @example
* <g attr1="val1">
* <g attr2="val2">
* text
* </g>
* <circle attr2="val2" attr3="val3"/>
* </g>
* ⬇
* <g attr1="val1" attr2="val2">
* <g>
* text
* </g>
* <circle attr3="val3"/>
* </g>
*
* @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]);
}
});
}
}
};

18
plugins/removeComments.js Normal file
View File

@ -0,0 +1,18 @@
/**
* Remove comments.
*
* @example
* <!-- Generator: Adobe Illustrator 15.0.0, SVG Export
* Plug-In . SVG Version: 6.00 Build 0) -->
*
* @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();
};

View File

@ -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");
});
}
};

25
plugins/removeDoctype.js Normal file
View File

@ -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
* <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
* q"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
*
* @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();
};

View File

@ -0,0 +1,49 @@
var editorNamespaces = require('./_collections').editorNamespaces,
prefixes = [];
/**
* Remove editors namespaces, elements and attributes.
*
* @example
* <svg xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd">
* <sodipodi:namedview/>
* <path sodipodi:nodetypes="cccc"/>
*
* @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);
// <svg xmlns:sodipodi="">
item.removeAttr(attr.name);
}
});
}
// <sodipodi:*>
if (prefixes.indexOf(item.prefix) > -1) {
return false;
}
// <* sodipodi:*="">
item.eachAttr(function(attr) {
if (prefixes.indexOf(attr.prefix) > -1) {
item.removeAttr(attr.name);
}
});
}
};

View File

@ -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);
}
});
}
};

View File

@ -0,0 +1,24 @@
var container = require('./_collections').elems.container;
/**
* Remove empty containers.
*
* @see http://www.w3.org/TR/SVG/intro.html#TermContainerElement
*
* @example
* <defs/>
*
* @example
* <g><marker><a/></marker></g>
*
* @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());
};

View File

@ -0,0 +1,45 @@
/**
* Remove empty Text elements.
*
* @see http://www.w3.org/TR/SVG/text.html
*
* @example
* Remove empty text element:
* <text/>
*
* Remove empty tspan element:
* <tspan/>
*
* Remove tref with empty xlink:href attribute:
* <tref xlink:href=""/>
*
* @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;
};

View File

@ -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"
//
// <circle r="0">
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"
//
// <ellipse rx="0">
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"
//
// <ellipse ry="0">
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"
//
// <rect width="0">
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"
//
// <rect height="0">
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)"
//
// <pattern width="0">
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)"
//
// <pattern height="0">
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"
//
// <image width="0">
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"
//
// <image height="0">
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
//
// <path d=""/>
if (
params.pathEmptyD &&
item.isElem('path') &&
!item.hasAttr('d')
) return false;
// Polyline with empty points
//
// http://www.w3.org/TR/SVG/shapes.html#PolylineElementPointsAttribute
//
// <polyline points="">
if (
params.polylineEmptyPoints &&
item.isElem('polyline') &&
!item.hasAttr('points')
) return false;
// Polygon with empty points
//
// http://www.w3.org/TR/SVG/shapes.html#PolygonElementPointsAttribute
//
// <polygon points="">
if (
params.polygonEmptyPoints &&
item.isElem('polygon') &&
!item.hasAttr('points')
) return false;
}
};

16
plugins/removeMetadata.js Normal file
View File

@ -0,0 +1,16 @@
/**
* Remove <metadata>.
*
* 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');
};

48
plugins/removeSVGAttrs.js Normal file
View File

@ -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
* <svg id="svg49">
*/
if (params.id) item.removeAttr('id');
/**
* Remove version attribute.
*
* @see http://www.w3.org/TR/SVG/struct.html#SVGElementVersionAttribute
*
* @example
* <svg version="1.1">
*/
if (params.version) item.removeAttr('version');
/**
* Remove xnl:space attribute.
*
* @see http://www.w3.org/TR/SVG/struct.html#XMLSpaceAttribute
*
* @example
* <svg xml:space="preserve">
*/
if (params.xmlspace) item.removeAttr('xml:space');
}
};

View File

@ -0,0 +1,17 @@
/**
* Remove XML Processing Instruction.
*
* @example
* <?xml version="1.0" encoding="utf-8"?>
*
* @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();
};

13
test/config.cfg Normal file
View File

@ -0,0 +1,13 @@
{
"plugins": {
"directPass": [
{
"name": "removeDoctype",
"active": false
},{
"name": "myTestPlugin",
"active": true
}
]
}
}

113
test/config.js Normal file
View File

@ -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
});
});
});
});

180
test/svg2js.js Normal file
View File

@ -0,0 +1,180 @@
var should = require('should'),
svg2js = require('../lib/svg2js');
describe('svg2json\n\n | <svg xmlns="http://www.w3.org/2000/svg">\n | <g>123</g>\n | </svg>\n', function() {
var svg = '<svg xmlns="http://www.w3.org/2000/svg"><g/></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;
});
});
});
});