grunt-svgstore
Version:
Merge SVGs from a folder
427 lines (358 loc) • 13.9 kB
JavaScript
/*
* grunt-svgstore
* https://github.com/FWeinb/grunt-svgstore
*
* Copyright (c) 2014 Fabrice Weinberg
* Licensed under the MIT license.
*/
'use strict';
module.exports = function (grunt) {
var path = require('path');
var beautify = require('js-beautify').html;
var cheerio = require('cheerio');
var chalk = require('chalk');
var handlebars = require('handlebars');
// Matching an url() reference. To correct references broken by making ids unique to the source svg
var urlPattern = /url\(\s*#([^ ]+?)\s*\)/g;
// Default Template
var defaultTemplate = `<!doctype html>
<html>
<head>
<style>
svg{
width:50px;
height:50px;
fill:black !important;
}
</style>
<head>
<body>
{{{svg}}}
{{#each icons}}
<svg>
<use xlink:href="#{{name}}" />
</svg>
{{/each}}
</body>
</html>`;
// Default function used to extract an id from a name
var defaultConvertNameToId = function (name) {
var dotPos = name.indexOf('.');
if (dotPos > -1) {
name = name.substring(0, dotPos);
}
return name;
};
// Please see the Grunt documentation for more information regarding task
// creation: http://gruntjs.com/creating-tasks
grunt.registerMultiTask('svgstore', 'Merge SVGs from a folder.', function () {
// Merge task-specific and/or target-specific options with these defaults.
var options = this.options({
allowDuplicateItems: false,
cleanupdefs: false,
convertNameToId: defaultConvertNameToId,
externalDefs: false,
fixedSizeVersion: false,
formatting: false,
includedemo: false,
includeTitleElement: true,
inheritviewbox: false,
prefix: '',
preserveDescElement: true,
removeEmptyGroupElements: true,
removeWithId: null,
setUniqueIds: true,
svg: {
'xmlns': 'http://www.w3.org/2000/svg'
},
symbol: {}
});
var cleanupAttributes = [];
if (options.cleanup && typeof options.cleanup === 'boolean') {
// For backwards compatibility (introduced in 0.2.6).
cleanupAttributes = ['style'];
} else if (Array.isArray(options.cleanup)) {
cleanupAttributes = options.cleanup;
}
this.files.forEach(function (file) {
var $resultDocument = cheerio.load('<svg><defs></defs></svg>', { xmlMode: true });
var $resultSvg = $resultDocument('svg');
var $resultDefs = $resultDocument('defs').first();
var fileSrc = options.AllowDuplicateItems ? file.orig.src : file.src;
var iconNameViewBoxArray = []; // Used to store information of all icons that are added
// { name : '' }
// Merge in SVG attributes from option
for (var attr in options.svg) {
$resultSvg.attr(attr, options.svg[attr]);
}
fileSrc.filter(function (filepath) {
if (!grunt.file.exists(filepath)) {
grunt.log.warn('File "' + filepath + '" not found.');
return false;
} else {
return true;
}
}).map(function (filepath) {
var filename = path.basename(filepath, '.svg');
var id = options.convertNameToId(filename);
var contentStr = grunt.file.read(filepath);
var $ = cheerio.load(contentStr, {
normalizeWhitespace: true,
xmlMode: true
});
// Remove empty g elements
if (options.removeEmptyGroupElements) {
$('g').each(function () {
var $elem = $(this);
if (!$elem.children().length) {
$elem.remove();
}
});
}
// Map to store references from id to uniqueId + id;
var mappedIds = {};
function getUniqueId (oldId) {
return id + '-' + oldId;
}
if (options.setUniqueIds) {
$('[id]').each(function () {
var $elem = $(this);
var id = $elem.attr('id');
var uid = getUniqueId(id);
mappedIds[id] = {
id: uid,
referenced: false,
$elem: $elem
};
// If asked to remove elements with ID, otherwise, map unique id
if (options.removeWithId && id === options.removeWithId) {
$elem.remove();
} else {
$elem.attr('id', uid);
}
});
}
$('*').each(function () {
var $elem = $(this);
var attrs = $elem.attr();
Object.keys(attrs).forEach(function (key) {
var value = attrs[key];
var id; var match; var isFillCurrentColor; var isStrokeCurrentColor; var preservedKey = '';
while ((match = urlPattern.exec(value)) !== null) {
id = match[1];
if (mappedIds[id]) {
mappedIds[id].referenced = true;
$elem.attr(key, value.replace(match[0], 'url(#' + mappedIds[id].id + ')'));
}
}
if (key === 'xlink:href') {
id = value.substring(1);
var idObj = mappedIds[id];
if (idObj) {
idObj.referenced = false;
$elem.attr(key, '#' + idObj.id);
}
}
// IDs are handled separately
if (key !== 'id') {
if (options.cleanupdefs || !$elem.parents('defs').length) {
if (key.match(/preserve--/)) {
// Strip off the preserve--
preservedKey = key.substring(10);
}
if (cleanupAttributes.indexOf(key) > -1 || cleanupAttributes.indexOf(preservedKey) > -1) {
isFillCurrentColor = key === 'fill' && $elem.attr('fill') === 'currentColor';
isStrokeCurrentColor = key === 'stroke' && $elem.attr('stroke') === 'currentColor';
if (preservedKey && preservedKey.length) {
// Add the new key preserving value
$elem.attr(preservedKey, $elem.attr(key));
// Remove the old preserve--foo key
$elem.removeAttr(key);
} else if (!(isFillCurrentColor || isStrokeCurrentColor)) {
// Letting fill inherit the `currentColor` allows shared inline defs to
// be styled differently based on an SVG element's `color` so we leave these
$elem.removeAttr(key);
}
} else {
if (preservedKey && preservedKey.length) {
// Add the new key preserving value
$elem.attr(preservedKey, $elem.attr(key));
// Remove the old preserve--foo key
$elem.removeAttr(key);
}
}
}
}
});
});
if (cleanupAttributes.indexOf('id') > -1) {
Object.keys(mappedIds).forEach(function (id) {
var idObj = mappedIds[id];
if (!idObj.referenced) {
idObj.$elem.removeAttr('id');
}
});
}
var $svg = $('svg');
var $title = $('title');
var $desc = $('desc');
var $def = $('defs').first();
var defContent = $def.length && $def.html();
// Merge in the defs from this svg in the result defs block
if (defContent) {
$resultDefs.append(defContent);
}
var title = $title.first().html();
var desc = $desc.first().html();
// Remove def, title, desc from this svg
$def.remove();
$title.remove();
$desc.remove();
// If there is no title use the filename
title = title || id;
// Generate symbol
var $res = cheerio.load('<symbol>' + $svg.html() + '</symbol>', { xmlMode: true });
var $symbol = $res('symbol').first();
// Merge in symbol attributes from option
for (var attr in options.symbol) {
$symbol.attr(attr, options.symbol[attr]);
}
// Add title and desc (if provided)
if (desc && options.preserveDescElement) {
$symbol.prepend('<desc>' + desc + '</desc>');
}
if (title && options.includeTitleElement) {
$symbol.prepend('<title>' + title + '</title>');
}
// Add viewBox (if present on SVG w/ optional width/height fallback)
var viewBox = $svg.attr('viewBox');
// In case the user didn't use proper caps, but all lower-case.
if (!viewBox && $svg.attr('viewbox')) {
viewBox = $svg.attr('viewbox');
}
if (!viewBox && options.inheritviewbox) {
var width = $svg.attr('width');
var height = $svg.attr('height');
var pxSize = /^\d+(\.\d+)?(px)?$/;
if (pxSize.test(width) && pxSize.test(height)) {
viewBox = '0 0 ' + parseFloat(width) + ' ' + parseFloat(height);
}
}
if (viewBox) {
$symbol.attr('viewBox', viewBox);
}
// Add ID to symbol
var graphicId = options.prefix + id;
$symbol.attr('id', graphicId);
// Extract gradients and pattern
var addToDefs = function () {
var $elem = $res(this);
$resultDefs.append($elem.toString());
$elem.remove();
};
$res('linearGradient').each(addToDefs);
$res('radialGradient').each(addToDefs);
$res('pattern').each(addToDefs);
// Append <symbol> to resulting SVG
$resultSvg.append($res.html());
// Add icon to the demo.html array
if (options.includedemo) {
iconNameViewBoxArray.push({
name: graphicId,
title: title,
description: desc
});
}
if (viewBox && !!options.fixedSizeVersion) {
var fixedWidth = options.fixedSizeVersion.width || 50;
var fixedHeight = options.fixedSizeVersion.width || 50;
var $resFixed = cheerio.load('<symbol><use></use></symbol>', { lowerCaseAttributeNames: false });
var fixedId = graphicId + (options.fixedSizeVersion.suffix || '-fixed-size');
var $symbolFixed = $resFixed('symbol')
.first()
.attr('viewBox', [0, 0, fixedWidth, fixedHeight].join(' '))
.attr('id', fixedId);
Object.keys(options.symbol).forEach(function (key) {
$symbolFixed.attr(key, options.symbol[key]);
});
if (desc) {
$symbolFixed.prepend('<desc>' + desc + '</desc>');
}
if (title) {
$symbolFixed.prepend('<title>' + title + '</title>');
}
var originalViewBox = viewBox
.split(' ')
.map(function (string) {
return parseInt(string);
});
var translationX = ((fixedWidth - originalViewBox[2]) / 2) + originalViewBox[0];
var translationY = ((fixedHeight - originalViewBox[3]) / 2) + originalViewBox[1];
var scale = Math.max.apply(null, [originalViewBox[2], originalViewBox[3]]) /
Math.max.apply(null, [fixedWidth, fixedHeight]);
$symbolFixed
.find('use')
.attr('xlink:href', '#' + graphicId)
.attr('transform', [
'scale(' + parseFloat(scale.toFixed(options.fixedSizeVersion.maxDigits.scale || 4)).toPrecision() + ')',
'translate(' + [
parseFloat(translationX.toFixed(options.fixedSizeVersion.maxDigits.translation || 4)).toPrecision(),
parseFloat(translationY.toFixed(options.fixedSizeVersion.maxDigits.translation || 4)).toPrecision()
].join(', ') + ')'
].join(' '));
$resultSvg.append($resFixed.html());
if (options.includedemo) {
iconNameViewBoxArray.push({
name: fixedId
});
}
}
});
if (options.externalDefs) {
var filepath = options.externalDefs;
if (!grunt.file.exists(filepath)) {
grunt.log.error('File "' + chalk.red(filepath) + '" not found.');
return false;
}
var $file = cheerio.load(grunt.file.read(filepath), {
xmlMode: true,
normalizeWhitespace: true
});
var defs = $file('defs').html();
if (defs === null) {
grunt.log.warn('File "' + chalk.yellow(filepath) + '" contains no defs.');
} else {
$resultDefs.append(defs);
}
}
// Remove defs block if empty
if ($resultDefs.html().trim() === '') {
$resultDefs.remove();
}
var result = options.formatting ? beautify($resultDocument.html(), options.formatting) : $resultDocument.html();
var destName = path.basename(file.dest, '.svg');
grunt.file.write(file.dest, result);
grunt.log.writeln('File ' + chalk.cyan(file.dest) + ' created.');
if (options.includedemo) {
$resultSvg.attr('style', 'width:0;height:0;visibility:hidden;');
var demoHTML;
var viewData = {
svg: $resultDocument.html(),
icons: iconNameViewBoxArray
};
if (typeof options.includedemo === 'function') {
demoHTML = options.includedemo(viewData);
} else {
var template = defaultTemplate;
if (typeof options.includedemo === 'string') {
template = options.includedemo;
}
demoHTML = handlebars.compile(template)(viewData);
}
var demoPath = path.resolve(path.dirname(file.dest), destName + '-demo.html');
grunt.file.write(demoPath, demoHTML);
grunt.log.writeln('Demo file ' + chalk.cyan(demoPath) + ' created.');
}
});
});
};