August 4, 2016
A toolbox for SVG icon and style assets workflows
The use of stylesheets with standalone SVG files is not ubiquitious. Browsers understand them, some renderers (like librsvg ) do not. Workflow in editors mostly do not support their usage, or are at least not helpfull, even if they understand them.
I have published a node.js module that aims to bridge that state of affairs by offering a workflow that
- inserts stylesheets into SVG files, optionally compiling them from Sass
- inlines stylesheets by distributing all styles into element style attributes
Secondly, it exports single objects from such a file to PNG or SVG (icon) files. This part of the workflow depends on the command line features of Inkscape for
- identifying the bounding box of objects: a major undertaking not easily done, so the module takes advantage of the existing solution
- PNG export: to ensure the correct interpretation of stylesheets
Icon sets (or grafical assets, for example of a theme) aim to share a common design in terms of colors, sizes, strokes or more complex characteristics like imagery or metaphors.
The best way for a designer to keep track of those characteristics is to work in an integrated workspace, using DRY language to describe communalities only once, and reuse. In short: seperate the shape design in a SVG file from style properties in a stylesheet - those preferably in a higher language like Sass.
For applications, other formats may be needed. Icons may have to be shown in separate files, and in pixel and vector format. The conversion is a purely technical process and can be automated with this module.
🔗 XML manipulation with Cheerio
I found Cheerio to be a fast and fluently written library to manipulate the SVG files. It parses XML/HTML files into a DOM-like tree, but lacks any functionality a headless browser would bring to emulate rendering. If it doesn't concern the XML text, it isn't there. Cheerio features an API that is almost identical to jQuery. Load a string with
var $ = cheerio.load(svgString, {
xmlMode: true
});
and use the familiar functions. Here is an example:
I implemented inlining styles as a two-step process: first, the style rules from the stylesheet are attached to all elements the selectors state.
/*
* rule = {
+ selectors: string[] list of selectors, split on commas,
+ declarations: declaration[] list of objects with "property" and "value"
+ }
*/
rule.selectors.forEach((selector) => {
// select all elements the selector applies to
var $selected = $(selector);
$selected.each((i, elem) => {
// attach a data object with the selector as key (simplified)
$(elem).data(selector, rule);
});
});
Then, after all rules are distributed, the styles are written out into a style attribute taking the cascade rules into account:
// $el is a single element
// sort all applicable rules by their specificity
var sorted = sortSpecificity(Object.keys($el.data()));
sorted.forEach((selector) => {
// get the rule object from the attached data
var rule = $el.data(selector);
rule.declarations.forEach((declaration) => {
// compare each declaration with properties already present in the style attribute
// highest specificity gets written first, later declarations fall through
if (!$el.css(declaration.property)) {
// write new properties to the style attribute
$el.css(declaration.property, declaration.value);
}
}, this);
});