109 lines
2.5 KiB
JavaScript
109 lines
2.5 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
exports.name = 'sortAttrs';
|
||
|
exports.description = 'Sort element attributes for better compression';
|
||
|
|
||
|
/**
|
||
|
* Sort element attributes for better compression
|
||
|
*
|
||
|
* @author Nikolay Frantsev
|
||
|
*
|
||
|
* @type {import('./plugins-types').Plugin<'sortAttrs'>}
|
||
|
*/
|
||
|
exports.fn = (_root, params) => {
|
||
|
const {
|
||
|
order = [
|
||
|
'id',
|
||
|
'width',
|
||
|
'height',
|
||
|
'x',
|
||
|
'x1',
|
||
|
'x2',
|
||
|
'y',
|
||
|
'y1',
|
||
|
'y2',
|
||
|
'cx',
|
||
|
'cy',
|
||
|
'r',
|
||
|
'fill',
|
||
|
'stroke',
|
||
|
'marker',
|
||
|
'd',
|
||
|
'points',
|
||
|
],
|
||
|
xmlnsOrder = 'front',
|
||
|
} = params;
|
||
|
|
||
|
/**
|
||
|
* @type {(name: string) => number}
|
||
|
*/
|
||
|
const getNsPriority = (name) => {
|
||
|
if (xmlnsOrder === 'front') {
|
||
|
// put xmlns first
|
||
|
if (name === 'xmlns') {
|
||
|
return 3;
|
||
|
}
|
||
|
// xmlns:* attributes second
|
||
|
if (name.startsWith('xmlns:')) {
|
||
|
return 2;
|
||
|
}
|
||
|
}
|
||
|
// other namespaces after and sort them alphabetically
|
||
|
if (name.includes(':')) {
|
||
|
return 1;
|
||
|
}
|
||
|
// other attributes
|
||
|
return 0;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @type {(a: [string, string], b: [string, string]) => number}
|
||
|
*/
|
||
|
const compareAttrs = ([aName], [bName]) => {
|
||
|
// sort namespaces
|
||
|
const aPriority = getNsPriority(aName);
|
||
|
const bPriority = getNsPriority(bName);
|
||
|
const priorityNs = bPriority - aPriority;
|
||
|
if (priorityNs !== 0) {
|
||
|
return priorityNs;
|
||
|
}
|
||
|
// extract the first part from attributes
|
||
|
// for example "fill" from "fill" and "fill-opacity"
|
||
|
const [aPart] = aName.split('-');
|
||
|
const [bPart] = bName.split('-');
|
||
|
// rely on alphabetical sort when the first part is the same
|
||
|
if (aPart !== bPart) {
|
||
|
const aInOrderFlag = order.includes(aPart) ? 1 : 0;
|
||
|
const bInOrderFlag = order.includes(bPart) ? 1 : 0;
|
||
|
// sort by position in order param
|
||
|
if (aInOrderFlag === 1 && bInOrderFlag === 1) {
|
||
|
return order.indexOf(aPart) - order.indexOf(bPart);
|
||
|
}
|
||
|
// put attributes from order param before others
|
||
|
const priorityOrder = bInOrderFlag - aInOrderFlag;
|
||
|
if (priorityOrder !== 0) {
|
||
|
return priorityOrder;
|
||
|
}
|
||
|
}
|
||
|
// sort alphabetically
|
||
|
return aName < bName ? -1 : 1;
|
||
|
};
|
||
|
|
||
|
return {
|
||
|
element: {
|
||
|
enter: (node) => {
|
||
|
const attrs = Object.entries(node.attributes);
|
||
|
attrs.sort(compareAttrs);
|
||
|
/**
|
||
|
* @type {Record<string, string>}
|
||
|
*/
|
||
|
const sortedAttributes = {};
|
||
|
for (const [name, value] of attrs) {
|
||
|
sortedAttributes[name] = value;
|
||
|
}
|
||
|
node.attributes = sortedAttributes;
|
||
|
},
|
||
|
},
|
||
|
};
|
||
|
};
|