fn = (_root, params) => {
  const {
    remove = true,
    minify = true,
    preserve = [],
    preservePrefixes = [],
    force = false,
  } = params;
  const preserveIds = new Set(
    Array.isArray(preserve) ? preserve : preserve ? [preserve] : [],
  );
  const preserveIdPrefixes = Array.isArray(preservePrefixes)
    ? preservePrefixes
    : preservePrefixes
      ? [preservePrefixes]
      : [];
  /** @type {Map<string, import('../lib/types.js').XastElement>} */
  const nodeById = new Map();
  /** @type {Map<string, {element: import('../lib/types.js').XastElement, name: string }[]>} */
  const referencesById = new Map();
  let deoptimized = false;

  return {
    element: {
      enter: (node) => {
        if (!force) {
          // deoptimize if style or scripts are present
          if (
            (node.name === 'style' && node.children.length !== 0) ||
            hasScripts(node)
          ) {
            deoptimized = true;
            return;
          }

          // avoid removing IDs if the whole SVG consists only of defs
          if (node.name === 'svg') {
            let hasDefsOnly = true;
            for (const child of node.children) {
              if (child.type !== 'element' || child.name !== 'defs') {
                hasDefsOnly = false;
                break;
              }
            }
            if (hasDefsOnly) {
              return visitSkip;
            }
          }
        }

        for (const [name, value] of Object.entries(node.attributes)) {
          if (name === 'id') {
            // collect all ids
            const id = value;
            if (nodeById.has(id)) {
              delete node.attributes.id; // remove repeated id
            } else {
              nodeById.set(id, node);
            }
          } else {
            const ids = findReferences(name, value);
            for (const id of ids) {
              let refs = referencesById.get(id);
              if (refs == null) {
                refs = [];
                referencesById.set(id, refs);
              }
              refs.push({ element: node, name });
            }
          }
        }
      },
    },

    root: {
      exit: () => {
        if (deoptimized) {
          return;
        }
        /**
         * @param {string} id
         * @returns {boolean}
         */
        const isIdPreserved = (id) =>
          preserveIds.has(id) || hasStringPrefix(id, preserveIdPrefixes);
        /** @type {?number[]} */
        let currentId = null;
        for (const [id, refs] of referencesById) {
          const node = nodeById.get(id);
          if (node != null) {
            // replace referenced IDs with the minified ones
            if (minify && isIdPreserved(id) === false) {
              /** @type {?string} */
              let currentIdString;
              do {
                currentId = generateId(currentId);
                currentIdString = getIdString(currentId);
              } while (
                isIdPreserved(currentIdString) ||
                (referencesById.has(currentIdString) &&
                  nodeById.get(currentIdString) == null)
              );
              node.attributes.id = currentIdString;
              for (const { element, name } of refs) {
                const value = element.attributes[name];
                if (value.includes('#')) {
                  // replace id in href and url()
                  element.attributes[name] = value
                    .replace(`#${encodeURI(id)}`, `#${currentIdString}`)
                    .replace(`#${id}`, `#${currentIdString}`);
                } else {
                  // replace id in begin attribute
                  element.attributes[name] = value.replace(
                    `${id}.`,
                    `${currentIdString}.`,
                  );
                }
              }
            }
            // keep referenced node
            nodeById.delete(id);
          }
        }
        // remove non-referenced IDs attributes from elements
        if (remove) {
          for (const [id, node] of nodeById) {
            if (isIdPreserved(id) === false) {
              delete node.attributes.id;
            }
          }
        }
      },
    },
  };
}
Remove unused and minify used IDs (only if there are no `<style>` or
`<script>` nodes).

@author Kir Belevich

@type {import('../lib/types.js').Plugin<CleanupIdsParams>}