[6a3a178] | 1 | 'use strict';
|
---|
| 2 |
|
---|
| 3 | exports.type = 'visitor';
|
---|
| 4 | exports.name = 'sortAttrs';
|
---|
| 5 | exports.active = false;
|
---|
| 6 | exports.description = 'Sort element attributes for better compression';
|
---|
| 7 |
|
---|
| 8 | /**
|
---|
| 9 | * Sort element attributes for better compression
|
---|
| 10 | *
|
---|
| 11 | * @author Nikolay Frantsev
|
---|
| 12 | *
|
---|
| 13 | * @type {import('../lib/types').Plugin<{
|
---|
| 14 | * order?: Array<string>
|
---|
| 15 | * xmlnsOrder?: 'front' | 'alphabetical'
|
---|
| 16 | * }>}
|
---|
| 17 | */
|
---|
| 18 | exports.fn = (_root, params) => {
|
---|
| 19 | const {
|
---|
| 20 | order = [
|
---|
| 21 | 'id',
|
---|
| 22 | 'width',
|
---|
| 23 | 'height',
|
---|
| 24 | 'x',
|
---|
| 25 | 'x1',
|
---|
| 26 | 'x2',
|
---|
| 27 | 'y',
|
---|
| 28 | 'y1',
|
---|
| 29 | 'y2',
|
---|
| 30 | 'cx',
|
---|
| 31 | 'cy',
|
---|
| 32 | 'r',
|
---|
| 33 | 'fill',
|
---|
| 34 | 'stroke',
|
---|
| 35 | 'marker',
|
---|
| 36 | 'd',
|
---|
| 37 | 'points',
|
---|
| 38 | ],
|
---|
| 39 | xmlnsOrder = 'front',
|
---|
| 40 | } = params;
|
---|
| 41 |
|
---|
| 42 | /**
|
---|
| 43 | * @type {(name: string) => number}
|
---|
| 44 | */
|
---|
| 45 | const getNsPriority = (name) => {
|
---|
| 46 | if (xmlnsOrder === 'front') {
|
---|
| 47 | // put xmlns first
|
---|
| 48 | if (name === 'xmlns') {
|
---|
| 49 | return 3;
|
---|
| 50 | }
|
---|
| 51 | // xmlns:* attributes second
|
---|
| 52 | if (name.startsWith('xmlns:')) {
|
---|
| 53 | return 2;
|
---|
| 54 | }
|
---|
| 55 | }
|
---|
| 56 | // other namespaces after and sort them alphabetically
|
---|
| 57 | if (name.includes(':')) {
|
---|
| 58 | return 1;
|
---|
| 59 | }
|
---|
| 60 | // other attributes
|
---|
| 61 | return 0;
|
---|
| 62 | };
|
---|
| 63 |
|
---|
| 64 | /**
|
---|
| 65 | * @type {(a: [string, string], b: [string, string]) => number}
|
---|
| 66 | */
|
---|
| 67 | const compareAttrs = ([aName], [bName]) => {
|
---|
| 68 | // sort namespaces
|
---|
| 69 | const aPriority = getNsPriority(aName);
|
---|
| 70 | const bPriority = getNsPriority(bName);
|
---|
| 71 | const priorityNs = bPriority - aPriority;
|
---|
| 72 | if (priorityNs !== 0) {
|
---|
| 73 | return priorityNs;
|
---|
| 74 | }
|
---|
| 75 | // extract the first part from attributes
|
---|
| 76 | // for example "fill" from "fill" and "fill-opacity"
|
---|
| 77 | const [aPart] = aName.split('-');
|
---|
| 78 | const [bPart] = bName.split('-');
|
---|
| 79 | // rely on alphabetical sort when the first part is the same
|
---|
| 80 | if (aPart !== bPart) {
|
---|
| 81 | const aInOrderFlag = order.includes(aPart) ? 1 : 0;
|
---|
| 82 | const bInOrderFlag = order.includes(bPart) ? 1 : 0;
|
---|
| 83 | // sort by position in order param
|
---|
| 84 | if (aInOrderFlag === 1 && bInOrderFlag === 1) {
|
---|
| 85 | return order.indexOf(aPart) - order.indexOf(bPart);
|
---|
| 86 | }
|
---|
| 87 | // put attributes from order param before others
|
---|
| 88 | const priorityOrder = bInOrderFlag - aInOrderFlag;
|
---|
| 89 | if (priorityOrder !== 0) {
|
---|
| 90 | return priorityOrder;
|
---|
| 91 | }
|
---|
| 92 | }
|
---|
| 93 | // sort alphabetically
|
---|
| 94 | return aName < bName ? -1 : 1;
|
---|
| 95 | };
|
---|
| 96 |
|
---|
| 97 | return {
|
---|
| 98 | element: {
|
---|
| 99 | enter: (node) => {
|
---|
| 100 | const attrs = Object.entries(node.attributes);
|
---|
| 101 | attrs.sort(compareAttrs);
|
---|
| 102 | /**
|
---|
| 103 | * @type {Record<string, string>}
|
---|
| 104 | */
|
---|
| 105 | const sortedAttributes = {};
|
---|
| 106 | for (const [name, value] of attrs) {
|
---|
| 107 | sortedAttributes[name] = value;
|
---|
| 108 | }
|
---|
| 109 | node.attributes = sortedAttributes;
|
---|
| 110 | },
|
---|
| 111 | },
|
---|
| 112 | };
|
---|
| 113 | };
|
---|