sortAttrs.js 2.7 KB

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