mergePaths.js•4.21 kB
'use strict';
/**
 * @typedef {import("../lib/types").PathDataItem} PathDataItem
 * @typedef {import('../lib/types').XastChild} XastChild
 * @typedef {import('../lib/types').XastElement} XastElement
 */
const { collectStylesheet, computeStyle } = require('../lib/style.js');
const { path2js, js2path, intersects } = require('./_path.js');
exports.name = 'mergePaths';
exports.description = 'merges multiple paths in one if possible';
/**
 * Merge multiple Paths into one.
 *
 * @author Kir Belevich, Lev Solntsev
 *
 * @type {import('./plugins-types').Plugin<'mergePaths'>}
 */
exports.fn = (root, params) => {
  const {
    force = false,
    floatPrecision,
    noSpaceAfterFlags = false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20
  } = params;
  const stylesheet = collectStylesheet(root);
  return {
    element: {
      enter: (node) => {
        if (node.children.length <= 1) {
          return;
        }
        /** @type {XastChild[]} */
        const elementsToRemove = [];
        let prevChild = node.children[0];
        let prevPathData = null;
        /**
         * @param {XastElement} child
         * @param {PathDataItem[]} pathData
         */
        const updatePreviousPath = (child, pathData) => {
          js2path(child, pathData, {
            floatPrecision,
            noSpaceAfterFlags,
          });
          prevPathData = null;
        };
        for (let i = 1; i < node.children.length; i++) {
          const child = node.children[i];
          if (
            prevChild.type !== 'element' ||
            prevChild.name !== 'path' ||
            prevChild.children.length !== 0 ||
            prevChild.attributes.d == null
          ) {
            if (prevPathData && prevChild.type === 'element') {
              updatePreviousPath(prevChild, prevPathData);
            }
            prevChild = child;
            continue;
          }
          if (
            child.type !== 'element' ||
            child.name !== 'path' ||
            child.children.length !== 0 ||
            child.attributes.d == null
          ) {
            if (prevPathData) {
              updatePreviousPath(prevChild, prevPathData);
            }
            prevChild = child;
            continue;
          }
          const computedStyle = computeStyle(stylesheet, child);
          if (
            computedStyle['marker-start'] ||
            computedStyle['marker-mid'] ||
            computedStyle['marker-end']
          ) {
            if (prevPathData) {
              updatePreviousPath(prevChild, prevPathData);
            }
            prevChild = child;
            continue;
          }
          const childAttrs = Object.keys(child.attributes);
          if (childAttrs.length !== Object.keys(prevChild.attributes).length) {
            if (prevPathData) {
              updatePreviousPath(prevChild, prevPathData);
            }
            prevChild = child;
            continue;
          }
          const areAttrsEqual = childAttrs.some((attr) => {
            return (
              attr !== 'd' &&
              prevChild.type === 'element' &&
              prevChild.attributes[attr] !== child.attributes[attr]
            );
          });
          if (areAttrsEqual) {
            if (prevPathData) {
              updatePreviousPath(prevChild, prevPathData);
            }
            prevChild = child;
            continue;
          }
          const hasPrevPath = prevPathData != null;
          const currentPathData = path2js(child);
          prevPathData = prevPathData ?? path2js(prevChild);
          if (force || !intersects(prevPathData, currentPathData)) {
            prevPathData.push(...currentPathData);
            elementsToRemove.push(child);
            continue;
          }
          if (hasPrevPath) {
            updatePreviousPath(prevChild, prevPathData);
          }
          prevChild = child;
          prevPathData = null;
        }
        if (prevPathData && prevChild.type === 'element') {
          updatePreviousPath(prevChild, prevPathData);
        }
        node.children = node.children.filter(
          (child) => !elementsToRemove.includes(child),
        );
      },
    },
  };
};