0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-13 16:21:57 -05:00

Removed svgcleaner

This commit is contained in:
alonso.torres 2021-03-24 18:04:34 +01:00 committed by Andrey Antukh
parent c380400578
commit ecff4c5dce
79 changed files with 8 additions and 15953 deletions

File diff suppressed because one or more lines are too long

View file

@ -17,7 +17,6 @@ done
cp ./resources/log4j2.xml ./target/dist/log4j2.xml
cp -r ./src ./target/dist/main
cp -r ./resources/emails ./target/dist/main/
cp -r ./resources/svgclean.js ./target/dist/main/
cp -r ./resources/error-report.tmpl ./target/dist/main/
cp -r ../common ./target/dist/common

View file

@ -156,13 +156,9 @@
:client-id (:gitlab-client-id config)
:client-secret (:gitlab-client-secret config)}
:app.svgparse/svgc
{:metrics (ig/ref :app.metrics/metrics)}
;; HTTP Handler for SVG parsing
:app.svgparse/handler
{:metrics (ig/ref :app.metrics/metrics)
:svgc (ig/ref :app.svgparse/svgc)}
{:metrics (ig/ref :app.metrics/metrics)}
;; RLimit definition for password hashing
:app.rlimits/password
@ -184,8 +180,7 @@
:metrics (ig/ref :app.metrics/metrics)
:storage (ig/ref :app.storage/storage)
:msgbus (ig/ref :app.msgbus/msgbus)
:rlimits (ig/ref :app.rlimits/all)
:svgc (ig/ref :app.svgparse/svgc)}
:rlimits (ig/ref :app.rlimits/all)}
:app.notifications/handler
{:msgbus (ig/ref :app.msgbus/msgbus)

View file

@ -92,7 +92,7 @@
(defn create-file-media-object
[{:keys [conn storage svgc] :as cfg} {:keys [file-id is-local name content] :as params}]
[{:keys [conn storage] :as cfg} {:keys [file-id is-local name content] :as params}]
(media/validate-media-type (:content-type content))
(let [storage (assoc storage :conn conn)
source-path (fs/path (:tempfile content))
@ -108,7 +108,7 @@
:path source-path})))
image (if (= (:mtype source-info) "image/svg+xml")
(let [data (svgc (slurp source-path))]
(let [data (slurp source-path)]
(sto/put-object storage {:content (sto/content data)
:content-type (:mtype source-info)}))
(sto/put-object storage {:content (sto/content source-path)

View file

@ -21,74 +21,6 @@
java.util.function.Consumer
org.apache.commons.io.IOUtils))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SVG Clean
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare clean-svg)
(declare prepare-context-pool)
(defmethod ig/pre-init-spec ::svgc [_]
(s/keys :req-un [::mtx/metrics]))
(defmethod ig/init-key ::svgc
[_ {:keys [metrics] :as cfg}]
(let [pool (prepare-context-pool cfg)
cfg (assoc cfg :pool pool)
handler #(clean-svg cfg %)
handler (->> {:registry (:registry metrics)
:type :summary
:name "svgc_timing"
:help "svg optimization function timing"}
(mtx/instrument handler))]
(with-meta handler {::pool pool})))
(defmethod ig/halt-key! ::svgc
[_ f]
(let [{:keys [::pool]} (meta f)]
(pool/clear! pool)
(pool/close! pool)))
(defn- prepare-context-pool
[cfg]
(pool/create
{:min-idle (:min-idle cfg 0)
:max-idle (:max-idle cfg 3)
:max-total (:max-total cfg 3)
:create
(fn []
(let [ctx (graal/context "js")]
(->> (graal/source "js" (io/resource "svgclean.js"))
(graal/eval! ctx))
ctx))
:destroy
(fn [ctx]
(graal/close! ctx))}))
(defn- clean-svg
[{:keys [pool]} data]
(with-open [ctx (pool/acquire pool)]
(let [res (promise)
optimize (-> (graal/get-bindings @ctx "js")
(graal/get-member "svgc")
(graal/get-member "optimize"))
resultp (graal/invoke optimize data)]
(graal/invoke-member resultp "then"
(reify Consumer
(accept [_ val]
(deliver res val))))
(graal/invoke-member resultp "catch"
(reify Consumer
(accept [_ err]
(deliver res err))))
(let [result (deref res)]
(if (instance? Throwable result)
(throw result)
result)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Handler
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -96,10 +28,8 @@
(declare handler)
(declare process-request)
(s/def ::svgc fn?)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::mtx/metrics ::svgc]))
(s/keys :req-un [::mtx/metrics]))
(defmethod ig/init-key ::handler
[_ {:keys [metrics] :as cfg}]
@ -117,7 +47,7 @@
:code :unsupported-mime-type
:mime (get headers "content-type")))
{:status 200
:body (process-request cfg body)})
:body (process-request body)})
(defn parse
[data]
@ -129,8 +59,7 @@
:code :invalid-svg-file))))
(defn process-request
[{:keys [svgc] :as cfg} body]
(let [data (slurp body)
data (svgc data)]
[body]
(let [data (slurp body)]
(parse data)))

View file

@ -1,24 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2021 UXBOX Labs SL
(ns app.tests.test-svgc
(:require
[app.tests.helpers :as th]
[clojure.java.io :as io]
[clojure.test :as t]
[mockery.core :refer [with-mocks]]))
(t/use-fixtures :once th/state-init)
(t/deftest run-svgc-over-sample-file
(let [svgc (:app.svgparse/svgc th/*system*)
data (slurp (io/resource "app/tests/_files/sample1.svg"))
res (svgc data)]
(t/is (string? res))
(t/is (= 2609 (count res)))))

View file

@ -20,7 +20,6 @@
[app.common.data :as d]
[app.common.pages :as cp]
[app.common.spec :as us]
["./svgclean" :as svgc]
["xml-js" :as xml]
["child_process" :as chp]
["os" :as os]
@ -115,7 +114,6 @@
(p/then (constantly pbmpath)))))
(p/then trace-color-mask)
(p/then read-file)
(p/then clean-svg)
(p/then (fn [data]
(p/let [data (parse-xml data)
data (get-in data ["elements" 0])]
@ -141,10 +139,6 @@
(-> (p/all (map (partial generate-color-mask ppmpath) colors))
(p/then join-color-layers)))
(clean-svg [data]
(log/trace :fn :clean-svg)
(svgc/optimize data))
(trace-single-node [{:keys [data] :as node}]
(log/trace :fn :trace-single-node)
(p/let [tdpath (create-tmpdir! "svgexport-")

File diff suppressed because one or more lines are too long

View file

@ -1,11 +0,0 @@
node_modules/
lib-cov/
html-report/
lcov.info
bin/svgo-profiling
*.sublime-*
*.log
.DS_Store
.idea
.vscode
.npmrc

View file

@ -1,25 +0,0 @@
The MIT License
Copyright © 2021 Andrey Antukh <niwi@niwi.nz>
Copyright © 20122016 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,101 +0,0 @@
'use strict';
var FS = require('fs'),
PATH = require('path'),
SVGO = require('../lib/svgo'),
filepath = PATH.resolve(__dirname, 'test.svg'),
svgo = new SVGO({
plugins: [{
cleanupAttrs: true,
}, {
removeDoctype: true,
},{
removeXMLProcInst: true,
},{
removeComments: true,
},{
removeMetadata: true,
},{
removeTitle: true,
},{
removeDesc: true,
},{
removeUselessDefs: true,
},{
removeEditorsNSData: true,
},{
removeEmptyAttrs: true,
},{
removeHiddenElems: true,
},{
removeEmptyText: true,
},{
removeEmptyContainers: true,
},{
removeViewBox: false,
},{
cleanupEnableBackground: true,
},{
convertStyleToAttrs: true,
},{
convertColors: true,
},{
convertPathData: true,
},{
convertTransform: true,
},{
removeUnknownsAndDefaults: true,
},{
removeNonInheritableGroupAttrs: true,
},{
removeUselessStrokeAndFill: true,
},{
removeUnusedNS: true,
},{
cleanupIDs: true,
},{
cleanupNumericValues: true,
},{
moveElemsAttrsToGroup: true,
},{
moveGroupAttrsToElems: true,
},{
collapseGroups: true,
},{
removeRasterImages: false,
},{
mergePaths: true,
},{
convertShapeToPath: true,
},{
sortAttrs: true,
},{
removeDimensions: true,
},{
removeAttrs: {attrs: '(stroke|fill)'},
}]
});
FS.readFile(filepath, 'utf8', function(err, data) {
if (err) {
throw err;
}
svgo.optimize(data, {path: filepath}).then(function(result) {
console.log(result);
// {
// // optimized SVG data string
// data: '<svg width="10" height="20">test</svg>'
// // additional info such as width/height
// info: {
// width: '10',
// height: '20'
// }
// }
});
});

View file

@ -1,3 +0,0 @@
<svg version="1.1" width="10" height="20">
test
</svg>

Before

Width:  |  Height:  |  Size: 59 B

View file

@ -1,45 +0,0 @@
const plugins = [
{ "minifyStyles" : false },
{ "convertStyleToAttrs" : false },
{
"cleanupIDs" : {
remove: false,
minify: false,
force: false
}
},
{ "cleanupListOfValues" : true },
{ "removeUnknownsAndDefaults" : false },
{ "removeViewBox" : false },
{ "convertShapeToPath" : false },
{ "convertEllipseToCircle" : false },
{ "moveElemsAttrsToGroup" : false },
{ "moveGroupAttrsToElems" : false },
{ "collapseGroups" : false },
{
"convertPathData" : {
lineShorthands: false,
curveSmoothShorthands: false,
forceAbsolutePath: true,
}
},
{ "convertTransform" : false },
{ "removeEmptyContainers" : false },
{ "mergePaths" : false },
{ "sortDefsChildren" : false },
{ "removeDimensions" : true },
{ "removeStyleElement" : false },
{ "removeScriptElement" : true },
{ "removeOffCanvasPaths" : false },
{ "cleanupNumericValues": true}
];
const svgc = require("./src/svgclean.js");
const inst = svgc.configure({plugins});
exports.optimize = function(data) {
return svgc.optimize(inst, data)
.then((result) => result.data);
};

3976
vendor/svgclean/package-lock.json generated vendored

File diff suppressed because it is too large Load diff

View file

@ -1,40 +0,0 @@
{
"name": "svgclean",
"version": "1.0.0",
"description": "SVG Cleaner (SVGO Fork)",
"main": "./src/svgo.js",
"scripts": {
"test": "set NODE_ENV=test && mocha",
"lint": "jshint --show-non-errors .",
"build": "browserify -s svgc main.js -o bundle.js",
"minify": "terser bundle.js -c -m -o bundle.min.js",
"copy": "cp ./bundle.min.js ../../backend/resources/svgclean.js && cp ./bundle.min.js ../../exporter/src/app/http/svgclean.js",
"dist": "npm run build && npm run minify && npm run copy"
},
"dependencies": {
"chalk": "^4.1.0",
"css-select": "^3.1.2",
"css-select-base-adapter": "^0.1.1",
"css-tree": "^1.1.2",
"csso": "^4.2.0",
"object.values": "^1.1.2",
"sax": "^1.2.4",
"stable": "^0.1.8",
"unquote": "^1.1.1",
"util.promisify": "^1.1.1"
},
"devDependencies": {
"browserify": "^17.0.0",
"coveralls": "^3.1.0",
"fs-extra": "^9.0.1",
"istanbul": "^0.4.5",
"jshint": "^2.12.0",
"mocha": "^8.2.1",
"mocha-istanbul": "^0.3.0",
"mock-stdin": "^1.0.0",
"should": "~13.2.3",
"svgo": "^1.3.2",
"terser": "^5.5.1"
},
"license": "MIT"
}

View file

@ -1,59 +0,0 @@
'use strict';
// encodeSVGDatauri = require('./svgclean/tools.js').encodeSVGDatauri,
const cfg = require("./svgclean/config.js");
const svgToJs = require('./svgclean/svg2js.js');
const jsToSvg = require('./svgclean/js2svg.js');
exports.configure = function(config={}) {
const plugins = cfg.loadPlugins(config);
return Object.assign({}, config, {plugins});
};
exports.optimize = function(config, svgstr) {
const info = {};
return new Promise((resolve, reject) => {
let maxPassCount = config.multipass ? 10 : 1;
let counter = 0;
let prevResultSize = Number.POSITIVE_INFINITY;
function optimize(root) {
if (root.error) {
reject(root.error);
return;
}
info.multipassCount = counter;
if (++counter < maxPassCount && root.data.length < prevResultSize) {
prevResultSize = root.data.length;
runOptimizations(config, root.data, info, optimize);
} else {
// if (config.datauri) {
// root.data = encodeSVGDatauri(root.data, config.datauri);
// }
if (info && info.path) {
root.path = info.path;
}
resolve(root);
}
};
runOptimizations(config, svgstr, info, optimize);
});
};
function runOptimizations(config, svgstr, info, callback) {
const plugins = config.plugins;
svgToJs(svgstr).then(function(doc) {
doc = cfg.executePlugins(plugins, doc, info);
// TODO: pass formating (js2svg) config
callback(jsToSvg(doc, config.format || {}));
}, function(error) {
callback({error: error});
});
};

View file

@ -1,288 +0,0 @@
'use strict';
const _collections = require('./plugins/_collections');
const _path = require('./plugins/_path');
const _transforms = require('./plugins/_transforms');
const addAttributesToSVGElement = require('./plugins/addAttributesToSVGElement');
const addClassesToSVGElement = require('./plugins/addClassesToSVGElement');
const cleanupAttrs = require('./plugins/cleanupAttrs');
const cleanupEnableBackground = require('./plugins/cleanupEnableBackground');
const cleanupIDs = require('./plugins/cleanupIDs');
const cleanupListOfValues = require('./plugins/cleanupListOfValues');
const cleanupNumericValues = require('./plugins/cleanupNumericValues');
const collapseGroups = require('./plugins/collapseGroups');
const convertColors = require('./plugins/convertColors');
const convertEllipseToCircle = require('./plugins/convertEllipseToCircle');
const convertPathData = require('./plugins/convertPathData');
const convertShapeToPath = require('./plugins/convertShapeToPath');
const convertStyleToAttrs = require('./plugins/convertStyleToAttrs');
const convertTransform = require('./plugins/convertTransform');
const inlineStyles = require('./plugins/inlineStyles');
const mergePaths = require('./plugins/mergePaths');
const minifyStyles = require('./plugins/minifyStyles');
const moveElemsAttrsToGroup = require('./plugins/moveElemsAttrsToGroup');
const moveGroupAttrsToElems = require('./plugins/moveGroupAttrsToElems');
const prefixIds = require('./plugins/prefixIds');
const removeAttributesBySelector = require('./plugins/removeAttributesBySelector');
const removeAttrs = require('./plugins/removeAttrs');
const removeComments = require('./plugins/removeComments');
const removeDesc = require('./plugins/removeDesc');
const removeDimensions = require('./plugins/removeDimensions');
const removeDoctype = require('./plugins/removeDoctype');
const removeEditorsNSData = require('./plugins/removeEditorsNSData');
const removeElementsByAttr = require('./plugins/removeElementsByAttr');
const removeEmptyAttrs = require('./plugins/removeEmptyAttrs');
const removeEmptyContainers = require('./plugins/removeEmptyContainers');
const removeEmptyText = require('./plugins/removeEmptyText');
const removeHiddenElems = require('./plugins/removeHiddenElems');
const removeMetadata = require('./plugins/removeMetadata');
const removeNonInheritableGroupAttrs = require('./plugins/removeNonInheritableGroupAttrs');
const removeOffCanvasPaths = require('./plugins/removeOffCanvasPaths');
const removeRasterImages = require('./plugins/removeRasterImages');
const removeScriptElement = require('./plugins/removeScriptElement');
const removeStyleElement = require('./plugins/removeStyleElement');
const removeTitle = require('./plugins/removeTitle');
const removeUnknownsAndDefaults = require('./plugins/removeUnknownsAndDefaults');
const removeUnusedNS = require('./plugins/removeUnusedNS');
const removeUselessDefs = require('./plugins/removeUselessDefs');
const removeUselessStrokeAndFill = require('./plugins/removeUselessStrokeAndFill');
const removeViewBox = require('./plugins/removeViewBox');
const removeXMLNS = require('./plugins/removeXMLNS');
const removeXMLProcInst = require('./plugins/removeXMLProcInst');
const reusePaths = require('./plugins/reusePaths');
const sortAttrs = require('./plugins/sortAttrs');
const sortDefsChildren = require('./plugins/sortDefsChildren');
const builtinPlugins = {
removeOffCanvasPaths,
_collections,
_path,
_transforms,
addAttributesToSVGElement,
addClassesToSVGElement,
cleanupAttrs,
cleanupEnableBackground,
cleanupIDs,
cleanupListOfValues,
cleanupNumericValues,
collapseGroups,
convertColors,
convertEllipseToCircle,
convertPathData,
convertShapeToPath,
convertStyleToAttrs,
convertTransform,
inlineStyles,
mergePaths,
minifyStyles,
moveElemsAttrsToGroup,
moveGroupAttrsToElems,
prefixIds,
removeAttributesBySelector,
removeAttrs,
removeComments,
removeDesc,
removeDimensions,
removeDoctype,
removeEditorsNSData,
removeElementsByAttr,
removeEmptyAttrs,
removeEmptyContainers,
removeEmptyText,
removeHiddenElems,
removeMetadata,
removeNonInheritableGroupAttrs,
removeRasterImages,
removeScriptElement,
removeStyleElement,
removeTitle,
removeUnknownsAndDefaults,
removeUnusedNS,
removeUselessDefs,
removeUselessStrokeAndFill,
removeViewBox,
removeXMLNS,
removeXMLProcInst,
reusePaths,
sortAttrs,
sortDefsChildren,
};
const defaultPlugins = [
"removeDoctype",
"removeXMLProcInst",
"removeComments",
"removeMetadata",
"removeXMLNS",
"removeEditorsNSData",
"cleanupAttrs",
"inlineStyles",
"minifyStyles",
"convertStyleToAttrs",
"cleanupIDs",
"prefixIds",
"removeRasterImages",
"removeUselessDefs",
"cleanupNumericValues",
"cleanupListOfValues",
"convertColors",
"removeUnknownsAndDefaults",
"removeNonInheritableGroupAttrs",
"removeUselessStrokeAndFill",
"removeViewBox",
"cleanupEnableBackground",
"removeHiddenElems",
"removeEmptyText",
"convertShapeToPath",
"convertEllipseToCircle",
"moveElemsAttrsToGroup",
"moveGroupAttrsToElems",
"collapseGroups",
"convertPathData",
"convertTransform",
"removeEmptyAttrs",
"removeEmptyContainers",
"mergePaths",
"removeUnusedNS",
"sortAttrs",
"sortDefsChildren",
"removeTitle",
"removeDesc",
"removeDimensions",
"removeAttrs",
"removeAttributesBySelector",
"removeElementsByAttr",
"addClassesToSVGElement",
"removeStyleElement",
"removeScriptElement",
"addAttributesToSVGElement",
"removeOffCanvasPaths",
"reusePaths"
];
function optimizePlugins(plugins) {
let prev;
return plugins.reduce(function(plugins, item) {
if (prev && item.type == prev[0].type) {
prev.push(item);
} else {
plugins.push(prev = [item]);
}
return plugins;
}, []);
}
exports.loadPlugins = function(config={}) {
let configured = [];
for (let name of defaultPlugins) {
const plugin = Object.assign({}, builtinPlugins[name]);
plugin.name = name;
configured.push(plugin);
}
for (let item of config.plugins || []) {
const name = Object.keys(item)[0];
const opts = item[name];
if (typeof opts === "object") {
configured.forEach(function(plugin) {
if (plugin.name === name) {
plugin.active = true;
plugin.params = Object.assign({}, plugin.params, opts);
}
});
} else if (typeof opts === "boolean") {
configured.forEach(function(plugin) {
if (plugin.name === name) {
plugin.active = opts;
}
});
}
}
return optimizePlugins(configured);
};
exports.executePlugins = function(plugins, data, info) {
plugins.forEach(function(group) {
switch(group[0].type) {
case 'perItem':
data = perItem(data, info, group);
break;
case 'perItemReverse':
data = perItem(data, info, group, true);
break;
case 'full':
data = full(data, info, group);
break;
}
});
return data;
}
/**
* Direct or reverse per-item loop.
*
* @param {Object} data input data
* @param {Object} info extra information
* @param {Array} plugins plugins list to process
* @param {Boolean} [reverse] reverse pass?
* @return {Object} output data
*/
function perItem(data, info, plugins, reverse) {
function monkeys(items) {
items.content = items.content.filter(function(item) {
// reverse pass
if (reverse && item.content) {
monkeys(item);
}
// main filter
let filter = true;
for (let i = 0; filter && i < plugins.length; i++) {
let plugin = plugins[i];
if (plugin.active && plugin.fn(item, plugin.params, info) === false) {
filter = false;
}
}
// direct pass
if (!reverse && item.content) {
monkeys(item);
}
return filter;
});
return items;
}
return monkeys(data);
}
/**
* "Full" plugins.
*
* @param {Object} data input data
* @param {Object} info extra information
* @param {Array} plugins plugins list to process
* @return {Object} output data
*/
function full(data, info, plugins) {
plugins.forEach(function(plugin) {
if (plugin.active) {
data = plugin.fn(data, plugin.params, info);
}
});
return data;
}

View file

@ -1,138 +0,0 @@
'use strict';
var values = require('object.values');
if (!Object.values) {
values.shim();
}
var CSSClassList = function(node) {
this.parentNode = node;
this.classNames = new Set();
this.classAttr = null;
//this.classValue = null;
};
/**
* Performs a deep clone of this object.
*
* @param parentNode the parentNode to assign to the cloned result
*/
CSSClassList.prototype.clone = function(parentNode) {
var node = this;
var nodeData = {};
Object.keys(node).forEach(function(key) {
if (key !== 'parentNode') {
nodeData[key] = node[key];
}
});
// Deep-clone node data.
nodeData = JSON.parse(JSON.stringify(nodeData));
var clone = new CSSClassList(parentNode);
Object.assign(clone, nodeData);
return clone;
};
CSSClassList.prototype.hasClass = function() {
this.classAttr = { // empty class attr
'name': 'class',
'value': null
};
this.addClassHandler();
};
// attr.class
CSSClassList.prototype.addClassHandler = function() {
Object.defineProperty(this.parentNode.attrs, 'class', {
get: this.getClassAttr.bind(this),
set: this.setClassAttr.bind(this),
enumerable: true,
configurable: true
});
this.addClassValueHandler();
};
// attr.class.value
CSSClassList.prototype.addClassValueHandler = function() {
Object.defineProperty(this.classAttr, 'value', {
get: this.getClassValue.bind(this),
set: this.setClassValue.bind(this),
enumerable: true,
configurable: true
});
};
CSSClassList.prototype.getClassAttr = function() {
return this.classAttr;
};
CSSClassList.prototype.setClassAttr = function(newClassAttr) {
this.setClassValue(newClassAttr.value); // must before applying value handler!
this.classAttr = newClassAttr;
this.addClassValueHandler();
};
CSSClassList.prototype.getClassValue = function() {
var arrClassNames = Array.from(this.classNames);
return arrClassNames.join(' ');
};
CSSClassList.prototype.setClassValue = function(newValue) {
if(typeof newValue === 'undefined') {
this.classNames.clear();
return;
}
var arrClassNames = newValue.split(' ');
this.classNames = new Set(arrClassNames);
};
CSSClassList.prototype.add = function(/* variadic */) {
this.hasClass();
Object.values(arguments).forEach(this._addSingle.bind(this));
};
CSSClassList.prototype._addSingle = function(className) {
this.classNames.add(className);
};
CSSClassList.prototype.remove = function(/* variadic */) {
this.hasClass();
Object.values(arguments).forEach(this._removeSingle.bind(this));
};
CSSClassList.prototype._removeSingle = function(className) {
this.classNames.delete(className);
};
CSSClassList.prototype.item = function(index) {
var arrClassNames = Array.from(this.classNames);
return arrClassNames[index];
};
CSSClassList.prototype.toggle = function(className, force) {
if(this.contains(className) || force === false) {
this.classNames.delete(className);
}
this.classNames.add(className);
};
CSSClassList.prototype.contains = function(className) {
return this.classNames.has(className);
};
module.exports = CSSClassList;

View file

@ -1,53 +0,0 @@
'use strict';
var baseCssAdapter = require('css-select-base-adapter');
/**
* DOMUtils API for SVGO AST (used by css-select)
*/
var svgoCssSelectAdapterMin = {
// is the node a tag?
// isTag: ( node:Node ) => isTag:Boolean
isTag: function(node) {
return node.isElem();
},
// get the parent of the node
// getParent: ( node:Node ) => parentNode:Node
// returns null when no parent exists
getParent: function(node) {
return node.parentNode || null;
},
// get the node's children
// getChildren: ( node:Node ) => children:[Node]
getChildren: function(node) {
return node.content || [];
},
// get the name of the tag
// getName: ( elem:ElementNode ) => tagName:String
getName: function(elemAst) {
return elemAst.elem;
},
// get the text content of the node, and its children if it has any
// getText: ( node:Node ) => text:String
// returns empty string when there is no text
getText: function(node) {
return node.content[0].text || node.content[0].cdata || '';
},
// get the attribute value
// getAttributeValue: ( elem:ElementNode, name:String ) => value:String
// returns null when attribute doesn't exist
getAttributeValue: function(elem, name) {
return elem.hasAttr(name) ? elem.attr(name).value : null;
}
};
// use base adapter for default implementation
var svgoCssSelectAdapter = baseCssAdapter(svgoCssSelectAdapterMin);
module.exports = svgoCssSelectAdapter;

View file

@ -1,285 +0,0 @@
'use strict';
var csstree = require('css-tree'),
csstools = require('./css-tools');
var CSSStyleDeclaration = function(node) {
this.parentNode = node;
this.properties = new Map();
this.hasSynced = false;
this.styleAttr = null;
this.styleValue = null;
this.parseError = false;
};
/**
* Performs a deep clone of this object.
*
* @param parentNode the parentNode to assign to the cloned result
*/
CSSStyleDeclaration.prototype.clone = function(parentNode) {
var node = this;
var nodeData = {};
Object.keys(node).forEach(function(key) {
if (key !== 'parentNode') {
nodeData[key] = node[key];
}
});
// Deep-clone node data.
nodeData = JSON.parse(JSON.stringify(nodeData));
var clone = new CSSStyleDeclaration(parentNode);
Object.assign(clone, nodeData);
return clone;
};
CSSStyleDeclaration.prototype.hasStyle = function() {
this.addStyleHandler();
};
// attr.style
CSSStyleDeclaration.prototype.addStyleHandler = function() {
this.styleAttr = { // empty style attr
'name': 'style',
'value': null
};
Object.defineProperty(this.parentNode.attrs, 'style', {
get: this.getStyleAttr.bind(this),
set: this.setStyleAttr.bind(this),
enumerable: true,
configurable: true
});
this.addStyleValueHandler();
};
// attr.style.value
CSSStyleDeclaration.prototype.addStyleValueHandler = function() {
Object.defineProperty(this.styleAttr, 'value', {
get: this.getStyleValue.bind(this),
set: this.setStyleValue.bind(this),
enumerable: true,
configurable: true
});
};
CSSStyleDeclaration.prototype.getStyleAttr = function() {
return this.styleAttr;
};
CSSStyleDeclaration.prototype.setStyleAttr = function(newStyleAttr) {
this.setStyleValue(newStyleAttr.value); // must before applying value handler!
this.styleAttr = newStyleAttr;
this.addStyleValueHandler();
this.hasSynced = false; // raw css changed
};
CSSStyleDeclaration.prototype.getStyleValue = function() {
return this.getCssText();
};
CSSStyleDeclaration.prototype.setStyleValue = function(newValue) {
this.properties.clear(); // reset all existing properties
this.styleValue = newValue;
this.hasSynced = false; // raw css changed
};
CSSStyleDeclaration.prototype._loadCssText = function() {
if (this.hasSynced) {
return;
}
this.hasSynced = true; // must be set here to prevent loop in setProperty(...)
if (!this.styleValue || this.styleValue.length === 0) {
return;
}
var inlineCssStr = this.styleValue;
var declarations = {};
try {
declarations = csstree.parse(inlineCssStr, {
context: 'declarationList',
parseValue: false
});
} catch (parseError) {
this.parseError = parseError;
return;
}
this.parseError = false;
var self = this;
declarations.children.each(function(declaration) {
try {
var styleDeclaration = csstools.csstreeToStyleDeclaration(declaration);
self.setProperty(styleDeclaration.name, styleDeclaration.value, styleDeclaration.priority);
} catch(styleError) {
if(styleError.message !== 'Unknown node type: undefined') {
self.parseError = styleError;
}
}
});
};
// only reads from properties
/**
* Get the textual representation of the declaration block (equivalent to .cssText attribute).
*
* @return {String} Textual representation of the declaration block (empty string for no properties)
*/
CSSStyleDeclaration.prototype.getCssText = function() {
var properties = this.getProperties();
if (this.parseError) {
// in case of a parse error, pass through original styles
return this.styleValue;
}
var cssText = [];
properties.forEach(function(property, propertyName) {
var strImportant = property.priority === 'important' ? '!important' : '';
cssText.push(propertyName.trim() + ':' + property.value.trim() + strImportant);
});
return cssText.join(';');
};
CSSStyleDeclaration.prototype._handleParseError = function() {
if (this.parseError) {
console.warn('Warning: Parse error when parsing inline styles, style properties of this element cannot be used. The raw styles can still be get/set using .attr(\'style\').value. Error details: ' + this.parseError);
}
};
CSSStyleDeclaration.prototype._getProperty = function(propertyName) {
if(typeof propertyName === 'undefined') {
throw Error('1 argument required, but only 0 present.');
}
var properties = this.getProperties();
this._handleParseError();
var property = properties.get(propertyName.trim());
return property;
};
/**
* Return the optional priority, "important".
*
* @param {String} propertyName representing the property name to be checked.
* @return {String} priority that represents the priority (e.g. "important") if one exists. If none exists, returns the empty string.
*/
CSSStyleDeclaration.prototype.getPropertyPriority = function(propertyName) {
var property = this._getProperty(propertyName);
return property ? property.priority : '';
};
/**
* Return the property value given a property name.
*
* @param {String} propertyName representing the property name to be checked.
* @return {String} value containing the value of the property. If not set, returns the empty string.
*/
CSSStyleDeclaration.prototype.getPropertyValue = function(propertyName) {
var property = this._getProperty(propertyName);
return property ? property.value : null;
};
/**
* Return a property name.
*
* @param {Number} index of the node to be fetched. The index is zero-based.
* @return {String} propertyName that is the name of the CSS property at the specified index.
*/
CSSStyleDeclaration.prototype.item = function(index) {
if(typeof index === 'undefined') {
throw Error('1 argument required, but only 0 present.');
}
var properties = this.getProperties();
this._handleParseError();
return Array.from(properties.keys())[index];
};
/**
* Return all properties of the node.
*
* @return {Map} properties that is a Map with propertyName as key and property (propertyValue + propertyPriority) as value.
*/
CSSStyleDeclaration.prototype.getProperties = function() {
this._loadCssText();
return this.properties;
};
// writes to properties
/**
* Remove a property from the CSS declaration block.
*
* @param {String} propertyName representing the property name to be removed.
* @return {String} oldValue equal to the value of the CSS property before it was removed.
*/
CSSStyleDeclaration.prototype.removeProperty = function(propertyName) {
if(typeof propertyName === 'undefined') {
throw Error('1 argument required, but only 0 present.');
}
this.hasStyle();
var properties = this.getProperties();
this._handleParseError();
var oldValue = this.getPropertyValue(propertyName);
properties.delete(propertyName.trim());
return oldValue;
};
/**
* Modify an existing CSS property or creates a new CSS property in the declaration block.
*
* @param {String} propertyName representing the CSS property name to be modified.
* @param {String} [value] containing the new property value. If not specified, treated as the empty string. value must not contain "!important" -- that should be set using the priority parameter.
* @param {String} [priority] allowing the "important" CSS priority to be set. If not specified, treated as the empty string.
* @return {undefined}
*/
CSSStyleDeclaration.prototype.setProperty = function(propertyName, value, priority) {
if(typeof propertyName === 'undefined') {
throw Error('propertyName argument required, but only not present.');
}
this.hasStyle();
var properties = this.getProperties();
this._handleParseError();
var property = {
value: value.trim(),
priority: priority.trim()
};
properties.set(propertyName.trim(), property);
return property;
};
module.exports = CSSStyleDeclaration;

View file

@ -1,222 +0,0 @@
'use strict';
var csstree = require('css-tree'),
List = csstree.List,
stable = require('stable'),
specificity = require('csso/lib/restructure/prepare/specificity');
/**
* Flatten a CSS AST to a selectors list.
*
* @param {Object} cssAst css-tree AST to flatten
* @return {Array} selectors
*/
function flattenToSelectors(cssAst) {
var selectors = [];
csstree.walk(cssAst, {visit: 'Rule', enter: function(node) {
if (node.type !== 'Rule') {
return;
}
var atrule = this.atrule;
var rule = node;
node.prelude.children.each(function(selectorNode, selectorItem) {
var selector = {
item: selectorItem,
atrule: atrule,
rule: rule,
pseudos: []
};
selectorNode.children.each(function(selectorChildNode, selectorChildItem, selectorChildList) {
if (selectorChildNode.type === 'PseudoClassSelector' ||
selectorChildNode.type === 'PseudoElementSelector') {
selector.pseudos.push({
item: selectorChildItem,
list: selectorChildList
});
}
});
selectors.push(selector);
});
}});
return selectors;
}
/**
* Filter selectors by Media Query.
*
* @param {Array} selectors to filter
* @param {Array} useMqs Array with strings of media queries that should pass (<name> <expression>)
* @return {Array} Filtered selectors that match the passed media queries
*/
function filterByMqs(selectors, useMqs) {
return selectors.filter(function(selector) {
if (selector.atrule === null) {
return ~useMqs.indexOf('');
}
var mqName = selector.atrule.name;
var mqStr = mqName;
if (selector.atrule.expression &&
selector.atrule.expression.children.first().type === 'MediaQueryList') {
var mqExpr = csstree.generate(selector.atrule.expression);
mqStr = [mqName, mqExpr].join(' ');
}
return ~useMqs.indexOf(mqStr);
});
}
/**
* Filter selectors by the pseudo-elements and/or -classes they contain.
*
* @param {Array} selectors to filter
* @param {Array} usePseudos Array with strings of single or sequence of pseudo-elements and/or -classes that should pass
* @return {Array} Filtered selectors that match the passed pseudo-elements and/or -classes
*/
function filterByPseudos(selectors, usePseudos) {
return selectors.filter(function(selector) {
var pseudoSelectorsStr = csstree.generate({
type: 'Selector',
children: new List().fromArray(selector.pseudos.map(function(pseudo) {
return pseudo.item.data;
}))
});
return ~usePseudos.indexOf(pseudoSelectorsStr);
});
}
/**
* Remove pseudo-elements and/or -classes from the selectors for proper matching.
*
* @param {Array} selectors to clean
* @return {Array} Selectors without pseudo-elements and/or -classes
*/
function cleanPseudos(selectors) {
selectors.forEach(function(selector) {
selector.pseudos.forEach(function(pseudo) {
pseudo.list.remove(pseudo.item);
});
});
}
/**
* Compares two selector specificities.
* extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211
*
* @param {Array} aSpecificity Specificity of selector A
* @param {Array} bSpecificity Specificity of selector B
* @return {Number} Score of selector specificity A compared to selector specificity B
*/
function compareSpecificity(aSpecificity, bSpecificity) {
for (var i = 0; i < 4; i += 1) {
if (aSpecificity[i] < bSpecificity[i]) {
return -1;
} else if (aSpecificity[i] > bSpecificity[i]) {
return 1;
}
}
return 0;
}
/**
* Compare two simple selectors.
*
* @param {Object} aSimpleSelectorNode Simple selector A
* @param {Object} bSimpleSelectorNode Simple selector B
* @return {Number} Score of selector A compared to selector B
*/
function compareSimpleSelectorNode(aSimpleSelectorNode, bSimpleSelectorNode) {
var aSpecificity = specificity(aSimpleSelectorNode),
bSpecificity = specificity(bSimpleSelectorNode);
return compareSpecificity(aSpecificity, bSpecificity);
}
function _bySelectorSpecificity(selectorA, selectorB) {
return compareSimpleSelectorNode(selectorA.item.data, selectorB.item.data);
}
/**
* Sort selectors stably by their specificity.
*
* @param {Array} selectors to be sorted
* @return {Array} Stable sorted selectors
*/
function sortSelectors(selectors) {
return stable(selectors, _bySelectorSpecificity);
}
/**
* Convert a css-tree AST style declaration to CSSStyleDeclaration property.
*
* @param {Object} declaration css-tree style declaration
* @return {Object} CSSStyleDeclaration property
*/
function csstreeToStyleDeclaration(declaration) {
var propertyName = declaration.property,
propertyValue = csstree.generate(declaration.value),
propertyPriority = (declaration.important ? 'important' : '');
return {
name: propertyName,
value: propertyValue,
priority: propertyPriority
};
}
/**
* Gets the CSS string of a style element
*
* @param {Object} element style element
* @return {String|Array} CSS string or empty array if no styles are set
*/
function getCssStr(elem) {
return elem.content[0].text || elem.content[0].cdata || [];
}
/**
* Sets the CSS string of a style element
*
* @param {Object} element style element
* @param {String} CSS string to be set
* @return {Object} reference to field with CSS
*/
function setCssStr(elem, css) {
// in case of cdata field
if(elem.content[0].cdata) {
elem.content[0].cdata = css;
return elem.content[0].cdata;
}
// in case of text field + if nothing was set yet
elem.content[0].text = css;
return elem.content[0].text;
}
module.exports.flattenToSelectors = flattenToSelectors;
module.exports.filterByMqs = filterByMqs;
module.exports.filterByPseudos = filterByPseudos;
module.exports.cleanPseudos = cleanPseudos;
module.exports.compareSpecificity = compareSpecificity;
module.exports.compareSimpleSelectorNode = compareSimpleSelectorNode;
module.exports.sortSelectors = sortSelectors;
module.exports.csstreeToStyleDeclaration = csstreeToStyleDeclaration;
module.exports.getCssStr = getCssStr;
module.exports.setCssStr = setCssStr;

View file

@ -1,346 +0,0 @@
'use strict';
const EOL = "\n";
const textElem = require('./plugins/_collections.js').elemsGroups.textContent.concat('title');
var defaults = {
doctypeStart: '<!DOCTYPE',
doctypeEnd: '>',
procInstStart: '<?',
procInstEnd: '?>',
tagOpenStart: '<',
tagOpenEnd: '>',
tagCloseStart: '</',
tagCloseEnd: '>',
tagShortStart: '<',
tagShortEnd: '/>',
attrStart: '="',
attrEnd: '"',
commentStart: '<!--',
commentEnd: '-->',
cdataStart: '<![CDATA[',
cdataEnd: ']]>',
textStart: '',
textEnd: '',
indent: 4,
regEntities: /[&'"<>]/g,
regValEntities: /[&"<>]/g,
encodeEntity: encodeEntity,
pretty: false,
useShortTags: true
};
var entities = {
'&': '&amp;',
'\'': '&apos;',
'"': '&quot;',
'>': '&gt;',
'<': '&lt;',
};
/**
* Convert SVG-as-JS object to SVG (XML) string.
*
* @param {Object} data input data
* @param {Object} config config
*
* @return {Object} output data
*/
module.exports = function(data, config) {
return new JS2SVG(config).convert(data);
};
function JS2SVG(config) {
if (config) {
this.config = Object.assign({}, defaults, config);
} else {
this.config = Object.assign({}, defaults);
}
var indent = this.config.indent;
if (typeof indent == 'number' && !isNaN(indent)) {
this.config.indent = (indent < 0) ? '\t' : ' '.repeat(indent);
} else if (typeof indent != 'string') {
this.config.indent = ' ';
}
if (this.config.pretty) {
this.config.doctypeEnd += EOL;
this.config.procInstEnd += EOL;
this.config.commentEnd += EOL;
this.config.cdataEnd += EOL;
this.config.tagShortEnd += EOL;
this.config.tagOpenEnd += EOL;
this.config.tagCloseEnd += EOL;
this.config.textEnd += EOL;
}
this.indentLevel = 0;
this.textContext = null;
}
function encodeEntity(char) {
return entities[char];
}
/**
* Start conversion.
*
* @param {Object} data input data
*
* @return {String}
*/
JS2SVG.prototype.convert = function(data) {
var svg = '';
if (data.content) {
this.indentLevel++;
data.content.forEach(function(item) {
if (item.elem) {
svg += this.createElem(item);
} else if (item.text) {
svg += this.createText(item.text);
} else if (item.doctype) {
svg += this.createDoctype(item.doctype);
} else if (item.processinginstruction) {
svg += this.createProcInst(item.processinginstruction);
} else if (item.comment) {
svg += this.createComment(item.comment);
} else if (item.cdata) {
svg += this.createCDATA(item.cdata);
}
}, this);
}
this.indentLevel--;
return {
data: svg,
info: {
width: this.width,
height: this.height
}
};
};
/**
* Create indent string in accordance with the current node level.
*
* @return {String}
*/
JS2SVG.prototype.createIndent = function() {
var indent = '';
if (this.config.pretty && !this.textContext) {
indent = this.config.indent.repeat(this.indentLevel - 1);
}
return indent;
};
/**
* Create doctype tag.
*
* @param {String} doctype doctype body string
*
* @return {String}
*/
JS2SVG.prototype.createDoctype = function(doctype) {
return this.config.doctypeStart +
doctype +
this.config.doctypeEnd;
};
/**
* Create XML Processing Instruction tag.
*
* @param {Object} instruction instruction object
*
* @return {String}
*/
JS2SVG.prototype.createProcInst = function(instruction) {
return this.config.procInstStart +
instruction.name +
' ' +
instruction.body +
this.config.procInstEnd;
};
/**
* Create comment tag.
*
* @param {String} comment comment body
*
* @return {String}
*/
JS2SVG.prototype.createComment = function(comment) {
return this.config.commentStart +
comment +
this.config.commentEnd;
};
/**
* Create CDATA section.
*
* @param {String} cdata CDATA body
*
* @return {String}
*/
JS2SVG.prototype.createCDATA = function(cdata) {
return this.createIndent() +
this.config.cdataStart +
cdata +
this.config.cdataEnd;
};
/**
* Create element tag.
*
* @param {Object} data element object
*
* @return {String}
*/
JS2SVG.prototype.createElem = function(data) {
// beautiful injection for obtaining SVG information :)
if (
data.isElem('svg') &&
data.hasAttr('width') &&
data.hasAttr('height')
) {
this.width = data.attr('width').value;
this.height = data.attr('height').value;
}
// empty element and short tag
if (data.isEmpty()) {
if (this.config.useShortTags) {
return this.createIndent() +
this.config.tagShortStart +
data.elem +
this.createAttrs(data) +
this.config.tagShortEnd;
} else {
return this.createIndent() +
this.config.tagShortStart +
data.elem +
this.createAttrs(data) +
this.config.tagOpenEnd +
this.config.tagCloseStart +
data.elem +
this.config.tagCloseEnd;
}
// non-empty element
} else {
var tagOpenStart = this.config.tagOpenStart,
tagOpenEnd = this.config.tagOpenEnd,
tagCloseStart = this.config.tagCloseStart,
tagCloseEnd = this.config.tagCloseEnd,
openIndent = this.createIndent(),
textIndent = '',
processedData = '',
dataEnd = '';
if (this.textContext) {
tagOpenStart = defaults.tagOpenStart;
tagOpenEnd = defaults.tagOpenEnd;
tagCloseStart = defaults.tagCloseStart;
tagCloseEnd = defaults.tagCloseEnd;
openIndent = '';
} else if (data.isElem(textElem)) {
if (this.config.pretty) {
textIndent += openIndent + this.config.indent;
}
this.textContext = data;
}
processedData += this.convert(data).data;
if (this.textContext == data) {
this.textContext = null;
if (this.config.pretty) dataEnd = EOL;
}
return openIndent +
tagOpenStart +
data.elem +
this.createAttrs(data) +
tagOpenEnd +
textIndent +
processedData +
dataEnd +
this.createIndent() +
tagCloseStart +
data.elem +
tagCloseEnd;
}
};
/**
* Create element attributes.
*
* @param {Object} elem attributes object
*
* @return {String}
*/
JS2SVG.prototype.createAttrs = function(elem) {
var attrs = '';
elem.eachAttr(function(attr) {
if (attr.value !== undefined) {
attrs += ' ' +
attr.name +
this.config.attrStart +
String(attr.value).replace(this.config.regValEntities, this.config.encodeEntity) +
this.config.attrEnd;
}
else {
attrs += ' ' +
attr.name;
}
}, this);
return attrs;
};
/**
* Create text node.
*
* @param {String} text text
*
* @return {String}
*/
JS2SVG.prototype.createText = function(text) {
return this.createIndent() +
this.config.textStart +
text.replace(this.config.regEntities, this.config.encodeEntity) +
(this.textContext ? '' : this.config.textEnd);
};

View file

@ -1,372 +0,0 @@
'use strict';
var cssSelect = require('css-select');
var svgoCssSelectAdapter = require('./css-select-adapter');
var cssSelectOpts = {
xmlMode: true,
adapter: svgoCssSelectAdapter
};
var JSAPI = module.exports = function(data, parentNode) {
Object.assign(this, data);
if (parentNode) {
Object.defineProperty(this, 'parentNode', {
writable: true,
value: parentNode
});
}
};
/**
* Perform a deep clone of this node.
*
* @return {Object} element
*/
JSAPI.prototype.clone = function() {
var node = this;
var nodeData = {};
Object.keys(node).forEach(function(key) {
if (key !== 'class' && key !== 'style' && key !== 'content') {
nodeData[key] = node[key];
}
});
// Deep-clone node data.
nodeData = JSON.parse(JSON.stringify(nodeData));
// parentNode gets set to a proper object by the parent clone,
// but it needs to be true/false now to do the right thing
// in the constructor.
var clonedNode = new JSAPI(nodeData, !!node.parentNode);
if (node.class) {
clonedNode.class = node.class.clone(clonedNode);
}
if (node.style) {
clonedNode.style = node.style.clone(clonedNode);
}
if (node.content) {
clonedNode.content = node.content.map(function(childNode) {
var clonedChild = childNode.clone();
clonedChild.parentNode = clonedNode;
return clonedChild;
});
}
return clonedNode;
};
/**
* 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}
*/
JSAPI.prototype.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;
};
/**
* Renames an element
*
* @param {String} name new element name
* @return {Object} element
*/
JSAPI.prototype.renameElem = function(name) {
if (name && typeof name === 'string')
this.elem = this.local = name;
return this;
};
/**
* Determine if element is empty.
*
* @return {Boolean}
*/
JSAPI.prototype.isEmpty = function() {
return !this.content || !this.content.length;
};
/**
* Find the closest ancestor of the current element.
* @param elemName
*
* @return {?Object}
*/
JSAPI.prototype.closestElem = function(elemName) {
var elem = this;
while ((elem = elem.parentNode) && !elem.isElem(elemName));
return elem;
};
/**
* Changes content by removing elements and/or adding new elements.
*
* @param {Number} start Index at which to start changing the content.
* @param {Number} n Number of elements to remove.
* @param {Array|Object} [insertion] Elements to add to the content.
* @return {Array} Removed elements.
*/
JSAPI.prototype.spliceContent = function(start, n, insertion) {
if (arguments.length < 2) return [];
if (!Array.isArray(insertion))
insertion = Array.apply(null, arguments).slice(2);
insertion.forEach(function(inner) { inner.parentNode = this }, this);
return this.content.splice.apply(this.content, [start, n].concat(insertion));
};
/**
* Determine if element has an attribute
* (any, or by name or by name + value).
*
* @param {String} [name] attribute name
* @param {String} [val] attribute value (will be toString()'ed)
* @return {Boolean}
*/
JSAPI.prototype.hasAttr = function(name, val) {
if (!this.attrs || !Object.keys(this.attrs).length) return false;
if (!arguments.length) return !!this.attrs;
if (val !== undefined) return !!this.attrs[name] && this.attrs[name].value === val.toString();
return !!this.attrs[name];
};
/**
* Determine if element has an attribute by local name
* (any, or by name or by name + value).
*
* @param {String} [localName] local attribute name
* @param {Number|String|RegExp|Function} [val] attribute value (will be toString()'ed or executed, otherwise ignored)
* @return {Boolean}
*/
JSAPI.prototype.hasAttrLocal = function(localName, val) {
if (!this.attrs || !Object.keys(this.attrs).length) return false;
if (!arguments.length) return !!this.attrs;
var callback;
switch (val != null && val.constructor && val.constructor.name) {
case 'Number': // same as String
case 'String': callback = stringValueTest; break;
case 'RegExp': callback = regexpValueTest; break;
case 'Function': callback = funcValueTest; break;
default: callback = nameTest;
}
return this.someAttr(callback);
function nameTest(attr) {
return attr.local === localName;
}
function stringValueTest(attr) {
return attr.local === localName && val == attr.value;
}
function regexpValueTest(attr) {
return attr.local === localName && val.test(attr.value);
}
function funcValueTest(attr) {
return attr.local === localName && val(attr.value);
}
};
/**
* 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|Undefined}
*/
JSAPI.prototype.attr = function(name, val) {
if (!this.hasAttr() || !arguments.length) return undefined;
if (val !== undefined) return this.hasAttr(name, val) ? this.attrs[name] : undefined;
return this.attrs[name];
};
/**
* Get computed attribute value from an element
*
* @param {String} name attribute name
* @return {Object|Undefined}
*/
JSAPI.prototype.computedAttr = function(name, val) {
/* jshint eqnull: true */
if (!arguments.length) return;
for (var elem = this; elem && (!elem.hasAttr(name) || !elem.attr(name).value); elem = elem.parentNode);
if (val != null) {
return elem ? elem.hasAttr(name, val) : false;
} else if (elem && elem.hasAttr(name)) {
return elem.attrs[name].value;
}
};
/**
* Remove a specific attribute.
*
* @param {String|Array} name attribute name
* @param {String} [val] attribute value
* @return {Boolean}
*/
JSAPI.prototype.removeAttr = function(name, val, recursive) {
if (!arguments.length) return false;
if (Array.isArray(name)) {
name.forEach(this.removeAttr, this);
return false;
}
if (!this.hasAttr(name)) return false;
if (!recursive && val && this.attrs[name].value !== val) return false;
delete this.attrs[name];
if (!Object.keys(this.attrs).length) delete this.attrs;
return true;
};
/**
* Add attribute.
*
* @param {Object} [attr={}] attribute object
* @return {Object|Boolean} created attribute or false if no attr was passed in
*/
JSAPI.prototype.addAttr = function(attr) {
attr = attr || {};
if (attr.name === undefined ||
attr.prefix === undefined ||
attr.local === undefined
) return false;
this.attrs = this.attrs || {};
this.attrs[attr.name] = attr;
if(attr.name === 'class') { // newly added class attribute
this.class.hasClass();
}
if(attr.name === 'style') { // newly added style attribute
this.style.hasStyle();
}
return this.attrs[attr.name];
};
/**
* Iterates over all attributes.
*
* @param {Function} callback callback
* @param {Object} [context] callback context
* @return {Boolean} false if there are no any attributes
*/
JSAPI.prototype.eachAttr = function(callback, context) {
if (!this.hasAttr()) return false;
for (var name in this.attrs) {
callback.call(context, this.attrs[name]);
}
return true;
};
/**
* Tests whether some attribute passes the test.
*
* @param {Function} callback callback
* @param {Object} [context] callback context
* @return {Boolean} false if there are no any attributes
*/
JSAPI.prototype.someAttr = function(callback, context) {
if (!this.hasAttr()) return false;
for (var name in this.attrs) {
if (callback.call(context, this.attrs[name])) return true;
}
return false;
};
/**
* Evaluate a string of CSS selectors against the element and returns matched elements.
*
* @param {String} selectors CSS selector(s) string
* @return {Array} null if no elements matched
*/
JSAPI.prototype.querySelectorAll = function(selectors) {
var matchedEls = cssSelect.selectAll(selectors, this, cssSelectOpts);
return matchedEls.length > 0 ? matchedEls : null;
};
/**
* Evaluate a string of CSS selectors against the element and returns only the first matched element.
*
* @param {String} selectors CSS selector(s) string
* @return {Array} null if no element matched
*/
JSAPI.prototype.querySelector = function(selectors) {
return cssSelect.selectOne(selectors, this, cssSelectOpts);
};
/**
* Test if a selector matches a given element.
*
* @param {String} selector CSS selector string
* @return {Boolean} true if element would be selected by selector string, false if it does not
*/
JSAPI.prototype.matches = function(selector) {
return cssSelect.is(this, selector, cssSelectOpts);
};

File diff suppressed because it is too large Load diff

View file

@ -1,988 +0,0 @@
/* global a2c */
'use strict';
var rNumber = String.raw`[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?\s*`,
rCommaWsp = String.raw`(?:\s,?\s*|,\s*)`,
rNumberCommaWsp = `(${rNumber})` + rCommaWsp,
rFlagCommaWsp = `([01])${rCommaWsp}?`,
rCoordinatePair = String.raw`(${rNumber})${rCommaWsp}?(${rNumber})`,
rArcSeq = (rNumberCommaWsp + '?').repeat(2) + rNumberCommaWsp + rFlagCommaWsp.repeat(2) + rCoordinatePair;
var regPathInstructions = /([MmLlHhVvCcSsQqTtAaZz])\s*/,
regCoordinateSequence = new RegExp(rNumber, 'g'),
regArcArgumentSequence = new RegExp(rArcSeq, 'g'),
regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/,
transform2js = require('./_transforms').transform2js,
transformsMultiply = require('./_transforms').transformsMultiply,
transformArc = require('./_transforms').transformArc,
collections = require('./_collections.js'),
referencesProps = collections.referencesProps,
defaultStrokeWidth = collections.attrsGroupsDefaults.presentation['stroke-width'],
cleanupOutData = require('../tools').cleanupOutData,
removeLeadingZero = require('../tools').removeLeadingZero,
prevCtrlPoint;
/**
* Convert path string to JS representation.
*
* @param {String} pathString input string
* @param {Object} params plugin params
* @return {Array} output array
*/
exports.path2js = function(path) {
if (path.pathJS) return path.pathJS;
var paramsLength = { // Number of parameters of every path command
H: 1, V: 1, M: 2, L: 2, T: 2, Q: 4, S: 4, C: 6, A: 7,
h: 1, v: 1, m: 2, l: 2, t: 2, q: 4, s: 4, c: 6, a: 7
},
pathData = [], // JS representation of the path data
instruction, // current instruction context
startMoveto = false;
// splitting path string into array like ['M', '10 50', 'L', '20 30']
path.attr('d').value.split(regPathInstructions).forEach(function(data) {
if (!data) return;
if (!startMoveto) {
if (data == 'M' || data == 'm') {
startMoveto = true;
} else return;
}
// instruction item
if (regPathInstructions.test(data)) {
instruction = data;
// z - instruction w/o data
if (instruction == 'Z' || instruction == 'z') {
pathData.push({
instruction: 'z'
});
}
// data item
} else {
/* jshint boss: true */
if (instruction == 'A' || instruction == 'a') {
var newData = [];
for (var args; (args = regArcArgumentSequence.exec(data));) {
for (var i = 1; i < args.length; i++) {
newData.push(args[i]);
}
}
data = newData;
} else {
data = data.match(regCoordinateSequence);
}
if (!data) return;
data = data.map(Number);
// Subsequent moveto pairs of coordinates are threated as implicit lineto commands
// http://www.w3.org/TR/SVG/paths.html#PathDataMovetoCommands
if (instruction == 'M' || instruction == 'm') {
pathData.push({
instruction: pathData.length == 0 ? 'M' : instruction,
data: data.splice(0, 2)
});
instruction = instruction == 'M' ? 'L' : 'l';
}
for (var pair = paramsLength[instruction]; data.length;) {
pathData.push({
instruction: instruction,
data: data.splice(0, pair)
});
}
}
});
// First moveto is actually absolute. Subsequent coordinates were separated above.
if (pathData.length && pathData[0].instruction == 'm') {
pathData[0].instruction = 'M';
}
path.pathJS = pathData;
return pathData;
};
/**
* Convert relative Path data to absolute.
*
* @param {Array} data input data
* @return {Array} output data
*/
var relative2absolute = exports.relative2absolute = function(data) {
var currentPoint = [0, 0],
subpathPoint = [0, 0],
i;
return data.map(function(item) {
var instruction = item.instruction,
itemData = item.data && item.data.slice();
if (instruction == 'M') {
set(currentPoint, itemData);
set(subpathPoint, itemData);
} else if ('mlcsqt'.indexOf(instruction) > -1) {
for (i = 0; i < itemData.length; i++) {
itemData[i] += currentPoint[i % 2];
}
set(currentPoint, itemData);
if (instruction == 'm') {
set(subpathPoint, itemData);
}
} else if (instruction == 'a') {
itemData[5] += currentPoint[0];
itemData[6] += currentPoint[1];
set(currentPoint, itemData);
} else if (instruction == 'h') {
itemData[0] += currentPoint[0];
currentPoint[0] = itemData[0];
} else if (instruction == 'v') {
itemData[0] += currentPoint[1];
currentPoint[1] = itemData[0];
} else if ('MZLCSQTA'.indexOf(instruction) > -1) {
set(currentPoint, itemData);
} else if (instruction == 'H') {
currentPoint[0] = itemData[0];
} else if (instruction == 'V') {
currentPoint[1] = itemData[0];
} else if (instruction == 'z') {
set(currentPoint, subpathPoint);
}
return instruction == 'z' ?
{ instruction: 'z' } :
{
instruction: instruction.toUpperCase(),
data: itemData
};
});
};
/**
* Apply transformation(s) to the Path data.
*
* @param {Object} elem current element
* @param {Array} path input path data
* @param {Object} params whether to apply transforms to stroked lines and transform precision (used for stroke width)
* @return {Array} output path data
*/
exports.applyTransforms = function(elem, path, params) {
// if there are no 'stroke' attr and references to other objects such as
// gradiends or clip-path which are also subjects to transform.
if (!elem.hasAttr('transform') || !elem.attr('transform').value ||
elem.someAttr(function(attr) {
return ~referencesProps.indexOf(attr.name) && ~attr.value.indexOf('url(');
}))
return path;
var matrix = transformsMultiply(transform2js(elem.attr('transform').value)),
stroke = elem.computedAttr('stroke'),
id = elem.computedAttr('id'),
transformPrecision = params.transformPrecision,
newPoint, scale;
if (stroke && stroke != 'none') {
if (!params.applyTransformsStroked ||
(matrix.data[0] != matrix.data[3] || matrix.data[1] != -matrix.data[2]) &&
(matrix.data[0] != -matrix.data[3] || matrix.data[1] != matrix.data[2]))
return path;
// "stroke-width" should be inside the part with ID, otherwise it can be overrided in <use>
if (id) {
var idElem = elem,
hasStrokeWidth = false;
do {
if (idElem.hasAttr('stroke-width')) hasStrokeWidth = true;
} while (!idElem.hasAttr('id', id) && !hasStrokeWidth && (idElem = idElem.parentNode));
if (!hasStrokeWidth) return path;
}
scale = +Math.sqrt(matrix.data[0] * matrix.data[0] + matrix.data[1] * matrix.data[1]).toFixed(transformPrecision);
if (scale !== 1) {
var strokeWidth = elem.computedAttr('stroke-width') || defaultStrokeWidth;
if (!elem.hasAttr('vector-effect') || elem.attr('vector-effect').value !== 'non-scaling-stroke') {
if (elem.hasAttr('stroke-width')) {
elem.attrs['stroke-width'].value = elem.attrs['stroke-width'].value.trim()
.replace(regNumericValues, function(num) {
return removeLeadingZero(num * scale);
});
} else {
elem.addAttr({
name: 'stroke-width',
prefix: '',
local: 'stroke-width',
value: strokeWidth.replace(regNumericValues, function(num) {
return removeLeadingZero(num * scale);
})
});
}
}
}
} else if (id) { // Stroke and stroke-width can be redefined with <use>
return path;
}
path.forEach(function(pathItem) {
if (pathItem.data) {
// h -> l
if (pathItem.instruction === 'h') {
pathItem.instruction = 'l';
pathItem.data[1] = 0;
// v -> l
} else if (pathItem.instruction === 'v') {
pathItem.instruction = 'l';
pathItem.data[1] = pathItem.data[0];
pathItem.data[0] = 0;
}
// if there is a translate() transform
if (pathItem.instruction === 'M' &&
(matrix.data[4] !== 0 ||
matrix.data[5] !== 0)
) {
// then apply it only to the first absoluted M
newPoint = transformPoint(matrix.data, pathItem.data[0], pathItem.data[1]);
set(pathItem.data, newPoint);
set(pathItem.coords, newPoint);
// clear translate() data from transform matrix
matrix.data[4] = 0;
matrix.data[5] = 0;
} else {
if (pathItem.instruction == 'a') {
transformArc(pathItem.data, matrix.data);
// reduce number of digits in rotation angle
if (Math.abs(pathItem.data[2]) > 80) {
var a = pathItem.data[0],
rotation = pathItem.data[2];
pathItem.data[0] = pathItem.data[1];
pathItem.data[1] = a;
pathItem.data[2] = rotation + (rotation > 0 ? -90 : 90);
}
newPoint = transformPoint(matrix.data, pathItem.data[5], pathItem.data[6]);
pathItem.data[5] = newPoint[0];
pathItem.data[6] = newPoint[1];
} else {
for (var i = 0; i < pathItem.data.length; i += 2) {
newPoint = transformPoint(matrix.data, pathItem.data[i], pathItem.data[i + 1]);
pathItem.data[i] = newPoint[0];
pathItem.data[i + 1] = newPoint[1];
}
}
pathItem.coords[0] = pathItem.base[0] + pathItem.data[pathItem.data.length - 2];
pathItem.coords[1] = pathItem.base[1] + pathItem.data[pathItem.data.length - 1];
}
}
});
// remove transform attr
elem.removeAttr('transform');
return path;
};
/**
* Apply transform 3x3 matrix to x-y point.
*
* @param {Array} matrix transform 3x3 matrix
* @param {Array} point x-y point
* @return {Array} point with new coordinates
*/
function transformPoint(matrix, x, y) {
return [
matrix[0] * x + matrix[2] * y + matrix[4],
matrix[1] * x + matrix[3] * y + matrix[5]
];
}
/**
* Compute Cubic Bézie bounding box.
*
* @see http://processingjs.nihongoresources.com/bezierinfo/
*
* @param {Float} xa
* @param {Float} ya
* @param {Float} xb
* @param {Float} yb
* @param {Float} xc
* @param {Float} yc
* @param {Float} xd
* @param {Float} yd
*
* @return {Object}
*/
exports.computeCubicBoundingBox = function(xa, ya, xb, yb, xc, yc, xd, yd) {
var minx = Number.POSITIVE_INFINITY,
miny = Number.POSITIVE_INFINITY,
maxx = Number.NEGATIVE_INFINITY,
maxy = Number.NEGATIVE_INFINITY,
ts,
t,
x,
y,
i;
// X
if (xa < minx) { minx = xa; }
if (xa > maxx) { maxx = xa; }
if (xd < minx) { minx= xd; }
if (xd > maxx) { maxx = xd; }
ts = computeCubicFirstDerivativeRoots(xa, xb, xc, xd);
for (i = 0; i < ts.length; i++) {
t = ts[i];
if (t >= 0 && t <= 1) {
x = computeCubicBaseValue(t, xa, xb, xc, xd);
// y = computeCubicBaseValue(t, ya, yb, yc, yd);
if (x < minx) { minx = x; }
if (x > maxx) { maxx = x; }
}
}
// Y
if (ya < miny) { miny = ya; }
if (ya > maxy) { maxy = ya; }
if (yd < miny) { miny = yd; }
if (yd > maxy) { maxy = yd; }
ts = computeCubicFirstDerivativeRoots(ya, yb, yc, yd);
for (i = 0; i < ts.length; i++) {
t = ts[i];
if (t >= 0 && t <= 1) {
// x = computeCubicBaseValue(t, xa, xb, xc, xd);
y = computeCubicBaseValue(t, ya, yb, yc, yd);
if (y < miny) { miny = y; }
if (y > maxy) { maxy = y; }
}
}
return {
minx: minx,
miny: miny,
maxx: maxx,
maxy: maxy
};
};
// compute the value for the cubic bezier function at time=t
function computeCubicBaseValue(t, a, b, c, d) {
var mt = 1 - t;
return mt * mt * mt * a + 3 * mt * mt * t * b + 3 * mt * t * t * c + t * t * t * d;
}
// compute the value for the first derivative of the cubic bezier function at time=t
function computeCubicFirstDerivativeRoots(a, b, c, d) {
var result = [-1, -1],
tl = -a + 2 * b - c,
tr = -Math.sqrt(-a * (c - d) + b * b - b * (c + d) + c * c),
dn = -a + 3 * b - 3 * c + d;
if (dn !== 0) {
result[0] = (tl + tr) / dn;
result[1] = (tl - tr) / dn;
}
return result;
}
/**
* Compute Quadratic Bézier bounding box.
*
* @see http://processingjs.nihongoresources.com/bezierinfo/
*
* @param {Float} xa
* @param {Float} ya
* @param {Float} xb
* @param {Float} yb
* @param {Float} xc
* @param {Float} yc
*
* @return {Object}
*/
exports.computeQuadraticBoundingBox = function(xa, ya, xb, yb, xc, yc) {
var minx = Number.POSITIVE_INFINITY,
miny = Number.POSITIVE_INFINITY,
maxx = Number.NEGATIVE_INFINITY,
maxy = Number.NEGATIVE_INFINITY,
t,
x,
y;
// X
if (xa < minx) { minx = xa; }
if (xa > maxx) { maxx = xa; }
if (xc < minx) { minx = xc; }
if (xc > maxx) { maxx = xc; }
t = computeQuadraticFirstDerivativeRoot(xa, xb, xc);
if (t >= 0 && t <= 1) {
x = computeQuadraticBaseValue(t, xa, xb, xc);
// y = computeQuadraticBaseValue(t, ya, yb, yc);
if (x < minx) { minx = x; }
if (x > maxx) { maxx = x; }
}
// Y
if (ya < miny) { miny = ya; }
if (ya > maxy) { maxy = ya; }
if (yc < miny) { miny = yc; }
if (yc > maxy) { maxy = yc; }
t = computeQuadraticFirstDerivativeRoot(ya, yb, yc);
if (t >= 0 && t <=1 ) {
// x = computeQuadraticBaseValue(t, xa, xb, xc);
y = computeQuadraticBaseValue(t, ya, yb, yc);
if (y < miny) { miny = y; }
if (y > maxy) { maxy = y ; }
}
return {
minx: minx,
miny: miny,
maxx: maxx,
maxy: maxy
};
};
// compute the value for the quadratic bezier function at time=t
function computeQuadraticBaseValue(t, a, b, c) {
var mt = 1 - t;
return mt * mt * a + 2 * mt * t * b + t * t * c;
}
// compute the value for the first derivative of the quadratic bezier function at time=t
function computeQuadraticFirstDerivativeRoot(a, b, c) {
var t = -1,
denominator = a - 2 * b + c;
if (denominator !== 0) {
t = (a - b) / denominator;
}
return t;
}
/**
* Convert path array to string.
*
* @param {Array} path input path data
* @param {Object} params plugin params
* @return {String} output path string
*/
exports.js2path = function(path, data, params) {
path.pathJS = data;
if (params.collapseRepeated) {
data = collapseRepeated(data);
}
path.attr('d').value = data.reduce(function(pathString, item) {
var strData = '';
if (item.data) {
strData = cleanupOutData(item.data, params, item.instruction);
}
return pathString += item.instruction + strData;
}, '');
};
/**
* Collapse repeated instructions data
*
* @param {Array} path input path data
* @return {Array} output path data
*/
function collapseRepeated(data) {
var prev,
prevIndex;
// copy an array and modifieds item to keep original data untouched
data = data.reduce(function(newPath, item) {
if (
prev && item.data &&
item.instruction == prev.instruction
) {
// concat previous data with current
if (item.instruction != 'M') {
prev = newPath[prevIndex] = {
instruction: prev.instruction,
data: prev.data.concat(item.data),
coords: item.coords,
base: prev.base
};
} else {
prev.data = item.data;
prev.coords = item.coords;
}
} else {
newPath.push(item);
prev = item;
prevIndex = newPath.length - 1;
}
return newPath;
}, []);
return data;
}
function set(dest, source) {
dest[0] = source[source.length - 2];
dest[1] = source[source.length - 1];
return dest;
}
/**
* Checks if two paths have an intersection by checking convex hulls
* collision using Gilbert-Johnson-Keerthi distance algorithm
* http://entropyinteractive.com/2011/04/gjk-algorithm/
*
* @param {Array} path1 JS path representation
* @param {Array} path2 JS path representation
* @return {Boolean}
*/
exports.intersects = function(path1, path2) {
if (path1.length < 3 || path2.length < 3) return false; // nothing to fill
// Collect points of every subpath.
var points1 = relative2absolute(path1).reduce(gatherPoints, []),
points2 = relative2absolute(path2).reduce(gatherPoints, []);
// Axis-aligned bounding box check.
if (points1.maxX <= points2.minX || points2.maxX <= points1.minX ||
points1.maxY <= points2.minY || points2.maxY <= points1.minY ||
points1.every(function (set1) {
return points2.every(function (set2) {
return set1[set1.maxX][0] <= set2[set2.minX][0] ||
set2[set2.maxX][0] <= set1[set1.minX][0] ||
set1[set1.maxY][1] <= set2[set2.minY][1] ||
set2[set2.maxY][1] <= set1[set1.minY][1];
});
})
) return false;
// Get a convex hull from points of each subpath. Has the most complexity O(n·log n).
var hullNest1 = points1.map(convexHull),
hullNest2 = points2.map(convexHull);
// Check intersection of every subpath of the first path with every subpath of the second.
return hullNest1.some(function(hull1) {
if (hull1.length < 3) return false;
return hullNest2.some(function(hull2) {
if (hull2.length < 3) return false;
var simplex = [getSupport(hull1, hull2, [1, 0])], // create the initial simplex
direction = minus(simplex[0]); // set the direction to point towards the origin
var iterations = 1e4; // infinite loop protection, 10 000 iterations is more than enough
while (true) {
if (iterations-- == 0) {
console.error('Error: infinite loop while processing mergePaths plugin.');
return true; // true is the safe value that means “do nothing with paths”
}
// add a new point
simplex.push(getSupport(hull1, hull2, direction));
// see if the new point was on the correct side of the origin
if (dot(direction, simplex[simplex.length - 1]) <= 0) return false;
// process the simplex
if (processSimplex(simplex, direction)) return true;
}
});
});
function getSupport(a, b, direction) {
return sub(supportPoint(a, direction), supportPoint(b, minus(direction)));
}
// Computes farthest polygon point in particular direction.
// Thanks to knowledge of min/max x and y coordinates we can choose a quadrant to search in.
// Since we're working on convex hull, the dot product is increasing until we find the farthest point.
function supportPoint(polygon, direction) {
var index = direction[1] >= 0 ?
direction[0] < 0 ? polygon.maxY : polygon.maxX :
direction[0] < 0 ? polygon.minX : polygon.minY,
max = -Infinity,
value;
while ((value = dot(polygon[index], direction)) > max) {
max = value;
index = ++index % polygon.length;
}
return polygon[(index || polygon.length) - 1];
}
};
function processSimplex(simplex, direction) {
/* jshint -W004 */
// we only need to handle to 1-simplex and 2-simplex
if (simplex.length == 2) { // 1-simplex
var a = simplex[1],
b = simplex[0],
AO = minus(simplex[1]),
AB = sub(b, a);
// AO is in the same direction as AB
if (dot(AO, AB) > 0) {
// get the vector perpendicular to AB facing O
set(direction, orth(AB, a));
} else {
set(direction, AO);
// only A remains in the simplex
simplex.shift();
}
} else { // 2-simplex
var a = simplex[2], // [a, b, c] = simplex
b = simplex[1],
c = simplex[0],
AB = sub(b, a),
AC = sub(c, a),
AO = minus(a),
ACB = orth(AB, AC), // the vector perpendicular to AB facing away from C
ABC = orth(AC, AB); // the vector perpendicular to AC facing away from B
if (dot(ACB, AO) > 0) {
if (dot(AB, AO) > 0) { // region 4
set(direction, ACB);
simplex.shift(); // simplex = [b, a]
} else { // region 5
set(direction, AO);
simplex.splice(0, 2); // simplex = [a]
}
} else if (dot(ABC, AO) > 0) {
if (dot(AC, AO) > 0) { // region 6
set(direction, ABC);
simplex.splice(1, 1); // simplex = [c, a]
} else { // region 5 (again)
set(direction, AO);
simplex.splice(0, 2); // simplex = [a]
}
} else // region 7
return true;
}
return false;
}
function minus(v) {
return [-v[0], -v[1]];
}
function sub(v1, v2) {
return [v1[0] - v2[0], v1[1] - v2[1]];
}
function dot(v1, v2) {
return v1[0] * v2[0] + v1[1] * v2[1];
}
function orth(v, from) {
var o = [-v[1], v[0]];
return dot(o, minus(from)) < 0 ? minus(o) : o;
}
function gatherPoints(points, item, index, path) {
var subPath = points.length && points[points.length - 1],
prev = index && path[index - 1],
basePoint = subPath.length && subPath[subPath.length - 1],
data = item.data,
ctrlPoint = basePoint;
switch (item.instruction) {
case 'M':
points.push(subPath = []);
break;
case 'H':
addPoint(subPath, [data[0], basePoint[1]]);
break;
case 'V':
addPoint(subPath, [basePoint[0], data[0]]);
break;
case 'Q':
addPoint(subPath, data.slice(0, 2));
prevCtrlPoint = [data[2] - data[0], data[3] - data[1]]; // Save control point for shorthand
break;
case 'T':
if (prev.instruction == 'Q' || prev.instruction == 'T') {
ctrlPoint = [basePoint[0] + prevCtrlPoint[0], basePoint[1] + prevCtrlPoint[1]];
addPoint(subPath, ctrlPoint);
prevCtrlPoint = [data[0] - ctrlPoint[0], data[1] - ctrlPoint[1]];
}
break;
case 'C':
// Approximate quibic Bezier curve with middle points between control points
addPoint(subPath, [.5 * (basePoint[0] + data[0]), .5 * (basePoint[1] + data[1])]);
addPoint(subPath, [.5 * (data[0] + data[2]), .5 * (data[1] + data[3])]);
addPoint(subPath, [.5 * (data[2] + data[4]), .5 * (data[3] + data[5])]);
prevCtrlPoint = [data[4] - data[2], data[5] - data[3]]; // Save control point for shorthand
break;
case 'S':
if (prev.instruction == 'C' || prev.instruction == 'S') {
addPoint(subPath, [basePoint[0] + .5 * prevCtrlPoint[0], basePoint[1] + .5 * prevCtrlPoint[1]]);
ctrlPoint = [basePoint[0] + prevCtrlPoint[0], basePoint[1] + prevCtrlPoint[1]];
}
addPoint(subPath, [.5 * (ctrlPoint[0] + data[0]), .5 * (ctrlPoint[1]+ data[1])]);
addPoint(subPath, [.5 * (data[0] + data[2]), .5 * (data[1] + data[3])]);
prevCtrlPoint = [data[2] - data[0], data[3] - data[1]];
break;
case 'A':
// Convert the arc to bezier curves and use the same approximation
var curves = a2c.apply(0, basePoint.concat(data));
for (var cData; (cData = curves.splice(0,6).map(toAbsolute)).length;) {
addPoint(subPath, [.5 * (basePoint[0] + cData[0]), .5 * (basePoint[1] + cData[1])]);
addPoint(subPath, [.5 * (cData[0] + cData[2]), .5 * (cData[1] + cData[3])]);
addPoint(subPath, [.5 * (cData[2] + cData[4]), .5 * (cData[3] + cData[5])]);
if (curves.length) addPoint(subPath, basePoint = cData.slice(-2));
}
break;
}
// Save final command coordinates
if (data && data.length >= 2) addPoint(subPath, data.slice(-2));
return points;
function toAbsolute(n, i) { return n + basePoint[i % 2] }
// Writes data about the extreme points on each axle
function addPoint(path, point) {
if (!path.length || point[1] > path[path.maxY][1]) {
path.maxY = path.length;
points.maxY = points.length ? Math.max(point[1], points.maxY) : point[1];
}
if (!path.length || point[0] > path[path.maxX][0]) {
path.maxX = path.length;
points.maxX = points.length ? Math.max(point[0], points.maxX) : point[0];
}
if (!path.length || point[1] < path[path.minY][1]) {
path.minY = path.length;
points.minY = points.length ? Math.min(point[1], points.minY) : point[1];
}
if (!path.length || point[0] < path[path.minX][0]) {
path.minX = path.length;
points.minX = points.length ? Math.min(point[0], points.minX) : point[0];
}
path.push(point);
}
}
/**
* Forms a convex hull from set of points of every subpath using monotone chain convex hull algorithm.
* http://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
*
* @param points An array of [X, Y] coordinates
*/
function convexHull(points) {
/* jshint -W004 */
points.sort(function(a, b) {
return a[0] == b[0] ? a[1] - b[1] : a[0] - b[0];
});
var lower = [],
minY = 0,
bottom = 0;
for (var i = 0; i < points.length; i++) {
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], points[i]) <= 0) {
lower.pop();
}
if (points[i][1] < points[minY][1]) {
minY = i;
bottom = lower.length;
}
lower.push(points[i]);
}
var upper = [],
maxY = points.length - 1,
top = 0;
for (var i = points.length; i--;) {
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], points[i]) <= 0) {
upper.pop();
}
if (points[i][1] > points[maxY][1]) {
maxY = i;
top = upper.length;
}
upper.push(points[i]);
}
// last points are equal to starting points of the other part
upper.pop();
lower.pop();
var hull = lower.concat(upper);
hull.minX = 0; // by sorting
hull.maxX = lower.length;
hull.minY = bottom;
hull.maxY = (lower.length + top) % hull.length;
return hull;
}
function cross(o, a, b) {
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
}
/* Based on code from Snap.svg (Apache 2 license). http://snapsvg.io/
* Thanks to Dmitry Baranovskiy for his great work!
*/
// jshint ignore: start
function a2c(x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) {
// for more information of where this Math came from visit:
// http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
var _120 = Math.PI * 120 / 180,
rad = Math.PI / 180 * (+angle || 0),
res = [],
rotateX = function(x, y, rad) { return x * Math.cos(rad) - y * Math.sin(rad) },
rotateY = function(x, y, rad) { return x * Math.sin(rad) + y * Math.cos(rad) };
if (!recursive) {
x1 = rotateX(x1, y1, -rad);
y1 = rotateY(x1, y1, -rad);
x2 = rotateX(x2, y2, -rad);
y2 = rotateY(x2, y2, -rad);
var x = (x1 - x2) / 2,
y = (y1 - y2) / 2;
var h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
if (h > 1) {
h = Math.sqrt(h);
rx = h * rx;
ry = h * ry;
}
var rx2 = rx * rx,
ry2 = ry * ry,
k = (large_arc_flag == sweep_flag ? -1 : 1) *
Math.sqrt(Math.abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))),
cx = k * rx * y / ry + (x1 + x2) / 2,
cy = k * -ry * x / rx + (y1 + y2) / 2,
f1 = Math.asin(((y1 - cy) / ry).toFixed(9)),
f2 = Math.asin(((y2 - cy) / ry).toFixed(9));
f1 = x1 < cx ? Math.PI - f1 : f1;
f2 = x2 < cx ? Math.PI - f2 : f2;
f1 < 0 && (f1 = Math.PI * 2 + f1);
f2 < 0 && (f2 = Math.PI * 2 + f2);
if (sweep_flag && f1 > f2) {
f1 = f1 - Math.PI * 2;
}
if (!sweep_flag && f2 > f1) {
f2 = f2 - Math.PI * 2;
}
} else {
f1 = recursive[0];
f2 = recursive[1];
cx = recursive[2];
cy = recursive[3];
}
var df = f2 - f1;
if (Math.abs(df) > _120) {
var f2old = f2,
x2old = x2,
y2old = y2;
f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
x2 = cx + rx * Math.cos(f2);
y2 = cy + ry * Math.sin(f2);
res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]);
}
df = f2 - f1;
var c1 = Math.cos(f1),
s1 = Math.sin(f1),
c2 = Math.cos(f2),
s2 = Math.sin(f2),
t = Math.tan(df / 4),
hx = 4 / 3 * rx * t,
hy = 4 / 3 * ry * t,
m = [
- hx * s1, hy * c1,
x2 + hx * s2 - x1, y2 - hy * c2 - y1,
x2 - x1, y2 - y1
];
if (recursive) {
return m.concat(res);
} else {
res = m.concat(res);
var newres = [];
for (var i = 0, n = res.length; i < n; i++) {
newres[i] = i % 2 ? rotateY(res[i - 1], res[i], rad) : rotateX(res[i], res[i + 1], rad);
}
return newres;
}
}
// jshint ignore: end

View file

@ -1,310 +0,0 @@
'use strict';
var regTransformTypes = /matrix|translate|scale|rotate|skewX|skewY/,
regTransformSplit = /\s*(matrix|translate|scale|rotate|skewX|skewY)\s*\(\s*(.+?)\s*\)[\s,]*/,
regNumericValues = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
/**
* Convert transform string to JS representation.
*
* @param {String} transformString input string
* @param {Object} params plugin params
* @return {Array} output array
*/
exports.transform2js = function(transformString) {
// JS representation of the transform data
var transforms = [],
// current transform context
current;
// split value into ['', 'translate', '10 50', '', 'scale', '2', '', 'rotate', '-45', '']
transformString.split(regTransformSplit).forEach(function(item) {
/*jshint -W084 */
var num;
if (item) {
// if item is a translate function
if (regTransformTypes.test(item)) {
// then collect it and change current context
transforms.push(current = { name: item });
// else if item is data
} else {
// then split it into [10, 50] and collect as context.data
while (num = regNumericValues.exec(item)) {
num = Number(num);
if (current.data)
current.data.push(num);
else
current.data = [num];
}
}
}
});
// return empty array if broken transform (no data)
return current && current.data ? transforms : [];
};
/**
* Multiply transforms into one.
*
* @param {Array} input transforms array
* @return {Array} output matrix array
*/
exports.transformsMultiply = function(transforms) {
// convert transforms objects to the matrices
transforms = transforms.map(function(transform) {
if (transform.name === 'matrix') {
return transform.data;
}
return transformToMatrix(transform);
});
// multiply all matrices into one
transforms = {
name: 'matrix',
data: transforms.length > 0 ? transforms.reduce(multiplyTransformMatrices) : []
};
return transforms;
};
/**
* Do math like a schoolgirl.
*
* @type {Object}
*/
var mth = exports.mth = {
rad: function(deg) {
return deg * Math.PI / 180;
},
deg: function(rad) {
return rad * 180 / Math.PI;
},
cos: function(deg) {
return Math.cos(this.rad(deg));
},
acos: function(val, floatPrecision) {
return +(this.deg(Math.acos(val)).toFixed(floatPrecision));
},
sin: function(deg) {
return Math.sin(this.rad(deg));
},
asin: function(val, floatPrecision) {
return +(this.deg(Math.asin(val)).toFixed(floatPrecision));
},
tan: function(deg) {
return Math.tan(this.rad(deg));
},
atan: function(val, floatPrecision) {
return +(this.deg(Math.atan(val)).toFixed(floatPrecision));
}
};
/**
* Decompose matrix into simple transforms. See
* http://frederic-wang.fr/decomposition-of-2d-transform-matrices.html
*
* @param {Object} data matrix transform object
* @return {Object|Array} transforms array or original transform object
*/
exports.matrixToTransform = function(transform, params) {
var floatPrecision = params.floatPrecision,
data = transform.data,
transforms = [],
sx = +Math.hypot(data[0], data[1]).toFixed(params.transformPrecision),
sy = +((data[0] * data[3] - data[1] * data[2]) / sx).toFixed(params.transformPrecision),
colsSum = data[0] * data[2] + data[1] * data[3],
rowsSum = data[0] * data[1] + data[2] * data[3],
scaleBefore = rowsSum != 0 || sx == sy;
// [..., ..., ..., ..., tx, ty] → translate(tx, ty)
if (data[4] || data[5]) {
transforms.push({ name: 'translate', data: data.slice(4, data[5] ? 6 : 5) });
}
// [sx, 0, tan(a)·sy, sy, 0, 0] → skewX(a)·scale(sx, sy)
if (!data[1] && data[2]) {
transforms.push({ name: 'skewX', data: [mth.atan(data[2] / sy, floatPrecision)] });
// [sx, sx·tan(a), 0, sy, 0, 0] → skewY(a)·scale(sx, sy)
} else if (data[1] && !data[2]) {
transforms.push({ name: 'skewY', data: [mth.atan(data[1] / data[0], floatPrecision)] });
sx = data[0];
sy = data[3];
// [sx·cos(a), sx·sin(a), sy·-sin(a), sy·cos(a), x, y] → rotate(a[, cx, cy])·(scale or skewX) or
// [sx·cos(a), sy·sin(a), sx·-sin(a), sy·cos(a), x, y] → scale(sx, sy)·rotate(a[, cx, cy]) (if !scaleBefore)
} else if (!colsSum || (sx == 1 && sy == 1) || !scaleBefore) {
if (!scaleBefore) {
sx = (data[0] < 0 ? -1 : 1) * Math.hypot(data[0], data[2]);
sy = (data[3] < 0 ? -1 : 1) * Math.hypot(data[1], data[3]);
transforms.push({ name: 'scale', data: [sx, sy] });
}
var angle = Math.min(Math.max(-1, data[0] / sx), 1),
rotate = [mth.acos(angle, floatPrecision) * ((scaleBefore ? 1 : sy) * data[1] < 0 ? -1 : 1)];
if (rotate[0]) transforms.push({ name: 'rotate', data: rotate });
if (rowsSum && colsSum) transforms.push({
name: 'skewX',
data: [mth.atan(colsSum / (sx * sx), floatPrecision)]
});
// rotate(a, cx, cy) can consume translate() within optional arguments cx, cy (rotation point)
if (rotate[0] && (data[4] || data[5])) {
transforms.shift();
var cos = data[0] / sx,
sin = data[1] / (scaleBefore ? sx : sy),
x = data[4] * (scaleBefore || sy),
y = data[5] * (scaleBefore || sx),
denom = (Math.pow(1 - cos, 2) + Math.pow(sin, 2)) * (scaleBefore || sx * sy);
rotate.push(((1 - cos) * x - sin * y) / denom);
rotate.push(((1 - cos) * y + sin * x) / denom);
}
// Too many transformations, return original matrix if it isn't just a scale/translate
} else if (data[1] || data[2]) {
return transform;
}
if (scaleBefore && (sx != 1 || sy != 1) || !transforms.length) transforms.push({
name: 'scale',
data: sx == sy ? [sx] : [sx, sy]
});
return transforms;
};
/**
* Convert transform to the matrix data.
*
* @param {Object} transform transform object
* @return {Array} matrix data
*/
function transformToMatrix(transform) {
if (transform.name === 'matrix') return transform.data;
var matrix;
switch (transform.name) {
case 'translate':
// [1, 0, 0, 1, tx, ty]
matrix = [1, 0, 0, 1, transform.data[0], transform.data[1] || 0];
break;
case 'scale':
// [sx, 0, 0, sy, 0, 0]
matrix = [transform.data[0], 0, 0, transform.data[1] || transform.data[0], 0, 0];
break;
case 'rotate':
// [cos(a), sin(a), -sin(a), cos(a), x, y]
var cos = mth.cos(transform.data[0]),
sin = mth.sin(transform.data[0]),
cx = transform.data[1] || 0,
cy = transform.data[2] || 0;
matrix = [cos, sin, -sin, cos, (1 - cos) * cx + sin * cy, (1 - cos) * cy - sin * cx];
break;
case 'skewX':
// [1, 0, tan(a), 1, 0, 0]
matrix = [1, 0, mth.tan(transform.data[0]), 1, 0, 0];
break;
case 'skewY':
// [1, tan(a), 0, 1, 0, 0]
matrix = [1, mth.tan(transform.data[0]), 0, 1, 0, 0];
break;
}
return matrix;
}
/**
* Applies transformation to an arc. To do so, we represent ellipse as a matrix, multiply it
* by the transformation matrix and use a singular value decomposition to represent in a form
* rotate(θ)·scale(a b)·rotate(φ). This gives us new ellipse params a, b and θ.
* SVD is being done with the formulae provided by Wolffram|Alpha (svd {{m0, m2}, {m1, m3}})
*
* @param {Array} arc [a, b, rotation in deg]
* @param {Array} transform transformation matrix
* @return {Array} arc transformed input arc
*/
exports.transformArc = function(arc, transform) {
var a = arc[0],
b = arc[1],
rot = arc[2] * Math.PI / 180,
cos = Math.cos(rot),
sin = Math.sin(rot),
h = Math.pow(arc[5] * cos + arc[6] * sin, 2) / (4 * a * a) +
Math.pow(arc[6] * cos - arc[5] * sin, 2) / (4 * b * b);
if (h > 1) {
h = Math.sqrt(h);
a *= h;
b *= h;
}
var ellipse = [a * cos, a * sin, -b * sin, b * cos, 0, 0],
m = multiplyTransformMatrices(transform, ellipse),
// Decompose the new ellipse matrix
lastCol = m[2] * m[2] + m[3] * m[3],
squareSum = m[0] * m[0] + m[1] * m[1] + lastCol,
root = Math.hypot(m[0] - m[3], m[1] + m[2]) * Math.hypot(m[0] + m[3], m[1] - m[2]);
if (!root) { // circle
arc[0] = arc[1] = Math.sqrt(squareSum / 2);
arc[2] = 0;
} else {
var majorAxisSqr = (squareSum + root) / 2,
minorAxisSqr = (squareSum - root) / 2,
major = Math.abs(majorAxisSqr - lastCol) > 1e-6,
sub = (major ? majorAxisSqr : minorAxisSqr) - lastCol,
rowsSum = m[0] * m[2] + m[1] * m[3],
term1 = m[0] * sub + m[2] * rowsSum,
term2 = m[1] * sub + m[3] * rowsSum;
arc[0] = Math.sqrt(majorAxisSqr);
arc[1] = Math.sqrt(minorAxisSqr);
arc[2] = ((major ? term2 < 0 : term1 > 0) ? -1 : 1) *
Math.acos((major ? term1 : term2) / Math.hypot(term1, term2)) * 180 / Math.PI;
}
if ((transform[0] < 0) !== (transform[3] < 0)) {
// Flip the sweep flag if coordinates are being flipped horizontally XOR vertically
arc[4] = 1 - arc[4];
}
return arc;
};
/**
* Multiply transformation matrices.
*
* @param {Array} a matrix A data
* @param {Array} b matrix B data
* @return {Array} result
*/
function multiplyTransformMatrices(a, b) {
return [
a[0] * b[0] + a[2] * b[1],
a[1] * b[0] + a[3] * b[1],
a[0] * b[2] + a[2] * b[3],
a[1] * b[2] + a[3] * b[3],
a[0] * b[4] + a[2] * b[5] + a[4],
a[1] * b[4] + a[3] * b[5] + a[5]
];
}

View file

@ -1,82 +0,0 @@
'use strict';
exports.type = 'full';
exports.active = false;
exports.description = 'adds attributes to an outer <svg> element';
var ENOCLS = `Error in plugin "addAttributesToSVGElement": absent parameters.
It should have a list of "attributes" or one "attribute".
Config example:
plugins:
- addAttributesToSVGElement:
attribute: "mySvg"
plugins:
- addAttributesToSVGElement:
attributes: ["mySvg", "size-big"]
plugins:
- addAttributesToSVGElement:
attributes:
- focusable: false
- data-image: icon`;
/**
* Add attributes to an outer <svg> element. Example config:
*
* plugins:
* - addAttributesToSVGElement:
* attribute: 'data-icon'
*
* plugins:
* - addAttributesToSVGElement:
* attributes: ['data-icon', 'data-disabled']
*
* plugins:
* - addAttributesToSVGElement:
* attributes:
* - focusable: false
* - data-image: icon
*
* @author April Arcus
*/
exports.fn = function(data, params) {
if (!params || !(Array.isArray(params.attributes) || params.attribute)) {
console.error(ENOCLS);
return data;
}
var attributes = params.attributes || [ params.attribute ],
svg = data.content[0];
if (svg.isElem('svg')) {
attributes.forEach(function (attribute) {
if (typeof attribute === 'string') {
if (!svg.hasAttr(attribute)) {
svg.addAttr({
name: attribute,
prefix: '',
local: attribute
});
}
} else if (typeof attribute === 'object') {
Object.keys(attribute).forEach(function (key) {
if (!svg.hasAttr(key)) {
svg.addAttr({
name: key,
value: attribute[key],
prefix: '',
local: key
});
}
});
}
});
}
return data;
};

View file

@ -1,50 +0,0 @@
'use strict';
exports.type = 'full';
exports.active = false;
exports.description = 'adds classnames to an outer <svg> element';
var ENOCLS = `Error in plugin "addClassesToSVGElement": absent parameters.
It should have a list of classes in "classNames" or one "className".
Config example:
plugins:
- addClassesToSVGElement:
className: "mySvg"
plugins:
- addClassesToSVGElement:
classNames: ["mySvg", "size-big"]
`;
/**
* Add classnames to an outer <svg> element. Example config:
*
* plugins:
* - addClassesToSVGElement:
* className: 'mySvg'
*
* plugins:
* - addClassesToSVGElement:
* classNames: ['mySvg', 'size-big']
*
* @author April Arcus
*/
exports.fn = function(data, params) {
if (!params || !(Array.isArray(params.classNames) && params.classNames.some(String) || params.className)) {
console.error(ENOCLS);
return data;
}
var classNames = params.classNames || [ params.className ],
svg = data.content[0];
if (svg.isElem('svg')) {
svg.class.add.apply(svg.class, classNames);
}
return data;
};

View file

@ -1,56 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'cleanups attributes from newlines, trailing and repeating spaces';
exports.params = {
newlines: true,
trim: true,
spaces: true
};
var regNewlinesNeedSpace = /(\S)\r?\n(\S)/g,
regNewlines = /\r?\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.fn = function(item, params) {
if (item.isElem()) {
item.eachAttr(function(attr) {
if (params.newlines) {
// new line which requires a space instead of themselve
attr.value = attr.value.replace(regNewlinesNeedSpace, function(match, p1, p2) {
return p1 + ' ' + p2;
});
// simple new line
attr.value = attr.value.replace(regNewlines, '');
}
if (params.trim) {
attr.value = attr.value.trim();
}
if (params.spaces) {
attr.value = attr.value.replace(regSpaces, ' ');
}
});
}
};

View file

@ -1,84 +0,0 @@
'use strict';
exports.type = 'full';
exports.active = true;
exports.description = 'remove or cleanup enable-background attribute when possible';
/**
* Remove or cleanup enable-background attr which coincides with a width/height box.
*
* @see http://www.w3.org/TR/SVG/filters.html#EnableBackgroundProperty
*
* @example
* <svg width="100" height="50" enable-background="new 0 0 100 50">
*
* <svg width="100" height="50">
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(data) {
var regEnableBackground = /^new\s0\s0\s([\-+]?\d*\.?\d+([eE][\-+]?\d+)?)\s([\-+]?\d*\.?\d+([eE][\-+]?\d+)?)$/,
hasFilter = false,
elems = ['svg', 'mask', 'pattern'];
function checkEnableBackground(item) {
if (
item.isElem(elems) &&
item.hasAttr('enable-background') &&
item.hasAttr('width') &&
item.hasAttr('height')
) {
var match = item.attr('enable-background').value.match(regEnableBackground);
if (match) {
if (
item.attr('width').value === match[1] &&
item.attr('height').value === match[3]
) {
if (item.isElem('svg')) {
item.removeAttr('enable-background');
} else {
item.attr('enable-background').value = 'new';
}
}
}
}
}
function checkForFilter(item) {
if (item.isElem('filter')) {
hasFilter = true;
}
}
function monkeys(items, fn) {
items.content.forEach(function(item) {
fn(item);
if (item.content) {
monkeys(item, fn);
}
});
return items;
}
var firstStep = monkeys(data, function(item) {
checkEnableBackground(item);
if (!hasFilter) {
checkForFilter(item);
}
});
return hasFilter ? firstStep : monkeys(firstStep, function(item) {
//we don't need 'enable-background' if we have no filters
item.removeAttr('enable-background');
});
};

View file

@ -1,212 +0,0 @@
'use strict';
exports.type = 'full';
exports.active = true;
exports.description = 'removes unused IDs and minifies used';
exports.params = {
remove: true,
minify: true,
prefix: '',
preserve: [],
preservePrefixes: [],
force: false
};
var referencesProps = new Set(require('./_collections').referencesProps),
regReferencesUrl = /\burl\(("|')?#(.+?)\1\)/,
regReferencesHref = /^#(.+?)$/,
regReferencesBegin = /(\w+)\./,
styleOrScript = ['style', 'script'],
generateIDchars = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
],
maxIDindex = generateIDchars.length - 1;
/**
* Remove unused and minify used IDs
* (only if there are no any <style> or <script>).
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
*
* @author Kir Belevich
*/
exports.fn = function(data, params) {
var currentID,
currentIDstring,
IDs = new Map(),
referencesIDs = new Map(),
hasStyleOrScript = false,
preserveIDs = new Set(Array.isArray(params.preserve) ? params.preserve : params.preserve ? [params.preserve] : []),
preserveIDPrefixes = new Set(Array.isArray(params.preservePrefixes) ? params.preservePrefixes : (params.preservePrefixes ? [params.preservePrefixes] : [])),
idValuePrefix = '#',
idValuePostfix = '.';
/**
* Bananas!
*
* @param {Array} items input items
* @return {Array} output items
*/
function monkeys(items) {
for (var i = 0; i < items.content.length && !hasStyleOrScript; i++) {
var item = items.content[i];
// quit if <style> or <script> present ('force' param prevents quitting)
if (!params.force) {
var isNotEmpty = Boolean(item.content);
if (item.isElem(styleOrScript) && isNotEmpty) {
hasStyleOrScript = true;
continue;
}
// Don't remove IDs if the whole SVG consists only of defs.
if (item.isElem('svg')) {
var hasDefsOnly = true;
for (var j = 0; j < item.content.length; j++) {
if (!item.content[j].isElem('defs')) {
hasDefsOnly = false;
break;
}
}
if (hasDefsOnly) {
break;
}
}
}
// …and don't remove any ID if yes
if (item.isElem()) {
item.eachAttr(function(attr) {
var key, match;
// save IDs
if (attr.name === 'id') {
key = attr.value;
if (IDs.has(key)) {
item.removeAttr('id'); // remove repeated id
} else {
IDs.set(key, item);
}
return;
}
// save references
if (referencesProps.has(attr.name) && (match = attr.value.match(regReferencesUrl))) {
key = match[2]; // url() reference
} else if (
attr.local === 'href' && (match = attr.value.match(regReferencesHref)) ||
attr.name === 'begin' && (match = attr.value.match(regReferencesBegin))
) {
key = match[1]; // href reference
}
if (key) {
var ref = referencesIDs.get(key) || [];
ref.push(attr);
referencesIDs.set(key, ref);
}
});
}
// go deeper
if (item.content) {
monkeys(item);
}
}
return items;
}
data = monkeys(data);
if (hasStyleOrScript) {
return data;
}
const idPreserved = id => preserveIDs.has(id) || idMatchesPrefix(preserveIDPrefixes, id);
for (var ref of referencesIDs) {
var key = ref[0];
if (IDs.has(key)) {
// replace referenced IDs with the minified ones
if (params.minify && !idPreserved(key)) {
do {
currentIDstring = getIDstring(currentID = generateID(currentID), params);
} while (idPreserved(currentIDstring));
IDs.get(key).attr('id').value = currentIDstring;
for (var attr of ref[1]) {
attr.value = attr.value.includes(idValuePrefix) ?
attr.value.replace(idValuePrefix + key, idValuePrefix + currentIDstring) :
attr.value.replace(key + idValuePostfix, currentIDstring + idValuePostfix);
}
}
// don't remove referenced IDs
IDs.delete(key);
}
}
// remove non-referenced IDs attributes from elements
if (params.remove) {
for(var keyElem of IDs) {
if (!idPreserved(keyElem[0])) {
keyElem[1].removeAttr('id');
}
}
}
return data;
};
/**
* Check if an ID starts with any one of a list of strings.
*
* @param {Array} of prefix strings
* @param {String} current ID
* @return {Boolean} if currentID starts with one of the strings in prefixArray
*/
function idMatchesPrefix(prefixArray, currentID) {
if (!currentID) return false;
for (var prefix of prefixArray) if (currentID.startsWith(prefix)) return true;
return false;
}
/**
* Generate unique minimal ID.
*
* @param {Array} [currentID] current ID
* @return {Array} generated ID array
*/
function generateID(currentID) {
if (!currentID) return [0];
currentID[currentID.length - 1]++;
for(var i = currentID.length - 1; i > 0; i--) {
if (currentID[i] > maxIDindex) {
currentID[i] = 0;
if (currentID[i - 1] !== undefined) {
currentID[i - 1]++;
}
}
}
if (currentID[0] > maxIDindex) {
currentID[0] = 0;
currentID.unshift(0);
}
return currentID;
}
/**
* Get string from generated ID array.
*
* @param {Array} arr input ID array
* @return {String} output ID string
*/
function getIDstring(arr, params) {
var str = params.prefix;
return str + arr.map(i => generateIDchars[i]).join('');
}

View file

@ -1,139 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = false;
exports.description = 'rounds list of values to the fixed precision';
exports.params = {
floatPrecision: 3,
leadingZero: true,
defaultPx: true,
convertToPx: true
};
var regNumericValues = /^([\-+]?\d*\.?\d+([eE][\-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/,
regSeparator = /\s+,?\s*|,\s*/,
removeLeadingZero = require('../tools').removeLeadingZero,
absoluteLengths = { // relative to px
cm: 96/2.54,
mm: 96/25.4,
in: 96,
pt: 4/3,
pc: 16
};
/**
* Round list of values to the fixed precision.
*
* @example
* <svg viewBox="0 0 200.28423 200.28423" enable-background="new 0 0 200.28423 200.28423">
*
* <svg viewBox="0 0 200.284 200.284" enable-background="new 0 0 200.284 200.284">
*
*
* <polygon points="208.250977 77.1308594 223.069336 ... "/>
*
* <polygon points="208.251 77.131 223.069 ... "/>
*
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
*
* @author kiyopikko
*/
exports.fn = function(item, params) {
if ( item.hasAttr('points') ) {
roundValues(item.attrs.points);
}
if ( item.hasAttr('enable-background') ) {
roundValues(item.attrs['enable-background']);
}
if ( item.hasAttr('viewBox') ) {
roundValues(item.attrs.viewBox);
}
if ( item.hasAttr('stroke-dasharray') ) {
roundValues(item.attrs['stroke-dasharray']);
}
if ( item.hasAttr('dx') ) {
roundValues(item.attrs.dx);
}
if ( item.hasAttr('dy') ) {
roundValues(item.attrs.dy);
}
if ( item.hasAttr('x') ) {
roundValues(item.attrs.x);
}
if ( item.hasAttr('y') ) {
roundValues(item.attrs.y);
}
function roundValues($prop){
var num, units,
match,
matchNew,
lists = $prop.value,
listsArr = lists.split(regSeparator),
roundedListArr = [],
roundedList;
listsArr.forEach(function(elem){
match = elem.match(regNumericValues);
matchNew = elem.match(/new/);
// if attribute value matches regNumericValues
if (match) {
// round it to the fixed precision
num = +(+match[1]).toFixed(params.floatPrecision),
units = match[3] || '';
// convert absolute values to pixels
if (params.convertToPx && units && (units in absoluteLengths)) {
var pxNum = +(absoluteLengths[units] * match[1]).toFixed(params.floatPrecision);
if (String(pxNum).length < match[0].length)
num = pxNum,
units = 'px';
}
// and remove leading zero
if (params.leadingZero) {
num = removeLeadingZero(num);
}
// remove default 'px' units
if (params.defaultPx && units === 'px') {
units = '';
}
roundedListArr.push(num+units);
}
// if attribute value is "new"(only enable-background).
else if (matchNew) {
roundedListArr.push('new');
} else if (elem) {
roundedListArr.push(elem);
}
});
roundedList = roundedListArr.join(' ');
$prop.value = roundedList;
}
};

View file

@ -1,85 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'rounds numeric values to the fixed precision, removes default px units';
exports.params = {
floatPrecision: 3,
leadingZero: false,
defaultPx: true,
convertToPx: true
};
var regNumericValues = /^([\-+]?\d*\.?\d+([eE][\-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/,
removeLeadingZero = require('../tools').removeLeadingZero,
absoluteLengths = { // relative to px
cm: 96/2.54,
mm: 96/25.4,
in: 96,
pt: 4/3,
pc: 16,
em: 16
};
/**
* Round numeric values to the fixed precision,
* remove default 'px' 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.fn = function(item, params) {
if (item.isElem()) {
var floatPrecision = params.floatPrecision;
if (item.hasAttr('viewBox')) {
var nums = item.attr('viewBox').value.split(/\s,?\s*|,\s*/g);
item.attr('viewBox').value = nums.map(function(value) {
var num = +value;
return isNaN(num) ? value : +num.toFixed(floatPrecision);
}).join(' ');
}
item.eachAttr(function(attr) {
// The `version` attribute is a text string and cannot be rounded
if (attr.name === 'version') { return }
var match = attr.value.match(regNumericValues);
// if attribute value matches regNumericValues
if (match) {
// round it to the fixed precision
var num = +(+match[1]).toFixed(floatPrecision),
units = match[3] || '';
// convert absolute values to pixels
if (params.convertToPx && units && (units in absoluteLengths)) {
num = +(absoluteLengths[units] * match[1]).toFixed(floatPrecision);
units = 'px';
}
// and remove leading zero
if (params.leadingZero) {
num = removeLeadingZero(num);
}
// remove default 'px' units
if (params.defaultPx && units === 'px') {
units = '';
}
attr.value = num + units;
}
});
}
};

View file

@ -1,87 +0,0 @@
'use strict';
exports.type = 'perItemReverse';
exports.active = true;
exports.description = 'collapses useless groups';
var collections = require('./_collections'),
attrsInheritable = collections.inheritableAttrs,
animationElems = collections.elemsGroups.animation;
function hasAnimatedAttr(item) {
/* jshint validthis:true */
return item.isElem(animationElems) && item.hasAttr('attributeName', this) ||
!item.isEmpty() && item.content.some(hasAnimatedAttr, this);
}
/*
* Collapse useless groups.
*
* @example
* <g>
* <g attr1="val1">
* <path d="..."/>
* </g>
* </g>
*
* <g>
* <g>
* <path attr1="val1" d="..."/>
* </g>
* </g>
*
* <path attr1="val1" d="..."/>
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item) {
// non-empty elements
if (item.isElem() && !item.isElem('switch') && !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() && !inner.hasAttr('id') && !g.hasAttr('filter') &&
!(g.hasAttr('class') && inner.hasAttr('class')) && (
!g.hasAttr('clip-path') && !g.hasAttr('mask') ||
inner.isElem('g') && !g.hasAttr('transform') && !inner.hasAttr('transform')
)
) {
g.eachAttr(function(attr) {
if (g.content.some(hasAnimatedAttr, attr.name)) return;
if (!inner.hasAttr(attr.name)) {
inner.addAttr(attr);
} else if (attr.name == 'transform') {
inner.attr(attr.name).value = attr.value + ' ' + inner.attr(attr.name).value;
} else if (inner.hasAttr(attr.name, 'inherit')) {
inner.attr(attr.name).value = attr.value;
} else if (
attrsInheritable.indexOf(attr.name) < 0 &&
!inner.hasAttr(attr.name, attr.value)
) {
return;
}
g.removeAttr(attr.name);
});
}
}
// collapse groups without attributes
if (!g.hasAttr() && !g.content.some(function(item) { return item.isElem(animationElems) })) {
item.spliceContent(i, 1, g.content);
}
}
});
}
};

View file

@ -1,130 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'converts colors: rgb() to #rrggbb and #rrggbb to #rgb';
exports.params = {
currentColor: false,
names2hex: true,
rgb2hex: true,
shorthex: true,
shortname: true
};
var collections = require('./_collections'),
rNumber = '([+-]?(?:\\d*\\.\\d+|\\d+\\.?)%?)',
rComma = '\\s*,\\s*',
regRGB = new RegExp('^rgb\\(\\s*' + rNumber + rComma + rNumber + rComma + rNumber + '\\s*\\)$'),
regHEX = /^\#(([a-fA-F0-9])\2){3}$/,
none = /\bnone\b/i;
/**
* 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
*
* Convert hex to short name
* #000080 navy
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item, params) {
if (item.elem) {
item.eachAttr(function(attr) {
if (collections.colorsProps.indexOf(attr.name) > -1) {
var val = attr.value,
match;
// Convert colors to currentColor
if (params.currentColor) {
if (typeof params.currentColor === 'string') {
match = val === params.currentColor;
} else if (params.currentColor.exec) {
match = params.currentColor.exec(val);
} else {
match = !val.match(none);
}
if (match) {
val = 'currentColor';
}
}
// Convert color name keyword to long hex
if (params.names2hex && val.toLowerCase() in collections.colorsNames) {
val = collections.colorsNames[val.toLowerCase()];
}
// 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 Math.max(0, Math.min(m, 255));
});
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];
}
// Convert hex to short name
if (params.shortname) {
var lowerVal = val.toLowerCase();
if (lowerVal in collections.colorsShortNames) {
val = collections.colorsShortNames[lowerVal];
}
}
attr.value = val;
}
});
}
};
/**
* 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 '#' + ('00000' + (rgb[0] << 16 | rgb[1] << 8 | rgb[2]).toString(16)).slice(-6).toUpperCase();
}

View file

@ -1,39 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'converts non-eccentric <ellipse>s to <circle>s';
/**
* Converts non-eccentric <ellipse>s to <circle>s.
*
* @see http://www.w3.org/TR/SVG/shapes.html
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Taylor Hunt
*/
exports.fn = function(item) {
if (item.isElem('ellipse')) {
var rx = item.attr('rx').value || 0;
var ry = item.attr('ry').value || 0;
if (rx === ry ||
rx === 'auto' || ry === 'auto' // SVG2
) {
var radius = rx !== 'auto' ? rx : ry;
item.renameElem('circle');
item.removeAttr(['rx', 'ry']);
item.addAttr({
name: 'r',
value: radius,
prefix: '',
local: 'r',
});
}
}
return;
};

View file

@ -1,971 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'optimizes path data: writes in shorter form, applies transformations';
exports.params = {
applyTransforms: true,
applyTransformsStroked: true,
makeArcs: {
threshold: 2.5, // coefficient of rounding error
tolerance: 0.5 // percentage of radius
},
straightCurves: true,
lineShorthands: true,
curveSmoothShorthands: true,
floatPrecision: 3,
transformPrecision: 5,
removeUseless: true,
collapseRepeated: true,
utilizeAbsolute: true,
leadingZero: true,
negativeExtraSpace: true,
noSpaceAfterFlags: true,
forceAbsolutePath: false
};
var pathElems = require('./_collections.js').pathElems,
path2js = require('./_path.js').path2js,
js2path = require('./_path.js').js2path,
applyTransforms = require('./_path.js').applyTransforms,
cleanupOutData = require('../tools').cleanupOutData,
roundData,
precision,
error,
arcThreshold,
arcTolerance,
hasMarkerMid,
hasStrokeLinecap;
/**
* Convert absolute Path to relative,
* collapse repeated instructions,
* detect and convert Lineto shorthands,
* remove useless instructions like "l0,0",
* trim useless delimiters and leading zeros,
* decrease accuracy of floating-point numbers.
*
* @see http://www.w3.org/TR/SVG/paths.html#PathData
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item, params) {
if (item.isElem(pathElems) && item.hasAttr('d')) {
precision = params.floatPrecision;
error = precision !== false ? +Math.pow(.1, precision).toFixed(precision) : 1e-2;
roundData = precision > 0 && precision < 20 ? strongRound : round;
if (params.makeArcs) {
arcThreshold = params.makeArcs.threshold;
arcTolerance = params.makeArcs.tolerance;
}
hasMarkerMid = item.hasAttr('marker-mid');
var stroke = item.computedAttr('stroke'),
strokeLinecap = item.computedAttr('stroke');
hasStrokeLinecap = stroke && stroke != 'none' && strokeLinecap && strokeLinecap != 'butt';
var data = path2js(item);
// TODO: get rid of functions returns
if (data.length) {
convertToRelative(data);
if (params.applyTransforms) {
data = applyTransforms(item, data, params);
}
data = filters(data, params);
if (params.utilizeAbsolute) {
data = convertToMixed(data, params);
}
js2path(item, data, params);
}
}
};
/**
* Convert absolute path data coordinates to relative.
*
* @param {Array} path input path data
* @param {Object} params plugin params
* @return {Array} output path data
*/
function convertToRelative(path) {
var point = [0, 0],
subpathPoint = [0, 0],
baseItem;
path.forEach(function(item, index) {
var instruction = item.instruction,
data = item.data;
// data !== !z
if (data) {
// already relative
// recalculate current point
if ('mcslqta'.indexOf(instruction) > -1) {
point[0] += data[data.length - 2];
point[1] += data[data.length - 1];
if (instruction === 'm') {
subpathPoint[0] = point[0];
subpathPoint[1] = point[1];
baseItem = item;
}
} else if (instruction === 'h') {
point[0] += data[0];
} else if (instruction === 'v') {
point[1] += data[0];
}
// convert absolute path data coordinates to relative
// if "M" was not transformed from "m"
// M → m
if (instruction === 'M') {
if (index > 0) instruction = 'm';
data[0] -= point[0];
data[1] -= point[1];
subpathPoint[0] = point[0] += data[0];
subpathPoint[1] = point[1] += data[1];
baseItem = item;
}
// L → l
// T → t
else if ('LT'.indexOf(instruction) > -1) {
instruction = instruction.toLowerCase();
// x y
// 0 1
data[0] -= point[0];
data[1] -= point[1];
point[0] += data[0];
point[1] += data[1];
// C → c
} else if (instruction === 'C') {
instruction = 'c';
// x1 y1 x2 y2 x y
// 0 1 2 3 4 5
data[0] -= point[0];
data[1] -= point[1];
data[2] -= point[0];
data[3] -= point[1];
data[4] -= point[0];
data[5] -= point[1];
point[0] += data[4];
point[1] += data[5];
// S → s
// Q → q
} else if ('SQ'.indexOf(instruction) > -1) {
instruction = instruction.toLowerCase();
// x1 y1 x y
// 0 1 2 3
data[0] -= point[0];
data[1] -= point[1];
data[2] -= point[0];
data[3] -= point[1];
point[0] += data[2];
point[1] += data[3];
// A → a
} else if (instruction === 'A') {
instruction = 'a';
// rx ry x-axis-rotation large-arc-flag sweep-flag x y
// 0 1 2 3 4 5 6
data[5] -= point[0];
data[6] -= point[1];
point[0] += data[5];
point[1] += data[6];
// H → h
} else if (instruction === 'H') {
instruction = 'h';
data[0] -= point[0];
point[0] += data[0];
// V → v
} else if (instruction === 'V') {
instruction = 'v';
data[0] -= point[1];
point[1] += data[0];
}
item.instruction = instruction;
item.data = data;
// store absolute coordinates for later use
item.coords = point.slice(-2);
}
// !data === z, reset current point
else if (instruction == 'z') {
if (baseItem) {
item.coords = baseItem.coords;
}
point[0] = subpathPoint[0];
point[1] = subpathPoint[1];
}
item.base = index > 0 ? path[index - 1].coords : [0, 0];
});
return path;
}
/**
* Main filters loop.
*
* @param {Array} path input path data
* @param {Object} params plugin params
* @return {Array} output path data
*/
function filters(path, params) {
var stringify = data2Path.bind(null, params),
relSubpoint = [0, 0],
pathBase = [0, 0],
prev = {};
path = path.filter(function(item, index, path) {
var instruction = item.instruction,
data = item.data,
next = path[index + 1];
if (data) {
var sdata = data,
circle;
if (instruction === 's') {
sdata = [0, 0].concat(data);
if ('cs'.indexOf(prev.instruction) > -1) {
var pdata = prev.data,
n = pdata.length;
// (-x, -y) of the prev tangent point relative to the current point
sdata[0] = pdata[n - 2] - pdata[n - 4];
sdata[1] = pdata[n - 1] - pdata[n - 3];
}
}
// convert curves to arcs if possible
if (
params.makeArcs &&
(instruction == 'c' || instruction == 's') &&
isConvex(sdata) &&
(circle = findCircle(sdata))
) {
var r = roundData([circle.radius])[0],
angle = findArcAngle(sdata, circle),
sweep = sdata[5] * sdata[0] - sdata[4] * sdata[1] > 0 ? 1 : 0,
arc = {
instruction: 'a',
data: [r, r, 0, 0, sweep, sdata[4], sdata[5]],
coords: item.coords.slice(),
base: item.base
},
output = [arc],
// relative coordinates to adjust the found circle
relCenter = [circle.center[0] - sdata[4], circle.center[1] - sdata[5]],
relCircle = { center: relCenter, radius: circle.radius },
arcCurves = [item],
hasPrev = 0,
suffix = '',
nextLonghand;
if (
prev.instruction == 'c' && isConvex(prev.data) && isArcPrev(prev.data, circle) ||
prev.instruction == 'a' && prev.sdata && isArcPrev(prev.sdata, circle)
) {
arcCurves.unshift(prev);
arc.base = prev.base;
arc.data[5] = arc.coords[0] - arc.base[0];
arc.data[6] = arc.coords[1] - arc.base[1];
var prevData = prev.instruction == 'a' ? prev.sdata : prev.data;
var prevAngle = findArcAngle(prevData,
{
center: [prevData[4] + circle.center[0], prevData[5] + circle.center[1]],
radius: circle.radius
}
);
angle += prevAngle;
if (angle > Math.PI) arc.data[3] = 1;
hasPrev = 1;
}
// check if next curves are fitting the arc
for (var j = index; (next = path[++j]) && ~'cs'.indexOf(next.instruction);) {
var nextData = next.data;
if (next.instruction == 's') {
nextLonghand = makeLonghand({instruction: 's', data: next.data.slice() },
path[j - 1].data);
nextData = nextLonghand.data;
nextLonghand.data = nextData.slice(0, 2);
suffix = stringify([nextLonghand]);
}
if (isConvex(nextData) && isArc(nextData, relCircle)) {
angle += findArcAngle(nextData, relCircle);
if (angle - 2 * Math.PI > 1e-3) break; // more than 360°
if (angle > Math.PI) arc.data[3] = 1;
arcCurves.push(next);
if (2 * Math.PI - angle > 1e-3) { // less than 360°
arc.coords = next.coords;
arc.data[5] = arc.coords[0] - arc.base[0];
arc.data[6] = arc.coords[1] - arc.base[1];
} else {
// full circle, make a half-circle arc and add a second one
arc.data[5] = 2 * (relCircle.center[0] - nextData[4]);
arc.data[6] = 2 * (relCircle.center[1] - nextData[5]);
arc.coords = [arc.base[0] + arc.data[5], arc.base[1] + arc.data[6]];
arc = {
instruction: 'a',
data: [r, r, 0, 0, sweep,
next.coords[0] - arc.coords[0], next.coords[1] - arc.coords[1]],
coords: next.coords,
base: arc.coords
};
output.push(arc);
j++;
break;
}
relCenter[0] -= nextData[4];
relCenter[1] -= nextData[5];
} else break;
}
if ((stringify(output) + suffix).length < stringify(arcCurves).length) {
if (path[j] && path[j].instruction == 's') {
makeLonghand(path[j], path[j - 1].data);
}
if (hasPrev) {
var prevArc = output.shift();
roundData(prevArc.data);
relSubpoint[0] += prevArc.data[5] - prev.data[prev.data.length - 2];
relSubpoint[1] += prevArc.data[6] - prev.data[prev.data.length - 1];
prev.instruction = 'a';
prev.data = prevArc.data;
item.base = prev.coords = prevArc.coords;
}
arc = output.shift();
if (arcCurves.length == 1) {
item.sdata = sdata.slice(); // preserve curve data for future checks
} else if (arcCurves.length - 1 - hasPrev > 0) {
// filter out consumed next items
path.splice.apply(path, [index + 1, arcCurves.length - 1 - hasPrev].concat(output));
}
if (!arc) return false;
instruction = 'a';
data = arc.data;
item.coords = arc.coords;
}
}
// Rounding relative coordinates, taking in account accummulating error
// to get closer to absolute coordinates. Sum of rounded value remains same:
// l .25 3 .25 2 .25 3 .25 2 -> l .3 3 .2 2 .3 3 .2 2
if (precision !== false) {
if ('mltqsc'.indexOf(instruction) > -1) {
for (var i = data.length; i--;) {
data[i] += item.base[i % 2] - relSubpoint[i % 2];
}
} else if (instruction == 'h') {
data[0] += item.base[0] - relSubpoint[0];
} else if (instruction == 'v') {
data[0] += item.base[1] - relSubpoint[1];
} else if (instruction == 'a') {
data[5] += item.base[0] - relSubpoint[0];
data[6] += item.base[1] - relSubpoint[1];
}
roundData(data);
if (instruction == 'h') relSubpoint[0] += data[0];
else if (instruction == 'v') relSubpoint[1] += data[0];
else {
relSubpoint[0] += data[data.length - 2];
relSubpoint[1] += data[data.length - 1];
}
roundData(relSubpoint);
if (instruction.toLowerCase() == 'm') {
pathBase[0] = relSubpoint[0];
pathBase[1] = relSubpoint[1];
}
}
// convert straight curves into lines segments
if (params.straightCurves) {
if (
instruction === 'c' &&
isCurveStraightLine(data) ||
instruction === 's' &&
isCurveStraightLine(sdata)
) {
if (next && next.instruction == 's')
makeLonghand(next, data); // fix up next curve
instruction = 'l';
data = data.slice(-2);
}
else if (
instruction === 'q' &&
isCurveStraightLine(data)
) {
if (next && next.instruction == 't')
makeLonghand(next, data); // fix up next curve
instruction = 'l';
data = data.slice(-2);
}
else if (
instruction === 't' &&
prev.instruction !== 'q' &&
prev.instruction !== 't'
) {
instruction = 'l';
data = data.slice(-2);
}
else if (
instruction === 'a' &&
(data[0] === 0 || data[1] === 0)
) {
instruction = 'l';
data = data.slice(-2);
}
}
// horizontal and vertical line shorthands
// l 50 0 → h 50
// l 0 50 → v 50
if (
params.lineShorthands &&
instruction === 'l'
) {
if (data[1] === 0) {
instruction = 'h';
data.pop();
} else if (data[0] === 0) {
instruction = 'v';
data.shift();
}
}
// collapse repeated commands
// h 20 h 30 -> h 50
if (
params.collapseRepeated &&
!hasMarkerMid &&
('mhv'.indexOf(instruction) > -1) &&
prev.instruction &&
instruction == prev.instruction.toLowerCase() &&
(
(instruction != 'h' && instruction != 'v') ||
(prev.data[0] >= 0) == (data[0] >= 0)
)) {
prev.data[0] += data[0];
if (instruction != 'h' && instruction != 'v') {
prev.data[1] += data[1];
}
prev.coords = item.coords;
path[index] = prev;
return false;
}
// convert curves into smooth shorthands
if (params.curveSmoothShorthands && prev.instruction) {
// curveto
if (instruction === 'c') {
// c + c → c + s
if (
prev.instruction === 'c' &&
data[0] === -(prev.data[2] - prev.data[4]) &&
data[1] === -(prev.data[3] - prev.data[5])
) {
instruction = 's';
data = data.slice(2);
}
// s + c → s + s
else if (
prev.instruction === 's' &&
data[0] === -(prev.data[0] - prev.data[2]) &&
data[1] === -(prev.data[1] - prev.data[3])
) {
instruction = 's';
data = data.slice(2);
}
// [^cs] + c → [^cs] + s
else if (
'cs'.indexOf(prev.instruction) === -1 &&
data[0] === 0 &&
data[1] === 0
) {
instruction = 's';
data = data.slice(2);
}
}
// quadratic Bézier curveto
else if (instruction === 'q') {
// q + q → q + t
if (
prev.instruction === 'q' &&
data[0] === (prev.data[2] - prev.data[0]) &&
data[1] === (prev.data[3] - prev.data[1])
) {
instruction = 't';
data = data.slice(2);
}
// t + q → t + t
else if (
prev.instruction === 't' &&
data[2] === prev.data[0] &&
data[3] === prev.data[1]
) {
instruction = 't';
data = data.slice(2);
}
}
}
// remove useless non-first path segments
if (params.removeUseless && !hasStrokeLinecap) {
// l 0,0 / h 0 / v 0 / q 0,0 0,0 / t 0,0 / c 0,0 0,0 0,0 / s 0,0 0,0
if (
(
'lhvqtcs'.indexOf(instruction) > -1
) &&
data.every(function(i) { return i === 0; })
) {
path[index] = prev;
return false;
}
// a 25,25 -30 0,1 0,0
if (
instruction === 'a' &&
data[5] === 0 &&
data[6] === 0
) {
path[index] = prev;
return false;
}
}
item.instruction = instruction;
item.data = data;
prev = item;
} else {
// z resets coordinates
relSubpoint[0] = pathBase[0];
relSubpoint[1] = pathBase[1];
if (prev.instruction == 'z') return false;
prev = item;
}
return true;
});
return path;
}
/**
* Writes data in shortest form using absolute or relative coordinates.
*
* @param {Array} data input path data
* @return {Boolean} output
*/
function convertToMixed(path, params) {
var prev = path[0];
path = path.filter(function(item, index) {
if (index == 0) return true;
if (!item.data) {
prev = item;
return true;
}
var instruction = item.instruction,
data = item.data,
adata = data && data.slice(0);
if ('mltqsc'.indexOf(instruction) > -1) {
for (var i = adata.length; i--;) {
adata[i] += item.base[i % 2];
}
} else if (instruction == 'h') {
adata[0] += item.base[0];
} else if (instruction == 'v') {
adata[0] += item.base[1];
} else if (instruction == 'a') {
adata[5] += item.base[0];
adata[6] += item.base[1];
}
roundData(adata);
var absoluteDataStr = cleanupOutData(adata, params),
relativeDataStr = cleanupOutData(data, params);
// Convert to absolute coordinates if it's shorter or forceAbsolutePath is true.
// v-20 -> V0
// Don't convert if it fits following previous instruction.
// l20 30-10-50 instead of l20 30L20 30
if (
params.forceAbsolutePath || (
absoluteDataStr.length < relativeDataStr.length &&
!(
params.negativeExtraSpace &&
instruction == prev.instruction &&
prev.instruction.charCodeAt(0) > 96 &&
absoluteDataStr.length == relativeDataStr.length - 1 &&
(data[0] < 0 || /^0\./.test(data[0]) && prev.data[prev.data.length - 1] % 1)
))
) {
item.instruction = instruction.toUpperCase();
item.data = adata;
}
prev = item;
return true;
});
return path;
}
/**
* Checks if curve is convex. Control points of such a curve must form
* a convex quadrilateral with diagonals crosspoint inside of it.
*
* @param {Array} data input path data
* @return {Boolean} output
*/
function isConvex(data) {
var center = getIntersection([0, 0, data[2], data[3], data[0], data[1], data[4], data[5]]);
return center &&
(data[2] < center[0] == center[0] < 0) &&
(data[3] < center[1] == center[1] < 0) &&
(data[4] < center[0] == center[0] < data[0]) &&
(data[5] < center[1] == center[1] < data[1]);
}
/**
* Computes lines equations by two points and returns their intersection point.
*
* @param {Array} coords 8 numbers for 4 pairs of coordinates (x,y)
* @return {Array|undefined} output coordinate of lines' crosspoint
*/
function getIntersection(coords) {
// Prev line equation parameters.
var a1 = coords[1] - coords[3], // y1 - y2
b1 = coords[2] - coords[0], // x2 - x1
c1 = coords[0] * coords[3] - coords[2] * coords[1], // x1 * y2 - x2 * y1
// Next line equation parameters
a2 = coords[5] - coords[7], // y1 - y2
b2 = coords[6] - coords[4], // x2 - x1
c2 = coords[4] * coords[7] - coords[5] * coords[6], // x1 * y2 - x2 * y1
denom = (a1 * b2 - a2 * b1);
if (!denom) return; // parallel lines havn't an intersection
var cross = [
(b1 * c2 - b2 * c1) / denom,
(a1 * c2 - a2 * c1) / -denom
];
if (
!isNaN(cross[0]) && !isNaN(cross[1]) &&
isFinite(cross[0]) && isFinite(cross[1])
) {
return cross;
}
}
/**
* Decrease accuracy of floating-point numbers
* in path data keeping a specified number of decimals.
* Smart rounds values like 2.3491 to 2.35 instead of 2.349.
* Doesn't apply "smartness" if the number precision fits already.
*
* @param {Array} data input data array
* @return {Array} output data array
*/
function strongRound(data) {
for (var i = data.length; i-- > 0;) {
if (data[i].toFixed(precision) != data[i]) {
var rounded = +data[i].toFixed(precision - 1);
data[i] = +Math.abs(rounded - data[i]).toFixed(precision + 1) >= error ?
+data[i].toFixed(precision) :
rounded;
}
}
return data;
}
/**
* Simple rounding function if precision is 0.
*
* @param {Array} data input data array
* @return {Array} output data array
*/
function round(data) {
for (var i = data.length; i-- > 0;) {
data[i] = Math.round(data[i]);
}
return data;
}
/**
* Checks if a curve is a straight line by measuring distance
* from middle points to the line formed by end points.
*
* @param {Array} xs array of curve points x-coordinates
* @param {Array} ys array of curve points y-coordinates
* @return {Boolean}
*/
function isCurveStraightLine(data) {
// Get line equation a·x + b·y + c = 0 coefficients a, b (c = 0) by start and end points.
var i = data.length - 2,
a = -data[i + 1], // y1 y2 (y1 = 0)
b = data[i], // x2 x1 (x1 = 0)
d = 1 / (a * a + b * b); // same part for all points
if (i <= 1 || !isFinite(d)) return false; // curve that ends at start point isn't the case
// Distance from point (x0, y0) to the line is sqrt((c a·x0 b·y0)² / (a² + b²))
while ((i -= 2) >= 0) {
if (Math.sqrt(Math.pow(a * data[i] + b * data[i + 1], 2) * d) > error)
return false;
}
return true;
}
/**
* Converts next curve from shorthand to full form using the current curve data.
*
* @param {Object} item curve to convert
* @param {Array} data current curve data
*/
function makeLonghand(item, data) {
switch (item.instruction) {
case 's': item.instruction = 'c'; break;
case 't': item.instruction = 'q'; break;
}
item.data.unshift(data[data.length - 2] - data[data.length - 4], data[data.length - 1] - data[data.length - 3]);
return item;
}
/**
* Returns distance between two points
*
* @param {Array} point1 first point coordinates
* @param {Array} point2 second point coordinates
* @return {Number} distance
*/
function getDistance(point1, point2) {
return Math.hypot(point1[0] - point2[0], point1[1] - point2[1]);
}
/**
* Returns coordinates of the curve point corresponding to the certain t
* a·(1 - t)³·p1 + b·(1 - t)²·t·p2 + c·(1 - t)··p3 + d··p4,
* where pN are control points and p1 is zero due to relative coordinates.
*
* @param {Array} curve array of curve points coordinates
* @param {Number} t parametric position from 0 to 1
* @return {Array} Point coordinates
*/
function getCubicBezierPoint(curve, t) {
var sqrT = t * t,
cubT = sqrT * t,
mt = 1 - t,
sqrMt = mt * mt;
return [
3 * sqrMt * t * curve[0] + 3 * mt * sqrT * curve[2] + cubT * curve[4],
3 * sqrMt * t * curve[1] + 3 * mt * sqrT * curve[3] + cubT * curve[5]
];
}
/**
* Finds circle by 3 points of the curve and checks if the curve fits the found circle.
*
* @param {Array} curve
* @return {Object|undefined} circle
*/
function findCircle(curve) {
var midPoint = getCubicBezierPoint(curve, 1/2),
m1 = [midPoint[0] / 2, midPoint[1] / 2],
m2 = [(midPoint[0] + curve[4]) / 2, (midPoint[1] + curve[5]) / 2],
center = getIntersection([
m1[0], m1[1],
m1[0] + m1[1], m1[1] - m1[0],
m2[0], m2[1],
m2[0] + (m2[1] - midPoint[1]), m2[1] - (m2[0] - midPoint[0])
]),
radius = center && getDistance([0, 0], center),
tolerance = Math.min(arcThreshold * error, arcTolerance * radius / 100);
if (center && radius < 1e15 &&
[1/4, 3/4].every(function(point) {
return Math.abs(getDistance(getCubicBezierPoint(curve, point), center) - radius) <= tolerance;
}))
return { center: center, radius: radius};
}
/**
* Checks if a curve fits the given circle.
*
* @param {Object} circle
* @param {Array} curve
* @return {Boolean}
*/
function isArc(curve, circle) {
var tolerance = Math.min(arcThreshold * error, arcTolerance * circle.radius / 100);
return [0, 1/4, 1/2, 3/4, 1].every(function(point) {
return Math.abs(getDistance(getCubicBezierPoint(curve, point), circle.center) - circle.radius) <= tolerance;
});
}
/**
* Checks if a previous curve fits the given circle.
*
* @param {Object} circle
* @param {Array} curve
* @return {Boolean}
*/
function isArcPrev(curve, circle) {
return isArc(curve, {
center: [circle.center[0] + curve[4], circle.center[1] + curve[5]],
radius: circle.radius
});
}
/**
* Finds angle of a curve fitting the given arc.
* @param {Array} curve
* @param {Object} relCircle
* @return {Number} angle
*/
function findArcAngle(curve, relCircle) {
var x1 = -relCircle.center[0],
y1 = -relCircle.center[1],
x2 = curve[4] - relCircle.center[0],
y2 = curve[5] - relCircle.center[1];
return Math.acos(
(x1 * x2 + y1 * y2) /
Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2))
);
}
/**
* Converts given path data to string.
*
* @param {Object} params
* @param {Array} pathData
* @return {String}
*/
function data2Path(params, pathData) {
return pathData.reduce(function(pathString, item) {
var strData = '';
if (item.data) {
strData = cleanupOutData(roundData(item.data.slice()), params);
}
return pathString + item.instruction + strData;
}, '');
}

View file

@ -1,149 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'converts basic shapes to more compact path form';
exports.params = {
convertArcs: false
};
var none = { value: 0 },
regNumber = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
/**
* Converts basic shape to more compact path.
* It also allows further optimizations like
* combining paths with similar attributes.
*
* @see http://www.w3.org/TR/SVG/shapes.html
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
*
* @author Lev Solntsev
*/
exports.fn = function(item, params) {
var convertArcs = params && params.convertArcs;
if (
item.isElem('rect') &&
item.hasAttr('width') &&
item.hasAttr('height') &&
!item.hasAttr('rx') &&
!item.hasAttr('ry')
) {
var x = +(item.attr('x') || none).value,
y = +(item.attr('y') || none).value,
width = +item.attr('width').value,
height = +item.attr('height').value;
// Values like '100%' compute to NaN, thus running after
// cleanupNumericValues when 'px' units has already been removed.
// TODO: Calculate sizes from % and non-px units if possible.
if (isNaN(x - y + width - height)) return;
var pathData =
'M' + x + ' ' + y +
'H' + (x + width) +
'V' + (y + height) +
'H' + x +
'z';
item.addAttr({
name: 'd',
value: pathData,
prefix: '',
local: 'd'
});
item.renameElem('path')
.removeAttr(['x', 'y', 'width', 'height']);
} else if (item.isElem('line')) {
var x1 = +(item.attr('x1') || none).value,
y1 = +(item.attr('y1') || none).value,
x2 = +(item.attr('x2') || none).value,
y2 = +(item.attr('y2') || none).value;
if (isNaN(x1 - y1 + x2 - y2)) return;
item.addAttr({
name: 'd',
value: 'M' + x1 + ' ' + y1 + 'L' + x2 + ' ' + y2,
prefix: '',
local: 'd'
});
item.renameElem('path')
.removeAttr(['x1', 'y1', 'x2', 'y2']);
} else if ((
item.isElem('polyline') ||
item.isElem('polygon')
) &&
item.hasAttr('points')
) {
var coords = (item.attr('points').value.match(regNumber) || []).map(Number);
if (coords.length < 4) return false;
item.addAttr({
name: 'd',
value: 'M' + coords.slice(0,2).join(' ') +
'L' + coords.slice(2).join(' ') +
(item.isElem('polygon') ? 'z' : ''),
prefix: '',
local: 'd'
});
item.renameElem('path')
.removeAttr('points');
} else if (item.isElem('circle') && convertArcs) {
var cx = +(item.attr('cx') || none).value;
var cy = +(item.attr('cy') || none).value;
var r = +(item.attr('r') || none).value;
if (isNaN(cx - cy + r)) {
return;
}
var cPathData =
'M' + cx + ' ' + (cy - r) +
'A' + r + ' ' + r + ' 0 1 0 ' + cx + ' ' + (cy + r) +
'A' + r + ' ' + r + ' 0 1 0 ' + cx + ' ' + (cy - r) +
'Z';
item.addAttr({
name: 'd',
value: cPathData,
prefix: '',
local: 'd',
});
item.renameElem('path').removeAttr(['cx', 'cy', 'r']);
} else if (item.isElem('ellipse') && convertArcs) {
var ecx = +(item.attr('cx') || none).value;
var ecy = +(item.attr('cy') || none).value;
var rx = +(item.attr('rx') || none).value;
var ry = +(item.attr('ry') || none).value;
if (isNaN(ecx - ecy + rx - ry)) {
return;
}
var ePathData =
'M' + ecx + ' ' + (ecy - ry) +
'A' + rx + ' ' + ry + ' 0 1 0 ' + ecx + ' ' + (ecy + ry) +
'A' + rx + ' ' + ry + ' 0 1 0 ' + ecx + ' ' + (ecy - ry) +
'Z';
item.addAttr({
name: 'd',
value: ePathData,
prefix: '',
local: 'd',
});
item.renameElem('path').removeAttr(['cx', 'cy', 'rx', 'ry']);
}
};

View file

@ -1,125 +0,0 @@
/* jshint quotmark: false */
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'converts style to attributes';
exports.params = {
keepImportant: false
};
var stylingProps = require('./_collections').attrsGroups.presentation,
rEscape = '\\\\(?:[0-9a-f]{1,6}\\s?|\\r\\n|.)', // Like \" or \2051. Code points consume one space.
rAttr = '\\s*(' + g('[^:;\\\\]', rEscape) + '*?)\\s*', // attribute name like fill
rSingleQuotes = "'(?:[^'\\n\\r\\\\]|" + rEscape + ")*?(?:'|$)", // string in single quotes: 'smth'
rQuotes = '"(?:[^"\\n\\r\\\\]|' + rEscape + ')*?(?:"|$)', // string in double quotes: "smth"
rQuotedString = new RegExp('^' + g(rSingleQuotes, rQuotes) + '$'),
// Parentheses, E.g.: url(data:image/png;base64,iVBO...).
// ':' and ';' inside of it should be threated as is. (Just like in strings.)
rParenthesis = '\\(' + g('[^\'"()\\\\]+', rEscape, rSingleQuotes, rQuotes) + '*?' + '\\)',
// The value. It can have strings and parentheses (see above). Fallbacks to anything in case of unexpected input.
rValue = '\\s*(' + g('[^!\'"();\\\\]+?', rEscape, rSingleQuotes, rQuotes, rParenthesis, '[^;]*?') + '*?' + ')',
// End of declaration. Spaces outside of capturing groups help to do natural trimming.
rDeclEnd = '\\s*(?:;\\s*|$)',
// Important rule
rImportant = '(\\s*!important(?![-(\w]))?',
// Final RegExp to parse CSS declarations.
regDeclarationBlock = new RegExp(rAttr + ':' + rValue + rImportant + rDeclEnd, 'ig'),
// Comments expression. Honors escape sequences and strings.
regStripComments = new RegExp(g(rEscape, rSingleQuotes, rQuotes, '/\\*[^]*?\\*/'), 'ig');
/**
* Convert style in attributes. Cleanups comments and illegal declarations (without colon) as a side effect.
*
* @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" style="-webkit-blah: blah">
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item, params) {
/* jshint boss: true */
if (item.elem && item.hasAttr('style')) {
// ['opacity: 1', 'color: #000']
var styleValue = item.attr('style').value,
styles = [],
attrs = {};
// Strip CSS comments preserving escape sequences and strings.
styleValue = styleValue.replace(regStripComments, function(match) {
return match[0] == '/' ? '' :
match[0] == '\\' && /[-g-z]/i.test(match[1]) ? match[1] : match;
});
regDeclarationBlock.lastIndex = 0;
for (var rule; rule = regDeclarationBlock.exec(styleValue);) {
if (!params.keepImportant || !rule[3]) {
styles.push([rule[1], rule[2]]);
}
}
if (styles.length) {
styles = styles.filter(function(style) {
if (style[0]) {
var prop = style[0].toLowerCase(),
val = style[1];
if (rQuotedString.test(val)) {
val = val.slice(1, -1);
}
if (stylingProps.indexOf(prop) > -1) {
attrs[prop] = {
name: prop,
value: val,
local: prop,
prefix: ''
};
return false;
}
}
return true;
});
Object.assign(item.attrs, attrs);
if (styles.length) {
item.attr('style').value = styles
.map(function(declaration) { return declaration.join(':') })
.join(';');
} else {
item.removeAttr('style');
}
}
}
};
function g() {
return '(?:' + Array.prototype.join.call(arguments, '|') + ')';
}

View file

@ -1,363 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'collapses multiple transformations and optimizes it';
exports.params = {
convertToShorts: true,
// degPrecision: 3, // transformPrecision (or matrix precision) - 2 by default
floatPrecision: 3,
transformPrecision: 5,
matrixToTransform: true,
shortTranslate: true,
shortScale: true,
shortRotate: true,
removeUseless: true,
collapseIntoOne: true,
leadingZero: true,
negativeExtraSpace: false
};
var cleanupOutData = require('../tools').cleanupOutData,
transform2js = require('./_transforms.js').transform2js,
transformsMultiply = require('./_transforms.js').transformsMultiply,
matrixToTransform = require('./_transforms.js').matrixToTransform,
degRound,
floatRound,
transformRound;
/**
* Convert matrices to the short aliases,
* convert long translate, scale or rotate transform notations to the shorts ones,
* convert transforms to the matrices and multiply them all into one,
* remove useless transforms.
*
* @see http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item, params) {
if (item.elem) {
// transform
if (item.hasAttr('transform')) {
convertTransform(item, 'transform', params);
}
// gradientTransform
if (item.hasAttr('gradientTransform')) {
convertTransform(item, 'gradientTransform', params);
}
// patternTransform
if (item.hasAttr('patternTransform')) {
convertTransform(item, 'patternTransform', params);
}
}
};
/**
* Main function.
*
* @param {Object} item input item
* @param {String} attrName attribute name
* @param {Object} params plugin params
*/
function convertTransform(item, attrName, params) {
var data = transform2js(item.attr(attrName).value);
params = definePrecision(data, params);
if (params.collapseIntoOne && data.length > 1) {
data = [transformsMultiply(data)];
}
if (params.convertToShorts) {
data = convertToShorts(data, params);
} else {
data.forEach(roundTransform);
}
if (params.removeUseless) {
data = removeUseless(data);
}
if (data.length) {
item.attr(attrName).value = js2transform(data, params);
} else {
item.removeAttr(attrName);
}
}
/**
* Defines precision to work with certain parts.
* transformPrecision - for scale and four first matrix parameters (needs a better precision due to multiplying),
* floatPrecision - for translate including two last matrix and rotate parameters,
* degPrecision - for rotate and skew. By default it's equal to (rougly)
* transformPrecision - 2 or floatPrecision whichever is lower. Can be set in params.
*
* @param {Array} transforms input array
* @param {Object} params plugin params
* @return {Array} output array
*/
function definePrecision(data, params) {
/* jshint validthis: true */
var matrixData = data.reduce(getMatrixData, []),
significantDigits = params.transformPrecision;
// Clone params so it don't affect other elements transformations.
params = Object.assign({}, params);
// Limit transform precision with matrix one. Calculating with larger precision doesn't add any value.
if (matrixData.length) {
params.transformPrecision = Math.min(params.transformPrecision,
Math.max.apply(Math, matrixData.map(floatDigits)) || params.transformPrecision);
significantDigits = Math.max.apply(Math, matrixData.map(function(n) {
return String(n).replace(/\D+/g, '').length; // Number of digits in a number. 123.45 → 5
}));
}
// No sense in angle precision more then number of significant digits in matrix.
if (!('degPrecision' in params)) {
params.degPrecision = Math.max(0, Math.min(params.floatPrecision, significantDigits - 2));
}
floatRound = params.floatPrecision >= 1 && params.floatPrecision < 20 ?
smartRound.bind(this, params.floatPrecision) :
round;
degRound = params.degPrecision >= 1 && params.floatPrecision < 20 ?
smartRound.bind(this, params.degPrecision) :
round;
transformRound = params.transformPrecision >= 1 && params.floatPrecision < 20 ?
smartRound.bind(this, params.transformPrecision) :
round;
return params;
}
/**
* Gathers four first matrix parameters.
*
* @param {Array} a array of data
* @param {Object} transform
* @return {Array} output array
*/
function getMatrixData(a, b) {
return b.name == 'matrix' ? a.concat(b.data.slice(0, 4)) : a;
}
/**
* Returns number of digits after the point. 0.125 3
*/
function floatDigits(n) {
return (n = String(n)).slice(n.indexOf('.')).length - 1;
}
/**
* Convert transforms to the shorthand alternatives.
*
* @param {Array} transforms input array
* @param {Object} params plugin params
* @return {Array} output array
*/
function convertToShorts(transforms, params) {
for(var i = 0; i < transforms.length; i++) {
var transform = transforms[i];
// convert matrix to the short aliases
if (
params.matrixToTransform &&
transform.name === 'matrix'
) {
var decomposed = matrixToTransform(transform, params);
if (decomposed != transform &&
js2transform(decomposed, params).length <= js2transform([transform], params).length) {
transforms.splice.apply(transforms, [i, 1].concat(decomposed));
}
transform = transforms[i];
}
// fixed-point numbers
// 12.754997 → 12.755
roundTransform(transform);
// convert long translate transform notation to the shorts one
// translate(10 0) → translate(10)
if (
params.shortTranslate &&
transform.name === 'translate' &&
transform.data.length === 2 &&
!transform.data[1]
) {
transform.data.pop();
}
// convert long scale transform notation to the shorts one
// scale(2 2) → scale(2)
if (
params.shortScale &&
transform.name === 'scale' &&
transform.data.length === 2 &&
transform.data[0] === transform.data[1]
) {
transform.data.pop();
}
// convert long rotate transform notation to the short one
// translate(cx cy) rotate(a) translate(-cx -cy) → rotate(a cx cy)
if (
params.shortRotate &&
transforms[i - 2] &&
transforms[i - 2].name === 'translate' &&
transforms[i - 1].name === 'rotate' &&
transforms[i].name === 'translate' &&
transforms[i - 2].data[0] === -transforms[i].data[0] &&
transforms[i - 2].data[1] === -transforms[i].data[1]
) {
transforms.splice(i - 2, 3, {
name: 'rotate',
data: [
transforms[i - 1].data[0],
transforms[i - 2].data[0],
transforms[i - 2].data[1]
]
});
// splice compensation
i -= 2;
transform = transforms[i];
}
}
return transforms;
}
/**
* Remove useless transforms.
*
* @param {Array} transforms input array
* @return {Array} output array
*/
function removeUseless(transforms) {
return transforms.filter(function(transform) {
// translate(0), rotate(0[, cx, cy]), skewX(0), skewY(0)
if (
['translate', 'rotate', 'skewX', 'skewY'].indexOf(transform.name) > -1 &&
(transform.data.length == 1 || transform.name == 'rotate') &&
!transform.data[0] ||
// translate(0, 0)
transform.name == 'translate' &&
!transform.data[0] &&
!transform.data[1] ||
// scale(1)
transform.name == 'scale' &&
transform.data[0] == 1 &&
(transform.data.length < 2 || transform.data[1] == 1) ||
// matrix(1 0 0 1 0 0)
transform.name == 'matrix' &&
transform.data[0] == 1 &&
transform.data[3] == 1 &&
!(transform.data[1] || transform.data[2] || transform.data[4] || transform.data[5])
) {
return false;
}
return true;
});
}
/**
* Convert transforms JS representation to string.
*
* @param {Array} transformJS JS representation array
* @param {Object} params plugin params
* @return {String} output string
*/
function js2transform(transformJS, params) {
var transformString = '';
// collect output value string
transformJS.forEach(function(transform) {
roundTransform(transform);
transformString += (transformString && ' ') + transform.name + '(' + cleanupOutData(transform.data, params) + ')';
});
return transformString;
}
function roundTransform(transform) {
switch (transform.name) {
case 'translate':
transform.data = floatRound(transform.data);
break;
case 'rotate':
transform.data = degRound(transform.data.slice(0, 1)).concat(floatRound(transform.data.slice(1)));
break;
case 'skewX':
case 'skewY':
transform.data = degRound(transform.data);
break;
case 'scale':
transform.data = transformRound(transform.data);
break;
case 'matrix':
transform.data = transformRound(transform.data.slice(0, 4)).concat(floatRound(transform.data.slice(4)));
break;
}
return transform;
}
/**
* Rounds numbers in array.
*
* @param {Array} data input data array
* @return {Array} output data array
*/
function round(data) {
return data.map(Math.round);
}
/**
* Decrease accuracy of floating-point numbers
* in transforms keeping a specified number of decimals.
* Smart rounds values like 2.349 to 2.35.
*
* @param {Number} fixed number of decimals
* @param {Array} data input data array
* @return {Array} output data array
*/
function smartRound(precision, data) {
for (var i = data.length, tolerance = +Math.pow(.1, precision).toFixed(precision); i--;) {
if (data[i].toFixed(precision) != data[i]) {
var rounded = +data[i].toFixed(precision - 1);
data[i] = +Math.abs(rounded - data[i]).toFixed(precision + 1) >= tolerance ?
+data[i].toFixed(precision) :
rounded;
}
}
return data;
}

View file

@ -1,245 +0,0 @@
'use strict';
exports.type = 'full';
exports.active = true;
exports.params = {
onlyMatchedOnce: true,
removeMatchedSelectors: true,
useMqs: ['', 'screen'],
usePseudos: ['']
};
exports.description = 'inline styles (additional options)';
var csstree = require('css-tree'),
cssTools = require('../css-tools');
/**
* Moves + merges styles from style elements to element styles
*
* Options
* onlyMatchedOnce (default: true)
* inline only selectors that match once
*
* removeMatchedSelectors (default: true)
* clean up matched selectors,
* leave selectors that hadn't matched
*
* useMqs (default: ['', 'screen'])
* what media queries to be used
* empty string element for styles outside media queries
*
* usePseudos (default: [''])
* what pseudo-classes/-elements to be used
* empty string element for all non-pseudo-classes and/or -elements
*
* @param {Object} document document element
* @param {Object} opts plugin params
*
* @author strarsis <strarsis@gmail.com>
*/
exports.fn = function(document, opts) {
// collect <style/>s
var styleEls = document.querySelectorAll('style');
//no <styles/>s, nothing to do
if (styleEls === null) {
return document;
}
var styles = [],
selectors = [];
for (var styleEl of styleEls) {
if (styleEl.isEmpty() || styleEl.closestElem('foreignObject')) {
// skip empty <style/>s or <foreignObject> content.
continue;
}
var cssStr = cssTools.getCssStr(styleEl);
// collect <style/>s and their css ast
var cssAst = {};
try {
cssAst = csstree.parse(cssStr, {
parseValue: false,
parseCustomProperty: false
});
} catch (parseError) {
// console.warn('Warning: Parse error of styles of <style/> element, skipped. Error details: ' + parseError);
continue;
}
styles.push({
styleEl: styleEl,
cssAst: cssAst
});
selectors = selectors.concat(cssTools.flattenToSelectors(cssAst));
}
// filter for mediaqueries to be used or without any mediaquery
var selectorsMq = cssTools.filterByMqs(selectors, opts.useMqs);
// filter for pseudo elements to be used
var selectorsPseudo = cssTools.filterByPseudos(selectorsMq, opts.usePseudos);
// remove PseudoClass from its SimpleSelector for proper matching
cssTools.cleanPseudos(selectorsPseudo);
// stable sort selectors
var sortedSelectors = cssTools.sortSelectors(selectorsPseudo).reverse();
var selector,
selectedEl;
// match selectors
for (selector of sortedSelectors) {
var selectorStr = csstree.generate(selector.item.data),
selectedEls = null;
try {
selectedEls = document.querySelectorAll(selectorStr);
} catch (selectError) {
if (selectError.constructor === SyntaxError) {
// console.warn('Warning: Syntax error when trying to select \n\n' + selectorStr + '\n\n, skipped. Error details: ' + selectError);
continue;
}
throw selectError;
}
if (selectedEls === null) {
// nothing selected
continue;
}
selector.selectedEls = selectedEls;
}
// apply <style/> styles to matched elements
for (selector of sortedSelectors) {
if(!selector.selectedEls) {
continue;
}
if (opts.onlyMatchedOnce && selector.selectedEls !== null && selector.selectedEls.length > 1) {
// skip selectors that match more than once if option onlyMatchedOnce is enabled
continue;
}
// apply <style/> to matched elements
for (selectedEl of selector.selectedEls) {
if (selector.rule === null) {
continue;
}
// merge declarations
csstree.walk(selector.rule, {visit: 'Declaration', enter: function(styleCsstreeDeclaration) {
// existing inline styles have higher priority
// no inline styles, external styles, external styles used
// inline styles, external styles same priority as inline styles, inline styles used
// inline styles, external styles higher priority than inline styles, external styles used
var styleDeclaration = cssTools.csstreeToStyleDeclaration(styleCsstreeDeclaration);
if (selectedEl.style.getPropertyValue(styleDeclaration.name) !== null &&
selectedEl.style.getPropertyPriority(styleDeclaration.name) >= styleDeclaration.priority) {
return;
}
selectedEl.style.setProperty(styleDeclaration.name, styleDeclaration.value, styleDeclaration.priority);
}});
}
if (opts.removeMatchedSelectors && selector.selectedEls !== null && selector.selectedEls.length > 0) {
// clean up matching simple selectors if option removeMatchedSelectors is enabled
selector.rule.prelude.children.remove(selector.item);
}
}
if (!opts.removeMatchedSelectors) {
return document; // no further processing required
}
// clean up matched class + ID attribute values
for (selector of sortedSelectors) {
if(!selector.selectedEls) {
continue;
}
if (opts.onlyMatchedOnce && selector.selectedEls !== null && selector.selectedEls.length > 1) {
// skip selectors that match more than once if option onlyMatchedOnce is enabled
continue;
}
for (selectedEl of selector.selectedEls) {
// class
var firstSubSelector = selector.item.data.children.first();
if(firstSubSelector.type === 'ClassSelector') {
selectedEl.class.remove(firstSubSelector.name);
}
// clean up now empty class attributes
if(typeof selectedEl.class.item(0) === 'undefined') {
selectedEl.removeAttr('class');
}
// ID
if(firstSubSelector.type === 'IdSelector') {
selectedEl.removeAttr('id', firstSubSelector.name);
}
}
}
// clean up now empty elements
for (var style of styles) {
csstree.walk(style.cssAst, {visit: 'Rule', enter: function(node, item, list) {
// clean up <style/> atrules without any rulesets left
if (node.type === 'Atrule' &&
// only Atrules containing rulesets
node.block !== null &&
node.block.children.isEmpty()) {
list.remove(item);
return;
}
// clean up <style/> rulesets without any css selectors left
if (node.type === 'Rule' &&
node.prelude.children.isEmpty()) {
list.remove(item);
}
}});
if (style.cssAst.children.isEmpty()) {
// clean up now emtpy <style/>s
var styleParentEl = style.styleEl.parentNode;
styleParentEl.spliceContent(styleParentEl.content.indexOf(style.styleEl), 1);
if (styleParentEl.elem === 'defs' &&
styleParentEl.content.length === 0) {
// also clean up now empty <def/>s
var defsParentEl = styleParentEl.parentNode;
defsParentEl.spliceContent(defsParentEl.content.indexOf(styleParentEl), 1);
}
continue;
}
// update existing, left over <style>s
cssTools.setCssStr(style.styleEl, csstree.generate(style.cssAst));
}
return document;
};

View file

@ -1,73 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'merges multiple paths in one if possible';
exports.params = {
collapseRepeated: true,
force: false,
leadingZero: true,
negativeExtraSpace: true,
noSpaceAfterFlags: true
};
var path2js = require('./_path.js').path2js,
js2path = require('./_path.js').js2path,
intersects = require('./_path.js').intersects;
/**
* Merge multiple Paths into one.
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich, Lev Solntsev
*/
exports.fn = function(item, params) {
if (!item.isElem() || item.isEmpty()) return;
var prevContentItem = null,
prevContentItemKeys = null;
item.content = item.content.filter(function(contentItem) {
if (prevContentItem &&
prevContentItem.isElem('path') &&
prevContentItem.isEmpty() &&
prevContentItem.hasAttr('d') &&
contentItem.isElem('path') &&
contentItem.isEmpty() &&
contentItem.hasAttr('d')
) {
if (!prevContentItemKeys) {
prevContentItemKeys = Object.keys(prevContentItem.attrs);
}
var contentItemAttrs = Object.keys(contentItem.attrs),
equalData = prevContentItemKeys.length == contentItemAttrs.length &&
contentItemAttrs.every(function(key) {
return key == 'd' ||
prevContentItem.hasAttr(key) &&
prevContentItem.attr(key).value == contentItem.attr(key).value;
}),
prevPathJS = path2js(prevContentItem),
curPathJS = path2js(contentItem);
if (equalData && (params.force || !intersects(prevPathJS, curPathJS))) {
js2path(prevContentItem, prevPathJS.concat(curPathJS), params);
return false;
}
}
prevContentItem = contentItem;
prevContentItemKeys = null;
return true;
});
};

View file

@ -1,160 +0,0 @@
'use strict';
exports.type = 'full';
exports.active = true;
exports.description = 'minifies styles and removes unused styles based on usage data';
exports.params = {
// ... CSSO options goes here
// additional
usage: {
force: false, // force to use usage data even if it unsafe (document contains <script> or on* attributes)
ids: true,
classes: true,
tags: true
}
};
var csso = require('csso');
/**
* Minifies styles (<style> element + style attribute) using CSSO
*
* @author strarsis <strarsis@gmail.com>
*/
exports.fn = function(ast, options) {
options = options || {};
var minifyOptionsForStylesheet = cloneObject(options);
var minifyOptionsForAttribute = cloneObject(options);
var elems = findStyleElems(ast);
minifyOptionsForStylesheet.usage = collectUsageData(ast, options);
minifyOptionsForAttribute.usage = null;
elems.forEach(function(elem) {
if (elem.isElem('style')) {
// <style> element
var styleCss = elem.content[0].text || elem.content[0].cdata || [];
var DATA = styleCss.indexOf('>') >= 0 || styleCss.indexOf('<') >= 0 ? 'cdata' : 'text';
elem.content[0][DATA] = csso.minify(styleCss, minifyOptionsForStylesheet).css;
} else {
// style attribute
var elemStyle = elem.attr('style').value;
elem.attr('style').value = csso.minifyBlock(elemStyle, minifyOptionsForAttribute).css;
}
});
return ast;
};
function cloneObject(obj) {
var result = {};
for (var key in obj) {
result[key] = obj[key];
}
return result;
}
function findStyleElems(ast) {
function walk(items, styles) {
for (var i = 0; i < items.content.length; i++) {
var item = items.content[i];
// go deeper
if (item.content) {
walk(item, styles);
}
if (item.isElem('style') && !item.isEmpty()) {
styles.push(item);
} else if (item.isElem() && item.hasAttr('style')) {
styles.push(item);
}
}
return styles;
}
return walk(ast, []);
}
function shouldFilter(options, name) {
if ('usage' in options === false) {
return true;
}
if (options.usage && name in options.usage === false) {
return true;
}
return Boolean(options.usage && options.usage[name]);
}
function collectUsageData(ast, options) {
function walk(items, usageData) {
for (var i = 0; i < items.content.length; i++) {
var item = items.content[i];
// go deeper
if (item.content) {
walk(item, usageData);
}
if (item.isElem('script')) {
safe = false;
}
if (item.isElem()) {
usageData.tags[item.elem] = true;
if (item.hasAttr('id')) {
usageData.ids[item.attr('id').value] = true;
}
if (item.hasAttr('class')) {
item.attr('class').value.replace(/^\s+|\s+$/g, '').split(/\s+/).forEach(function(className) {
usageData.classes[className] = true;
});
}
if (item.attrs && Object.keys(item.attrs).some(function(name) { return /^on/i.test(name); })) {
safe = false;
}
}
}
return usageData;
}
var safe = true;
var usageData = {};
var hasData = false;
var rawData = walk(ast, {
ids: Object.create(null),
classes: Object.create(null),
tags: Object.create(null)
});
if (!safe && options.usage && options.usage.force) {
safe = true;
}
for (var key in rawData) {
if (shouldFilter(options, key)) {
usageData[key] = Object.keys(rawData[key]);
hasData = true;
}
}
return safe && hasData ? usageData : null;
}

View file

@ -1,126 +0,0 @@
'use strict';
exports.type = 'perItemReverse';
exports.active = true;
exports.description = 'moves elements attributes to the existing group wrapper';
var inheritableAttrs = require('./_collections').inheritableAttrs,
pathElems = require('./_collections.js').pathElems;
/**
* Collapse content's intersected and inheritable
* 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
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item) {
if (item.isElem('g') && !item.isEmpty() && item.content.length > 1) {
var intersection = {},
hasTransform = false,
hasClip = item.hasAttr('clip-path') || item.hasAttr('mask'),
intersected = item.content.every(function(inner) {
if (inner.isElem() && inner.hasAttr()) {
// don't mess with possible styles (hack until CSS parsing is implemented)
if (inner.hasAttr('class')) return false;
if (!Object.keys(intersection).length) {
intersection = inner.attrs;
} else {
intersection = intersectInheritableAttrs(intersection, inner.attrs);
if (!intersection) return false;
}
return true;
}
}),
allPath = item.content.every(function(inner) {
return inner.isElem(pathElems);
});
if (intersected) {
item.content.forEach(function(g) {
for (var name in intersection) {
if (!allPath && !hasClip || name !== 'transform') {
g.removeAttr(name);
if (name === 'transform') {
if (!hasTransform) {
if (item.hasAttr('transform')) {
item.attr('transform').value += ' ' + intersection[name].value;
} else {
item.addAttr(intersection[name]);
}
hasTransform = true;
}
} else {
item.addAttr(intersection[name]);
}
}
}
});
}
}
};
/**
* Intersect inheritable attributes.
*
* @param {Object} a first attrs object
* @param {Object} b second attrs object
*
* @return {Object} intersected attrs object
*/
function intersectInheritableAttrs(a, b) {
var c = {};
for (var n in a) {
if (
b.hasOwnProperty(n) &&
inheritableAttrs.indexOf(n) > -1 &&
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];
}
}
if (!Object.keys(c).length) return false;
return c;
}

View file

@ -1,64 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'moves some group attributes to the content elements';
var collections = require('./_collections.js'),
pathElems = collections.pathElems.concat(['g', 'text']),
referencesProps = collections.referencesProps,
inheritableAttrs = collections.inheritableAttrs;
/**
* Move group attrs to the content elements.
*
* @example
* <g transform="scale(2)">
* <path transform="rotate(45)" d="M0,0 L10,20"/>
* <path transform="translate(10, 20)" d="M0,10 L20,30"/>
* </g>
*
* <g>
* <path transform="scale(2) rotate(45)" d="M0,0 L10,20"/>
* <path transform="scale(2) translate(10, 20)" d="M0,10 L20,30"/>
* </g>
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item) {
if (item.isElem('g') && !item.isEmpty()) {
inheritableAttrs.forEach(function(currentAttr) {
if (item.hasAttr(currentAttr)) {
var attr = item.attr(currentAttr);
item.content.forEach(function(inner) {
if (currentAttr === 'transform' && inner.hasAttr(currentAttr)) {
// if attr is transform and the inner has transform we concatenate it
inner.attr(currentAttr).value = attr.value + ' ' + inner.attr(currentAttr).value;
} else if (!inner.hasAttr(currentAttr)){
// If the inner has the attr already we don't override it
inner.addAttr({
...attr
});
}
});
item.removeAttr(currentAttr);
}
});
}
};

View file

@ -1,274 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = false;
exports.params = {
delim: '__',
prefixIds: true,
prefixClassNames: true,
};
exports.description = 'prefix IDs';
var path = require('path'),
csstree = require('css-tree'),
unquote = require('unquote'),
collections = require('./_collections.js'),
referencesProps = collections.referencesProps,
rxId = /^#(.*)$/, // regular expression for matching an ID + extracing its name
addPrefix = null;
// Escapes a string for being used as ID
var escapeIdentifierName = function(str) {
return str.replace(/[\. ]/g, '_');
};
// Matches an #ID value, captures the ID name
var matchId = function(urlVal) {
var idUrlMatches = urlVal.match(rxId);
if (idUrlMatches === null) {
return false;
}
return idUrlMatches[1];
};
// Matches an url(...) value, captures the URL
var matchUrl = function(val) {
var urlMatches = /url\((.*?)\)/gi.exec(val);
if (urlMatches === null) {
return false;
}
return urlMatches[1];
};
// Checks if attribute is empty
var attrNotEmpty = function(attr) {
return (attr && attr.value && attr.value.length > 0);
};
// prefixes an #ID
var prefixId = function(val) {
var idName = matchId(val);
if (!idName) {
return false;
}
return '#' + addPrefix(idName);
};
// attr.value helper methods
// prefixes a class attribute value
var addPrefixToClassAttr = function(attr) {
if (!attrNotEmpty(attr)) {
return;
}
attr.value = attr.value.split(/\s+/).map(addPrefix).join(' ');
};
// prefixes an ID attribute value
var addPrefixToIdAttr = function(attr) {
if (!attrNotEmpty(attr)) {
return;
}
attr.value = addPrefix(attr.value);
};
// prefixes a href attribute value
var addPrefixToHrefAttr = function(attr) {
if (!attrNotEmpty(attr)) {
return;
}
var idPrefixed = prefixId(attr.value);
if (!idPrefixed) {
return;
}
attr.value = idPrefixed;
};
// prefixes an URL attribute value
var addPrefixToUrlAttr = function(attr) {
if (!attrNotEmpty(attr)) {
return;
}
// url(...) in value
var urlVal = matchUrl(attr.value);
if (!urlVal) {
return;
}
var idPrefixed = prefixId(urlVal);
if (!idPrefixed) {
return;
}
attr.value = 'url(' + idPrefixed + ')';
};
// prefixes begin/end attribute value
var addPrefixToBeginEndAttr = function(attr) {
if (!attrNotEmpty(attr)) {
return;
}
var parts = attr.value.split('; ').map(function(val) {
val = val.trim();
if (val.endsWith('.end') || val.endsWith('.start')) {
var idPostfix = val.split('.'),
id = idPostfix[0],
postfix = idPostfix[1];
var idPrefixed = prefixId(`#${id}`);
if (!idPrefixed) {
return val;
}
idPrefixed = idPrefixed.slice(1);
return `${idPrefixed}.${postfix}`;
} else {
return val;
}
});
attr.value = parts.join('; ');
};
/**
* Prefixes identifiers
*
* @param {Object} node node
* @param {Object} opts plugin params
* @param {Object} extra plugin extra information
*
* @author strarsis <strarsis@gmail.com>
*/
exports.fn = function(node, opts, extra) {
// skip subsequent passes when multipass is used
if(extra.multipassCount && extra.multipassCount > 0) {
return node;
}
// prefix, from file name or option
var prefix = 'prefix';
if (opts.prefix) {
if (typeof opts.prefix === 'function') {
prefix = opts.prefix(node, extra);
} else {
prefix = opts.prefix;
}
} else if (opts.prefix === false) {
prefix = false;
} else if (extra && extra.path && extra.path.length > 0) {
var filename = path.basename(extra.path);
prefix = filename;
}
// prefixes a normal value
addPrefix = function(name) {
if(prefix === false){
return escapeIdentifierName(name);
}
return escapeIdentifierName(prefix + opts.delim + name);
};
// <style/> property values
if (node.elem === 'style') {
if (node.isEmpty()) {
// skip empty <style/>s
return node;
}
var cssStr = node.content[0].text || node.content[0].cdata || [];
var cssAst = {};
try {
cssAst = csstree.parse(cssStr, {
parseValue: true,
parseCustomProperty: false
});
} catch (parseError) {
console.warn('Warning: Parse error of styles of <style/> element, skipped. Error details: ' + parseError);
return node;
}
var idPrefixed = '';
csstree.walk(cssAst, function(node) {
// #ID, .class
if (((opts.prefixIds && node.type === 'IdSelector') ||
(opts.prefixClassNames && node.type === 'ClassSelector')) &&
node.name) {
node.name = addPrefix(node.name);
return;
}
// url(...) in value
if (node.type === 'Url' &&
node.value.value && node.value.value.length > 0) {
idPrefixed = prefixId(unquote(node.value.value));
if (!idPrefixed) {
return;
}
node.value.value = idPrefixed;
}
});
// update <style>s
node.content[0].text = csstree.generate(cssAst);
return node;
}
// element attributes
if (!node.attrs) {
return node;
}
// Nodes
if(opts.prefixIds) {
// ID
addPrefixToIdAttr(node.attrs.id);
}
if(opts.prefixClassNames) {
// Class
addPrefixToClassAttr(node.attrs.class);
}
// References
// href
addPrefixToHrefAttr(node.attrs.href);
// (xlink:)href (deprecated, must be still supported)
addPrefixToHrefAttr(node.attrs['xlink:href']);
// (referenceable) properties
for (var referencesProp of referencesProps) {
addPrefixToUrlAttr(node.attrs[referencesProp]);
}
addPrefixToBeginEndAttr(node.attrs.begin);
addPrefixToBeginEndAttr(node.attrs.end);
return node;
};

View file

@ -1,70 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = false;
exports.description = 'removes attributes of elements that match a css selector';
/**
* Removes attributes of elements that match a css selector.
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
*
* @example
* <caption>A selector removing a single attribute</caption>
* plugins:
* - removeAttributesBySelector:
* selector: "[fill='#00ff00']"
* attributes: "fill"
*
* <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/>
*
* <rect x="0" y="0" width="100" height="100" stroke="#00ff00"/>
*
* <caption>A selector removing multiple attributes</caption>
* plugins:
* - removeAttributesBySelector:
* selector: "[fill='#00ff00']"
* attributes:
* - fill
* - stroke
*
* <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/>
*
* <rect x="0" y="0" width="100" height="100"/>
*
* <caption>Multiple selectors removing attributes</caption>
* plugins:
* - removeAttributesBySelector:
* selectors:
* - selector: "[fill='#00ff00']"
* attributes: "fill"
*
* - selector: "#remove"
* attributes:
* - stroke
* - id
*
* <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/>
*
* <rect x="0" y="0" width="100" height="100"/>
*
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|MDN CSS Selectors}
*
* @author Bradley Mease
*/
exports.fn = function(item, params) {
var selectors = Array.isArray(params.selectors) ? params.selectors : [params];
selectors.map(function(i) {
if (item.matches(i.selector)) {
item.removeAttr(i.attributes);
}
});
};

View file

@ -1,150 +0,0 @@
'use strict';
var DEFAULT_SEPARATOR = ':';
exports.type = 'perItem';
exports.active = false;
exports.description = 'removes specified attributes';
exports.params = {
elemSeparator: DEFAULT_SEPARATOR,
preserveCurrentColor: false,
attrs: []
};
/**
* Remove attributes
*
* @param elemSeparator
* format: string
*
* @param preserveCurrentColor
* format: boolean
*
* @param attrs:
*
* format: [ element* : attribute* : value* ]
*
* element : regexp (wrapped into ^...$), single * or omitted > all elements (must be present when value is used)
* attribute : regexp (wrapped into ^...$)
* value : regexp (wrapped into ^...$), single * or omitted > all values
*
* examples:
*
* > basic: remove fill attribute
* ---
* removeAttrs:
* attrs: 'fill'
*
* > remove fill attribute on path element
* ---
* attrs: 'path:fill'
*
* > remove fill attribute on path element where value is none
* ---
* attrs: 'path:fill:none'
*
*
* > remove all fill and stroke attribute
* ---
* attrs:
* - 'fill'
* - 'stroke'
*
* [is same as]
*
* attrs: '(fill|stroke)'
*
* [is same as]
*
* attrs: '*:(fill|stroke)'
*
* [is same as]
*
* attrs: '.*:(fill|stroke)'
*
* [is same as]
*
* attrs: '.*:(fill|stroke):.*'
*
*
* > remove all stroke related attributes
* ----
* attrs: 'stroke.*'
*
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
*
* @author Benny Schudel
*/
exports.fn = function(item, params) {
// wrap into an array if params is not
if (!Array.isArray(params.attrs)) {
params.attrs = [params.attrs];
}
if (item.isElem()) {
var elemSeparator = typeof params.elemSeparator == 'string' ? params.elemSeparator : DEFAULT_SEPARATOR;
var preserveCurrentColor = typeof params.preserveCurrentColor == 'boolean' ? params.preserveCurrentColor : false;
// prepare patterns
var patterns = params.attrs.map(function(pattern) {
// if no element separators (:), assume it's attribute name, and apply to all elements *regardless of value*
if (pattern.indexOf(elemSeparator) === -1) {
pattern = ['.*', elemSeparator, pattern, elemSeparator, '.*'].join('');
// if only 1 separator, assume it's element and attribute name, and apply regardless of attribute value
} else if (pattern.split(elemSeparator).length < 3) {
pattern = [pattern, elemSeparator, '.*'].join('');
}
// create regexps for element, attribute name, and attribute value
return pattern.split(elemSeparator)
.map(function(value) {
// adjust single * to match anything
if (value === '*') { value = '.*'; }
return new RegExp(['^', value, '$'].join(''), 'i');
});
});
// loop patterns
patterns.forEach(function(pattern) {
// matches element
if (pattern[0].test(item.elem)) {
// loop attributes
item.eachAttr(function(attr) {
var name = attr.name;
var value = attr.value;
var isFillCurrentColor = preserveCurrentColor && name == 'fill' && value == 'currentColor';
var isStrokeCurrentColor = preserveCurrentColor && name == 'stroke' && value == 'currentColor';
if (!(isFillCurrentColor || isStrokeCurrentColor)) {
// matches attribute name
if (pattern[1].test(name)) {
// matches attribute value
if (pattern[2].test(attr.value)) {
item.removeAttr(name);
}
}
}
});
}
});
}
};

View file

@ -1,27 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes comments';
/**
* 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
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item) {
if (item.comment && item.comment.charAt(0) !== '!') {
return false;
}
};

View file

@ -1,32 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.params = {
removeAny: true
};
exports.description = 'removes <desc>';
var standardDescs = /^(Created with|Created using)/;
/**
* Removes <desc>.
* Removes only standard editors content or empty elements 'cause it can be used for accessibility.
* Enable parameter 'removeAny' to remove any description.
*
* https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Daniel Wabyick
*/
exports.fn = function(item, params) {
return !item.isElem('desc') || !(params.removeAny || item.isEmpty() ||
standardDescs.test(item.content[0].text));
};

View file

@ -1,49 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = false;
exports.description = 'removes width and height in presence of viewBox (opposite to removeViewBox, disable it first)';
/**
* Remove width/height attributes and add the viewBox attribute if it's missing
*
* @example
* <svg width="100" height="50" />
*
* <svg viewBox="0 0 100 50" />
*
* @param {Object} item current iteration item
* @return {Boolean} if true, with and height will be filtered out
*
* @author Benny Schudel
*/
exports.fn = function(item) {
if (item.isElem('svg')) {
if (item.hasAttr('viewBox')) {
item.removeAttr('width');
item.removeAttr('height');
} else if (
item.hasAttr('width') &&
item.hasAttr('height') &&
!isNaN(Number(item.attr('width').value)) &&
!isNaN(Number(item.attr('height').value))
) {
item.addAttr({
name: 'viewBox',
value:
'0 0 ' +
Number(item.attr('width').value) +
' ' +
Number(item.attr('height').value),
prefix: '',
local: 'viewBox'
});
item.removeAttr('width');
item.removeAttr('height');
}
}
};

View file

@ -1,40 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes doctype declaration';
/**
* 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">
*
* @example
* <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
* "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" [
* <!-- an internal subset can be embedded here -->
* ]>
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item) {
if (item.doctype) {
return false;
}
};

View file

@ -1,65 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes editors namespaces, elements and attributes';
var editorNamespaces = require('./_collections').editorNamespaces,
prefixes = [];
exports.params = {
additionalNamespaces: []
};
/**
* 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.fn = function(item, params) {
if (Array.isArray(params.additionalNamespaces)) {
editorNamespaces = editorNamespaces.concat(params.additionalNamespaces);
}
if (item.elem) {
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:*="">
item.eachAttr(function(attr) {
if (prefixes.indexOf(attr.prefix) > -1) {
item.removeAttr(attr.name);
}
});
// <sodipodi:*>
if (prefixes.indexOf(item.prefix) > -1) {
return false;
}
}
};

View file

@ -1,80 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = false;
exports.description = 'removes arbitrary elements by ID or className (disabled by default)';
exports.params = {
id: [],
class: []
};
/**
* Remove arbitrary SVG elements by ID or className.
*
* @param id
* examples:
*
* > single: remove element with ID of `elementID`
* ---
* removeElementsByAttr:
* id: 'elementID'
*
* > list: remove multiple elements by ID
* ---
* removeElementsByAttr:
* id:
* - 'elementID'
* - 'anotherID'
*
* @param class
* examples:
*
* > single: remove all elements with class of `elementClass`
* ---
* removeElementsByAttr:
* class: 'elementClass'
*
* > list: remove all elements with class of `elementClass` or `anotherClass`
* ---
* removeElementsByAttr:
* class:
* - 'elementClass'
* - 'anotherClass'
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
*
* @author Eli Dupuis (@elidupuis)
*/
exports.fn = function(item, params) {
var elemId, elemClass;
// wrap params in an array if not already
['id', 'class'].forEach(function(key) {
if (!Array.isArray(params[key])) {
params[key] = [ params[key] ];
}
});
// abort if current item is no an element
if (!item.isElem()) {
return;
}
// remove element if it's `id` matches configured `id` params
elemId = item.attr('id');
if (elemId) {
return params.id.indexOf(elemId.value) === -1;
}
// remove element if it's `class` contains any of the configured `class` params
elemClass = item.attr('class');
if (elemClass) {
var hasClassRegex = new RegExp(params.class.join('|'));
return !hasClassRegex.test(elemClass.value);
}
};

View file

@ -1,29 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes empty attributes';
/**
* Remove attributes with empty values.
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item) {
if (item.elem) {
item.eachAttr(function(attr) {
if (attr.value === '') {
item.removeAttr(attr.name);
}
});
}
};

View file

@ -1,32 +0,0 @@
'use strict';
exports.type = 'perItemReverse';
exports.active = true;
exports.description = 'removes empty container elements';
var container = require('./_collections').elemsGroups.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
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item) {
return !(item.isElem(container) && !item.isElem('svg') && item.isEmpty() &&
(!item.isElem('pattern') || !item.hasAttrLocal('href')));
};

View file

@ -1,59 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes empty <text> elements';
exports.params = {
text: true,
tspan: true,
tref: true
};
/**
* 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.fn = 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.hasAttrLocal('href')
) return false;
};

View file

@ -1,225 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes hidden elements (zero sized, with absent attributes)';
exports.params = {
isHidden: true,
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
};
var regValidPath = /M\s*(?:[-+]?(?:\d*\.\d+|\d+(?:\.|(?!\.)))([eE][-+]?\d+)?(?!\d)\s*,?\s*){2}\D*\d/i;
/**
* 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.fn = function (item, params) {
if (item.elem) {
// Removes hidden elements
// https://www.w3schools.com/cssref/pr_class_visibility.asp
if (
params.isHidden &&
item.hasAttr('visibility', 'hidden')
) return false;
// display="none"
//
// 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.isEmpty() &&
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.isEmpty() &&
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.isEmpty() &&
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.isEmpty() &&
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.isEmpty() &&
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') || !regValidPath.test(item.attr('d').value))
) 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;
}
};

View file

@ -1,23 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes <metadata>';
/**
* Remove <metadata>.
*
* http://www.w3.org/TR/SVG/metadata.html
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item) {
return !item.isElem('metadata');
};

View file

@ -1,37 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes non-inheritable groups presentational attributes';
var inheritableAttrs = require('./_collections').inheritableAttrs,
attrsGroups = require('./_collections').attrsGroups,
applyGroups = require('./_collections').presentationNonInheritableGroupAttrs;
/**
* Remove non-inheritable group's "presentation" attributes.
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item) {
if (item.isElem('g')) {
item.eachAttr(function(attr) {
if (
~attrsGroups.presentation.indexOf(attr.name) &&
!~inheritableAttrs.indexOf(attr.name) &&
!~applyGroups.indexOf(attr.name)
) {
item.removeAttr(attr.name);
}
});
}
};

View file

@ -1,134 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = false;
exports.description = 'removes elements that are drawn outside of the viewbox (disabled by default)';
var tools = require("../tools"),
_path = require('./_path.js'),
intersects = _path.intersects,
path2js = _path.path2js,
viewBox,
viewBoxJS;
/**
* Remove elements that are drawn outside of the viewbox.
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author JoshyPHP
*/
exports.fn = function(item) {
if (item.isElem('path') && item.hasAttr('d') && typeof viewBox !== 'undefined')
{
// Consider that any item with a transform attribute or a M instruction
// within the viewBox is visible
if (hasTransform(item) || pathMovesWithinViewBox(item.attr('d').value))
{
return true;
}
var pathJS = path2js(item);
if (pathJS.length === 2)
{
// Use a closed clone of the path if it's too short for intersects()
pathJS = JSON.parse(JSON.stringify(pathJS));
pathJS.push({ instruction: 'z' });
}
return intersects(viewBoxJS, pathJS);
}
if (item.isElem('svg'))
{
parseViewBox(item);
}
return true;
};
/**
* Test whether given item or any of its ancestors has a transform attribute.
*
* @param {String} path
* @return {Boolean}
*/
function hasTransform(item)
{
return item.hasAttr('transform') || (item.parentNode && hasTransform(item.parentNode));
}
/**
* Parse the viewBox coordinates and compute the JS representation of its path.
*
* @param {Object} svg svg element item
*/
function parseViewBox(svg)
{
var viewBoxData = '';
if (svg.hasAttr('viewBox'))
{
// Remove commas and plus signs, normalize and trim whitespace
viewBoxData = svg.attr('viewBox').value;
}
else if (svg.hasAttr('height') && svg.hasAttr('width'))
{
viewBoxData = '0 0 ' + svg.attr('width').value + ' ' + svg.attr('height').value;
}
// Remove commas and plus signs, normalize and trim whitespace
viewBoxData = viewBoxData.replace(/[,+]|px/g, ' ').replace(/\s+/g, ' ').replace(/^\s*|\s*$/g, '');
// Ensure that the dimensions are 4 values separated by space
var m = /^(-?\d*\.?\d+) (-?\d*\.?\d+) (\d*\.?\d+) (\d*\.?\d+)$/.exec(viewBoxData);
if (!m)
{
return;
}
// Store the viewBox boundaries
viewBox = {
left: parseFloat(m[1]),
top: parseFloat(m[2]),
right: parseFloat(m[1]) + parseFloat(m[3]),
bottom: parseFloat(m[2]) + parseFloat(m[4])
};
var path = tools.createContentItem({
elem: 'path',
prefix: '',
local: 'path'
});
path.addAttr({
name: 'd',
prefix: '',
local: 'd',
value: 'M' + m[1] + ' ' + m[2] + 'h' + m[3] + 'v' + m[4] + 'H' + m[1] + 'z'
});
viewBoxJS = path2js(path);
}
/**
* Test whether given path has a M instruction with coordinates within the viewBox.
*
* @param {String} path
* @return {Boolean}
*/
function pathMovesWithinViewBox(path)
{
var regexp = /M\s*(-?\d*\.?\d+)(?!\d)\s*(-?\d*\.?\d+)/g, m;
while (null !== (m = regexp.exec(path)))
{
if (m[1] >= viewBox.left && m[1] <= viewBox.right && m[2] >= viewBox.top && m[2] <= viewBox.bottom)
{
return true;
}
}
return false;
}

View file

@ -1,28 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = false;
exports.description = 'removes raster images (disabled by default)';
/**
* Remove raster images references in <image>.
*
* @see https://bugs.webkit.org/show_bug.cgi?id=63548
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item) {
if (
item.isElem('image') &&
item.hasAttrLocal('href', /(\.|image\/)(jpg|png|gif)/)
) {
return false;
}
};

View file

@ -1,23 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = false;
exports.description = 'removes <script> elements (disabled by default)';
/**
* Remove <script>.
*
* https://www.w3.org/TR/SVG/script.html
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Patrick Klingemann
*/
exports.fn = function(item) {
return !item.isElem('script');
};

View file

@ -1,23 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = false;
exports.description = 'removes <style> element (disabled by default)';
/**
* Remove <style>.
*
* http://www.w3.org/TR/SVG/styling.html#StyleElement
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Betsy Dupuis
*/
exports.fn = function(item) {
return !item.isElem('style');
};

View file

@ -1,23 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes <title>';
/**
* Remove <title>.
*
* https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Igor Kalashnikov
*/
exports.fn = function(item) {
return !item.isElem('title');
};

View file

@ -1,150 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes unknown elements content and attributes, removes attrs with default values';
exports.params = {
unknownContent: true,
unknownAttrs: true,
defaultAttrs: true,
uselessOverrides: true,
keepDataAttrs: true,
keepAriaAttrs: true,
keepRoleAttr: false
};
var collections = require('./_collections'),
elems = collections.elems,
attrsGroups = collections.attrsGroups,
elemsGroups = collections.elemsGroups,
attrsGroupsDefaults = collections.attrsGroupsDefaults,
attrsInheritable = collections.inheritableAttrs,
applyGroups = collections.presentationNonInheritableGroupAttrs;
// collect and extend all references
for (var elem in elems) {
elem = elems[elem];
if (elem.attrsGroups) {
elem.attrs = elem.attrs || [];
elem.attrsGroups.forEach(function(attrsGroupName) {
elem.attrs = elem.attrs.concat(attrsGroups[attrsGroupName]);
var groupDefaults = attrsGroupsDefaults[attrsGroupName];
if (groupDefaults) {
elem.defaults = elem.defaults || {};
for (var attrName in groupDefaults) {
elem.defaults[attrName] = groupDefaults[attrName];
}
}
});
}
if (elem.contentGroups) {
elem.content = elem.content || [];
elem.contentGroups.forEach(function(contentGroupName) {
elem.content = elem.content.concat(elemsGroups[contentGroupName]);
});
}
}
/**
* Remove unknown elements content and attributes,
* remove attributes with default 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.fn = function(item, params) {
// elems w/o namespace prefix
if (item.isElem() && !item.prefix) {
var elem = item.elem;
// remove unknown element's content
if (
params.unknownContent &&
!item.isEmpty() &&
elems[elem] && // make sure we know of this element before checking its children
elem !== 'foreignObject' // Don't check foreignObject
) {
item.content.forEach(function(content, i) {
if (
content.isElem() &&
!content.prefix &&
(
(
elems[elem].content && // Do we have a record of its permitted content?
elems[elem].content.indexOf(content.elem) === -1
) ||
(
!elems[elem].content && // we dont know about its permitted content
!elems[content.elem] // check that we know about the element at all
)
)
) {
item.content.splice(i, 1);
}
});
}
// remove element's unknown attrs and attrs with default values
if (elems[elem] && elems[elem].attrs) {
item.eachAttr(function(attr) {
if (
attr.name !== 'xmlns' &&
(attr.prefix === 'xml' || !attr.prefix) &&
(!params.keepDataAttrs || attr.name.indexOf('data-') != 0) &&
(!params.keepAriaAttrs || attr.name.indexOf('aria-') != 0) &&
(!params.keepRoleAttr || attr.name != 'role')
) {
if (
// unknown attrs
(
params.unknownAttrs &&
elems[elem].attrs.indexOf(attr.name) === -1
) ||
// attrs with default values
(
params.defaultAttrs &&
!item.hasAttr('id') &&
elems[elem].defaults &&
elems[elem].defaults[attr.name] === attr.value && (
attrsInheritable.indexOf(attr.name) < 0 ||
!item.parentNode.computedAttr(attr.name)
)
) ||
// useless overrides
(
params.uselessOverrides &&
!item.hasAttr('id') &&
applyGroups.indexOf(attr.name) < 0 &&
attrsInheritable.indexOf(attr.name) > -1 &&
item.parentNode.computedAttr(attr.name, attr.value)
)
) {
item.removeAttr(attr.name);
}
}
});
}
}
};

View file

@ -1,109 +0,0 @@
'use strict';
exports.type = 'full';
exports.active = true;
exports.description = 'removes unused namespaces declaration';
/**
* Remove unused namespaces declaration.
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(data) {
var svgElem,
xmlnsCollection = [];
/**
* Remove namespace from collection.
*
* @param {String} ns namescape name
*/
function removeNSfromCollection(ns) {
var pos = xmlnsCollection.indexOf(ns);
// if found - remove ns from the namespaces collection
if (pos > -1) {
xmlnsCollection.splice(pos, 1);
}
}
/**
* Bananas!
*
* @param {Array} items input items
*
* @return {Array} output items
*/
function monkeys(items) {
var i = 0,
length = items.content.length;
while(i < length) {
var item = items.content[i];
if (item.isElem('svg')) {
item.eachAttr(function(attr) {
// collect namespaces
if (attr.prefix === 'xmlns' && attr.local) {
xmlnsCollection.push(attr.local);
}
});
// if svg element has ns-attr
if (xmlnsCollection.length) {
// save svg element
svgElem = item;
}
}
if (xmlnsCollection.length) {
// check item for the ns-attrs
if (item.prefix) {
removeNSfromCollection(item.prefix);
}
// check each attr for the ns-attrs
item.eachAttr(function(attr) {
removeNSfromCollection(attr.prefix);
});
}
// if nothing is found - go deeper
if (xmlnsCollection.length && item.content) {
monkeys(item);
}
i++;
}
return items;
}
data = monkeys(data);
// remove svg element ns-attributes if they are not used even once
if (xmlnsCollection.length) {
xmlnsCollection.forEach(function(name) {
svgElem.removeAttr('xmlns:' + name);
});
}
return data;
};

View file

@ -1,53 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes elements in <defs> without id';
var nonRendering = require('./_collections').elemsGroups.nonRendering;
/**
* Removes content of defs and properties that aren't rendered directly without ids.
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Lev Solntsev
*/
exports.fn = function(item) {
if (item.isElem('defs')) {
if (item.content) {
item.content = getUsefulItems(item, []);
}
if (item.isEmpty()) return false;
} else if (item.isElem(nonRendering) && !item.hasAttr('id')) {
return false;
}
};
function getUsefulItems(item, usefulItems) {
item.content.forEach(function(child) {
if (child.hasAttr('id') || child.isElem('style')) {
usefulItems.push(child);
child.parentNode = item;
} else if (!child.isEmpty()) {
child.content = getUsefulItems(child, usefulItems);
}
});
return usefulItems;
}

View file

@ -1,100 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes useless stroke and fill attributes';
exports.params = {
stroke: true,
fill: true,
removeNone: false,
hasStyleOrScript: false
};
var shape = require('./_collections').elemsGroups.shape,
regStrokeProps = /^stroke/,
regFillProps = /^fill-/,
styleOrScript = ['style', 'script'];
/**
* Remove useless stroke and fill attrs.
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item, params) {
if (item.isElem(styleOrScript)) {
params.hasStyleOrScript = true;
}
if (!params.hasStyleOrScript && item.isElem(shape) && !item.computedAttr('id')) {
var stroke = params.stroke && item.computedAttr('stroke'),
fill = params.fill && !item.computedAttr('fill', 'none');
// remove stroke*
if (
params.stroke &&
(!stroke ||
stroke == 'none' ||
item.computedAttr('stroke-opacity', '0') ||
item.computedAttr('stroke-width', '0')
)
) {
var parentStroke = item.parentNode.computedAttr('stroke'),
declineStroke = parentStroke && parentStroke != 'none';
item.eachAttr(function(attr) {
if (regStrokeProps.test(attr.name)) {
item.removeAttr(attr.name);
}
});
if (declineStroke) item.addAttr({
name: 'stroke',
value: 'none',
prefix: '',
local: 'stroke'
});
}
// remove fill*
if (
params.fill &&
(!fill || item.computedAttr('fill-opacity', '0'))
) {
item.eachAttr(function(attr) {
if (regFillProps.test(attr.name)) {
item.removeAttr(attr.name);
}
});
if (fill) {
if (item.hasAttr('fill'))
item.attr('fill').value = 'none';
else
item.addAttr({
name: 'fill',
value: 'none',
prefix: '',
local: 'fill'
});
}
}
if (params.removeNone &&
(!stroke || item.hasAttr('stroke') && item.attr('stroke').value=='none') &&
(!fill || item.hasAttr('fill') && item.attr('fill').value=='none')) {
return false;
}
}
};

View file

@ -1,48 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes viewBox attribute when possible';
var viewBoxElems = ['svg', 'pattern', 'symbol'];
/**
* Remove viewBox attr which coincides with a width/height box.
*
* @see http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute
*
* @example
* <svg width="100" height="50" viewBox="0 0 100 50">
*
* <svg width="100" height="50">
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item) {
if (
item.isElem(viewBoxElems) &&
item.hasAttr('viewBox') &&
item.hasAttr('width') &&
item.hasAttr('height')
) {
var nums = item.attr('viewBox').value.split(/[ ,]+/g);
if (
nums[0] === '0' &&
nums[1] === '0' &&
item.attr('width').value.replace(/px$/, '') === nums[2] && // could use parseFloat too
item.attr('height').value.replace(/px$/, '') === nums[3]
) {
item.removeAttr('viewBox');
}
}
};

View file

@ -1,28 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = false;
exports.description = 'removes xmlns attribute (for inline svg, disabled by default)';
/**
* Remove the xmlns attribute when present.
*
* @example
* <svg viewBox="0 0 100 50" xmlns="http://www.w3.org/2000/svg">
*
* <svg viewBox="0 0 100 50">
*
* @param {Object} item current iteration item
* @return {Boolean} if true, xmlns will be filtered out
*
* @author Ricardo Tomasi
*/
exports.fn = function(item) {
if (item.isElem('svg') && item.hasAttr('xmlns')) {
item.removeAttr('xmlns');
}
};

View file

@ -1,24 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes XML processing instructions';
/**
* Remove XML Processing Instruction.
*
* @example
* <?xml version="1.0" encoding="utf-8"?>
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*/
exports.fn = function(item) {
return !(item.processinginstruction && item.processinginstruction.name === 'xml');
};

View file

@ -1,169 +0,0 @@
/**
* @license
* The MIT License
*
* Copyright © 20122016 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.
*
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*
* Лицензия MIT
*
* Copyright © 20122016 Кир Белевич
*
* Данная лицензия разрешает лицам, получившим копию
* данного
* программного обеспечения и сопутствующей
* документации
* (в дальнейшем именуемыми «Программное Обеспечение»),
* безвозмездно
* использовать Программное Обеспечение без
* ограничений, включая
* неограниченное право на использование, копирование,
* изменение,
* добавление, публикацию, распространение,
* сублицензирование
* и/или продажу копий Программного Обеспечения, также
* как и лицам,
* которым предоставляется данное Программное
* Обеспечение,
* при соблюдении следующих условий:
*
* Указанное выше уведомление об авторском праве и
* данные условия
* должны быть включены во все копии или значимые части
* данного
* Программного Обеспечения.
*
* ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК
* ЕСТЬ»,
* БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ
* ПОДРАЗУМЕВАЕМЫХ,
* ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ГАРАНТИЯМИ ТОВАРНОЙ
* ПРИГОДНОСТИ,
* СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И
* ОТСУТСТВИЯ НАРУШЕНИЙ
* ПРАВ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ
* НЕСУТ
* ОТВЕТСТВЕННОСТИ ПО ИСКАМ О ВОЗМЕЩЕНИИ УЩЕРБА, УБЫТКОВ
* ИЛИ ДРУГИХ
* ТРЕБОВАНИЙ ПО ДЕЙСТВУЮЩИМ КОНТРАКТАМ, ДЕЛИКТАМ ИЛИ
* ИНОМУ,
* ВОЗНИКШИМ ИЗ, ИМЕЮЩИМ ПРИЧИНОЙ ИЛИ СВЯЗАННЫМ С
* ПРОГРАММНЫМ
* ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ ПРОГРАММНОГО
* ОБЕСПЕЧЕНИЯ
* ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
*/
'use strict';
const tools = require("../tools");
exports.type = 'full';
exports.active = false;
exports.description = 'Finds <path> elements with the same d, fill, and ' +
'stroke, and converts them to <use> elements ' +
'referencing a single <path> def.';
/**
* Finds <path> elements with the same d, fill, and stroke, and converts them to
* <use> elements referencing a single <path> def.
*
* @author Jacob Howcroft
*/
exports.fn = function(data) {
const seen = new Map();
let count = 0;
const defs = [];
traverse(data, item => {
if (!item.isElem('path') || !item.hasAttr('d')) {
return;
}
const d = item.attr('d').value;
const fill = (item.hasAttr('fill') && item.attr('fill').value) || '';
const stroke = (item.hasAttr('stroke') && item.attr('stroke').value) || '';
const key = d + ';s:' + stroke + ';f:' + fill;
const hasSeen = seen.get(key);
if (!hasSeen) {
seen.set(key, {elem: item, reused: false});
return;
}
if (!hasSeen.reused) {
hasSeen.reused = true;
if (!hasSeen.elem.hasAttr('id')) {
hasSeen.elem.addAttr({name: 'id', local: 'id',
prefix: '', value: 'reuse-' + (count++)});
}
defs.push(hasSeen.elem);
}
item = convertToUse(item, hasSeen.elem.attr('id').value);
});
const defsTag = tools.createContentItem({
elem: 'defs', prefix: '', local: 'defs', content: [], attrs: []}, data);
data.content[0].spliceContent(0, 0, defsTag);
for (let def of defs) {
// Remove class and style before copying to avoid circular refs in
// JSON.stringify. This is fine because we don't actually want class or
// style information to be copied.
const style = def.style;
const defClass = def.class;
delete def.style;
delete def.class;
const defClone = def.clone();
def.style = style;
def.class = defClass;
defClone.removeAttr('transform');
defsTag.spliceContent(0, 0, defClone);
// Convert the original def to a use so the first usage isn't duplicated.
def = convertToUse(def, defClone.attr('id').value);
def.removeAttr('id');
}
return data;
};
/** */
function convertToUse(item, href) {
item.renameElem('use');
item.removeAttr('d');
item.removeAttr('stroke');
item.removeAttr('fill');
item.addAttr({name: 'xlink:href', local: 'xlink:href',
prefix: 'none', value: '#' + href});
delete item.pathJS;
return item;
}
/** */
function traverse(parent, callback) {
if (parent.isEmpty()) {
return;
}
for (let child of parent.content) {
callback(child);
traverse(child, callback);
}
}

View file

@ -1,84 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = false;
exports.description = 'sorts element attributes (disabled by default)';
exports.params = {
order: [
'id',
'width', 'height',
'x', 'x1', 'x2',
'y', 'y1', 'y2',
'cx', 'cy', 'r',
'fill', 'stroke', 'marker',
'd', 'points'
]
};
/**
* Sort element attributes for epic readability.
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
*
* @author Nikolay Frantsev
*/
exports.fn = function(item, params) {
var attrs = [],
sorted = {},
orderlen = params.order.length + 1,
xmlnsOrder = params.xmlnsOrder || 'front';
if (item.elem) {
item.eachAttr(function(attr) {
attrs.push(attr);
});
attrs.sort(function(a, b) {
if (a.prefix != b.prefix) {
// xmlns attributes implicitly have the prefix xmlns
if (xmlnsOrder == 'front') {
if (a.prefix == 'xmlns')
return -1;
if (b.prefix == 'xmlns')
return 1;
}
return a.prefix < b.prefix ? -1 : 1;
}
var aindex = orderlen;
var bindex = orderlen;
for (var i = 0; i < params.order.length; i++) {
if (a.name == params.order[i]) {
aindex = i;
} else if (a.name.indexOf(params.order[i] + '-') === 0) {
aindex = i + .5;
}
if (b.name == params.order[i]) {
bindex = i;
} else if (b.name.indexOf(params.order[i] + '-') === 0) {
bindex = i + .5;
}
}
if (aindex != bindex) {
return aindex - bindex;
}
return a.name < b.name ? -1 : 1;
});
attrs.forEach(function (attr) {
sorted[attr.name] = attr;
});
item.attrs = sorted;
}
};

View file

@ -1,47 +0,0 @@
'use strict';
exports.type = 'perItem';
exports.active = true;
exports.description = 'Sorts children of <defs> to improve compression';
/**
* Sorts children of defs in order to improve compression.
* Sorted first by frequency then by element name length then by element name (to ensure grouping).
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author David Leston
*/
exports.fn = function(item) {
if (item.isElem('defs')) {
if (item.content) {
var frequency = item.content.reduce(function (frequency, child) {
if (child.elem in frequency) {
frequency[child.elem]++;
} else {
frequency[child.elem] = 1;
}
return frequency;
}, {});
item.content.sort(function (a, b) {
var frequencyComparison = frequency[b.elem] - frequency[a.elem];
if (frequencyComparison !== 0 ) {
return frequencyComparison;
}
var lengthComparison = b.elem.length - a.elem.length;
if (lengthComparison !== 0) {
return lengthComparison;
}
return a.elem != b.elem ? a.elem > b.elem ? -1 : 1 : 0;
});
}
return true;
}
};

View file

@ -1,190 +0,0 @@
"use strict";
const SAX = require("sax");
const JSAPI = require("./jsAPI.js");
const CSSClassList = require("./css-class-list");
const CSSStyleDeclaration = require("./css-style-declaration");
const entityDeclaration = /<!ENTITY\s+(\S+)\s+(?:'([^\']+)'|"([^\"]+)")\s*>/g;
const config = {
strict: true,
trim: false,
normalize: true,
lowercase: true,
xmlns: true,
position: true
};
/**
* Convert SVG (XML) string to SVG-as-JS object.
*
* @param {String} data input data
* @param {Function} callback
*/
module.exports = function(data, callback) {
return new Promise(function(resolve, reject) {
let sax = SAX.parser(config.strict, config);
let root = new JSAPI({ elem: "#document", content: [] });
let current = root;
let stack = [root];
let textContext = null;
let parsingError = false;
function pushToContent(content) {
content = new JSAPI(content, current);
(current.content = current.content || []).push(content);
return content;
}
sax.ondoctype = function(doctype) {
pushToContent({
doctype: doctype
});
let subsetStart = doctype.indexOf("[");
let entityMatch;
if (subsetStart >= 0) {
entityDeclaration.lastIndex = subsetStart;
while ((entityMatch = entityDeclaration.exec(data)) != null) {
sax.ENTITIES[entityMatch[1]] = entityMatch[2] || entityMatch[3];
}
}
};
sax.onprocessinginstruction = function(data) {
pushToContent({
processinginstruction: data
});
};
sax.oncomment = function(comment) {
pushToContent({
comment: comment.trim()
});
};
sax.oncdata = function(cdata) {
pushToContent({
cdata: cdata
});
};
sax.onopentag = function(data) {
let elem = {
elem: data.name,
prefix: data.prefix,
local: data.local,
attrs: {}
};
elem.class = new CSSClassList(elem);
elem.style = new CSSStyleDeclaration(elem);
if (Object.keys(data.attributes).length) {
for (var name in data.attributes) {
if (name === "class") { // has class attribute
elem.class.hasClass();
}
if (name === "style") { // has style attribute
elem.style.hasStyle();
}
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;
// Save info about <text> tag to prevent trimming of meaningful whitespace
if (data.name == "text" && !data.prefix) {
textContext = current;
}
stack.push(elem);
};
sax.ontext = function(text) {
if (/\S/.test(text) || textContext) {
if (!textContext) text = text.trim();
pushToContent({
text: text
});
}
};
sax.onclosetag = function() {
var last = stack.pop();
// Trim text inside <text> tag.
if (last == textContext) {
trim(textContext);
textContext = null;
}
current = stack[stack.length - 1];
};
sax.onerror = function(e) {
e.message = "Error in parsing SVG: " + e.message;
if (e.message.indexOf("Unexpected end") < 0) {
reject(e);
}
};
sax.onend = function() {
if (!this.error) {
resolve(root);
} else {
reject(this.error);
}
};
try {
sax.write(data);
} catch (e) {
reject(e)
parsingError = true;
}
if (!parsingError) {
sax.close();
}
function trim(elem) {
if (!elem.content) return elem;
let start = elem.content[0];
let end = elem.content[elem.content.length - 1];
while (start && start.content && !start.text) {
start = start.content[0];
}
if (start && start.text) {
start.text = start.text.replace(/^\s+/, "");
}
while (end && end.content && !end.text) {
end = end.content[end.content.length - 1];
}
if (end && end.text) {
end.text = end.text.replace(/\s+$/, "");
}
return elem;
}
});
};

View file

@ -1,152 +0,0 @@
'use strict';
const JSAPI = require("./jsAPI");
/**
* The factory that creates a content item with the helper methods.
*
* @param {Object} data which is passed to jsAPI constructor
* @returns {JSAPI} content item
*/
exports.createContentItem = function(data) {
return new JSAPI(data);
};
/**
* Encode plain SVG data string into Data URI string.
*
* @param {String} str input string
* @param {String} type Data URI type
* @return {String} output string
*/
exports.encodeSVGDatauri = function(str, type) {
var prefix = 'data:image/svg+xml';
if (!type || type === 'base64') {
// base64
prefix += ';base64,';
if (Buffer.from) {
str = prefix + Buffer.from(str).toString('base64');
} else {
str = prefix + new Buffer(str).toString('base64');
}
} else if (type === 'enc') {
// URI encoded
str = prefix + ',' + encodeURIComponent(str);
} else if (type === 'unenc') {
// unencoded
str = prefix + ',' + str;
}
return str;
};
/**
* Decode SVG Data URI string into plain SVG string.
*
* @param {string} str input string
* @return {String} output string
*/
exports.decodeSVGDatauri = function(str) {
var regexp = /data:image\/svg\+xml(;charset=[^;,]*)?(;base64)?,(.*)/;
var match = regexp.exec(str);
// plain string
if (!match) return str;
var data = match[3];
if (match[2]) {
// base64
str = new Buffer(data, 'base64').toString('utf8');
} else if (data.charAt(0) === '%') {
// URI encoded
str = decodeURIComponent(data);
} else if (data.charAt(0) === '<') {
// unencoded
str = data;
}
return str;
};
exports.intersectArrays = function(a, b) {
return a.filter(function(n) {
return b.indexOf(n) > -1;
});
};
/**
* Convert a row of numbers to an optimized string view.
*
* @example
* [0, -1, .5, .5] "0-1 .5.5"
*
* @param {number[]} data
* @param {Object} params
* @param {string?} command path data instruction
* @return {string}
*/
exports.cleanupOutData = function(data, params, command) {
var str = '',
delimiter,
prev;
data.forEach(function(item, i) {
// space delimiter by default
delimiter = ' ';
// no extra space in front of first number
if (i == 0) delimiter = '';
// no extra space after 'arcto' command flags
if (params.noSpaceAfterFlags && (command == 'A' || command == 'a')) {
var pos = i % 7;
if (pos == 4 || pos == 5) delimiter = '';
}
// remove floating-point numbers leading zeros
// 0.5 → .5
// -0.5 → -.5
if (params.leadingZero) {
item = removeLeadingZero(item);
}
// no extra space in front of negative number or
// in front of a floating number if a previous number is floating too
if (
params.negativeExtraSpace &&
delimiter != '' &&
(item < 0 ||
(String(item).charCodeAt(0) == 46 && prev % 1 !== 0)
)
) {
delimiter = '';
}
// save prev item value
prev = item;
str += delimiter + item;
});
return str;
};
/**
* Remove floating-point numbers leading zero.
*
* @example
* 0.5 .5
*
* @example
* -0.5 -.5
*
* @param {Float} num input number
*
* @return {String} output number as string
*/
var removeLeadingZero = exports.removeLeadingZero = function(num) {
var strNum = num.toString();
if (0 < num && num < 1 && strNum.charCodeAt(0) == 48) {
strNum = strNum.slice(1);
} else if (-1 < num && num < 0 && strNum.charCodeAt(1) == 48) {
strNum = strNum.charAt(0) + strNum.slice(2);
}
return strNum;
};

View file

@ -1,10 +0,0 @@
const svgc = require("./main.js");
const fs = require("fs");
fs.readFile("./test2.svg", "utf-8", (err, data) => {
svgc.optimize(data).then((result) => {
fs.writeFileSync("./result.svg", result);
// console.log(result);
// console.log();
});
});

View file

@ -1,3 +0,0 @@
<svg version="1.1" width="10" height="20">
test
</svg>

Before

Width:  |  Height:  |  Size: 59 B