code:
'use strict';

// TODO implement as separate plugin

const {
  transformsMultiply,
  transform2js,
  transformArc,
} = require('./_transforms.js');
const { removeLeadingZero } = require('../lib/svgo/tools.js');
const { referencesProps, attrsGroupsDefaults } = require('./_collections.js');

const regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
const defaultStrokeWidth = attrsGroupsDefaults.presentation['stroke-width'];

/**
 * Apply transformation(s) to the Path data.
 *
 * @param {Object} elem current element
 * @param {Array} path input path data
 * @param {Object} params whether to apply transforms to stroked lines and transform precision (used for stroke width)
 * @return {Array} output path data
 */
const applyTransforms = (elem, pathData, params) => {
  // if there are no 'stroke' attr and references to other objects such as
  // gradiends or clip-path which are also subjects to transform.
  if (
    elem.attributes.transform == null ||
    elem.attributes.transform === '' ||
    // styles are not considered when applying transform
    // can be fixed properly with new style engine
    elem.attributes.style != null ||
    Object.entries(elem.attributes).some(
      ([name, value]) =>
        referencesProps.includes(name) && value.includes('url(')
    )
  ) {
    return;
  }

  const matrix = transformsMultiply(transform2js(elem.attributes.transform));
  const stroke = elem.computedAttr('stroke');
  const id = elem.computedAttr('id');
  const transformPrecision = params.transformPrecision;

  if (stroke && stroke != 'none') {
    if (
      !params.applyTransformsStroked ||
      ((matrix.data[0] != matrix.data[3] ||
        matrix.data[1] != -matrix.data[2]) &&
        (matrix.data[0] != -matrix.data[3] || matrix.data[1] != matrix.data[2]))
    )
      return;

    // "stroke-width" should be inside the part with ID, otherwise it can be overrided in <use>
    if (id) {
      let idElem = elem;
      let hasStrokeWidth = false;

      do {
        if (idElem.attributes['stroke-width']) {
          hasStrokeWidth = true;
        }
      } while (
        idElem.attributes.id !== id &&
        !hasStrokeWidth &&
        (idElem = idElem.parentNode)
      );

      if (!hasStrokeWidth) return;
    }

    const scale = +Math.sqrt(
      matrix.data[0] * matrix.data[0] + matrix.data[1] * matrix.data[1]
    ).toFixed(transformPrecision);

    if (scale !== 1) {
      const strokeWidth =
        elem.computedAttr('stroke-width') || defaultStrokeWidth;

      if (
        elem.attributes['vector-effect'] == null ||
        elem.attributes['vector-effect'] !== 'non-scaling-stroke'
      ) {
        if (elem.attributes['stroke-width'] != null) {
          elem.attributes['stroke-width'] = elem.attributes['stroke-width']
            .trim()
            .replace(regNumericValues, (num) => removeLeadingZero(num * scale));
        } else {
          elem.attributes['stroke-width'] = strokeWidth.replace(
            regNumericValues,
            (num) => removeLeadingZero(num * scale)
          );
        }

        if (elem.attributes['stroke-dashoffset'] != null) {
          elem.attributes['stroke-dashoffset'] = elem.attributes[
            'stroke-dashoffset'
          ]
            .trim()
            .replace(regNumericValues, (num) => removeLeadingZero(num * scale));
        }

        if (elem.attributes['stroke-dasharray'] != null) {
          elem.attributes['stroke-dasharray'] = elem.attributes[
            'stroke-dasharray'
          ]
            .trim()
            .replace(regNumericValues, (num) => removeLeadingZero(num * scale));
        }
      }
    }
  } else if (id) {
    // Stroke and stroke-width can be redefined with <use>
    return;
  }

  applyMatrixToPathData(pathData, matrix.data);

  // remove transform attr
  delete elem.attributes.transform;

  return;
};
exports.applyTransforms = applyTransforms;

const transformAbsolutePoint = (matrix, x, y) => {
  const newX = matrix[0] * x + matrix[2] * y + matrix[4];
  const newY = matrix[1] * x + matrix[3] * y + matrix[5];
  return [newX, newY];
};

const transformRelativePoint = (matrix, x, y) => {
  const newX = matrix[0] * x + matrix[2] * y;
  const newY = matrix[1] * x + matrix[3] * y;
  return [newX, newY];
};

const applyMatrixToPathData = (pathData, matrix) => {
  const start = [0, 0];
  const cursor = [0, 0];

  for (const pathItem of pathData) {
    let { command, args } = pathItem;

    // moveto (x y)
    if (command === 'M') {
      cursor[0] = args[0];
      cursor[1] = args[1];
      start[0] = cursor[0];
      start[1] = cursor[1];
      const [x, y] = transformAbsolutePoint(matrix, args[0], args[1]);
      args[0] = x;
      args[1] = y;
    }
    if (command === 'm') {
      cursor[0] += args[0];
      cursor[1] += args[1];
      start[0] = cursor[0];
      start[1] = cursor[1];
      const [x, y] = transformRelativePoint(matrix, args[0], args[1]);
      args[0] = x;
      args[1] = y;
    }

    // horizontal lineto (x)
    // convert to lineto to handle two-dimentional transforms
    if (command === 'H') {
      command = 'L';
      args = [args[0], cursor[1]];
    }
    if (command === 'h') {
      command = 'l';
      args = [args[0], 0];
    }

    // vertical lineto (y)
    // convert to lineto to handle two-dimentional transforms
    if (command === 'V') {
      command = 'L';
      args = [cursor[0], args[0]];
    }
    if (command === 'v') {
      command = 'l';
      args = [0, args[0]];
    }

    // lineto (x y)
    if (command === 'L') {
      cursor[0] = args[0];
      cursor[1] = args[1];
      const [x, y] = transformAbsolutePoint(matrix, args[0], args[1]);
      args[0] = x;
      args[1] = y;
    }
    if (command === 'l') {
      cursor[0] += args[0];
      cursor[1] += args[1];
      const [x, y] = transformRelativePoint(matrix, args[0], args[1]);
      args[0] = x;
      args[1] = y;
    }

    // curveto (x1 y1 x2 y2 x y)
    if (command === 'C') {
      cursor[0] = args[4];
      cursor[1] = args[5];
      const [x1, y1] = transformAbsolutePoint(matrix, args[0], args[1]);
      const [x2, y2] = transformAbsolutePoint(matrix, args[2], args[3]);
      const [x, y] = transformAbsolutePoint(matrix, args[4], args[5]);
      args[0] = x1;
      args[1] = y1;
      args[2] = x2;
      args[3] = y2;
      args[4] = x;
      args[5] = y;
    }
    if (command === 'c') {
      cursor[0] += args[4];
      cursor[1] += args[5];
      const [x1, y1] = transformRelativePoint(matrix, args[0], args[1]);
      const [x2, y2] = transformRelativePoint(matrix, args[2], args[3]);
      const [x, y] = transformRelativePoint(matrix, args[4], args[5]);
      args[0] = x1;
      args[1] = y1;
      args[2] = x2;
      args[3] = y2;
      args[4] = x;
      args[5] = y;
    }

    // smooth curveto (x2 y2 x y)
    if (command === 'S') {
      cursor[0] = args[2];
      cursor[1] = args[3];
      const [x2, y2] = transformAbsolutePoint(matrix, args[0], args[1]);
      const [x, y] = transformAbsolutePoint(matrix, args[2], args[3]);
      args[0] = x2;
      args[1] = y2;
      args[2] = x;
      args[3] = y;
    }
    if (command === 's') {
      cursor[0] += args[2];
      cursor[1] += args[3];
      const [x2, y2] = transformRelativePoint(matrix, args[0], args[1]);
      const [x, y] = transformRelativePoint(matrix, args[2], args[3]);
      args[0] = x2;
      args[1] = y2;
      args[2] = x;
      args[3] = y;
    }

    // quadratic Bézier curveto (x1 y1 x y)
    if (command === 'Q') {
      cursor[0] = args[2];
      cursor[1] = args[3];
      const [x1, y1] = transformAbsolutePoint(matrix, args[0], args[1]);
      const [x, y] = transformAbsolutePoint(matrix, args[2], args[3]);
      args[0] = x1;
      args[1] = y1;
      args[2] = x;
      args[3] = y;
    }
    if (command === 'q') {
      cursor[0] += args[2];
      cursor[1] += args[3];
      const [x1, y1] = transformRelativePoint(matrix, args[0], args[1]);
      const [x, y] = transformRelativePoint(matrix, args[2], args[3]);
      args[0] = x1;
      args[1] = y1;
      args[2] = x;
      args[3] = y;
    }

    // smooth quadratic Bézier curveto (x y)
    if (command === 'T') {
      cursor[0] = args[0];
      cursor[1] = args[1];
      const [x, y] = transformAbsolutePoint(matrix, args[0], args[1]);
      args[0] = x;
      args[1] = y;
    }
    if (command === 't') {
      cursor[0] += args[0];
      cursor[1] += args[1];
      const [x, y] = transformRelativePoint(matrix, args[0], args[1]);
      args[0] = x;
      args[1] = y;
    }

    // elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
    if (command === 'A') {
      transformArc(cursor, args, matrix);
      cursor[0] = args[5];
      cursor[1] = args[6];
      // reduce number of digits in rotation angle
      if (Math.abs(args[2]) > 80) {
        const a = args[0];
        const rotation = args[2];
        args[0] = args[1];
        args[1] = a;
        args[2] = rotation + (rotation > 0 ? -90 : 90);
      }
      const [x, y] = transformAbsolutePoint(matrix, args[5], args[6]);
      args[5] = x;
      args[6] = y;
    }
    if (command === 'a') {
      transformArc([0, 0], args, matrix);
      cursor[0] += args[5];
      cursor[1] += args[6];
      // reduce number of digits in rotation angle
      if (Math.abs(args[2]) > 80) {
        const a = args[0];
        const rotation = args[2];
        args[0] = args[1];
        args[1] = a;
        args[2] = rotation + (rotation > 0 ? -90 : 90);
      }
      const [x, y] = transformRelativePoint(matrix, args[5], args[6]);
      args[5] = x;
      args[6] = y;
    }

    // closepath
    if (command === 'z' || command === 'Z') {
      cursor[0] = start[0];
      cursor[1] = start[1];
    }

    pathItem.command = command;
    pathItem.args = args;
  }
};


code:
'use strict';

// https://www.w3.org/TR/SVG11/intro.html#Definitions

/**
 * @type {Record<string, Array<string>>}
 */
exports.elemsGroups = {
  animation: [
    'animate',
    'animateColor',
    'animateMotion',
    'animateTransform',
    'set',
  ],
  descriptive: ['desc', 'metadata', 'title'],
  shape: ['circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect'],
  structural: ['defs', 'g', 'svg', 'symbol', 'use'],
  paintServer: [
    'solidColor',
    'linearGradient',
    'radialGradient',
    'meshGradient',
    'pattern',
    'hatch',
  ],
  nonRendering: [
    'linearGradient',
    'radialGradient',
    'pattern',
    'clipPath',
    'mask',
    'marker',
    'symbol',
    'filter',
    'solidColor',
  ],
  container: [
    'a',
    'defs',
    'g',
    'marker',
    'mask',
    'missing-glyph',
    'pattern',
    'svg',
    'switch',
    'symbol',
    'foreignObject',
  ],
  textContent: [
    'altGlyph',
    'altGlyphDef',
    'altGlyphItem',
    'glyph',
    'glyphRef',
    'textPath',
    'text',
    'tref',
    'tspan',
  ],
  textContentChild: ['altGlyph', 'textPath', 'tref', 'tspan'],
  lightSource: [
    'feDiffuseLighting',
    'feSpecularLighting',
    'feDistantLight',
    'fePointLight',
    'feSpotLight',
  ],
  filterPrimitive: [
    'feBlend',
    'feColorMatrix',
    'feComponentTransfer',
    'feComposite',
    'feConvolveMatrix',
    'feDiffuseLighting',
    'feDisplacementMap',
    'feDropShadow',
    'feFlood',
    'feFuncA',
    'feFuncB',
    'feFuncG',
    'feFuncR',
    'feGaussianBlur',
    'feImage',
    'feMerge',
    'feMergeNode',
    'feMorphology',
    'feOffset',
    'feSpecularLighting',
    'feTile',
    'feTurbulence',
  ],
};

exports.textElems = exports.elemsGroups.textContent.concat('title');

exports.pathElems = ['path', 'glyph', 'missing-glyph'];

// https://www.w3.org/TR/SVG11/intro.html#Definitions
/**
 * @type {Record<string, Array<string>>}
 */
exports.attrsGroups = {
  animationAddition: ['additive', 'accumulate'],
  animationAttributeTarget: ['attributeType', 'attributeName'],
  animationEvent: ['onbegin', 'onend', 'onrepeat', 'onload'],
  animationTiming: [
    'begin',
    'dur',
    'end',
    'min',
    'max',
    'restart',
    'repeatCount',
    'repeatDur',
    'fill',
  ],
  animationValue: [
    'calcMode',
    'values',
    'keyTimes',
    'keySplines',
    'from',
    'to',
    'by',
  ],
  conditionalProcessing: [
    'requiredFeatures',
    'requiredExtensions',
    'systemLanguage',
  ],
  core: ['id', 'tabindex', 'xml:base', 'xml:lang', 'xml:space'],
  graphicalEvent: [
    'onfocusin',
    'onfocusout',
    'onactivate',
    'onclick',
    'onmousedown',
    'onmouseup',
    'onmouseover',
    'onmousemove',
    'onmouseout',
    'onload',
  ],
  presentation: [
    'alignment-baseline',
    'baseline-shift',
    'clip',
    'clip-path',
    'clip-rule',
    'color',
    'color-interpolation',
    'color-interpolation-filters',
    'color-profile',
    'color-rendering',
    'cursor',
    'direction',
    'display',
    'dominant-baseline',
    'enable-background',
    'fill',
    'fill-opacity',
    'fill-rule',
    'filter',
    'flood-color',
    'flood-opacity',
    'font-family',
    'font-size',
    'font-size-adjust',
    'font-stretch',
    'font-style',
    'font-variant',
    'font-weight',
    'glyph-orientation-horizontal',
    'glyph-orientation-vertical',
    'image-rendering',
    'letter-spacing',
    'lighting-color',
    'marker-end',
    'marker-mid',
    'marker-start',
    'mask',
    'opacity',
    'overflow',
    'paint-order',
    'pointer-events',
    'shape-rendering',
    'stop-color',
    'stop-opacity',
    'stroke',
    'stroke-dasharray',
    'stroke-dashoffset',
    'stroke-linecap',
    'stroke-linejoin',
    'stroke-miterlimit',
    'stroke-opacity',
    'stroke-width',
    'text-anchor',
    'text-decoration',
    'text-overflow',
    'text-rendering',
    'transform',
    'transform-origin',
    'unicode-bidi',
    'vector-effect',
    'visibility',
    'word-spacing',
    'writing-mode',
  ],
  xlink: [
    'xlink:href',
    'xlink:show',
    'xlink:actuate',
    'xlink:type',
    'xlink:role',
    'xlink:arcrole',
    'xlink:title',
  ],
  documentEvent: [
    'onunload',
    'onabort',
    'onerror',
    'onresize',
    'onscroll',
    'onzoom',
  ],
  filterPrimitive: ['x', 'y', 'width', 'height', 'result'],
  transferFunction: [
    'type',
    'tableValues',
    'slope',
    'intercept',
    'amplitude',
    'exponent',
    'offset',
  ],
};

/**
 * @type {Record<string, Record<string, string>>}
 */
exports.attrsGroupsDefaults = {
  core: { 'xml:space': 'default' },
  presentation: {
    clip: 'auto',
    'clip-path': 'none',
    'clip-rule': 'nonzero',
    mask: 'none',
    opacity: '1',
    'stop-color': '#000',
    'stop-opacity': '1',
    'fill-opacity': '1',
    'fill-rule': 'nonzero',
    fill: '#000',
    stroke: 'none',
    'stroke-width': '1',
    'stroke-linecap': 'butt',
    'stroke-linejoin': 'miter',
    'stroke-miterlimit': '4',
    'stroke-dasharray': 'none',
    'stroke-dashoffset': '0',
    'stroke-opacity': '1',
    'paint-order': 'normal',
    'vector-effect': 'none',
    display: 'inline',
    visibility: 'visible',
    'marker-start': 'none',
    'marker-mid': 'none',
    'marker-end': 'none',
    'color-interpolation': 'sRGB',
    'color-interpolation-filters': 'linearRGB',
    'color-rendering': 'auto',
    'shape-rendering': 'auto',
    'text-rendering': 'auto',
    'image-rendering': 'auto',
    'font-style': 'normal',
    'font-variant': 'normal',
    'font-weight': 'normal',
    'font-stretch': 'normal',
    'font-size': 'medium',
    'font-size-adjust': 'none',
    kerning: 'auto',
    'letter-spacing': 'normal',
    'word-spacing': 'normal',
    'text-decoration': 'none',
    'text-anchor': 'start',
    'text-overflow': 'clip',
    'writing-mode': 'lr-tb',
    'glyph-orientation-vertical': 'auto',
    'glyph-orientation-horizontal': '0deg',
    direction: 'ltr',
    'unicode-bidi': 'normal',
    'dominant-baseline': 'auto',
    'alignment-baseline': 'baseline',
    'baseline-shift': 'baseline',
  },
  transferFunction: {
    slope: '1',
    intercept: '0',
    amplitude: '1',
    exponent: '1',
    offset: '0',
  },
};

// https://www.w3.org/TR/SVG11/eltindex.html
/**
 * @type {Record<string, {
 *   attrsGroups: Array<string>,
 *   attrs?: Array<string>,
 *   defaults?: Record<string, string>,
 *   contentGroups?: Array<string>,
 *   content?: Array<string>,
 * }>}
 */
exports.elems = {
  a: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
      'xlink',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'transform',
      'target',
    ],
    defaults: {
      target: '_self',
    },
    contentGroups: [
      'animation',
      'descriptive',
      'shape',
      'structural',
      'paintServer',
    ],
    content: [
      'a',
      'altGlyphDef',
      'clipPath',
      'color-profile',
      'cursor',
      'filter',
      'font',
      'font-face',
      'foreignObject',
      'image',
      'marker',
      'mask',
      'pattern',
      'script',
      'style',
      'switch',
      'text',
      'view',
      // not spec compliant
      'tspan',
    ],
  },
  altGlyph: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
      'xlink',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'x',
      'y',
      'dx',
      'dy',
      'glyphRef',
      'format',
      'rotate',
    ],
  },
  altGlyphDef: {
    attrsGroups: ['core'],
    content: ['glyphRef'],
  },
  altGlyphItem: {
    attrsGroups: ['core'],
    content: ['glyphRef', 'altGlyphItem'],
  },
  animate: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'animationAddition',
      'animationAttributeTarget',
      'animationEvent',
      'animationTiming',
      'animationValue',
      'presentation',
      'xlink',
    ],
    attrs: ['externalResourcesRequired'],
    contentGroups: ['descriptive'],
  },
  animateColor: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'animationEvent',
      'xlink',
      'animationAttributeTarget',
      'animationTiming',
      'animationValue',
      'animationAddition',
      'presentation',
    ],
    attrs: ['externalResourcesRequired'],
    contentGroups: ['descriptive'],
  },
  animateMotion: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'animationEvent',
      'xlink',
      'animationTiming',
      'animationValue',
      'animationAddition',
    ],
    attrs: [
      'externalResourcesRequired',
      'path',
      'keyPoints',
      'rotate',
      'origin',
    ],
    defaults: {
      rotate: '0',
    },
    contentGroups: ['descriptive'],
    content: ['mpath'],
  },
  animateTransform: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'animationEvent',
      'xlink',
      'animationAttributeTarget',
      'animationTiming',
      'animationValue',
      'animationAddition',
    ],
    attrs: ['externalResourcesRequired', 'type'],
    contentGroups: ['descriptive'],
  },
  circle: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'transform',
      'cx',
      'cy',
      'r',
    ],
    defaults: {
      cx: '0',
      cy: '0',
    },
    contentGroups: ['animation', 'descriptive'],
  },
  clipPath: {
    attrsGroups: ['conditionalProcessing', 'core', 'presentation'],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'transform',
      'clipPathUnits',
    ],
    defaults: {
      clipPathUnits: 'userSpaceOnUse',
    },
    contentGroups: ['animation', 'descriptive', 'shape'],
    content: ['text', 'use'],
  },
  'color-profile': {
    attrsGroups: ['core', 'xlink'],
    attrs: ['local', 'name', 'rendering-intent'],
    defaults: {
      name: 'sRGB',
      'rendering-intent': 'auto',
    },
    contentGroups: ['descriptive'],
  },
  cursor: {
    attrsGroups: ['core', 'conditionalProcessing', 'xlink'],
    attrs: ['externalResourcesRequired', 'x', 'y'],
    defaults: {
      x: '0',
      y: '0',
    },
    contentGroups: ['descriptive'],
  },
  defs: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
    ],
    attrs: ['class', 'style', 'externalResourcesRequired', 'transform'],
    contentGroups: [
      'animation',
      'descriptive',
      'shape',
      'structural',
      'paintServer',
    ],
    content: [
      'a',
      'altGlyphDef',
      'clipPath',
      'color-profile',
      'cursor',
      'filter',
      'font',
      'font-face',
      'foreignObject',
      'image',
      'marker',
      'mask',
      'pattern',
      'script',
      'style',
      'switch',
      'text',
      'view',
    ],
  },
  desc: {
    attrsGroups: ['core'],
    attrs: ['class', 'style'],
  },
  ellipse: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'transform',
      'cx',
      'cy',
      'rx',
      'ry',
    ],
    defaults: {
      cx: '0',
      cy: '0',
    },
    contentGroups: ['animation', 'descriptive'],
  },
  feBlend: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: [
      'class',
      'style',
      // TODO: in - 'If no value is provided and this is the first filter primitive,
      // then this filter primitive will use SourceGraphic as its input'
      'in',
      'in2',
      'mode',
    ],
    defaults: {
      mode: 'normal',
    },
    content: ['animate', 'set'],
  },
  feColorMatrix: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: ['class', 'style', 'in', 'type', 'values'],
    defaults: {
      type: 'matrix',
    },
    content: ['animate', 'set'],
  },
  feComponentTransfer: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: ['class', 'style', 'in'],
    content: ['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR'],
  },
  feComposite: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: ['class', 'style', 'in', 'in2', 'operator', 'k1', 'k2', 'k3', 'k4'],
    defaults: {
      operator: 'over',
      k1: '0',
      k2: '0',
      k3: '0',
      k4: '0',
    },
    content: ['animate', 'set'],
  },
  feConvolveMatrix: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: [
      'class',
      'style',
      'in',
      'order',
      'kernelMatrix',
      // TODO: divisor - 'The default value is the sum of all values in kernelMatrix,
      // with the exception that if the sum is zero, then the divisor is set to 1'
      'divisor',
      'bias',
      // TODO: targetX - 'By default, the convolution matrix is centered in X over each
      // pixel of the input image (i.e., targetX = floor ( orderX / 2 ))'
      'targetX',
      'targetY',
      'edgeMode',
      // TODO: kernelUnitLength - 'The first number is the <dx> value. The second number
      // is the <dy> value. If the <dy> value is not specified, it defaults to the same value as <dx>'
      'kernelUnitLength',
      'preserveAlpha',
    ],
    defaults: {
      order: '3',
      bias: '0',
      edgeMode: 'duplicate',
      preserveAlpha: 'false',
    },
    content: ['animate', 'set'],
  },
  feDiffuseLighting: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: [
      'class',
      'style',
      'in',
      'surfaceScale',
      'diffuseConstant',
      'kernelUnitLength',
    ],
    defaults: {
      surfaceScale: '1',
      diffuseConstant: '1',
    },
    contentGroups: ['descriptive'],
    content: [
      // TODO: 'exactly one light source element, in any order'
      'feDistantLight',
      'fePointLight',
      'feSpotLight',
    ],
  },
  feDisplacementMap: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: [
      'class',
      'style',
      'in',
      'in2',
      'scale',
      'xChannelSelector',
      'yChannelSelector',
    ],
    defaults: {
      scale: '0',
      xChannelSelector: 'A',
      yChannelSelector: 'A',
    },
    content: ['animate', 'set'],
  },
  feDistantLight: {
    attrsGroups: ['core'],
    attrs: ['azimuth', 'elevation'],
    defaults: {
      azimuth: '0',
      elevation: '0',
    },
    content: ['animate', 'set'],
  },
  feFlood: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: ['class', 'style'],
    content: ['animate', 'animateColor', 'set'],
  },
  feFuncA: {
    attrsGroups: ['core', 'transferFunction'],
    content: ['set', 'animate'],
  },
  feFuncB: {
    attrsGroups: ['core', 'transferFunction'],
    content: ['set', 'animate'],
  },
  feFuncG: {
    attrsGroups: ['core', 'transferFunction'],
    content: ['set', 'animate'],
  },
  feFuncR: {
    attrsGroups: ['core', 'transferFunction'],
    content: ['set', 'animate'],
  },
  feGaussianBlur: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: ['class', 'style', 'in', 'stdDeviation'],
    defaults: {
      stdDeviation: '0',
    },
    content: ['set', 'animate'],
  },
  feImage: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive', 'xlink'],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'preserveAspectRatio',
      'href',
      'xlink:href',
    ],
    defaults: {
      preserveAspectRatio: 'xMidYMid meet',
    },
    content: ['animate', 'animateTransform', 'set'],
  },
  feMerge: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: ['class', 'style'],
    content: ['feMergeNode'],
  },
  feMergeNode: {
    attrsGroups: ['core'],
    attrs: ['in'],
    content: ['animate', 'set'],
  },
  feMorphology: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: ['class', 'style', 'in', 'operator', 'radius'],
    defaults: {
      operator: 'erode',
      radius: '0',
    },
    content: ['animate', 'set'],
  },
  feOffset: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: ['class', 'style', 'in', 'dx', 'dy'],
    defaults: {
      dx: '0',
      dy: '0',
    },
    content: ['animate', 'set'],
  },
  fePointLight: {
    attrsGroups: ['core'],
    attrs: ['x', 'y', 'z'],
    defaults: {
      x: '0',
      y: '0',
      z: '0',
    },
    content: ['animate', 'set'],
  },
  feSpecularLighting: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: [
      'class',
      'style',
      'in',
      'surfaceScale',
      'specularConstant',
      'specularExponent',
      'kernelUnitLength',
    ],
    defaults: {
      surfaceScale: '1',
      specularConstant: '1',
      specularExponent: '1',
    },
    contentGroups: [
      'descriptive',
      // TODO: exactly one 'light source element'
      'lightSource',
    ],
  },
  feSpotLight: {
    attrsGroups: ['core'],
    attrs: [
      'x',
      'y',
      'z',
      'pointsAtX',
      'pointsAtY',
      'pointsAtZ',
      'specularExponent',
      'limitingConeAngle',
    ],
    defaults: {
      x: '0',
      y: '0',
      z: '0',
      pointsAtX: '0',
      pointsAtY: '0',
      pointsAtZ: '0',
      specularExponent: '1',
    },
    content: ['animate', 'set'],
  },
  feTile: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: ['class', 'style', 'in'],
    content: ['animate', 'set'],
  },
  feTurbulence: {
    attrsGroups: ['core', 'presentation', 'filterPrimitive'],
    attrs: [
      'class',
      'style',
      'baseFrequency',
      'numOctaves',
      'seed',
      'stitchTiles',
      'type',
    ],
    defaults: {
      baseFrequency: '0',
      numOctaves: '1',
      seed: '0',
      stitchTiles: 'noStitch',
      type: 'turbulence',
    },
    content: ['animate', 'set'],
  },
  filter: {
    attrsGroups: ['core', 'presentation', 'xlink'],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'x',
      'y',
      'width',
      'height',
      'filterRes',
      'filterUnits',
      'primitiveUnits',
      'href',
      'xlink:href',
    ],
    defaults: {
      primitiveUnits: 'userSpaceOnUse',
      x: '-10%',
      y: '-10%',
      width: '120%',
      height: '120%',
    },
    contentGroups: ['descriptive', 'filterPrimitive'],
    content: ['animate', 'set'],
  },
  font: {
    attrsGroups: ['core', 'presentation'],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'horiz-origin-x',
      'horiz-origin-y',
      'horiz-adv-x',
      'vert-origin-x',
      'vert-origin-y',
      'vert-adv-y',
    ],
    defaults: {
      'horiz-origin-x': '0',
      'horiz-origin-y': '0',
    },
    contentGroups: ['descriptive'],
    content: ['font-face', 'glyph', 'hkern', 'missing-glyph', 'vkern'],
  },
  'font-face': {
    attrsGroups: ['core'],
    attrs: [
      'font-family',
      'font-style',
      'font-variant',
      'font-weight',
      'font-stretch',
      'font-size',
      'unicode-range',
      'units-per-em',
      'panose-1',
      'stemv',
      'stemh',
      'slope',
      'cap-height',
      'x-height',
      'accent-height',
      'ascent',
      'descent',
      'widths',
      'bbox',
      'ideographic',
      'alphabetic',
      'mathematical',
      'hanging',
      'v-ideographic',
      'v-alphabetic',
      'v-mathematical',
      'v-hanging',
      'underline-position',
      'underline-thickness',
      'strikethrough-position',
      'strikethrough-thickness',
      'overline-position',
      'overline-thickness',
    ],
    defaults: {
      'font-style': 'all',
      'font-variant': 'normal',
      'font-weight': 'all',
      'font-stretch': 'normal',
      'unicode-range': 'U+0-10FFFF',
      'units-per-em': '1000',
      'panose-1': '0 0 0 0 0 0 0 0 0 0',
      slope: '0',
    },
    contentGroups: ['descriptive'],
    content: [
      // TODO: "at most one 'font-face-src' element"
      'font-face-src',
    ],
  },
  // TODO: empty content
  'font-face-format': {
    attrsGroups: ['core'],
    attrs: ['string'],
  },
  'font-face-name': {
    attrsGroups: ['core'],
    attrs: ['name'],
  },
  'font-face-src': {
    attrsGroups: ['core'],
    content: ['font-face-name', 'font-face-uri'],
  },
  'font-face-uri': {
    attrsGroups: ['core', 'xlink'],
    attrs: ['href', 'xlink:href'],
    content: ['font-face-format'],
  },
  foreignObject: {
    attrsGroups: [
      'core',
      'conditionalProcessing',
      'graphicalEvent',
      'presentation',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'transform',
      'x',
      'y',
      'width',
      'height',
    ],
    defaults: {
      x: '0',
      y: '0',
    },
  },
  g: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
    ],
    attrs: ['class', 'style', 'externalResourcesRequired', 'transform'],
    contentGroups: [
      'animation',
      'descriptive',
      'shape',
      'structural',
      'paintServer',
    ],
    content: [
      'a',
      'altGlyphDef',
      'clipPath',
      'color-profile',
      'cursor',
      'filter',
      'font',
      'font-face',
      'foreignObject',
      'image',
      'marker',
      'mask',
      'pattern',
      'script',
      'style',
      'switch',
      'text',
      'view',
    ],
  },
  glyph: {
    attrsGroups: ['core', 'presentation'],
    attrs: [
      'class',
      'style',
      'd',
      'horiz-adv-x',
      'vert-origin-x',
      'vert-origin-y',
      'vert-adv-y',
      'unicode',
      'glyph-name',
      'orientation',
      'arabic-form',
      'lang',
    ],
    defaults: {
      'arabic-form': 'initial',
    },
    contentGroups: [
      'animation',
      'descriptive',
      'shape',
      'structural',
      'paintServer',
    ],
    content: [
      'a',
      'altGlyphDef',
      'clipPath',
      'color-profile',
      'cursor',
      'filter',
      'font',
      'font-face',
      'foreignObject',
      'image',
      'marker',
      'mask',
      'pattern',
      'script',
      'style',
      'switch',
      'text',
      'view',
    ],
  },
  glyphRef: {
    attrsGroups: ['core', 'presentation'],
    attrs: [
      'class',
      'style',
      'd',
      'horiz-adv-x',
      'vert-origin-x',
      'vert-origin-y',
      'vert-adv-y',
    ],
    contentGroups: [
      'animation',
      'descriptive',
      'shape',
      'structural',
      'paintServer',
    ],
    content: [
      'a',
      'altGlyphDef',
      'clipPath',
      'color-profile',
      'cursor',
      'filter',
      'font',
      'font-face',
      'foreignObject',
      'image',
      'marker',
      'mask',
      'pattern',
      'script',
      'style',
      'switch',
      'text',
      'view',
    ],
  },
  hatch: {
    attrsGroups: ['core', 'presentation', 'xlink'],
    attrs: [
      'class',
      'style',
      'x',
      'y',
      'pitch',
      'rotate',
      'hatchUnits',
      'hatchContentUnits',
      'transform',
    ],
    defaults: {
      hatchUnits: 'objectBoundingBox',
      hatchContentUnits: 'userSpaceOnUse',
      x: '0',
      y: '0',
      pitch: '0',
      rotate: '0',
    },
    contentGroups: ['animation', 'descriptive'],
    content: ['hatchPath'],
  },
  hatchPath: {
    attrsGroups: ['core', 'presentation', 'xlink'],
    attrs: ['class', 'style', 'd', 'offset'],
    defaults: {
      offset: '0',
    },
    contentGroups: ['animation', 'descriptive'],
  },
  hkern: {
    attrsGroups: ['core'],
    attrs: ['u1', 'g1', 'u2', 'g2', 'k'],
  },
  image: {
    attrsGroups: [
      'core',
      'conditionalProcessing',
      'graphicalEvent',
      'xlink',
      'presentation',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'preserveAspectRatio',
      'transform',
      'x',
      'y',
      'width',
      'height',
      'href',
      'xlink:href',
    ],
    defaults: {
      x: '0',
      y: '0',
      preserveAspectRatio: 'xMidYMid meet',
    },
    contentGroups: ['animation', 'descriptive'],
  },
  line: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'transform',
      'x1',
      'y1',
      'x2',
      'y2',
    ],
    defaults: {
      x1: '0',
      y1: '0',
      x2: '0',
      y2: '0',
    },
    contentGroups: ['animation', 'descriptive'],
  },
  linearGradient: {
    attrsGroups: ['core', 'presentation', 'xlink'],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'x1',
      'y1',
      'x2',
      'y2',
      'gradientUnits',
      'gradientTransform',
      'spreadMethod',
      'href',
      'xlink:href',
    ],
    defaults: {
      x1: '0',
      y1: '0',
      x2: '100%',
      y2: '0',
      spreadMethod: 'pad',
    },
    contentGroups: ['descriptive'],
    content: ['animate', 'animateTransform', 'set', 'stop'],
  },
  marker: {
    attrsGroups: ['core', 'presentation'],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'viewBox',
      'preserveAspectRatio',
      'refX',
      'refY',
      'markerUnits',
      'markerWidth',
      'markerHeight',
      'orient',
    ],
    defaults: {
      markerUnits: 'strokeWidth',
      refX: '0',
      refY: '0',
      markerWidth: '3',
      markerHeight: '3',
    },
    contentGroups: [
      'animation',
      'descriptive',
      'shape',
      'structural',
      'paintServer',
    ],
    content: [
      'a',
      'altGlyphDef',
      'clipPath',
      'color-profile',
      'cursor',
      'filter',
      'font',
      'font-face',
      'foreignObject',
      'image',
      'marker',
      'mask',
      'pattern',
      'script',
      'style',
      'switch',
      'text',
      'view',
    ],
  },
  mask: {
    attrsGroups: ['conditionalProcessing', 'core', 'presentation'],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'x',
      'y',
      'width',
      'height',
      'mask-type',
      'maskUnits',
      'maskContentUnits',
    ],
    defaults: {
      maskUnits: 'objectBoundingBox',
      maskContentUnits: 'userSpaceOnUse',
      x: '-10%',
      y: '-10%',
      width: '120%',
      height: '120%',
    },
    contentGroups: [
      'animation',
      'descriptive',
      'shape',
      'structural',
      'paintServer',
    ],
    content: [
      'a',
      'altGlyphDef',
      'clipPath',
      'color-profile',
      'cursor',
      'filter',
      'font',
      'font-face',
      'foreignObject',
      'image',
      'marker',
      'mask',
      'pattern',
      'script',
      'style',
      'switch',
      'text',
      'view',
    ],
  },
  metadata: {
    attrsGroups: ['core'],
  },
  'missing-glyph': {
    attrsGroups: ['core', 'presentation'],
    attrs: [
      'class',
      'style',
      'd',
      'horiz-adv-x',
      'vert-origin-x',
      'vert-origin-y',
      'vert-adv-y',
    ],
    contentGroups: [
      'animation',
      'descriptive',
      'shape',
      'structural',
      'paintServer',
    ],
    content: [
      'a',
      'altGlyphDef',
      'clipPath',
      'color-profile',
      'cursor',
      'filter',
      'font',
      'font-face',
      'foreignObject',
      'image',
      'marker',
      'mask',
      'pattern',
      'script',
      'style',
      'switch',
      'text',
      'view',
    ],
  },
  mpath: {
    attrsGroups: ['core', 'xlink'],
    attrs: ['externalResourcesRequired', 'href', 'xlink:href'],
    contentGroups: ['descriptive'],
  },
  path: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'transform',
      'd',
      'pathLength',
    ],
    contentGroups: ['animation', 'descriptive'],
  },
  pattern: {
    attrsGroups: ['conditionalProcessing', 'core', 'presentation', 'xlink'],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'viewBox',
      'preserveAspectRatio',
      'x',
      'y',
      'width',
      'height',
      'patternUnits',
      'patternContentUnits',
      'patternTransform',
      'href',
      'xlink:href',
    ],
    defaults: {
      patternUnits: 'objectBoundingBox',
      patternContentUnits: 'userSpaceOnUse',
      x: '0',
      y: '0',
      width: '0',
      height: '0',
      preserveAspectRatio: 'xMidYMid meet',
    },
    contentGroups: [
      'animation',
      'descriptive',
      'paintServer',
      'shape',
      'structural',
    ],
    content: [
      'a',
      'altGlyphDef',
      'clipPath',
      'color-profile',
      'cursor',
      'filter',
      'font',
      'font-face',
      'foreignObject',
      'image',
      'marker',
      'mask',
      'pattern',
      'script',
      'style',
      'switch',
      'text',
      'view',
    ],
  },
  polygon: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'transform',
      'points',
    ],
    contentGroups: ['animation', 'descriptive'],
  },
  polyline: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'transform',
      'points',
    ],
    contentGroups: ['animation', 'descriptive'],
  },
  radialGradient: {
    attrsGroups: ['core', 'presentation', 'xlink'],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'cx',
      'cy',
      'r',
      'fx',
      'fy',
      'fr',
      'gradientUnits',
      'gradientTransform',
      'spreadMethod',
      'href',
      'xlink:href',
    ],
    defaults: {
      gradientUnits: 'objectBoundingBox',
      cx: '50%',
      cy: '50%',
      r: '50%',
    },
    contentGroups: ['descriptive'],
    content: ['animate', 'animateTransform', 'set', 'stop'],
  },
  meshGradient: {
    attrsGroups: ['core', 'presentation', 'xlink'],
    attrs: ['class', 'style', 'x', 'y', 'gradientUnits', 'transform'],
    contentGroups: ['descriptive', 'paintServer', 'animation'],
    content: ['meshRow'],
  },
  meshRow: {
    attrsGroups: ['core', 'presentation'],
    attrs: ['class', 'style'],
    contentGroups: ['descriptive'],
    content: ['meshPatch'],
  },
  meshPatch: {
    attrsGroups: ['core', 'presentation'],
    attrs: ['class', 'style'],
    contentGroups: ['descriptive'],
    content: ['stop'],
  },
  rect: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'transform',
      'x',
      'y',
      'width',
      'height',
      'rx',
      'ry',
    ],
    defaults: {
      x: '0',
      y: '0',
    },
    contentGroups: ['animation', 'descriptive'],
  },
  script: {
    attrsGroups: ['core', 'xlink'],
    attrs: ['externalResourcesRequired', 'type', 'href', 'xlink:href'],
  },
  set: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'animation',
      'xlink',
      'animationAttributeTarget',
      'animationTiming',
    ],
    attrs: ['externalResourcesRequired', 'to'],
    contentGroups: ['descriptive'],
  },
  solidColor: {
    attrsGroups: ['core', 'presentation'],
    attrs: ['class', 'style'],
    contentGroups: ['paintServer'],
  },
  stop: {
    attrsGroups: ['core', 'presentation'],
    attrs: ['class', 'style', 'offset', 'path'],
    content: ['animate', 'animateColor', 'set'],
  },
  style: {
    attrsGroups: ['core'],
    attrs: ['type', 'media', 'title'],
    defaults: {
      type: 'text/css',
    },
  },
  svg: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'documentEvent',
      'graphicalEvent',
      'presentation',
    ],
    attrs: [
      'class',
      'style',
      'x',
      'y',
      'width',
      'height',
      'viewBox',
      'preserveAspectRatio',
      'zoomAndPan',
      'version',
      'baseProfile',
      'contentScriptType',
      'contentStyleType',
    ],
    defaults: {
      x: '0',
      y: '0',
      width: '100%',
      height: '100%',
      preserveAspectRatio: 'xMidYMid meet',
      zoomAndPan: 'magnify',
      version: '1.1',
      baseProfile: 'none',
      contentScriptType: 'application/ecmascript',
      contentStyleType: 'text/css',
    },
    contentGroups: [
      'animation',
      'descriptive',
      'shape',
      'structural',
      'paintServer',
    ],
    content: [
      'a',
      'altGlyphDef',
      'clipPath',
      'color-profile',
      'cursor',
      'filter',
      'font',
      'font-face',
      'foreignObject',
      'image',
      'marker',
      'mask',
      'pattern',
      'script',
      'style',
      'switch',
      'text',
      'view',
    ],
  },
  switch: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
    ],
    attrs: ['class', 'style', 'externalResourcesRequired', 'transform'],
    contentGroups: ['animation', 'descriptive', 'shape'],
    content: [
      'a',
      'foreignObject',
      'g',
      'image',
      'svg',
      'switch',
      'text',
      'use',
    ],
  },
  symbol: {
    attrsGroups: ['core', 'graphicalEvent', 'presentation'],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'preserveAspectRatio',
      'viewBox',
      'refX',
      'refY',
    ],
    defaults: {
      refX: '0',
      refY: '0',
    },
    contentGroups: [
      'animation',
      'descriptive',
      'shape',
      'structural',
      'paintServer',
    ],
    content: [
      'a',
      'altGlyphDef',
      'clipPath',
      'color-profile',
      'cursor',
      'filter',
      'font',
      'font-face',
      'foreignObject',
      'image',
      'marker',
      'mask',
      'pattern',
      'script',
      'style',
      'switch',
      'text',
      'view',
    ],
  },
  text: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'transform',
      'lengthAdjust',
      'x',
      'y',
      'dx',
      'dy',
      'rotate',
      'textLength',
    ],
    defaults: {
      x: '0',
      y: '0',
      lengthAdjust: 'spacing',
    },
    contentGroups: ['animation', 'descriptive', 'textContentChild'],
    content: ['a'],
  },
  textPath: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
      'xlink',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'href',
      'xlink:href',
      'startOffset',
      'method',
      'spacing',
      'd',
    ],
    defaults: {
      startOffset: '0',
      method: 'align',
      spacing: 'exact',
    },
    contentGroups: ['descriptive'],
    content: [
      'a',
      'altGlyph',
      'animate',
      'animateColor',
      'set',
      'tref',
      'tspan',
    ],
  },
  title: {
    attrsGroups: ['core'],
    attrs: ['class', 'style'],
  },
  tref: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
      'xlink',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'href',
      'xlink:href',
    ],
    contentGroups: ['descriptive'],
    content: ['animate', 'animateColor', 'set'],
  },
  tspan: {
    attrsGroups: [
      'conditionalProcessing',
      'core',
      'graphicalEvent',
      'presentation',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'x',
      'y',
      'dx',
      'dy',
      'rotate',
      'textLength',
      'lengthAdjust',
    ],
    contentGroups: ['descriptive'],
    content: [
      'a',
      'altGlyph',
      'animate',
      'animateColor',
      'set',
      'tref',
      'tspan',
    ],
  },
  use: {
    attrsGroups: [
      'core',
      'conditionalProcessing',
      'graphicalEvent',
      'presentation',
      'xlink',
    ],
    attrs: [
      'class',
      'style',
      'externalResourcesRequired',
      'transform',
      'x',
      'y',
      'width',
      'height',
      'href',
      'xlink:href',
    ],
    defaults: {
      x: '0',
      y: '0',
    },
    contentGroups: ['animation', 'descriptive'],
  },
  view: {
    attrsGroups: ['core'],
    attrs: [
      'externalResourcesRequired',
      'viewBox',
      'preserveAspectRatio',
      'zoomAndPan',
      'viewTarget',
    ],
    contentGroups: ['descriptive'],
  },
  vkern: {
    attrsGroups: ['core'],
    attrs: ['u1', 'g1', 'u2', 'g2', 'k'],
  },
};

// https://wiki.inkscape.org/wiki/index.php/Inkscape-specific_XML_attributes
exports.editorNamespaces = [
  'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd',
  'http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd',
  'http://www.inkscape.org/namespaces/inkscape',
  'http://www.bohemiancoding.com/sketch/ns',
  'http://ns.adobe.com/AdobeIllustrator/10.0/',
  'http://ns.adobe.com/Graphs/1.0/',
  'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/',
  'http://ns.adobe.com/Variables/1.0/',
  'http://ns.adobe.com/SaveForWeb/1.0/',
  'http://ns.adobe.com/Extensibility/1.0/',
  'http://ns.adobe.com/Flows/1.0/',
  'http://ns.adobe.com/ImageReplacement/1.0/',
  'http://ns.adobe.com/GenericCustomNamespace/1.0/',
  'http://ns.adobe.com/XPath/1.0/',
  'http://schemas.microsoft.com/visio/2003/SVGExtensions/',
  'http://taptrix.com/vectorillustrator/svg_extensions',
  'http://www.figma.com/figma/ns',
  'http://purl.org/dc/elements/1.1/',
  'http://creativecommons.org/ns#',
  'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
  'http://www.serif.com/',
  'http://www.vector.evaxdesign.sk',
];

// https://www.w3.org/TR/SVG11/linking.html#processingIRI
exports.referencesProps = [
  'clip-path',
  'color-profile',
  'fill',
  'filter',
  'marker-start',
  'marker-mid',
  'marker-end',
  'mask',
  'stroke',
  'style',
];

// https://www.w3.org/TR/SVG11/propidx.html
exports.inheritableAttrs = [
  'clip-rule',
  'color',
  'color-interpolation',
  'color-interpolation-filters',
  'color-profile',
  'color-rendering',
  'cursor',
  'direction',
  'dominant-baseline',
  'fill',
  'fill-opacity',
  'fill-rule',
  'font',
  'font-family',
  'font-size',
  'font-size-adjust',
  'font-stretch',
  'font-style',
  'font-variant',
  'font-weight',
  'glyph-orientation-horizontal',
  'glyph-orientation-vertical',
  'image-rendering',
  'letter-spacing',
  'marker',
  'marker-end',
  'marker-mid',
  'marker-start',
  'paint-order',
  'pointer-events',
  'shape-rendering',
  'stroke',
  'stroke-dasharray',
  'stroke-dashoffset',
  'stroke-linecap',
  'stroke-linejoin',
  'stroke-miterlimit',
  'stroke-opacity',
  'stroke-width',
  'text-anchor',
  'text-rendering',
  'transform',
  'visibility',
  'word-spacing',
  'writing-mode',
];

exports.presentationNonInheritableGroupAttrs = [
  'display',
  'clip-path',
  'filter',
  'mask',
  'opacity',
  'text-decoration',
  'transform',
  'unicode-bidi',
];

/**
 * https://www.w3.org/TR/SVG11/single-page.html#types-ColorKeywords
 *
 * @type {Record<string, string>}
 */
exports.colorsNames = {
  aliceblue: '#f0f8ff',
  antiquewhite: '#faebd7',
  aqua: '#0ff',
  aquamarine: '#7fffd4',
  azure: '#f0ffff',
  beige: '#f5f5dc',
  bisque: '#ffe4c4',
  black: '#000',
  blanchedalmond: '#ffebcd',
  blue: '#00f',
  blueviolet: '#8a2be2',
  brown: '#a52a2a',
  burlywood: '#deb887',
  cadetblue: '#5f9ea0',
  chartreuse: '#7fff00',
  chocolate: '#d2691e',
  coral: '#ff7f50',
  cornflowerblue: '#6495ed',
  cornsilk: '#fff8dc',
  crimson: '#dc143c',
  cyan: '#0ff',
  darkblue: '#00008b',
  darkcyan: '#008b8b',
  darkgoldenrod: '#b8860b',
  darkgray: '#a9a9a9',
  darkgreen: '#006400',
  darkgrey: '#a9a9a9',
  darkkhaki: '#bdb76b',
  darkmagenta: '#8b008b',
  darkolivegreen: '#556b2f',
  darkorange: '#ff8c00',
  darkorchid: '#9932cc',
  darkred: '#8b0000',
  darksalmon: '#e9967a',
  darkseagreen: '#8fbc8f',
  darkslateblue: '#483d8b',
  darkslategray: '#2f4f4f',
  darkslategrey: '#2f4f4f',
  darkturquoise: '#00ced1',
  darkviolet: '#9400d3',
  deeppink: '#ff1493',
  deepskyblue: '#00bfff',
  dimgray: '#696969',
  dimgrey: '#696969',
  dodgerblue: '#1e90ff',
  firebrick: '#b22222',
  floralwhite: '#fffaf0',
  forestgreen: '#228b22',
  fuchsia: '#f0f',
  gainsboro: '#dcdcdc',
  ghostwhite: '#f8f8ff',
  gold: '#ffd700',
  goldenrod: '#daa520',
  gray: '#808080',
  green: '#008000',
  greenyellow: '#adff2f',
  grey: '#808080',
  honeydew: '#f0fff0',
  hotpink: '#ff69b4',
  indianred: '#cd5c5c',
  indigo: '#4b0082',
  ivory: '#fffff0',
  khaki: '#f0e68c',
  lavender: '#e6e6fa',
  lavenderblush: '#fff0f5',
  lawngreen: '#7cfc00',
  lemonchiffon: '#fffacd',
  lightblue: '#add8e6',
  lightcoral: '#f08080',
  lightcyan: '#e0ffff',
  lightgoldenrodyellow: '#fafad2',
  lightgray: '#d3d3d3',
  lightgreen: '#90ee90',
  lightgrey: '#d3d3d3',
  lightpink: '#ffb6c1',
  lightsalmon: '#ffa07a',
  lightseagreen: '#20b2aa',
  lightskyblue: '#87cefa',
  lightslategray: '#789',
  lightslategrey: '#789',
  lightsteelblue: '#b0c4de',
  lightyellow: '#ffffe0',
  lime: '#0f0',
  limegreen: '#32cd32',
  linen: '#faf0e6',
  magenta: '#f0f',
  maroon: '#800000',
  mediumaquamarine: '#66cdaa',
  mediumblue: '#0000cd',
  mediumorchid: '#ba55d3',
  mediumpurple: '#9370db',
  mediumseagreen: '#3cb371',
  mediumslateblue: '#7b68ee',
  mediumspringgreen: '#00fa9a',
  mediumturquoise: '#48d1cc',
  mediumvioletred: '#c71585',
  midnightblue: '#191970',
  mintcream: '#f5fffa',
  mistyrose: '#ffe4e1',
  moccasin: '#ffe4b5',
  navajowhite: '#ffdead',
  navy: '#000080',
  oldlace: '#fdf5e6',
  olive: '#808000',
  olivedrab: '#6b8e23',
  orange: '#ffa500',
  orangered: '#ff4500',
  orchid: '#da70d6',
  palegoldenrod: '#eee8aa',
  palegreen: '#98fb98',
  paleturquoise: '#afeeee',
  palevioletred: '#db7093',
  papayawhip: '#ffefd5',
  peachpuff: '#ffdab9',
  peru: '#cd853f',
  pink: '#ffc0cb',
  plum: '#dda0dd',
  powderblue: '#b0e0e6',
  purple: '#800080',
  rebeccapurple: '#639',
  red: '#f00',
  rosybrown: '#bc8f8f',
  royalblue: '#4169e1',
  saddlebrown: '#8b4513',
  salmon: '#fa8072',
  sandybrown: '#f4a460',
  seagreen: '#2e8b57',
  seashell: '#fff5ee',
  sienna: '#a0522d',
  silver: '#c0c0c0',
  skyblue: '#87ceeb',
  slateblue: '#6a5acd',
  slategray: '#708090',
  slategrey: '#708090',
  snow: '#fffafa',
  springgreen: '#00ff7f',
  steelblue: '#4682b4',
  tan: '#d2b48c',
  teal: '#008080',
  thistle: '#d8bfd8',
  tomato: '#ff6347',
  turquoise: '#40e0d0',
  violet: '#ee82ee',
  wheat: '#f5deb3',
  white: '#fff',
  whitesmoke: '#f5f5f5',
  yellow: '#ff0',
  yellowgreen: '#9acd32',
};

/**
 * @type {Record<string, string>}
 */
exports.colorsShortNames = {
  '#f0ffff': 'azure',
  '#f5f5dc': 'beige',
  '#ffe4c4': 'bisque',
  '#a52a2a': 'brown',
  '#ff7f50': 'coral',
  '#ffd700': 'gold',
  '#808080': 'gray',
  '#008000': 'green',
  '#4b0082': 'indigo',
  '#fffff0': 'ivory',
  '#f0e68c': 'khaki',
  '#faf0e6': 'linen',
  '#800000': 'maroon',
  '#000080': 'navy',
  '#808000': 'olive',
  '#ffa500': 'orange',
  '#da70d6': 'orchid',
  '#cd853f': 'peru',
  '#ffc0cb': 'pink',
  '#dda0dd': 'plum',
  '#800080': 'purple',
  '#f00': 'red',
  '#ff0000': 'red',
  '#fa8072': 'salmon',
  '#a0522d': 'sienna',
  '#c0c0c0': 'silver',
  '#fffafa': 'snow',
  '#d2b48c': 'tan',
  '#008080': 'teal',
  '#ff6347': 'tomato',
  '#ee82ee': 'violet',
  '#f5deb3': 'wheat',
};

// https://www.w3.org/TR/SVG11/single-page.html#types-DataTypeColor
exports.colorsProps = [
  'color',
  'fill',
  'stroke',
  'stop-color',
  'flood-color',
  'lighting-color',
];

license: mit


code:
'use strict';

/**
 * @typedef {import('../lib/types').XastElement} XastElement
 * @typedef {import('../lib/types').PathDataItem} PathDataItem
 */

const { parsePathData, stringifyPathData } = require('../lib/path.js');

/**
 * @type {[number, number]}
 */
var prevCtrlPoint;

/**
 * Convert path string to JS representation.
 *
 * @type {(path: XastElement) => Array<PathDataItem>}
 */
const path2js = (path) => {
  // @ts-ignore legacy
  if (path.pathJS) return path.pathJS;
  /**
   * @type {Array<PathDataItem>}
   */
  const pathData = []; // JS representation of the path data
  const newPathData = parsePathData(path.attributes.d);
  for (const { command, args } of newPathData) {
    pathData.push({ command, args });
  }
  // First moveto is actually absolute. Subsequent coordinates were separated above.
  if (pathData.length && pathData[0].command == 'm') {
    pathData[0].command = 'M';
  }
  // @ts-ignore legacy
  path.pathJS = pathData;
  return pathData;
};
exports.path2js = path2js;

/**
 * Convert relative Path data to absolute.
 *
 * @type {(data: Array<PathDataItem>) => Array<PathDataItem>}
 *
 */
const convertRelativeToAbsolute = (data) => {
  /**
   * @type {Array<PathDataItem>}
   */
  const newData = [];
  let start = [0, 0];
  let cursor = [0, 0];

  for (let { command, args } of data) {
    args = args.slice();

    // moveto (x y)
    if (command === 'm') {
      args[0] += cursor[0];
      args[1] += cursor[1];
      command = 'M';
    }
    if (command === 'M') {
      cursor[0] = args[0];
      cursor[1] = args[1];
      start[0] = cursor[0];
      start[1] = cursor[1];
    }

    // horizontal lineto (x)
    if (command === 'h') {
      args[0] += cursor[0];
      command = 'H';
    }
    if (command === 'H') {
      cursor[0] = args[0];
    }

    // vertical lineto (y)
    if (command === 'v') {
      args[0] += cursor[1];
      command = 'V';
    }
    if (command === 'V') {
      cursor[1] = args[0];
    }

    // lineto (x y)
    if (command === 'l') {
      args[0] += cursor[0];
      args[1] += cursor[1];
      command = 'L';
    }
    if (command === 'L') {
      cursor[0] = args[0];
      cursor[1] = args[1];
    }

    // curveto (x1 y1 x2 y2 x y)
    if (command === 'c') {
      args[0] += cursor[0];
      args[1] += cursor[1];
      args[2] += cursor[0];
      args[3] += cursor[1];
      args[4] += cursor[0];
      args[5] += cursor[1];
      command = 'C';
    }
    if (command === 'C') {
      cursor[0] = args[4];
      cursor[1] = args[5];
    }

    // smooth curveto (x2 y2 x y)
    if (command === 's') {
      args[0] += cursor[0];
      args[1] += cursor[1];
      args[2] += cursor[0];
      args[3] += cursor[1];
      command = 'S';
    }
    if (command === 'S') {
      cursor[0] = args[2];
      cursor[1] = args[3];
    }

    // quadratic Bézier curveto (x1 y1 x y)
    if (command === 'q') {
      args[0] += cursor[0];
      args[1] += cursor[1];
      args[2] += cursor[0];
      args[3] += cursor[1];
      command = 'Q';
    }
    if (command === 'Q') {
      cursor[0] = args[2];
      cursor[1] = args[3];
    }

    // smooth quadratic Bézier curveto (x y)
    if (command === 't') {
      args[0] += cursor[0];
      args[1] += cursor[1];
      command = 'T';
    }
    if (command === 'T') {
      cursor[0] = args[0];
      cursor[1] = args[1];
    }

    // elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
    if (command === 'a') {
      args[5] += cursor[0];
      args[6] += cursor[1];
      command = 'A';
    }
    if (command === 'A') {
      cursor[0] = args[5];
      cursor[1] = args[6];
    }

    // closepath
    if (command === 'z' || command === 'Z') {
      cursor[0] = start[0];
      cursor[1] = start[1];
      command = 'z';
    }

    newData.push({ command, args });
  }
  return newData;
};

/**
 * @typedef {{ floatPrecision?: number, noSpaceAfterFlags?: boolean }} Js2PathParams
 */

/**
 * Convert path array to string.
 *
 * @type {(path: XastElement, data: Array<PathDataItem>, params: Js2PathParams) => void}
 */
exports.js2path = function (path, data, params) {
  // @ts-ignore legacy
  path.pathJS = data;

  const pathData = [];
  for (const item of data) {
    // remove moveto commands which are followed by moveto commands
    if (
      pathData.length !== 0 &&
      (item.command === 'M' || item.command === 'm')
    ) {
      const last = pathData[pathData.length - 1];
      if (last.command === 'M' || last.command === 'm') {
        pathData.pop();
      }
    }
    pathData.push({
      command: item.command,
      args: item.args,
    });
  }

  path.attributes.d = stringifyPathData({
    pathData,
    precision: params.floatPrecision,
    disableSpaceAfterFlags: params.noSpaceAfterFlags,
  });
};

/**
 * @type {(dest: Array<number>, source: Array<number>) => Array<number>}
 */
function set(dest, source) {
  dest[0] = source[source.length - 2];
  dest[1] = source[source.length - 1];
  return dest;
}

/**
 * Checks if two paths have an intersection by checking convex hulls
 * collision using Gilbert-Johnson-Keerthi distance algorithm
 * https://web.archive.org/web/20180822200027/http://entropyinteractive.com/2011/04/gjk-algorithm/
 *
 * @type {(path1: Array<PathDataItem>, path2: Array<PathDataItem>) => boolean}
 */
exports.intersects = function (path1, path2) {
  // Collect points of every subpath.
  const points1 = gatherPoints(convertRelativeToAbsolute(path1));
  const points2 = gatherPoints(convertRelativeToAbsolute(path2));

  // Axis-aligned bounding box check.
  if (
    points1.maxX <= points2.minX ||
    points2.maxX <= points1.minX ||
    points1.maxY <= points2.minY ||
    points2.maxY <= points1.minY ||
    points1.list.every((set1) => {
      return points2.list.every((set2) => {
        return (
          set1.list[set1.maxX][0] <= set2.list[set2.minX][0] ||
          set2.list[set2.maxX][0] <= set1.list[set1.minX][0] ||
          set1.list[set1.maxY][1] <= set2.list[set2.minY][1] ||
          set2.list[set2.maxY][1] <= set1.list[set1.minY][1]
        );
      });
    })
  )
    return false;

  // Get a convex hull from points of each subpath. Has the most complexity O(n·log n).
  const hullNest1 = points1.list.map(convexHull);
  const hullNest2 = points2.list.map(convexHull);

  // Check intersection of every subpath of the first path with every subpath of the second.
  return hullNest1.some(function (hull1) {
    if (hull1.list.length < 3) return false;

    return hullNest2.some(function (hull2) {
      if (hull2.list.length < 3) return false;

      var simplex = [getSupport(hull1, hull2, [1, 0])], // create the initial simplex
        direction = minus(simplex[0]); // set the direction to point towards the origin

      var iterations = 1e4; // infinite loop protection, 10 000 iterations is more than enough
      // eslint-disable-next-line no-constant-condition
      while (true) {
        // eslint-disable-next-line no-constant-condition
        if (iterations-- == 0) {
          console.error(
            'Error: infinite loop while processing mergePaths plugin.'
          );
          return true; // true is the safe value that means “do nothing with paths”
        }
        // add a new point
        simplex.push(getSupport(hull1, hull2, direction));
        // see if the new point was on the correct side of the origin
        if (dot(direction, simplex[simplex.length - 1]) <= 0) return false;
        // process the simplex
        if (processSimplex(simplex, direction)) return true;
      }
    });
  });

  /**
   * @type {(a: Point, b: Point, direction: Array<number>) => Array<number>}
   */
  function getSupport(a, b, direction) {
    return sub(supportPoint(a, direction), supportPoint(b, minus(direction)));
  }

  // Computes farthest polygon point in particular direction.
  // Thanks to knowledge of min/max x and y coordinates we can choose a quadrant to search in.
  // Since we're working on convex hull, the dot product is increasing until we find the farthest point.
  /**
   * @type {(polygon: Point, direction: Array<number>) => Array<number>}
   */
  function supportPoint(polygon, direction) {
    var index =
        direction[1] >= 0
          ? direction[0] < 0
            ? polygon.maxY
            : polygon.maxX
          : direction[0] < 0
          ? polygon.minX
          : polygon.minY,
      max = -Infinity,
      value;
    while ((value = dot(polygon.list[index], direction)) > max) {
      max = value;
      index = ++index % polygon.list.length;
    }
    return polygon.list[(index || polygon.list.length) - 1];
  }
};

/**
 * @type {(simplex: Array<Array<number>>, direction: Array<number>) => boolean}
 */
function processSimplex(simplex, direction) {
  // we only need to handle to 1-simplex and 2-simplex
  if (simplex.length == 2) {
    // 1-simplex
    let a = simplex[1],
      b = simplex[0],
      AO = minus(simplex[1]),
      AB = sub(b, a);
    // AO is in the same direction as AB
    if (dot(AO, AB) > 0) {
      // get the vector perpendicular to AB facing O
      set(direction, orth(AB, a));
    } else {
      set(direction, AO);
      // only A remains in the simplex
      simplex.shift();
    }
  } else {
    // 2-simplex
    let a = simplex[2], // [a, b, c] = simplex
      b = simplex[1],
      c = simplex[0],
      AB = sub(b, a),
      AC = sub(c, a),
      AO = minus(a),
      ACB = orth(AB, AC), // the vector perpendicular to AB facing away from C
      ABC = orth(AC, AB); // the vector perpendicular to AC facing away from B

    if (dot(ACB, AO) > 0) {
      if (dot(AB, AO) > 0) {
        // region 4
        set(direction, ACB);
        simplex.shift(); // simplex = [b, a]
      } else {
        // region 5
        set(direction, AO);
        simplex.splice(0, 2); // simplex = [a]
      }
    } else if (dot(ABC, AO) > 0) {
      if (dot(AC, AO) > 0) {
        // region 6
        set(direction, ABC);
        simplex.splice(1, 1); // simplex = [c, a]
      } else {
        // region 5 (again)
        set(direction, AO);
        simplex.splice(0, 2); // simplex = [a]
      }
    } // region 7
    else return true;
  }
  return false;
}

/**
 * @type {(v: Array<number>) => Array<number>}
 */
function minus(v) {
  return [-v[0], -v[1]];
}

/**
 * @type {(v1: Array<number>, v2: Array<number>) => Array<number>}
 */
function sub(v1, v2) {
  return [v1[0] - v2[0], v1[1] - v2[1]];
}

/**
 * @type {(v1: Array<number>, v2: Array<number>) => number}
 */
function dot(v1, v2) {
  return v1[0] * v2[0] + v1[1] * v2[1];
}

/**
 * @type {(v1: Array<number>, v2: Array<number>) => Array<number>}
 */
function orth(v, from) {
  var o = [-v[1], v[0]];
  return dot(o, minus(from)) < 0 ? minus(o) : o;
}

/**
 * @typedef {{
 *   list: Array<Array<number>>,
 *   minX: number,
 *   minY: number,
 *   maxX: number,
 *   maxY: number
 * }} Point
 */

/**
 * @typedef {{
 *   list: Array<Point>,
 *   minX: number,
 *   minY: number,
 *   maxX: number,
 *   maxY: number
 * }} Points
 */

/**
 * @type {(pathData: Array<PathDataItem>) => Points}
 */
function gatherPoints(pathData) {
  /**
   * @type {Points}
   */
  const points = { list: [], minX: 0, minY: 0, maxX: 0, maxY: 0 };

  // Writes data about the extreme points on each axle
  /**
   * @type {(path: Point, point: Array<number>) => void}
   */
  const addPoint = (path, point) => {
    if (!path.list.length || point[1] > path.list[path.maxY][1]) {
      path.maxY = path.list.length;
      points.maxY = points.list.length
        ? Math.max(point[1], points.maxY)
        : point[1];
    }
    if (!path.list.length || point[0] > path.list[path.maxX][0]) {
      path.maxX = path.list.length;
      points.maxX = points.list.length
        ? Math.max(point[0], points.maxX)
        : point[0];
    }
    if (!path.list.length || point[1] < path.list[path.minY][1]) {
      path.minY = path.list.length;
      points.minY = points.list.length
        ? Math.min(point[1], points.minY)
        : point[1];
    }
    if (!path.list.length || point[0] < path.list[path.minX][0]) {
      path.minX = path.list.length;
      points.minX = points.list.length
        ? Math.min(point[0], points.minX)
        : point[0];
    }
    path.list.push(point);
  };

  for (let i = 0; i < pathData.length; i += 1) {
    const pathDataItem = pathData[i];
    let subPath =
      points.list.length === 0
        ? { list: [], minX: 0, minY: 0, maxX: 0, maxY: 0 }
        : points.list[points.list.length - 1];
    let prev = i === 0 ? null : pathData[i - 1];
    let basePoint =
      subPath.list.length === 0 ? null : subPath.list[subPath.list.length - 1];
    let data = pathDataItem.args;
    let ctrlPoint = basePoint;

    /**
     * @type {(n: number, i: number) => number}
     * TODO fix null hack
     */
    const toAbsolute = (n, i) => n + (basePoint == null ? 0 : basePoint[i % 2]);

    switch (pathDataItem.command) {
      case 'M':
        subPath = { list: [], minX: 0, minY: 0, maxX: 0, maxY: 0 };
        points.list.push(subPath);
        break;

      case 'H':
        if (basePoint != null) {
          addPoint(subPath, [data[0], basePoint[1]]);
        }
        break;

      case 'V':
        if (basePoint != null) {
          addPoint(subPath, [basePoint[0], data[0]]);
        }
        break;

      case 'Q':
        addPoint(subPath, data.slice(0, 2));
        prevCtrlPoint = [data[2] - data[0], data[3] - data[1]]; // Save control point for shorthand
        break;

      case 'T':
        if (
          basePoint != null &&
          prev != null &&
          (prev.command == 'Q' || prev.command == 'T')
        ) {
          ctrlPoint = [
            basePoint[0] + prevCtrlPoint[0],
            basePoint[1] + prevCtrlPoint[1],
          ];
          addPoint(subPath, ctrlPoint);
          prevCtrlPoint = [data[0] - ctrlPoint[0], data[1] - ctrlPoint[1]];
        }
        break;

      case 'C':
        if (basePoint != null) {
          // Approximate quibic Bezier curve with middle points between control points
          addPoint(subPath, [
            0.5 * (basePoint[0] + data[0]),
            0.5 * (basePoint[1] + data[1]),
          ]);
        }
        addPoint(subPath, [
          0.5 * (data[0] + data[2]),
          0.5 * (data[1] + data[3]),
        ]);
        addPoint(subPath, [
          0.5 * (data[2] + data[4]),
          0.5 * (data[3] + data[5]),
        ]);
        prevCtrlPoint = [data[4] - data[2], data[5] - data[3]]; // Save control point for shorthand
        break;

      case 'S':
        if (
          basePoint != null &&
          prev != null &&
          (prev.command == 'C' || prev.command == 'S')
        ) {
          addPoint(subPath, [
            basePoint[0] + 0.5 * prevCtrlPoint[0],
            basePoint[1] + 0.5 * prevCtrlPoint[1],
          ]);
          ctrlPoint = [
            basePoint[0] + prevCtrlPoint[0],
            basePoint[1] + prevCtrlPoint[1],
          ];
        }
        if (ctrlPoint != null) {
          addPoint(subPath, [
            0.5 * (ctrlPoint[0] + data[0]),
            0.5 * (ctrlPoint[1] + data[1]),
          ]);
        }
        addPoint(subPath, [
          0.5 * (data[0] + data[2]),
          0.5 * (data[1] + data[3]),
        ]);
        prevCtrlPoint = [data[2] - data[0], data[3] - data[1]];
        break;

      case 'A':
        if (basePoint != null) {
          // Convert the arc to bezier curves and use the same approximation
          // @ts-ignore no idea what's going on here
          var curves = a2c.apply(0, basePoint.concat(data));
          for (
            var cData;
            (cData = curves.splice(0, 6).map(toAbsolute)).length;

          ) {
            if (basePoint != null) {
              addPoint(subPath, [
                0.5 * (basePoint[0] + cData[0]),
                0.5 * (basePoint[1] + cData[1]),
              ]);
            }
            addPoint(subPath, [
              0.5 * (cData[0] + cData[2]),
              0.5 * (cData[1] + cData[3]),
            ]);
            addPoint(subPath, [
              0.5 * (cData[2] + cData[4]),
              0.5 * (cData[3] + cData[5]),
            ]);
            if (curves.length) addPoint(subPath, (basePoint = cData.slice(-2)));
          }
        }
        break;
    }

    // Save final command coordinates
    if (data.length >= 2) addPoint(subPath, data.slice(-2));
  }

  return points;
}

/**
 * Forms a convex hull from set of points of every subpath using monotone chain convex hull algorithm.
 * https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
 *
 * @type {(points: Point) => Point}
 */
function convexHull(points) {
  points.list.sort(function (a, b) {
    return a[0] == b[0] ? a[1] - b[1] : a[0] - b[0];
  });

  var lower = [],
    minY = 0,
    bottom = 0;
  for (let i = 0; i < points.list.length; i++) {
    while (
      lower.length >= 2 &&
      cross(lower[lower.length - 2], lower[lower.length - 1], points.list[i]) <=
        0
    ) {
      lower.pop();
    }
    if (points.list[i][1] < points.list[minY][1]) {
      minY = i;
      bottom = lower.length;
    }
    lower.push(points.list[i]);
  }

  var upper = [],
    maxY = points.list.length - 1,
    top = 0;
  for (let i = points.list.length; i--; ) {
    while (
      upper.length >= 2 &&
      cross(upper[upper.length - 2], upper[upper.length - 1], points.list[i]) <=
        0
    ) {
      upper.pop();
    }
    if (points.list[i][1] > points.list[maxY][1]) {
      maxY = i;
      top = upper.length;
    }
    upper.push(points.list[i]);
  }

  // last points are equal to starting points of the other part
  upper.pop();
  lower.pop();

  const hullList = lower.concat(upper);

  /**
   * @type {Point}
   */
  const hull = {
    list: hullList,
    minX: 0, // by sorting
    maxX: lower.length,
    minY: bottom,
    maxY: (lower.length + top) % hullList.length,
  };

  return hull;
}

/**
 * @type {(o: Array<number>, a: Array<number>, b: Array<number>) => number}
 */
function cross(o, a, b) {
  return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
}

/**
 * Based on code from Snap.svg (Apache 2 license). http://snapsvg.io/
 * Thanks to Dmitry Baranovskiy for his great work!
 *
 * @type {(
 *  x1: number,
 *  y1: number,
 *  rx: number,
 *  ry: number,
 *  angle: number,
 *  large_arc_flag: number,
 *  sweep_flag: number,
 *  x2: number,
 *  y2: number,
 *  recursive: Array<number>
 * ) => Array<number>}
 */
const a2c = (
  x1,
  y1,
  rx,
  ry,
  angle,
  large_arc_flag,
  sweep_flag,
  x2,
  y2,
  recursive
) => {
  // for more information of where this Math came from visit:
  // https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
  const _120 = (Math.PI * 120) / 180;
  const rad = (Math.PI / 180) * (+angle || 0);
  /**
   * @type {Array<number>}
   */
  let res = [];
  /**
   * @type {(x: number, y: number, rad: number) => number}
   */
  const rotateX = (x, y, rad) => {
    return x * Math.cos(rad) - y * Math.sin(rad);
  };
  /**
   * @type {(x: number, y: number, rad: number) => number}
   */
  const rotateY = (x, y, rad) => {
    return x * Math.sin(rad) + y * Math.cos(rad);
  };
  if (!recursive) {
    x1 = rotateX(x1, y1, -rad);
    y1 = rotateY(x1, y1, -rad);
    x2 = rotateX(x2, y2, -rad);
    y2 = rotateY(x2, y2, -rad);
    var x = (x1 - x2) / 2,
      y = (y1 - y2) / 2;
    var h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
    if (h > 1) {
      h = Math.sqrt(h);
      rx = h * rx;
      ry = h * ry;
    }
    var rx2 = rx * rx;
    var ry2 = ry * ry;
    var k =
      (large_arc_flag == sweep_flag ? -1 : 1) *
      Math.sqrt(
        Math.abs(
          (rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x)
        )
      );
    var cx = (k * rx * y) / ry + (x1 + x2) / 2;
    var cy = (k * -ry * x) / rx + (y1 + y2) / 2;
    var f1 = Math.asin(Number(((y1 - cy) / ry).toFixed(9)));
    var f2 = Math.asin(Number(((y2 - cy) / ry).toFixed(9)));

    f1 = x1 < cx ? Math.PI - f1 : f1;
    f2 = x2 < cx ? Math.PI - f2 : f2;
    f1 < 0 && (f1 = Math.PI * 2 + f1);
    f2 < 0 && (f2 = Math.PI * 2 + f2);
    if (sweep_flag && f1 > f2) {
      f1 = f1 - Math.PI * 2;
    }
    if (!sweep_flag && f2 > f1) {
      f2 = f2 - Math.PI * 2;
    }
  } else {
    f1 = recursive[0];
    f2 = recursive[1];
    cx = recursive[2];
    cy = recursive[3];
  }
  var df = f2 - f1;
  if (Math.abs(df) > _120) {
    var f2old = f2,
      x2old = x2,
      y2old = y2;
    f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
    x2 = cx + rx * Math.cos(f2);
    y2 = cy + ry * Math.sin(f2);
    res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [
      f2,
      f2old,
      cx,
      cy,
    ]);
  }
  df = f2 - f1;
  var c1 = Math.cos(f1),
    s1 = Math.sin(f1),
    c2 = Math.cos(f2),
    s2 = Math.sin(f2),
    t = Math.tan(df / 4),
    hx = (4 / 3) * rx * t,
    hy = (4 / 3) * ry * t,
    m = [
      -hx * s1,
      hy * c1,
      x2 + hx * s2 - x1,
      y2 - hy * c2 - y1,
      x2 - x1,
      y2 - y1,
    ];
  if (recursive) {
    return m.concat(res);
  } else {
    res = m.concat(res);
    var newres = [];
    for (var i = 0, n = res.length; i < n; i++) {
      newres[i] =
        i % 2
          ? rotateY(res[i - 1], res[i], rad)
          : rotateX(res[i], res[i + 1], rad);
    }
    return newres;
  }
};


code:
'use strict';

const regTransformTypes = /matrix|translate|scale|rotate|skewX|skewY/;
const regTransformSplit =
  /\s*(matrix|translate|scale|rotate|skewX|skewY)\s*\(\s*(.+?)\s*\)[\s,]*/;
const regNumericValues = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;

/**
 * @typedef {{ name: string, data: Array<number> }} TransformItem
 */

/**
 * Convert transform string to JS representation.
 *
 * @type {(transformString: string) => Array<TransformItem>}
 */
exports.transform2js = (transformString) => {
  // JS representation of the transform data
  /**
   * @type {Array<TransformItem>}
   */
  const transforms = [];
  // current transform context
  /**
   * @type {null | TransformItem}
   */
  let current = null;
  // split value into ['', 'translate', '10 50', '', 'scale', '2', '', 'rotate', '-45', '']
  for (const item of transformString.split(regTransformSplit)) {
    var num;
    if (item) {
      // if item is a translate function
      if (regTransformTypes.test(item)) {
        // then collect it and change current context
        current = { name: item, data: [] };
        transforms.push(current);
        // else if item is data
      } else {
        // then split it into [10, 50] and collect as context.data
        // eslint-disable-next-line no-cond-assign
        while ((num = regNumericValues.exec(item))) {
          num = Number(num);
          if (current != null) {
            current.data.push(num);
          }
        }
      }
    }
  }
  // return empty array if broken transform (no data)
  return current == null || current.data.length == 0 ? [] : transforms;
};

/**
 * Multiply transforms into one.
 *
 * @type {(transforms: Array<TransformItem>) => TransformItem}
 */
exports.transformsMultiply = (transforms) => {
  // convert transforms objects to the matrices
  const matrixData = transforms.map((transform) => {
    if (transform.name === 'matrix') {
      return transform.data;
    }
    return transformToMatrix(transform);
  });
  // multiply all matrices into one
  const matrixTransform = {
    name: 'matrix',
    data:
      matrixData.length > 0 ? matrixData.reduce(multiplyTransformMatrices) : [],
  };
  return matrixTransform;
};

/**
 * math utilities in radians.
 */
const mth = {
  /**
   * @type {(deg: number) => number}
   */
  rad: (deg) => {
    return (deg * Math.PI) / 180;
  },

  /**
   * @type {(rad: number) => number}
   */
  deg: (rad) => {
    return (rad * 180) / Math.PI;
  },

  /**
   * @type {(deg: number) => number}
   */
  cos: (deg) => {
    return Math.cos(mth.rad(deg));
  },

  /**
   * @type {(val: number, floatPrecision: number) => number}
   */
  acos: (val, floatPrecision) => {
    return Number(mth.deg(Math.acos(val)).toFixed(floatPrecision));
  },

  /**
   * @type {(deg: number) => number}
   */
  sin: (deg) => {
    return Math.sin(mth.rad(deg));
  },

  /**
   * @type {(val: number, floatPrecision: number) => number}
   */
  asin: (val, floatPrecision) => {
    return Number(mth.deg(Math.asin(val)).toFixed(floatPrecision));
  },

  /**
   * @type {(deg: number) => number}
   */
  tan: (deg) => {
    return Math.tan(mth.rad(deg));
  },

  /**
   * @type {(val: number, floatPrecision: number) => number}
   */
  atan: (val, floatPrecision) => {
    return Number(mth.deg(Math.atan(val)).toFixed(floatPrecision));
  },
};

/**
 * @typedef {{
 *   convertToShorts: boolean,
 *   floatPrecision: number,
 *   transformPrecision: number,
 *   matrixToTransform: boolean,
 *   shortTranslate: boolean,
 *   shortScale: boolean,
 *   shortRotate: boolean,
 *   removeUseless: boolean,
 *   collapseIntoOne: boolean,
 *   leadingZero: boolean,
 *   negativeExtraSpace: boolean,
 * }} TransformParams
 */

/**
 * Decompose matrix into simple transforms. See
 * https://frederic-wang.fr/decomposition-of-2d-transform-matrices.html
 *
 * @type {(transform: TransformItem, params: TransformParams) => Array<TransformItem>}
 */
exports.matrixToTransform = (transform, params) => {
  let floatPrecision = params.floatPrecision;
  let data = transform.data;
  let transforms = [];
  let sx = Number(
    Math.hypot(data[0], data[1]).toFixed(params.transformPrecision)
  );
  let sy = Number(
    ((data[0] * data[3] - data[1] * data[2]) / sx).toFixed(
      params.transformPrecision
    )
  );
  let colsSum = data[0] * data[2] + data[1] * data[3];
  let rowsSum = data[0] * data[1] + data[2] * data[3];
  let scaleBefore = rowsSum != 0 || sx == sy;

  // [..., ..., ..., ..., tx, ty] → translate(tx, ty)
  if (data[4] || data[5]) {
    transforms.push({
      name: 'translate',
      data: data.slice(4, data[5] ? 6 : 5),
    });
  }

  // [sx, 0, tan(a)·sy, sy, 0, 0] → skewX(a)·scale(sx, sy)
  if (!data[1] && data[2]) {
    transforms.push({
      name: 'skewX',
      data: [mth.atan(data[2] / sy, floatPrecision)],
    });

    // [sx, sx·tan(a), 0, sy, 0, 0] → skewY(a)·scale(sx, sy)
  } else if (data[1] && !data[2]) {
    transforms.push({
      name: 'skewY',
      data: [mth.atan(data[1] / data[0], floatPrecision)],
    });
    sx = data[0];
    sy = data[3];

    // [sx·cos(a), sx·sin(a), sy·-sin(a), sy·cos(a), x, y] → rotate(a[, cx, cy])·(scale or skewX) or
    // [sx·cos(a), sy·sin(a), sx·-sin(a), sy·cos(a), x, y] → scale(sx, sy)·rotate(a[, cx, cy]) (if !scaleBefore)
  } else if (!colsSum || (sx == 1 && sy == 1) || !scaleBefore) {
    if (!scaleBefore) {
      sx = (data[0] < 0 ? -1 : 1) * Math.hypot(data[0], data[2]);
      sy = (data[3] < 0 ? -1 : 1) * Math.hypot(data[1], data[3]);
      transforms.push({ name: 'scale', data: [sx, sy] });
    }
    var angle = Math.min(Math.max(-1, data[0] / sx), 1),
      rotate = [
        mth.acos(angle, floatPrecision) *
          ((scaleBefore ? 1 : sy) * data[1] < 0 ? -1 : 1),
      ];

    if (rotate[0]) transforms.push({ name: 'rotate', data: rotate });

    if (rowsSum && colsSum)
      transforms.push({
        name: 'skewX',
        data: [mth.atan(colsSum / (sx * sx), floatPrecision)],
      });

    // rotate(a, cx, cy) can consume translate() within optional arguments cx, cy (rotation point)
    if (rotate[0] && (data[4] || data[5])) {
      transforms.shift();
      var cos = data[0] / sx,
        sin = data[1] / (scaleBefore ? sx : sy),
        x = data[4] * (scaleBefore ? 1 : sy),
        y = data[5] * (scaleBefore ? 1 : sx),
        denom =
          (Math.pow(1 - cos, 2) + Math.pow(sin, 2)) *
          (scaleBefore ? 1 : sx * sy);
      rotate.push(((1 - cos) * x - sin * y) / denom);
      rotate.push(((1 - cos) * y + sin * x) / denom);
    }

    // Too many transformations, return original matrix if it isn't just a scale/translate
  } else if (data[1] || data[2]) {
    return [transform];
  }

  if ((scaleBefore && (sx != 1 || sy != 1)) || !transforms.length)
    transforms.push({
      name: 'scale',
      data: sx == sy ? [sx] : [sx, sy],
    });

  return transforms;
};

/**
 * Convert transform to the matrix data.
 *
 * @type {(transform: TransformItem) => Array<number> }
 */
const transformToMatrix = (transform) => {
  if (transform.name === 'matrix') {
    return transform.data;
  }
  switch (transform.name) {
    case 'translate':
      // [1, 0, 0, 1, tx, ty]
      return [1, 0, 0, 1, transform.data[0], transform.data[1] || 0];
    case 'scale':
      // [sx, 0, 0, sy, 0, 0]
      return [
        transform.data[0],
        0,
        0,
        transform.data[1] || transform.data[0],
        0,
        0,
      ];
    case 'rotate':
      // [cos(a), sin(a), -sin(a), cos(a), x, y]
      var cos = mth.cos(transform.data[0]),
        sin = mth.sin(transform.data[0]),
        cx = transform.data[1] || 0,
        cy = transform.data[2] || 0;
      return [
        cos,
        sin,
        -sin,
        cos,
        (1 - cos) * cx + sin * cy,
        (1 - cos) * cy - sin * cx,
      ];
    case 'skewX':
      // [1, 0, tan(a), 1, 0, 0]
      return [1, 0, mth.tan(transform.data[0]), 1, 0, 0];
    case 'skewY':
      // [1, tan(a), 0, 1, 0, 0]
      return [1, mth.tan(transform.data[0]), 0, 1, 0, 0];
    default:
      throw Error(`Unknown transform ${transform.name}`);
  }
};

/**
 * Applies transformation to an arc. To do so, we represent ellipse as a matrix, multiply it
 * by the transformation matrix and use a singular value decomposition to represent in a form
 * rotate(θ)·scale(a b)·rotate(φ). This gives us new ellipse params a, b and θ.
 * SVD is being done with the formulae provided by Wolffram|Alpha (svd {{m0, m2}, {m1, m3}})
 *
 * @type {(
 *   cursor: [x: number, y: number],
 *   arc: Array<number>,
 *   transform: Array<number>
 * ) => Array<number>}
 */
exports.transformArc = (cursor, arc, transform) => {
  const x = arc[5] - cursor[0];
  const y = arc[6] - cursor[1];
  let a = arc[0];
  let b = arc[1];
  const rot = (arc[2] * Math.PI) / 180;
  const cos = Math.cos(rot);
  const sin = Math.sin(rot);
  // skip if radius is 0
  if (a > 0 && b > 0) {
    let h =
      Math.pow(x * cos + y * sin, 2) / (4 * a * a) +
      Math.pow(y * cos - x * sin, 2) / (4 * b * b);
    if (h > 1) {
      h = Math.sqrt(h);
      a *= h;
      b *= h;
    }
  }
  const ellipse = [a * cos, a * sin, -b * sin, b * cos, 0, 0];
  const m = multiplyTransformMatrices(transform, ellipse);
  // Decompose the new ellipse matrix
  const lastCol = m[2] * m[2] + m[3] * m[3];
  const squareSum = m[0] * m[0] + m[1] * m[1] + lastCol;
  const root =
    Math.hypot(m[0] - m[3], m[1] + m[2]) * Math.hypot(m[0] + m[3], m[1] - m[2]);

  if (!root) {
    // circle
    arc[0] = arc[1] = Math.sqrt(squareSum / 2);
    arc[2] = 0;
  } else {
    const majorAxisSqr = (squareSum + root) / 2;
    const minorAxisSqr = (squareSum - root) / 2;
    const major = Math.abs(majorAxisSqr - lastCol) > 1e-6;
    const sub = (major ? majorAxisSqr : minorAxisSqr) - lastCol;
    const rowsSum = m[0] * m[2] + m[1] * m[3];
    const term1 = m[0] * sub + m[2] * rowsSum;
    const term2 = m[1] * sub + m[3] * rowsSum;
    arc[0] = Math.sqrt(majorAxisSqr);
    arc[1] = Math.sqrt(minorAxisSqr);
    arc[2] =
      (((major ? term2 < 0 : term1 > 0) ? -1 : 1) *
        Math.acos((major ? term1 : term2) / Math.hypot(term1, term2)) *
        180) /
      Math.PI;
  }

  if (transform[0] < 0 !== transform[3] < 0) {
    // Flip the sweep flag if coordinates are being flipped horizontally XOR vertically
    arc[4] = 1 - arc[4];
  }

  return arc;
};

/**
 * Multiply transformation matrices.
 *
 * @type {(a: Array<number>, b: Array<number>) => Array<number>}
 */
const multiplyTransformMatrices = (a, b) => {
  return [
    a[0] * b[0] + a[2] * b[1],
    a[1] * b[0] + a[3] * b[1],
    a[0] * b[2] + a[2] * b[3],
    a[1] * b[2] + a[3] * b[3],
    a[0] * b[4] + a[2] * b[5] + a[4],
    a[1] * b[4] + a[3] * b[5] + a[5],
  ];
};


code:
'use strict';

exports.name = 'addAttributesToSVGElement';
exports.type = 'visitor';
exports.active = false;
exports.description = 'adds attributes to an outer <svg> element';

var ENOCLS = `Error in plugin "addAttributesToSVGElement": absent parameters.
It should have a list of "attributes" or one "attribute".
Config example:

plugins: [
  {
    name: 'addAttributesToSVGElement',
    params: {
      attribute: "mySvg"
    }
  }
]

plugins: [
  {
    name: 'addAttributesToSVGElement',
    params: {
      attributes: ["mySvg", "size-big"]
    }
  }
]

plugins: [
  {
    name: 'addAttributesToSVGElement',
    params: {
      attributes: [
        {
          focusable: false
        },
        {
          'data-image': icon
        }
      ]
    }
  }
]
`;

/**
 * Add attributes to an outer <svg> element. Example config:
 *
 * @author April Arcus
 *
 * @type {import('../lib/types').Plugin<{
 *   attribute?: string | Record<string, null | string>,
 *   attributes?: Array<string | Record<string, null | string>>
 * }>}
 */
exports.fn = (root, params) => {
  if (!Array.isArray(params.attributes) && !params.attribute) {
    console.error(ENOCLS);
    return null;
  }
  const attributes = params.attributes || [params.attribute];
  return {
    element: {
      enter: (node, parentNode) => {
        if (node.name === 'svg' && parentNode.type === 'root') {
          for (const attribute of attributes) {
            if (typeof attribute === 'string') {
              if (node.attributes[attribute] == null) {
                // @ts-ignore disallow explicit nullable attribute value
                node.attributes[attribute] = undefined;
              }
            }
            if (typeof attribute === 'object') {
              for (const key of Object.keys(attribute)) {
                if (node.attributes[key] == null) {
                  // @ts-ignore disallow explicit nullable attribute value
                  node.attributes[key] = attribute[key];
                }
              }
            }
          }
        }
      },
    },
  };
};


code:
'use strict';

exports.name = 'addClassesToSVGElement';
exports.type = 'visitor';
exports.active = false;
exports.description = 'adds classnames to an outer <svg> element';

var ENOCLS = `Error in plugin "addClassesToSVGElement": absent parameters.
It should have a list of classes in "classNames" or one "className".
Config example:

plugins: [
  {
    name: "addClassesToSVGElement",
    params: {
      className: "mySvg"
    }
  }
]

plugins: [
  {
    name: "addClassesToSVGElement",
    params: {
      classNames: ["mySvg", "size-big"]
    }
  }
]
`;

/**
 * Add classnames to an outer <svg> element. Example config:
 *
 * plugins: [
 *   {
 *     name: "addClassesToSVGElement",
 *     params: {
 *       className: "mySvg"
 *     }
 *   }
 * ]
 *
 * plugins: [
 *   {
 *     name: "addClassesToSVGElement",
 *     params: {
 *       classNames: ["mySvg", "size-big"]
 *     }
 *   }
 * ]
 *
 * @author April Arcus
 *
 * @type {import('../lib/types').Plugin<{
 *   className?: string,
 *   classNames?: Array<string>
 * }>}
 */
exports.fn = (root, params) => {
  if (
    !(Array.isArray(params.classNames) && params.classNames.some(String)) &&
    !params.className
  ) {
    console.error(ENOCLS);
    return null;
  }
  const classNames = params.classNames || [params.className];
  return {
    element: {
      enter: (node, parentNode) => {
        if (node.name === 'svg' && parentNode.type === 'root') {
          const classList = new Set(
            node.attributes.class == null
              ? null
              : node.attributes.class.split(' ')
          );
          for (const className of classNames) {
            if (className != null) {
              classList.add(className);
            }
          }
          node.attributes.class = Array.from(classList).join(' ');
        }
      },
    },
  };
};


code:
'use strict';

exports.name = 'cleanupAttrs';
exports.type = 'visitor';
exports.active = true;
exports.description =
  'cleanups attributes from newlines, trailing and repeating spaces';

const regNewlinesNeedSpace = /(\S)\r?\n(\S)/g;
const regNewlines = /\r?\n/g;
const regSpaces = /\s{2,}/g;

/**
 * Cleanup attributes values from newlines, trailing and repeating spaces.
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<{
 *   newlines?: boolean,
 *   trim?: boolean,
 *   spaces?: boolean
 * }>}
 */
exports.fn = (root, params) => {
  const { newlines = true, trim = true, spaces = true } = params;
  return {
    element: {
      enter: (node) => {
        for (const name of Object.keys(node.attributes)) {
          if (newlines) {
            // new line which requires a space instead of themselve
            node.attributes[name] = node.attributes[name].replace(
              regNewlinesNeedSpace,
              (match, p1, p2) => p1 + ' ' + p2
            );
            // simple new line
            node.attributes[name] = node.attributes[name].replace(
              regNewlines,
              ''
            );
          }
          if (trim) {
            node.attributes[name] = node.attributes[name].trim();
          }
          if (spaces) {
            node.attributes[name] = node.attributes[name].replace(
              regSpaces,
              ' '
            );
          }
        }
      },
    },
  };
};


code:
'use strict';

const { visit } = require('../lib/xast.js');

exports.type = 'visitor';
exports.name = 'cleanupEnableBackground';
exports.active = true;
exports.description =
  'remove or cleanup enable-background attribute when possible';

/**
 * Remove or cleanup enable-background attr which coincides with a width/height box.
 *
 * @see https://www.w3.org/TR/SVG11/filters.html#EnableBackgroundProperty
 *
 * @example
 * <svg width="100" height="50" enable-background="new 0 0 100 50">
 *             ⬇
 * <svg width="100" height="50">
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = (root) => {
  const regEnableBackground =
    /^new\s0\s0\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)$/;

  let hasFilter = false;
  visit(root, {
    element: {
      enter: (node) => {
        if (node.name === 'filter') {
          hasFilter = true;
        }
      },
    },
  });

  return {
    element: {
      enter: (node) => {
        if (node.attributes['enable-background'] == null) {
          return;
        }
        if (hasFilter) {
          if (
            (node.name === 'svg' ||
              node.name === 'mask' ||
              node.name === 'pattern') &&
            node.attributes.width != null &&
            node.attributes.height != null
          ) {
            const match =
              node.attributes['enable-background'].match(regEnableBackground);
            if (
              match != null &&
              node.attributes.width === match[1] &&
              node.attributes.height === match[3]
            ) {
              if (node.name === 'svg') {
                delete node.attributes['enable-background'];
              } else {
                node.attributes['enable-background'] = 'new';
              }
            }
          }
        } else {
          //we don't need 'enable-background' if we have no filters
          delete node.attributes['enable-background'];
        }
      },
    },
  };
};