Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add localStyles plugin, tests and dependencies. #447

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 25 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
"name": "svgo",
"version": "0.5.6",
"description": "Nodejs-based tool for optimizing SVG vector graphics files",
"keywords": [ "svgo", "svg", "optimize", "minify" ],
"keywords": [
"svgo",
"svg",
"optimize",
"minify"
],
"homepage": "https://github.com/svg/svgo",
"bugs": {
"url": "https://github.com/svg/svgo/issues",
Expand All @@ -13,15 +18,18 @@
"email": "kir@soulshine.in",
"url": "https://github.com/deepsweet"
},
"contributors": [{
"name": "Sergey Belov",
"email": "peimei@ya.ru",
"url": "http://github.com/arikon"
}, {
"name": "Lev Solntsev",
"email": "lev.sun@ya.ru",
"url": "http://github.com/GreLI"
}],
"contributors": [
{
"name": "Sergey Belov",
"email": "peimei@ya.ru",
"url": "http://github.com/arikon"
},
{
"name": "Lev Solntsev",
"email": "lev.sun@ya.ru",
"url": "http://github.com/GreLI"
}
],
"repository": {
"type": "git",
"url": "git://github.com/svg/svgo.git"
Expand All @@ -39,12 +47,15 @@
"test": "make test"
},
"dependencies": {
"sax": "~1.1.1",
"coa": "~1.0.1",
"js-yaml": "~3.3.1",
"colors": "~1.1.2",
"whet.extend": "~0.9.9",
"mkdirp": "~0.5.1"
"css": "^2.2.1",
"js-yaml": "~3.3.1",
"mkdirp": "~0.5.1",
"remove-value": "^1.0.0",
"sax": "~1.1.1",
"uniq": "^1.0.1",
"whet.extend": "~0.9.9"
},
"devDependencies": {
"mocha": "~2.2.5",
Expand Down
243 changes: 243 additions & 0 deletions plugins/localStyles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
'use strict';

exports.type = 'perItem';

exports.active = true;

exports.description = 'copies styles from <style> to element styles';


var cssParser = require('css'),
uniq = require('uniq'),
removeValue = require('remove-value'),
lookupRules = [],
svgElem = {},
styleCssAst = {};


// declarations (property-value paris) from rule
var getCssDeclarationsFromRule = function(cssRule) {
var declarations = [];
cssRule.declarations.forEach(function(declaration) {
declarations.push({ property: declaration.property, value: declaration.value });
});
return declarations;
};
// declarations from multiple rules
var getCssDeclarationsFromRules = function(cssRules) {
var declarations = [];
cssRules.forEach(function(cssRule) {
declarations = declarations.concat(getCssDeclarationsFromRule(cssRule));
});
return declarations;
};

var _trim = function(s) {
return s.trim();
};
// parse class attribute value
var parseClasses = function(item) {
return item.attr('class').value.split(' ').map(_trim);
};

// looks up style rules for passed selector
var lookupCssSelector = function(selector) {
var matchedRules = [];
lookupRules.forEach(function(lookupRule) {
if(lookupRule.selector == selector) {
matchedRules = matchedRules.concat(lookupRule);
}
});
return matchedRules;
};

var processCssSelector = function(selectorFind) {
var matchedRules = lookupCssSelector(selectorFind);
cleanupSelectorAst(selectorFind, matchedRules);
return getCssDeclarationsFromRules(matchedRules);
};

var cleanupSelectorAst = function(selectorFind, matchedRules) {
matchedRules.map(function(matchedRule) {
removeValue(matchedRule.astRule.selectors, selectorFind); // (global variable)
return matchedRule;
});
return;
};

var cleanupRulesAst = function(rulesAst) {
return rulesAst.filter(function(matchedRule) { // (global variable)
return(matchedRule.type != 'rule' || matchedRule.selectors.length > 0);
});
};


// parses css of a css rule (no selector)
var parseRulesCss = function(str) {
return cssParser
.parse('.dummy { ' + str + ' }')
.stylesheet.rules[0];
};

// prepares a full css ast from rules array
var prepareCssRulesAst = function(rules) {
var ast =
{ type : 'stylesheet',
stylesheet: {
rules: rules
}
};
return ast;
};

// generates an ast declaration per property-value pair
var prepareCssDeclarationsAst = function(declarations) {
var declarationsAst = [],
declarationAst = {};

declarations.forEach(function(declaration) {
declarationAst = { type : 'declaration',
property: declaration.property,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uh-oh

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can improve/add comments if necessary. :)

value : declaration.value
};
declarationsAst.push(declarationAst);
});

return declarationsAst;
};

// ast to rules css
var cssAstToRulesCss = function(ast) {
var cssDummy = cssParser.stringify(ast, { compress: true });
return extractRuleCss(cssDummy);
};
// rules to rules css
var stringifyCssRules = function(rules) {
var ast = prepareCssRulesAst(rules);
return cssAstToRulesCss(ast);
};
// declarations to rules css
var stringifyCssDeclarations = function(declarations) {

var dummyRule =
{ type : 'rule',
selectors: [ '.dummy' ],
declarations:
[]
};
dummyRule.declarations = prepareCssDeclarationsAst(declarations);

return stringifyCssRules([dummyRule]);
};
// helper to extract rules css from full css
var extractRuleCss = function(str) {
var strEx = str.match(/\.dummy{(.*)}/i)[1];
return strEx;
};


// returns true when two compared declarations got the same property (name)
var uniqueProperty = function(a,b) {
return (a.property == b.property) ? 0 : 1;
};


/**
* Copy styles from <style> to element styles.
*
* (1) run this plugin before the removeStyleElement plugin
* (2) this plugin won't remove the <style> element,
* use removeStyleElement after this plugin
* (3) run convertStyleToAttrs after this plugin
* (4) minify styles in <style> if necessary
* before this plugin (minified = "computed" styles)
* (5) this plugin currently works only with class and id selectors,
* advanced css selectors (e.g. :nth-child) aren't currently supported
* (6) inline styles will be written after the styles from <style>
* (7) classes are inlined by the order of their names in class element attribute
*
* 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 strarsis <strarsis@gmail.com>
*/
exports.fn = function(item) {

// TODO: although quite rarely used, add support for multiple <style> elements
if(item.elem && item.isElem('style')) {
// fetch global styles from style element
svgElem = item;
var styleCss = item.content[0].text;
styleCssAst = cssParser.parse(styleCss);

var styleCssRules = styleCssAst.stylesheet.rules,
styleCssDeclarations = [];

styleCssRules.forEach(function(styleCssRule) {
if(styleCssRule.type != 'rule') { return; } // skip anything nested like mediaqueries

styleCssDeclarations = getCssDeclarationsFromRule(styleCssRule);
styleCssRule.selectors.forEach(function(styleSelector) {
lookupRules.push({
selector: styleSelector,
declarations: styleCssDeclarations,
astRule: styleCssRule
});
});
});
return item;
}


if(lookupRules.length > 0 && item.elem) {

// primitive "selector engine"
var itemDeclarations = [];

// #id
if(item.hasAttr('id')) {
var idName = item.attr('id').value;
var selectorFind = '#' + idName;
itemDeclarations = itemDeclarations.concat(processCssSelector(selectorFind));
}

// .class
if(item.hasAttr('class')) {
var classNames = parseClasses(item);
classNames.forEach(function(className) {
var selectorFind = '.' + className;
itemDeclarations = itemDeclarations.concat(processCssSelector(selectorFind));
});
}

styleCssAst.stylesheet.rules = cleanupRulesAst(styleCssAst.stylesheet.rules);


// existing inline styles
// TODO: Opportunity for cleaning up global styles that converge with style attribute stlyes?
var itemExistingDeclarations = [];
if(item.hasAttr('style')) {
var itemExistingCss = item.attr('style').value;
var itemExistingCssAst = parseRulesCss(itemExistingCss);
itemExistingDeclarations = getCssDeclarationsFromRule(itemExistingCssAst);
}


var newDeclarations = itemExistingDeclarations.concat(itemDeclarations);
uniq(newDeclarations, uniqueProperty, true);

// apply new styles only when necessary
if(newDeclarations && newDeclarations.length > 0 &&
itemDeclarations && itemDeclarations.length > 0 ) {
var newCss = stringifyCssDeclarations(newDeclarations);
item.attr('style').value = newCss;
}

var newStyleCss = cssParser.stringify(styleCssAst, { compress: true });
svgElem.content[0].text = newStyleCss;
}

return item;
};
15 changes: 15 additions & 0 deletions test/plugins/localStyles.01.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions test/plugins/localStyles.02.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions test/plugins/localStyles.03.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions test/plugins/localStyles.04.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions test/plugins/localStyles.05.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.