You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
113 lines
2.7 KiB
113 lines
2.7 KiB
'use strict'; |
|
|
|
exports.type = 'visitor'; |
|
exports.name = 'sortAttrs'; |
|
exports.active = false; |
|
exports.description = 'Sort element attributes for better compression'; |
|
|
|
/** |
|
* Sort element attributes for better compression |
|
* |
|
* @author Nikolay Frantsev |
|
* |
|
* @type {import('../lib/types').Plugin<{ |
|
* order?: Array<string> |
|
* xmlnsOrder?: 'front' | 'alphabetical' |
|
* }>} |
|
*/ |
|
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; |
|
}, |
|
}, |
|
}; |
|
};
|
|
|