jcrushsvg
Version:
Javascript based SVG deduplicator and compressor
112 lines (106 loc) • 5.3 kB
JavaScript
;
/**
* @file
* JCrush SVG - A Javascript based SVG deduplicator & compressor.
*/
var fs = require('fs');
var path = require('path');
var jcrush = require('jcrush');
let jcrushsvg = opts => {
let svgItems = {}, joinString = '★', breakString = '•';
opts = { ...{ inDir: '', outDir: '', outFile: 'svg.js', bundle: 0, processSVG: null, processJS: null, prog: 1, tpl: 1, break: [],
tplEsc: 0, funcName: 'svg', wrap: 'custom', customPre: joinString, customPost: ' ', resVars: [], let: 1, maxLen: 40 }, ...opts };
if (opts.checkNew && fs.existsSync(opts.outFile) && !fs.readdirSync(opts.inDir).some(f => fs.statSync(path.join(opts.inDir, f)).mtime > fs.statSync(opts.outFile).mtime)) {
console.log(`JCrush SVG checked ${opts.inDir} and determined ${opts.outFile} is already up-to-date. ✔️`);
return;
}
opts.break.push(breakString);
opts.resVars.push('k');
opts.resVars.push('el');
if (!opts.outDir) opts.outDir = opts.inDir;
try {
fs.readdirSync(opts.inDir).filter(file => path.extname(file) == '.svg').forEach(file => {
opts.prog && console.log("Loading SVG file", file);
let filePath = path.join(opts.inDir, file),
svgTagMatch = fs.readFileSync(filePath, 'utf8').match(/<svg[^>]*>[\s\S]*<\/svg>/),
svgContent = svgTagMatch ? svgTagMatch[0] : null;
if (opts.processSVG) svgContent = opts.processSVG(filePath, svgContent);
svgItems[path.basename(file, '.svg')] = svgContent
// Whitespace
.replace(/\s+/g, ' ').trim() // Remove redundant whitespace
// Strip attrs
.replace(/\s*version="[^"]*"/gi, '') // Remove version attr
.replace(/\s*baseProfile="[^"]*"/gi, '') // Remove baseProfile attr
.replace(/\s*id="[^"]*"/gi, '') // Remove id attr
.replace(/\s*xmlns(:\w+)?="[^"]*"/gi, '') // Remove any xmlns attributes
.replace(/\s*xml:space="preserve"/gi, '') // Remove xml:space attr
.replace(/\s*enable-background="[^"]*"/gi, '') // Remove enable-background attr
// Crush
.replace(/\s*\/>/g, ' />') // Add a space before the end of self-closing tags (will be removed later)
.replace(/(<\w+[^>]*\b\w+=['"]?)0\.(\d)/g, '$1.$2') // Remove leading zero for decimals in attribute values inside tags
.replace(/>\s+</g, '><') // Remove spaces between tags - Gotcha: Can't rely on whitespace between tags for styling
.replace(/(<[a-zA-Z][^>]*>)\s+/g, '$1') // Remove whitespace after opening tags
.replace(/\s+(<\/[a-zA-Z]+>)/g, '$1') // Remove whitespace before closing tags
.replace(/(\w+)="([^"\s]+)(?="(?!\/>))"/g, (m, k, v) => `${k}=${v}`) // Remove " around attrs where possible (but not if followed by self-close)
.replace(/"\s+(?=\s*[\w-]+=|\s*\/?>)/g, '"') // Remove spaces **after** a closing quote (but not if followed by self-close)
.replace(/(?<=\w=")\s+/g, '') // Remove spaces **after** an opening quote
.replace(/\s*(["'])\s*\/>/g, '$1/>') // Remove space only between quote and />
.replace(/(\S)\s*\/>/g, '$1 />') // Ensure space before /> if no quote
.replace(/(?<=<[^>]+)\s+(?=>)/g, ''); // Remove space before > in tags
});
if (!Object.keys(svgItems).length) {
throw new Error(`Did not find any SVG files in ${opts.inDir} folder.`);
}
let funcCode,
keys = Object.keys(svgItems),
enc = jcrush.code(Object.values(svgItems).join(breakString), opts).split(joinString);
enc[1].split(breakString).forEach((v, k) => {
svgItems[keys[k]] = v;
});
if (opts.bundle) {
funcCode = `k => {
${enc[0]}
return {
${Object.entries(svgItems).map(([key, value]) => `${key}: \`${value}\``).join(",\n ")}
}[k];
}`;
}
else {
let ext = opts.appendExt ? '.svg.js' : '.js';
for (let key in svgItems) fs.writeFileSync(opts.outDir + '/' + key + ext, svgItems[key]);
funcCode = `(k, el) => {
${enc[0]}
return fetch(` + opts.outDir + '/${k}' + ext + `).then(r => r.text()).then(c => el.innerHTML = eval(c))
}`;
}
let jsContent = `// This file is generated automatically. Do not modify.
// It contains SVG code for use in the application.
// Generated from SVG files in the ${opts.inDir} folder.
let ${opts.funcName} = ${funcCode}`;
if (opts.processJS) jsContent = opts.processJS(opts.outFile, jsContent);
fs.writeFileSync(opts.outFile, jsContent);
opts.prog && console.log('Main SVG JS file created: ' + opts.outFile);
}
catch (err) {
console.error('🛑', err.message);
process.exit(1);
}
};
module.exports = jcrushsvg;
// CLI Usage
if (require.main === module) {
let args = process.argv.slice(2), opts = {};
args.forEach((arg, index) => {
let key = arg.slice(2), value = args[index + 1];
if (value === '1' || value === '0') opts[key] = value === '1';
else if (value && !value.startsWith('--')) opts[key] = value;
else opts[key] = true;
});
if (args.length < 2) {
console.log('Usage: jcrushsvg --inDir="/src/svg" --outDir="/svg" --outFile="svg.js"');
console.log("See README file for full list of arguments.");
process.exit(1);
}
jcrushsvg(opts);
}