UNPKG

mathup

Version:

Easy MathML authoring tool with a quick to write syntax

3,873 lines • 93.8 kB
/*! mathup v1.0.0 | (c) 2015-2025 (undefined) | undefined */
var mathup = (function () {
  'use strict';

  /**
   * @typedef {import("../tokenizer/index.js").Token} Token
   * @typedef {import("./index.js").Node} Node
   */

  /**
   * @param {Token} token
   * @returns {boolean}
   */
  function isPipeOperator(token) {
    return token.type === "operator" && token.value === "|";
  }

  /**
   * @param {Token} token
   * @returns {boolean}
   */
  function isDoublePipeOperator(token) {
    return token.type === "operator" && token.value === "∥";
  }

  /**
   * Double pipe defaults to the parallel-to character which is behaves
   * wrong when used as a fence.
   * @param {Node[]} items
   * @returns {void}
   */
  function maybeFixDoublePipe(items) {
    if (items.length < 2) {
      return;
    }
    const first = items.at(0);
    if (first?.type === "OperatorLiteral" && first.value === "∥") {
      first.value = "‖";
    }
    const last = items.at(-1);
    if (last?.type === "OperatorLiteral" && last.value === "∥") {
      last.value = "‖";
    }
  }

  /**
   * @param {Node} node
   * @returns {void}
   */
  function addZeroLSpaceToOperator(node) {
    let first = node;
    while (first && (first.type === "Term" || first.type === "UnaryOperation" || first.type === "BinaryOperation" || first.type === "TernaryOperation")) {
      [first] = first.items;
    }
    if (!first) {
      return;
    }
    if (first.type === "OperatorLiteral") {
      if (!first.attrs) {
        first.attrs = {};
      }
      if (typeof first.attrs.lspace === "undefined") {
        first.attrs.lspace = 0;
      }
    }
  }

  /**
   * @typedef {import("../../tokenizer/index.js").Token} Token
   * @typedef {import("../index.js").Node} Node
   * @typedef {import("../index.js").Term} Term
   * @typedef {import("../index.js").MultiScripts} MultiScripts
   */

  /** @returns {Term} */
  function empty$1() {
    return {
      type: "Term",
      items: []
    };
  }

  /**
   * @param {Token} token
   * @returns {boolean}
   */
  function isIndexBreak(token) {
    if (token.type === "sep.row" || token.type === "sep.col") {
      return true;
    }
    if (token.type !== "infix") {
      return false;
    }
    return token.value === "sub" || token.value === "sup";
  }

  /**
   * @param {Node[] | null} nodes
   * @returns {Node[]}
   */
  function prepareScript(nodes) {
    if (!nodes) {
      return [];
    }
    if (nodes.at(-1)?.type === "SpaceLiteral") {
      // ignore trailing space
      nodes.pop();
    }
    if (nodes.length !== 1) {
      return nodes;
    }
    const [node] = nodes;
    if (node.type !== "FencedGroup" || node.items.length !== 1) {
      return nodes;
    }
    const [cell] = node.items;
    if (cell.length === 1) {
      const [first] = cell;
      const term = first.type === "Term" && first.items.length === 1 ? first.items[0] : first;
      if (term.type.endsWith("Literal")) {
        // We fenced a single item for a reason, lets keep them.
        return nodes;
      }
    }
    return [{
      type: "Term",
      items: cell
    }];
  }

  /**
   * Parse the series of sub- and sup indices of the multiscript. Note
   * we assume the first two tokens have already been checked.
   *
   * @param {import("../parse.js").State} state
   * @returns {{ scripts: [Node[], Node[]][], end: number }}
   */
  function parseScripts(state) {
    let i = state.start + 1;
    let token = state.tokens.at(i);

    /** @type {[Node[], Node[]][]} */
    const scripts = [];
    /** @type {Node[] | null} */
    let sub = null;
    /** @type {Node[] | null} */
    let sup = null;

    /**
     * @returns {void}
     */
    function commit() {
      if (sub && sub.length > 0 || sup && sup.length > 0) {
        scripts.push([prepareScript(sub), prepareScript(sup)]);
      }
      sub = null;
      sup = null;
    }

    // Remember previous position to allow repeat positions.
    let position = token?.value ?? "sub";
    while (token && isIndexBreak(token)) {
      if (token.type === "infix") {
        // Update current position
        position = token.value;
      }
      i += 1;
      token = state.tokens[i];
      if (token && token.type === "space") {
        i += 1;
        token = state.tokens[i];
      }

      /** @type {Node[]} */
      const items = [];
      while (token && token.type !== "paren.close" && !isIndexBreak(token)) {
        const next = expr({
          ...state,
          start: i,
          stack: [],
          nestLevel: state.nestLevel + 1,
          stopAt(other) {
            return other.type === "infix" && (other.value === "sub" || other.value === "sup");
          }
        });
        items.push(next.node);
        i = next.end;
        token = state.tokens[i];
      }
      if (position === "sup") {
        if (sup) {
          commit();
        }
        sup = items;
      } else {
        if (sub) {
          commit();
        }
        sub = items;
      }
    }
    if (sub || sup) {
      commit();
    }
    if (token?.type === "paren.close") {
      i += 1;
    }
    return {
      scripts,
      end: i
    };
  }

  /**
   * Parse a multiscript. Note that we assume the first two tokens been
   * checked.
   *
   * @param {import("../parse.js").State} state
   * @returns {{ node: MultiScripts, end: number }}
   */
  function multiscripts$1(state) {
    let {
      scripts,
      end: i
    } = parseScripts(state);
    let token = state.tokens.at(i);

    /** @type {Node | undefined} */
    let base;
    /** @type {[Node[], Node[]][] | undefined} */
    let prescripts;
    if (!token || token.type === "space") {
      // There is nothing after the already parsed scripts. Apply as postscripts.
      base = state.stack.pop();
      if (base?.type === "SpaceLiteral") {
        base = empty$1();
      }
    } else {
      // existing scripts are prescripts. See if there are postscripts.
      prescripts = scripts;
      scripts = [];
      const next = expr({
        ...state,
        start: i,
        stack: [],
        nestLevel: state.nestLevel + 1,
        stopAt(other) {
          return other.type === "paren.open";
        }
      });
      base = next.node;
      i = next.end;
      token = state.tokens[i];
      if (token?.type === "paren.open") {
        const nextToken = state.tokens.at(i + 1);
        if (nextToken?.type === "infix" && (nextToken.value === "sub" || nextToken.value === "sup")) {
          ({
            scripts,
            end: i
          } = parseScripts({
            ...state,
            start: i
          }));
        }
      }
    }

    /** @type {MultiScripts} */
    const node = {
      type: "MultiScripts",
      base: base ?? empty$1(),
      post: scripts
    };
    if (prescripts) {
      node.pre = prescripts;
    }
    return {
      node,
      end: i
    };
  }

  /**
   * @typedef {import("../../tokenizer/index.js").Token} Token
   * @typedef {import("../index.js").Node} Node
   * @typedef {import("../index.js").FencedGroup} FencedGroup
   * @typedef {import("../index.js").MatrixGroup} MatrixGroup
   * @typedef {import("../index.js").MultiScripts} MultiScripts
   * @typedef {import("../index.js").LiteralAttrs} LiteralAttrs
   */

  /**
   * @param {Token} token
   * @returns {Omit<Token, "type">}
   */
  function omitType(token) {
    const {
      type: _type,
      ...rest
    } = token;
    return rest;
  }

  /**
   * @param {import("../parse.js").State} state
   * @returns {{ node: FencedGroup | MatrixGroup | MultiScripts, end: number }}
   */
  function group(state) {
    let i = state.start;
    let token = state.tokens[i];
    const open = token;

    /** @type {{ value: string, attrs?: LiteralAttrs }[]} */
    const seps = [];

    /** @type {Node[]} */
    let cell = [];

    /** @type {Node[][]} */
    let cols = [];

    /** @type {Node[][][]} */
    const rows = [];
    i += 1;
    token = state.tokens[i];
    if (token && token.type === "space") {
      // Ignore leading space.
      i += 1;
      token = state.tokens[i];
    }
    if (token && token.type === "infix" && (token.value === "sub" || token.value === "sup")) {
      return multiscripts$1({
        ...state,
        start: i - 1
      });
    }
    while (token && token.type !== "paren.close") {
      if (token.type === "space" && token.value === " ") {
        // No need to add tokens which don’t render elements to our
        // cell.
        i += 1;
        token = state.tokens[i];
        continue;
      }
      if (token.type === "sep.col") {
        /** @type {{ value: string, attrs?: LiteralAttrs }} */
        const sepToken = {
          value: token.value
        };
        if (token.attrs) {
          sepToken.attrs = token.attrs;
        }
        seps.push(sepToken);
        cols.push(cell);
        cell = [];
        i += 1;
        token = state.tokens[i];

        // Ignore leading space.
        if (token && token.type === "space") {
          i += 1;
          token = state.tokens[i];
        }
        continue;
      }
      if (token.type === "sep.row") {
        cols.push(cell);
        rows.push(cols);
        cell = [];
        cols = [];
        i += 1;
        token = state.tokens[i];

        // Ignore leading space.
        if (token && token.type === "space") {
          i += 1;
          token = state.tokens[i];
        }
        continue;
      }
      if (cell.length === 1) {
        // If first element is an operator it may throw alignment out
        // with its implicit lspace.
        addZeroLSpaceToOperator(cell[0]);
      }
      const next = expr({
        ...state,
        start: i,
        stack: cell,
        nestLevel: state.nestLevel + 1
      });
      cell.push(next.node);
      i = next.end;
      token = state.tokens[i];
    }
    if (cell.length > 0) {
      cols.push(cell);
    }
    const end = i + 1;
    const close = token && token.type === "paren.close" ? token : null;
    const attrs = {
      open: omitType(open),
      close: close ? omitType(close) : null,
      seps
    };
    if (attrs.close?.value === "|" && !open.value) {
      // Add a small space before the "evaluate at" operator
      if (!attrs.close.attrs) {
        attrs.close.attrs = {};
      }
      attrs.close.attrs.lspace = "0.35ex";
    }
    if (rows.length === 0) {
      return {
        node: {
          type: "FencedGroup",
          items: cols,
          attrs
        },
        end
      };
    }
    const rowItems = rows;
    if (cols.length > 0) {
      rowItems.push(cols);
    }
    return {
      node: {
        type: "MatrixGroup",
        items: rowItems,
        attrs
      },
      end
    };
  }

  /**
   * @typedef {import("../../tokenizer/index.js").Token} Token
   * @typedef {import("../index.js").Node} Node
   * @typedef {import("../index.js").UnaryOperation} UnaryOperation
   * @typedef {import("../index.js").BinaryOperation} BinaryOperation
   * @typedef {import("../index.js").TernaryOperation} TernaryOperation
   * @typedef {UnaryOperation | BinaryOperation | TernaryOperation} Operation
   * @typedef {import("../index.js").Term} Term
   * @typedef {import("../parse.js").State} State
   */

  /**
   * @param {Node} node
   * @param {string[]} transforms
   * @returns {Operation | Term}
   */
  function insertTransformNode(node, transforms) {
    if (node.type === "Term" && node.items.length > 0) {
      // Only apply transform to first node.
      const [first, ...rest] = node.items;
      return {
        ...node,
        items: [insertTransformNode(first, transforms), ...rest]
      };
    }
    if (node.type === "BinaryOperation") {
      const [left, right] = node.items;
      return {
        ...node,
        items: [insertTransformNode(left, transforms), right]
      };
    }
    if (node.type === "TernaryOperation") {
      const [a, b, c] = node.items;
      return {
        ...node,
        items: [insertTransformNode(a, transforms), b, c]
      };
    }
    return {
      type: "UnaryOperation",
      name: "command",
      transforms,
      items: [node]
    };
  }

  /**
   * @param {State} state
   * @returns {((token: Token) => boolean) | undefined}
   */
  function maybeStopAtPipe$1({
    start,
    tokens,
    stack,
    stopAt
  }) {
    if (stopAt) {
      return stopAt;
    }
    if (stack.length !== 1) {
      return undefined;
    }
    const lastToken = start > 0 ? tokens[start - 1] : undefined;
    if (!lastToken || lastToken.type !== "operator") {
      return undefined;
    }
    if (lastToken.value === "|") {
      return isPipeOperator;
    }
    if (lastToken.value === "∥") {
      return isDoublePipeOperator;
    }
    return undefined;
  }

  /**
   * @param {State} state
   * @returns {{ node: Operation | Term; end: number }}
   */
  function command(state) {
    const token = state.tokens[state.start];
    if (!token.name) {
      throw new Error("Got command token without a name");
    }

    /** @type {string[]} */
    const textTransforms = [];

    /** @type {Map<string, string>} */
    const styles = new Map();

    /**
     * @param {Token} token
     * @returns {void}
     */
    function handleCommandToken({
      name,
      value
    }) {
      if (!value) {
        return;
      }
      if (name === "text-transform") {
        textTransforms.push(value);
      } else if (name) {
        styles.set(name, value);
      }
    }
    const stopAt = maybeStopAtPipe$1(state);
    handleCommandToken(token);
    let pos = state.start + 1;
    let nextToken = state.tokens[pos];
    while (nextToken && (nextToken.type === "command" || nextToken.type === "space")) {
      if (nextToken.type === "command") {
        handleCommandToken(nextToken);
      }
      pos += 1;
      nextToken = state.tokens[pos];
    }
    const next = expr({
      ...state,
      stack: [],
      start: pos,
      nestLevel: state.nestLevel + 1,
      textTransforms,
      stopAt
    });
    if (textTransforms.length === 0) {
      // Only apply styles.
      return {
        node: {
          type: "UnaryOperation",
          name: "command",
          styles,
          items: [next.node]
        },
        end: next.end
      };
    }
    const node = insertTransformNode(next.node, textTransforms);
    if (styles.size > 0) {
      return {
        node: {
          type: "UnaryOperation",
          name: "command",
          styles,
          items: [node]
        },
        end: next.end
      };
    }
    return {
      node,
      end: next.end
    };
  }

  /**
   * @typedef {import("../../tokenizer/index.js").Token} Token
   * @typedef {import("../index.js").Node} Node
   * @typedef {import("../index.js").UnaryOperation} UnaryOperation
   * @typedef {import("../index.js").BinaryOperation} BinaryOperation
   * @typedef {import("../parse.js").State} State
   */

  /**
   * @param {Node[]} nodes
   * @returns {Node}
   */
  function toTermOrUnwrap(nodes) {
    if (nodes.length === 1) {
      return nodes[0];
    }
    return {
      type: "Term",
      items: nodes
    };
  }

  /**
   * @param {State} state
   * @returns {((token: Token) => boolean) | undefined}
   */
  function maybeStopAtPipe({
    start,
    tokens,
    stack,
    stopAt
  }) {
    if (stopAt) {
      return stopAt;
    }
    if (stack.length !== 1) {
      return undefined;
    }
    const token = tokens[start];
    if (!token || token.arity && token.arity !== 1) {
      return undefined;
    }
    const lastToken = start > 0 ? tokens[start - 1] : undefined;
    if (!lastToken || lastToken.type !== "operator") {
      return undefined;
    }
    if (lastToken.value === "|") {
      return isPipeOperator;
    }
    if (lastToken.value === "∥") {
      return isDoublePipeOperator;
    }
    return undefined;
  }

  /**
   * @param {import("../parse.js").State} state
   * @returns {{ node: UnaryOperation | BinaryOperation; end: number }}
   */
  function prefix(state) {
    const {
      tokens,
      start
    } = state;
    const token = tokens[start];
    const nestLevel = state.nestLevel + 1;
    if (!token.name) {
      throw new Error("Got prefix token without a name");
    }
    const stopAt = maybeStopAtPipe(state);
    let next = expr({
      ...state,
      stack: [],
      start: start + 1,
      nestLevel,
      stopAt
    });
    if (next && next.node && next.node.type === "SpaceLiteral") {
      next = expr({
        ...state,
        stack: [],
        start: next.end,
        nestLevel,
        stopAt
      });
    }

    // XXX: Arity > 2 not implemented.
    if (token.arity === 2) {
      if (next && next.node && next.node.type === "FencedGroup" && next.node.items.length === 2) {
        const [first, second] = next.node.items;
        /** @type {[Node, Node]} */
        const items = token.name === "root" ? [toTermOrUnwrap(second), toTermOrUnwrap(first)] : [toTermOrUnwrap(first), toTermOrUnwrap(second)];
        return {
          node: {
            type: "BinaryOperation",
            name: token.name,
            attrs: token.attrs,
            items
          },
          end: next.end
        };
      }
      const first = next;
      let second = next && expr({
        ...state,
        stack: [],
        start: next.end,
        nestLevel
      });
      if (second && second.node && second.node.type === "SpaceLiteral") {
        second = expr({
          ...state,
          stack: [],
          start: second.end,
          nestLevel
        });
      }

      /** @type {BinaryOperation} */
      const node = {
        type: "BinaryOperation",
        name: token.name,
        items: [first.node, second.node]
      };
      if (token.name === "root") {
        node.items = [second.node, first.node];
      }
      if (token.attrs) {
        node.attrs = token.attrs;
      }
      return {
        node,
        end: second.end
      };
    }

    /** @type {UnaryOperation} */
    const node = {
      type: "UnaryOperation",
      name: token.name,
      items: [next.node]
    };
    if (token.accent) {
      node.accent = token.accent;
    }
    if (token.attrs) {
      node.attrs = token.attrs;
    }
    if (next && next.node && next.node.type === "FencedGroup" && next.node.items.length === 1) {
      // The operand is not a matrix.

      node.items = [toTermOrUnwrap(next.node.items[0])];
    }
    return {
      node,
      end: next.end
    };
  }

  /**
   * @typedef {import("../parse.js").State} State
   * @typedef {import("../index.js").SpaceLiteral} SpaceLiteral
   */

  /**
   * @param {number} n - Number of space literals
   * @returns {number} - The width in units of ex
   */
  function spaceWidth(n) {
    if (n <= 0) {
      return 0;
    }
    if (n <= 3) {
      return 0.35 * (n - 1);
    }
    if (n <= 5) {
      return 0.5 * (n - 1);
    }
    return n - 3;
  }

  /**
   * @param {State} state
   * @returns {{ node: SpaceLiteral, end: number }}
   */
  function space(state) {
    const token = state.tokens[state.start];
    const lineBreak = token.value.startsWith("\n");
    const width = lineBreak ? 0 : token.value.length;
    return {
      node: {
        type: "SpaceLiteral",
        attrs: {
          width: `${spaceWidth(width)}ex`
        }
      },
      end: state.start + 1
    };
  }

  /**
   * @typedef {import("../../tokenizer/index.js").TokenType} TokenType
   * @typedef {import("../parse.js").State} State
   * @typedef {import("../index.js").Node} Node
   * @typedef {import("../index.js").Literal} Literal
   * @typedef {(state: State) => { node: Node, end: number }} Handler
   * @typedef {"Ident" | "Number" | "Operator" | "Text"} LiteralType
   */

  /**
   * @param {LiteralType} type
   * @returns {Handler}
   */
  const literal$1 = type => ({
    start,
    tokens
  }) => {
    const {
      value,
      attrs
    } = tokens[start];

    /** @type {Literal} */
    const node = {
      type: `${type}Literal`,
      value
    };
    if (attrs) {
      node.attrs = attrs;
    }
    return {
      node,
      end: start + 1
    };
  };

  /** @type {[TokenType, Handler][]} */
  const handlers = [["command", command], ["ident", literal$1("Ident")], ["number", literal$1("Number")], ["operator", literal$1("Operator")], ["text", literal$1("Text")], ["infix", infix], ["paren.open", group], ["prefix", prefix], ["space", space]];
  var handlers$1 = new Map(handlers);

  /**
   * @typedef {import("../parse.js").State} State
   * @typedef {import("../index.js").Node} Node
   * @typedef {import("../index.js").Term} Term
   * @typedef {import("../index.js").BinaryOperation} BinaryOperation
   * @typedef {import("../index.js").TernaryOperation} TernaryOperation
   */

  /** @returns {Term} */
  function empty() {
    return {
      type: "Term",
      items: []
    };
  }
  const SHOULD_STOP = ["ident", "number", "operator", "text"];

  /**
   * Remove surrounding brackets.
   *
   * @template {BinaryOperation | TernaryOperation} Operation
   * @param {Operation} node
   * @returns {Operation}
   */
  function maybeRemoveFence(node) {
    const mutated = node;
    mutated.items.forEach((item, i) => {
      if (item.type !== "FencedGroup" || item.items.length !== 1) {
        // No fences to remove.
        return;
      }
      if (i === 0 && node.name !== "frac") {
        // Keep fences around base in sub- and superscripts.
        return;
      }
      const [cell] = item.items;
      if (cell.length !== 1) {
        mutated.items[i] = {
          type: "Term",
          items: cell
        };
        return;
      }
      const [first] = cell;
      const term = first.type === "Term" && first.items.length === 1 ? first.items[0] : first;
      if (term.type.endsWith("Literal")) {
        // We fenced a single item for a reason, lets keep them.
        return;
      }
      mutated.items[i] = term;
    });
    return mutated;
  }

  /**
   * Change `lim` to `under`, and `sum` and `prod` to `under` or `over`.
   *
   * @template {BinaryOperation | TernaryOperation} Operation
   * @param {Operation} node
   * @returns {Operation}
   */
  function maybeApplyUnderOver(node) {
    const mutated = node;
    const [operator] = node.items;
    if (operator.type !== "OperatorLiteral") {
      return mutated;
    }
    if (node.name === "sub" && ["lim", "∑", "∏", "⋂", "⋃", "⋀", "⋁"].includes(operator.value)) {
      mutated.name = "under";
      return mutated;
    }
    if (node.name === "subsup" && ["∑", "∏", "⋂", "⋃", "⋀", "⋁"].includes(operator.value)) {
      mutated.name = "underover";
      return mutated;
    }
    return mutated;
  }

  /**
   * @template {BinaryOperation | TernaryOperation} Operation
   * @param {Operation} node
   * @returns {Operation}
   */
  function fixFracSpacing(node) {
    if (node.name !== "frac") {
      return node;
    }
    for (const item of node.items) {
      addZeroLSpaceToOperator(item);
    }
    return node;
  }

  /**
   * @template {BinaryOperation | TernaryOperation} Operation
   * @param {Operation} node
   * @returns {Operation}
   */
  function post(node) {
    return fixFracSpacing(maybeRemoveFence(maybeApplyUnderOver(node)));
  }

  /**
   * @param {string} op
   * @param {BinaryOperation} left
   * @param {Node} right
   * @returns {BinaryOperation | TernaryOperation}
   */
  function maybeTernary(op, left, right) {
    if (left.name === "sub" && op === "sup") {
      const [base, sub] = left.items;
      return {
        type: "TernaryOperation",
        name: "subsup",
        items: [base, sub, right]
      };
    }
    if (left.name === "sup" && op === "sub") {
      const [base, sup] = left.items;
      return {
        type: "TernaryOperation",
        name: "subsup",
        items: [base, right, sup]
      };
    }
    if (left.name === "under" && (op === "over" || op === "sup")) {
      const [base, under] = left.items;
      return {
        type: "TernaryOperation",
        name: "underover",
        items: [base, under, right]
      };
    }
    if (left.name === "over" && (op === "under" || op === "sub")) {
      const [base, over] = left.items;
      return {
        type: "TernaryOperation",
        name: "underover",
        items: [base, right, over]
      };
    }
    const node = post({
      type: "BinaryOperation",
      name: op,
      items: [left, right]
    });
    return rightAssociate(node.name, node.items);
  }

  /**
   * @param {string} op
   * @param {[Node, Node]} operands
   * @returns {BinaryOperation}
   */
  function rightAssociate(op, [left, right]) {
    if (left.type !== "BinaryOperation" || op === "frac") {
      return {
        type: "BinaryOperation",
        name: op,
        items: [left, right]
      };
    }
    const [a, b] = left.items;
    return {
      type: "BinaryOperation",
      name: left.name,
      items: [a, rightAssociate(op, [b, right])]
    };
  }

  /**
   * @param {Node[]} nodes
   * @returns {boolean}
   */
  function isPipeDelimited(nodes) {
    if (nodes.length < 3) {
      return false;
    }
    const open = nodes.at(0);
    const close = nodes.at(-1);
    return open?.type === "OperatorLiteral" && close?.type === "OperatorLiteral" && (open.value === "|" || open.value === "∥" || open.value === "‖") && open.value === close.value;
  }

  /**
   * @param {State} state
   * @returns {{ node: BinaryOperation | TernaryOperation; end: number }}
   */
  function infix(state) {
    const {
      tokens,
      start,
      stack
    } = state;
    const nestLevel = state.nestLevel + 1;
    const token = tokens[start];

    /** @type {Node | undefined} */
    let left;
    if (isPipeDelimited(stack)) {
      maybeFixDoublePipe(stack);
      left = {
        type: "Term",
        items: [...stack]
      };
      stack.splice(0, stack.length);
    } else {
      left = stack.pop();
      if (left?.type === "SpaceLiteral") {
        left = stack.pop();
      }
    }
    if (!left) {
      left = empty();
    }
    const nextToken = tokens[start + 1];
    let next;
    if (nextToken && SHOULD_STOP.includes(nextToken.type)) {
      const handleRight = handlers$1.get(nextToken.type);
      if (!handleRight) {
        throw new Error("Unknown handler");
      }
      next = handleRight({
        ...state,
        stack: [],
        start: start + 1,
        nestLevel
      });
    } else {
      next = expr({
        ...state,
        stack: [],
        start: start + 1,
        nestLevel
      });
    }
    if (next && next.node && next.node.type === "SpaceLiteral") {
      next = expr({
        ...state,
        stack: [],
        start: next.end,
        nestLevel
      });
    }
    const {
      end,
      node: right
    } = next;
    if (left.type === "BinaryOperation") {
      return {
        end,
        node: post(maybeTernary(token.value, left, right))
      };
    }
    return {
      end,
      node: post({
        type: "BinaryOperation",
        name: token.value,
        items: [left, right]
      })
    };
  }

  /**
   * @typedef {import("../index.js").IdentLiteral} IdentLiteral
   * @typedef {import("../index.js").Literal} Literal
   * @typedef {import("../index.js").LiteralAttrs} LiteralAttrs
   * @typedef {import("../index.js").Node} Node
   * @typedef {import("../index.js").OperatorLiteral} OperatorLiteral
   * @typedef {import("../index.js").Term} Term
   * @typedef {import("../index.js").UnaryOperation} UnaryOperation
   * @typedef {import("../parse.js").State} State
   */

  const KEEP_GOING_TYPES = ["command", "ident", "infix", "number", "operator", "paren.open", "prefix", "text"];

  /**
   * @param {Node[]} items
   * @param {string[]} [textTransforms]
   * @returns {void}
   */
  function maybeFixDifferential(items, textTransforms) {
    // We may want to make the differnetial d operator an actual
    // operator to fix some spacing during integration.
    if (items.length < 2) {
      return;
    }
    const [first, second] = items;
    if (first.type !== "IdentLiteral" || first.value !== "d") {
      return;
    }
    let operand = second;
    while (operand.type === "UnaryOperation" || operand.type === "BinaryOperation" || operand.type === "TernaryOperation") {
      [operand] = operand.items;
    }
    if (operand.type !== "IdentLiteral") {
      return;
    }
    const value = (textTransforms?.length ?? 0) > 0 ? first.value : "𝑑";

    /** @type {OperatorLiteral & { attrs: LiteralAttrs }} */
    const node = {
      ...items[0],
      type: "OperatorLiteral",
      value,
      attrs: {
        ...(first.attrs ?? {}),
        rspace: "0"
      }
    };
    items[0] = node;
  }

  /**
   * @param {State} state
   * @returns {{ node: Term; end: number }}
   */
  function term$1(state) {
    let i = state.start;
    let token = state.tokens[i];
    /** @type {Node[]} */
    const items = [];
    while (token && KEEP_GOING_TYPES.includes(token.type) &&
    // Perhaps the parent handler wants to use this token.
    !state.stopAt?.(token)) {
      const handler = handlers$1.get(token.type);
      if (!handler) {
        throw new Error("Unknown Hander");
      }
      const next = handler({
        ...state,
        start: i,
        stack: items
      });
      items.push(next.node);
      i = next.end;
      token = state.tokens[i];
    }
    maybeFixDifferential(items, state.textTransforms);
    maybeFixDoublePipe(items);
    return {
      node: {
        type: "Term",
        items
      },
      end: i
    };
  }

  /** @typedef {import("../index.js").Node} Node */

  /**
   * @param {import("../parse.js").State} state
   * @returns {{ node: Node; end: number }}
   */
  function expr(state) {
    if (state.start >= state.tokens.length) {
      return {
        node: {
          type: "Term",
          items: []
        },
        end: state.start
      };
    }
    const {
      type
    } = state.tokens[state.start];
    if (type === "paren.open") {
      return group(state);
    }
    if (type === "space") {
      return space(state);
    }
    if (type === "infix") {
      return infix(state);
    }
    if (type === "prefix") {
      return prefix(state);
    }
    return term$1(state);
  }

  /**
   * @typedef {import("../tokenizer/index.js").Token} Token
   * @typedef {import("./index.js").Node} Node
   * @typedef {import("./index.js").Sentence} Sentence
   *
   * @typedef {object} State
   * @property {Token[]} tokens
   * @property {number} start
   * @property {Node[]} stack
   * @property {number} nestLevel
   * @property {(token: Token) => boolean} [stopAt]
   * @property {string[]} [textTransforms]
   *
   * @param {Token[]} tokens
   * @returns {Sentence}
   */
  function parse(tokens) {
    const body = [];
    let pos = 0;
    while (pos < tokens.length) {
      const state = {
        tokens,
        start: pos,
        stack: body,
        nestLevel: 1
      };
      const next = expr(state);
      pos = next.end;
      body.push(next.node);
    }
    return {
      type: "Sentence",
      body
    };
  }

  /* eslint-env browser */

  const NS = "http://www.w3.org/1998/Math/MathML";

  /**
   * @typedef {Required<import("./index.js").RenderOptions>} Options
   * @param {import("../transformer/index.js").Tag} node
   * @param {Options} options
   * @returns {Element | DocumentFragment}
   */
  function toDOM(node, {
    bare
  }) {
    /** @type {Element | DocumentFragment} */
    let element;
    if (node.tag === "math" && bare) {
      element = document.createDocumentFragment();
    } else {
      element = document.createElementNS(NS, node.tag);
    }
    if (element instanceof Element && node.attrs) {
      for (const [name, value] of Object.entries(node.attrs)) {
        element.setAttribute(name, `${value}`);
      }
    }
    if (node.textContent) {
      element.textContent = node.textContent;
    }
    if (node.childNodes) {
      for (const childNode of node.childNodes) {
        if (childNode) {
          element.appendChild(toDOM(childNode, {
            bare: false
          }));
        }
      }
    }
    return element;
  }

  /**
   * @param {string} str
   * @returns {string}
   */
  function escapeTextContent(str) {
    return str.replace(/[&<]/g, c => {
      if (c === "&") {
        return "&amp;";
      }
      return "&lt;";
    });
  }

  /**
   * @param {string} str
   * @returns {string}
   */
  function escapeAttrValue(str) {
    return str.replace(/"/g, "&quot;");
  }

  /**
   * @param {import("../transformer/index.js").Tag} node
   * @param {Required<import("./index.js").RenderOptions>} options
   * @returns {string}
   */
  function toString(node, {
    bare
  }) {
    const attrString = Object.entries(node.attrs || {}).map(([name, value]) => `${name}="${escapeAttrValue(`${value}`)}"`).join(" ");
    const openContent = attrString ? `${node.tag} ${attrString}` : node.tag;
    if (node.textContent) {
      const textContent = escapeTextContent(node.textContent);
      return `<${openContent}>${textContent}</${node.tag}>`;
    }
    if (node.childNodes) {
      const content = node.childNodes.map(child => child ? toString(child, {
        bare: false
      }) : "").join("");
      if (node.tag === "math" && bare) {
        return content;
      }
      return `<${openContent}>${content}</${node.tag}>`;
    }
    return `<${openContent} />`;
  }

  /**
   * @yields {never}
   */
  function* nullIter() {}

  /**
   * @template {unknown[]} T - Tuple type with item type of each input iterator
   * @param {{ [K in keyof T]: Iterable<T[K]> }} iterables - The iterators to be
   *   zipped
   * @yields {T}
   */
  function* zip(iterables) {
    const iterators = iterables.map(iterable => iterable ? iterable[Symbol.iterator]() : nullIter());
    while (true) {
      const next = iterators.map(iterator => iterator.next());
      if (next.every(({
        done
      }) => done)) {
        return;
      }
      yield (/** @type {T} */next.map(({
        value
      }) => value));
    }
  }

  /**
   * @typedef {import("../transformer/index.js").Tag} Tag
   *
   * @param {Element} parent
   * @param {Tag} node
   * @param {Required<import("./index.js").RenderOptions>} options
   * @returns {void}
   */
  function updateDOM(parent, node, options) {
    if (!parent) {
      throw new Error("updateDOM called on null");
    }
    if (parent.tagName.toLowerCase() !== node.tag) {
      throw new Error("tag name mismatch");
    }
    if (!(node.tag === "math" && options.bare)) {
      const desiredAttrs = node.attrs || {};
      const removeAttrs = [];
      for (const attr of parent.attributes) {
        const newValue = desiredAttrs[attr.name];
        if (!newValue) {
          removeAttrs.push(attr.name);
        } else if (newValue !== attr.value) {
          parent.setAttribute(attr.name, `${newValue}`);
        }
      }
      for (const name of removeAttrs) {
        parent.removeAttribute(name);
      }
      for (const [name, value] of Object.entries(desiredAttrs)) {
        if (!parent.getAttribute(name)) {
          parent.setAttribute(name, `${value}`);
        }
      }
    }
    if (["mi", "mn", "mo", "mspace", "mtext"].includes(node.tag)) {
      if (parent.textContent !== node.textContent) {
        parent.textContent = node.textContent ?? "";
      }
      return;
    }

    // Collect in arrays to prevent the live updating from interfering
    // with the schedule.
    const appendChilds = [];
    const removeChilds = [];
    const replaceChilds = [];
    for (const [child, desired] of zip(/** @type {[HTMLCollection, (Tag | null)[]]} */[parent.children, node.childNodes])) {
      if (!child && !desired) {
        continue;
      }
      if (!desired) {
        // parent.removeChild(child);
        removeChilds.push(child);
      } else if (!child) {
        // parent.appendChild(toDOM(desired, options));
        appendChilds.push(toDOM(desired, options));
      } else if (child.tagName.toLowerCase() !== desired.tag) {
        // parent.replaceChild(toDOM(desired, options), child);
        replaceChilds.push([child, toDOM(desired, options)]);
      } else {
        updateDOM(child, desired, {
          bare: false
        });
      }
    }
    for (const child of removeChilds) {
      parent.removeChild(child);
    }
    for (const child of appendChilds) {
      parent.appendChild(child);
    }
    for (const [oldChild, desired] of replaceChilds) {
      parent.replaceChild(desired, oldChild);
    }
  }

  /**
   * @typedef {import("./index.js").Token} Token
   * @typedef {(char: string) => boolean} LeximeTest
   */

  const LETTER_RE = /^\p{L}/u;

  /** @type {LeximeTest} */
  function isAlphabetic(char) {
    if (!char) {
      return false;
    }
    return LETTER_RE.test(char);
  }
  const LETTER_NUMBER_RE = /^[\p{L}\p{N}]/u;

  /** @type {LeximeTest} */
  function isAlphanumeric(char) {
    if (!char) {
      return false;
    }
    return LETTER_NUMBER_RE.test(char);
  }
  const MARK_RE = /^\p{M}/u;

  /** @type {LeximeTest} */
  function isMark(char) {
    if (!char) {
      return false;
    }
    return MARK_RE.test(char);
  }

  // Duodecimal literals are in the So category.
  const NUMBER_RE = /^[\p{N}\u{218a}-\u{218b}]/u;

  /** @type {LeximeTest} */
  function isNumeric(char) {
    if (!char) {
      return false;
    }
    return NUMBER_RE.test(char);
  }

  // Invisible opperators are in the Cf category.
  const OPERATOR_RE = /^[\p{P}\p{Sm}\p{So}\u{2061}-\u{2064}]/u;

  /** @type {LeximeTest} */
  function isOperational(char) {
    if (!char) {
      return false;
    }
    return OPERATOR_RE.test(char);
  }
  const PUNCT_OPEN_RE = /^\p{Pe}/u;

  /** @type {LeximeTest} */
  function isPunctClose(char) {
    if (!char) {
      return false;
    }
    return PUNCT_OPEN_RE.test(char);
  }
  const PUNCT_CLOSE_RE = /^\p{Ps}/u;

  /** @type {LeximeTest} */
  function isPunctOpen(char) {
    if (!char) {
      return false;
    }
    return PUNCT_CLOSE_RE.test(char);
  }
  const FUNCTION_IDENT_ATTRS = {
    class: "mathup-function-ident"
  };
  const KNOWN_IDENTS = new Map([["CC", {
    value: "ℂ"
  }], ["Delta", {
    value: "Δ",
    attrs: {
      mathvariant: "normal"
    }
  }], ["Gamma", {
    value: "Γ",
    attrs: {
      mathvariant: "normal"
    }
  }], ["Lambda", {
    value: "Λ",
    attrs: {
      mathvariant: "normal"
    }
  }], ["NN", {
    value: "ℕ"
  }], ["O/", {
    value: "∅"
  }], ["Omega", {
    value: "Ω",
    attrs: {
      mathvariant: "normal"
    }
  }], ["Phi", {
    value: "Φ",
    attrs: {
      mathvariant: "normal"
    }
  }], ["Pi", {
    value: "Π",
    attrs: {
      mathvariant: "normal"
    }
  }], ["Psi", {
    value: "Ψ",
    attrs: {
      mathvariant: "normal"
    }
  }], ["QQ", {
    value: "ℚ"
  }], ["RR", {
    value: "ℝ"
  }], ["Sigma", {
    value: "Σ",
    attrs: {
      mathvariant: "normal"
    }
  }], ["Theta", {
    value: "Θ",
    attrs: {
      mathvariant: "normal"
    }
  }], ["Xi", {
    value: "Ξ",
    attrs: {
      mathvariant: "normal"
    }
  }], ["ZZ", {
    value: "ℤ"
  }], ["alpha", {
    value: "α"
  }], ["beta", {
    value: "β"
  }], ["chi", {
    value: "χ"
  }], ["cos", {
    value: "cos",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["cosh", {
    value: "cosh",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["cot", {
    value: "cot",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["csc", {
    value: "csc",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["cosec", {
    value: "cosec",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["delta", {
    value: "δ"
  }], ["det", {
    value: "det",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["dim", {
    value: "dim",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["epsilon", {
    value: "ɛ"
  }], ["eta", {
    value: "η"
  }], ["gamma", {
    value: "γ"
  }], ["gcd", {
    value: "gcd",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["iota", {
    value: "ι"
  }], ["kappa", {
    value: "κ"
  }], ["lambda", {
    value: "λ"
  }], ["lcm", {
    value: "lcm",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["ln", {
    value: "ln",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["log", {
    value: "log",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["max", {
    value: "max",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["min", {
    value: "min",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["mu", {
    value: "μ"
  }], ["nu", {
    value: "ν"
  }], ["omega", {
    value: "ω"
  }], ["oo", {
    value: "∞"
  }], ["phi", {
    value: "φ"
  }], ["phiv", {
    value: "ϕ"
  }], ["pi", {
    value: "π"
  }], ["psi", {
    value: "ψ"
  }], ["rho", {
    value: "ρ"
  }], ["sec", {
    value: "sec",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["sigma", {
    value: "σ"
  }], ["sin", {
    value: "sin",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["sinh", {
    value: "sinh",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["tan", {
    value: "tan",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["tanh", {
    value: "tanh",
    attrs: {
      ...FUNCTION_IDENT_ATTRS
    }
  }], ["tau", {
    value: "τ"
  }], ["theta", {
    value: "θ"
  }], ["upsilon", {
    value: "υ"
  }], ["xi", {
    value: "ξ"
  }], ["zeta", {
    value: "ζ"
  }]]);
  const KNOWN_OPS = new Map([["-", {
    value: "−"
  }], ["!=", {
    value: "≠"
  }], ["!==", {
    value: "≢"
  }], ["!in", {
    value: "∉"
  }], [".$", {
    value: "\u2061",
    attrs: {
      class: "mathup-function-application"
    }
  }], [".*", {
    value: "\u2062",
    attrs: {
      class: "mathup-invisible-times"
    }
  }], [".+", {
    value: "\u2064",
    attrs: {
      class: "mathup-invisible-add"
    }
  }], [".,", {
    value: "\u2063",
    attrs: {
      class: "mathup-invisible-separator"
    }
  }], ["'", {
    value: "′",
    attrs: {
      lspace: 0,
      rspace: 0
    }
  }], ["''", {
    value: "″",
    attrs: {
      lspace: 0,
      rspace: 0
    }
  }], ["'''", {
    value: "‴",
    attrs: {
      lspace: 0,
      rspace: 0
    }
  }], ["''''", {
    value: "⁗",
    attrs: {
      lspace: 0,
      rspace: 0
    }
  }], ["*", {
    value: "·"
  }], ["**", {
    value: "∗"
  }], ["***", {
    value: "⋆"
  }], ["+-", {
    value: "±"
  }], ["-+", {
    value: "∓"
  }], ["-:", {
    value: "÷"
  }], ["-<", {
    value: "≺"
  }], ["-<=", {
    value: "⪯"
  }], ["-=", {
    value: "≡"
  }], ["->", {
    value: "→"
  }], ["->>", {
    value: "↠"
  }], ["...", {
    value: "…"
  }], ["//", {
    value: "⁄"
  }], ["/_", {
    value: "∠"
  }], ["/_\\", {
    value: "△"
  }], [":.", {
    value: "∴"
  }], [":|:", {
    value: "|",
    attrs: {
      stretchy: true
    },
    sep: true
  }], ["<-", {
    value: "←"
  }], ["<<<", {
    value: "≪"
  }], ["<=", {
    value: "≤"
  }], ["<=>", {
    value: "⇔"
  }], ["<>", {
    value: "⋄"
  }], ["<|", {
    value: "⊲"
  }], ["==", {
    value: "≡"
  }], ["=>", {
    value: "⇒"
  }], [">-", {
    value: "≻"
  }], [">-=", {
    value: "⪰"
  }], [">->", {
    value: "↣"
  }], [">->>", {
    value: "⤖"
  }], ["><|", {
    value: "⋊"
  }], [">=", {
    value: "≥"
  }], [">>>", {
    value: "≫"
  }], ["@", {
    value: "∘"
  }], ["AA", {
    value: "∀"
  }], ["EE", {
    value: "∃"
  }], ["TT", {
    value: "⊤"
  }], ["[]", {
    value: "□"
  }], ["^^", {
    value: "∧"
  }], ["^^^", {
    value: "⋀"
  }], ["_|_", {
    value: "⊥"
  }], ["aleph", {
    value: "ℵ"
  }], ["and", {
    value: "and"
  }], ["cdots", {
    value: "⋯"
  }], ["darr", {
    value: "↓"
  }], ["ddots", {
    value: "⋱"
  }], ["del", {
    value: "∂"
  }], ["diamond", {
    value: "⋄"
  }], ["dint", {
    value: "∬"
  }], ["grad", {
    value: "∇"
  }], ["hArr", {
    value: "⇔"
  }], ["harr", {
    value: "↔"
  }], ["if", {
    value: "if"
  }], ["iff", {
    value: "⇔"
  }], ["in", {
    value: "∈"
  }], ["int", {
    value: "∫"
  }], ["lArr", {
    value: "⇐"
  }], ["larr", {
    value: "←"
  }], ["lim", {
    value: "lim"
  }], ["mod", {
    value: "mod"
  }], ["nn", {
    value: "∩"
  }], ["nnn", {
    value: "⋂"
  }], ["not", {
    value: "¬"
  }], ["o+", {
    value: "⊕"
  }], ["o.", {
    value: "⊙"
  }], ["oc", {
    value: "∝"
  }], ["oint", {
    value: "∮"
  }], ["or", {
    value: "or"
  }], ["otherwise", {
    value: "otherwise"
  }], ["ox", {
    value: "⊗"
  }], ["prod", {
    value: "∏"
  }], ["prop", {
    value: "∝"
  }], ["rArr", {
    value: "⇒"
  }], ["rarr", {
    value: "→"
  }], ["square", {
    value: "□"
  }], ["sub", {
    value: "⊂"
  }], ["sube", {
    value: "⊆"
  }], ["sum", {
    value: "∑"
  }], ["sup", {
    value: "⊃"
  }], ["supe", {
    value: "⊇"
  }], ["uarr", {
    value: "↑"
  }], ["uu", {
    value: "∪"
  }], ["uuu", {
    value: "⋃"
  }], ["vdots", {
    value: "⋮"
  }], ["vv", {
    value: "∨"
  }], ["vvv", {
    value: "⋁"
  }], ["xx", {
    value: "×"
  }], ["|--", {
    value: "⊢"
  }], ["|->", {
    value: "↦"
  }], ["|==", {
    value: "⊨"
  }], ["|>", {
    value: "⊳"
  }], ["|><", {
    value: "⋉"
  }], ["|><|", {
    value: "⋈"
  }], ["||", {
    value: "∥"
  }], ["~=", {
    value: "≅"
  }], ["~~", {
    value: "≈"
  }]]);

  /** @type {Map<string, Omit<Token, "type">>} */
  const KNOWN_PARENS_OPEN = new Map([["(:", {
    value: "⟨"
  }], ["<<", {
    value: "⟨"
  }], ["{:", {
    value: ""
  }], ["|(", {
    value: "|"
  }], ["|:", {
    value: "|"
  }], ["|__", {
    value: "⌊"
  }], ["||(", {
    value: "‖"
  }], ["||:", {
    value: "‖"
  }], ["|~", {
    value: "⌈"
  }], ["(mod", {
    value: "(",
    attrs: {
      lspace: "1.65ex"
    },
    extraTokensAfter: [{
      type: "operator",
      value: "mod",
      attrs: {
        lspace: 0
      }
    }]
  }]]);
  const KNOWN_PARENS_CLOSE = new Map([[")|", {
    value: "|"
  }], [")||", {
    value: "‖"
  }], [":)", {
    value: "⟩"
  }], [":|", {
    value: "|"
  }], [":||", {
    value: "‖"
  }], [":}", {
    value: ""
  }], [">>", {
    value: "⟩"
  }], ["__|", {
    value: "⌋"
  }], ["~|", {
    value: "⌉"
  }]]);
  const KNOWN_PREFIX = new Map([
  // Accents
  ["bar", {
    name: "over",
    accent: "‾"
  }], ["ddot", {
    name: "over",
    accent: "⋅⋅"
  }], ["dot", {
    name: "over",
    accent: "⋅"
  }], ["hat", {
    name: "over",
    accent: "^"
  }], ["obrace", {
    name: "over",
    accent: "⏞"
  }], ["obracket", {
    name: "over",
    accent: "⎴"
  }], ["oparen", {
    name: "over",
    accent: "⏜"
  }], ["oshell", {
    name: "over",
    accent: "⏠"
  }], ["tilde", {
    name: "over",
    accent: "˜"
  }], ["ubrace", {
    name: "under",
    accent: "⏟"
  }], ["ubrace", {
    name: "under",
    accent: "⏟"
  }], ["ubracket", {
    name: "under",
    accent: "⎵"
  }], ["ul", {
    name: "under",
    accent: "_"
  }], ["uparen", {
    name: "under",
    accent: "⏝"
  }], ["ushell", {
    name: "under",
    accent: "⏡"
  }], ["vec", {
    name: "over",
    accent: "→"
  }],
  // Groups
  ["abs", {
    name: "fence",
    attrs: {
      open: "|",
      close: "|"
    }
  }], ["binom", {
    name: "frac",
    arity: 2,
    attrs: {
      linethickness: 0,
      open: "(",
      close: ")"
    }
  }], ["ceil", {
    name: "fence",
    attrs: {
      open: "⌈",
      close: "⌉"
    }
  }], ["floor", {
    name: "fence",
    attrs: {
      open: "⌊",
      close: "⌋"
    }
  }], ["norm", {
    name: "fence",
    attrs: {
      open: "‖",
      close: "‖"
    }
  }],
  // Roots
  ["root", {
    name: "root",
    arity: 2
  }], ["sqrt", {
    name: "sqrt"
  }],
  // Enclose
  ["cancel", {
    name: "row",
    attrs: {
      class: "mathup-enclose-cancel"
    }
  }]]);
  const KNOWN_COMMANDS = new Map([
  // Fonts
  ["rm", {
    name: "text-transform",
    value: "normal"
  }], ["bf", {
    name: "text-transform",
    value: "bold"
  }], ["it", {
    name: "text-transform",
    value: "italic"
  }], ["bb", {
    name: "text-transform",
    value: "double-struck"
  }], ["cc", {
    name: "text-transform",
    value: "script"
  }], ["tt", {
    name: "text-transform",
    value: "monospace"
  }], ["fr", {
    name: "text-transform",
    value: "fraktur"
  }], ["sf", {
    name: "text-transform",
    value: "sans-serif"
  }],
  // Colors
  ["black", {
    name: "color",
    value: "black"
  }], ["\u{26ab}", {
    name: "color",
    value: "black"
  }], ["blue", {
    name: "color",
    value: "blue"
  }], ["\u{1f535}", {
    name: "color",
    value: "blue"
  }], ["brown", {
    name: "color",
    value: "brown"
  }], ["\u{1f7e4}", {
    name: "color",
    value: "brown"
  }], ["cyan", {
    name: "color",
    value: "cyan"
  }], ["gray", {
    name: "color",
    value: "gray"
  }], ["green", {
    name: "color",
    value: "green"
  }], ["\u{1f7e2}", {
    name: "color",
    value: "green"
  }], ["lightgray", {
    name: "color",
    value: "lightgray"
  }], ["orange", {
    name: "color",
    value: "orange"
  }], ["\u{1f7e0}", {
    name: "color",
    value: "orange"
  }], ["purple", {
    name: "color",
    value: "purple"
  }], ["\u{1f7e3}", {
    name: "color",
    value: "purple"
  }], ["red", {
    name: "color",
    value: "red"
  }], ["\u{1f534}", {
    name: "color",
    value: "red"
  }], ["white", {
    name: "color",
    value: "white"
  }], ["\u{26aa}", {
    name: "color",
    value: "white"
  }], ["yellow", {
    name: "color",
    value: "yellow"
  }], ["\u{1f7e1}", {
    name: "color",
    value: "yellow"
  }],
  // Background Colors
  ["bg.black", {
    name: "background",
    value: "black"
  }], ["\u{2b1b}", {
    name: "background",
    value: "black"
  }], ["bg.blue", {
    name: "background",
    value: "blue"
  }], ["\u{1f7e6}", {
    name: "background",
    value: "blue"
  }], ["bg.brown", {
    name: "background",
    value: "brown"
  }], ["\u{1f7eb}", {
    name: "background",
    value: "brown"
  }], ["bg.cyan", {
    name: "background",
    value: "cyan"
  }], ["bg.gray", {
    name: "background",
    value: "gray"
  }], ["bg.green", {
    name: "background",
    value: "green"
  }], ["\u{1f7e9}", {
    name: "background",
    value: "green"
  }], ["bg.lightgray", {
    name: "background",
    value: "lightgray"
  }], ["bg.orange", {
    name: "background",
    value: "orange"
  }], ["\u{1f7e7}", {
    name: "background",
    value: "orange"
  }], ["bg.purple", {
    name: "background",
    value: "purple"
  }], ["\u{1f7ea}", {
    name: "background",
    value: "purple"
  }], ["bg.red", {
    name: "background",
    value: "red"
  }], ["\u{1f7e5}", {
    name: "background",
    value: "red"
  }], ["bg.white", {
    name: "background",
    value: "white"
  }], ["\u{2b1c}", {
    name: "background",
    value: "white"
  }], ["bg.yellow", {
    name: "background",
    value: "yellow"
  }], ["\u{1f7e8}", {
    name: "background",
    value: "yellow"
  }]]);

  /**
   * @param {string} char
   * @param {number} pos
   * @param {string} input
   * @param {{ offset?: number }} options
   * @returns {{ value: string; end: number }}
   */
  function handleFence(char, pos, input, {
    offset = 0
  } = {}) {
    let len = 0;
    while (input[pos + offset + len] === char) {
      len += 1;
    }
    let value = "";
    let closeLen = 0;
    let i = pos + offset + len;
    while (closeLen < len) {
      if (i >= input.length) {
        break;
      }
      if (input[i] === char) {
        closeLen += 1;
      } else {
        if (closeLen > 0) {
          value += char.repeat(closeLen);
          closeLen = 0;
        }
        value += input[i];
      }
      i += 1;
    }
    return {
      value: value.trim(),
      end: i
    };
  }

  /**
   * @param {number} pos
   * @param {string} input
   * @param {{ offset?: number }} [options]
   * @returns {ReturnType<typeof handleFence>}
   */
  function handleBacktick(pos, input, options) {
    return handleFence("`", pos, input, options);
  }

  /**
   * @param {number} pos
   * @param {string} input
   * @param {{ offset?: number }} [options]
   * @returns {ReturnType<typeof handleFence>}
   */
  function handleQuote(pos, input, options) {
    return handleFence('"', pos, input, options);
  }

  /**
   * @param {string} prefix
   * @param {number} pos
   * @param {string} input
   * @returns {{ value: string; end: number }}
   */
  function handlePrefixed(prefix, pos, input) {
    const start = pos + prefix.length;
    let value = "";
    let [char] = input.slice(start);
    if (!char) {
      return {
        value: prefix,
        end: start
      };
    }
    if (isAlphanumeric(char)) {
      while (isAlphanumeric(char)) {
        value += char;
        [char] = input.slice(start + value.length);
      }
      return {
        value,
        end: start + value.length
      };
    }
    if (char === "`") {
      return handleBacktick(pos, input, {
        offset: prefix.length
      });
    }
    return {
      value: char,
      end: start + char.length
    };
  }

  /**
   * @param {string} input
   * @param {number} start
   * @returns {boolean}
   */
  function isKnownParenCloseAt(input, start) {
    for (const key of KNOWN_PARENS_CLOSE.keys()) {
      if (input.startsWith(key, start)) {
        return true;
      }
    }
    return false;
  }

  /**
   * @param {string} input
   * @param {number} start
   * @param {boolean} isGrouping
   * @returns {boolean}
   */
  function hasOperand(input, start, isGrouping) {
    let pos = start;
    let postPrefixChar = input.at(pos);
    while (postPrefixChar === " " || postPrefixChar === "\n") {
      pos += 1;
      postPrefixChar = input.at(pos);
    }
    if (!postPrefixChar) {
      return false;
    }
    return !(isGrouping && (isPunctClose(postPrefixChar) || isKnownParenCloseAt(input, pos)));
  }

  /**
   * @param {Pick<import("./index.js").Token, "name">} prefix
   * @returns {boolean}
   */
  function allowedStandalonePrefix(prefix) {
    return prefix.name === "root" || prefix.name === "sqrt";
  }

  /** @type {import("./index.js").Scanner} */
  function alphaScanner(char, input, {
    start,
    grouping
  }) {
    if (!isAlphabetic(char)) {
      return null;
    }
    let nextChar = char;
    let value = "";
    while (isAlphabetic(nextChar) || isMark(nextChar) || value.length > 0 && nextChar === ".") {
      const i = start + value.length + nextChar.length;
      value += nextChar;
      [nextChar] = input.slice(i);
    }

    // alpha may contain a period, but not never end with one.
    if (value.endsWith(".")) {
      value = value.slice(0, -1);
    }
    let end = start + value.length;
    {
      const operator = KNOWN_OPS.get(value);
      if (operator) {
        return {
          type: "operator",
          value: operator.value,
          attrs: operator.attrs,
          end: start + value.length
        };
      }
    }
    {
      const ident = KNOWN_IDENTS.get(value);
      if (ident) {
        return {
          type: "ident",
          value: ident.value,
          attrs: ident.attrs,
          end: start + value.length
        };
      }
    }
    const potential = `${value}${nextChar}`;
    const [nextNextChar] = input.slice(start + potential.length);
    {
      const operator = KNOWN_OPS.get(potential);
      if (operator && !isAlphanumeric(nextNextChar)) {
        return {
          type: "operator",
          value: operator.value,
          attrs: operator.attrs,
          end: start + potential.length
        };
      }
    }
    {
      const ident = KNOWN_IDENTS.get(potential);
      if (ident && !isAlphanumeric(nextNextChar)) {
        return {
          type: "ident",
          value: ident.value,
          attrs: ident.attrs,
          end: start + potential.length
        };
      }
    }
    {
      const prefix = KNOWN_PREFIX.get(value);
      if (prefix && (allowedStandalonePrefix(prefix) || hasOperand(input, end, grouping))) {
        return {
          type: "prefix",
          value: "",
          end,
          ...prefix
        };
      }
    }
    {
      const command = KNOWN_COMMANDS.get(value);
      if (command && hasOperand(input, end, grouping)) {
        return {
          type: "command",
          end,
          ...command
        };
      }
    }
    if (value.includes(".")) {
      // Period not used, lets toss it.
      value = value.slice(0, value.indexOf("."));
      end = start + value.length;
    }
    return {
      type: "ident",
      value,
      end,
      split: true
    };
  }

  /** @type {import("./index.js").Scanner} */
  function backslashScanner(char, input, {
    start
  }) {
    if (char !== "\\") {
      return null;
    }
    return {
      type: "operator",
      ...handlePrefixed("\\", start, input)
    };
  }

  /** @type {import("./index.js").Scanner} */
  function backtickScanner(char, input, {
    start
  }) {
    if (char !== "`") {
      return null;
    }
    const {
      end,
      value
    } = handleBacktick(start, input);
    return {
      type: "ident",
      value,
      end
    };
  }

  /** @type {import("./index.js").Scanner} */
  function groupSepScanner(_char, input, {
    grouping,
    start
  }, {
    colSep,
    rowSep
  }) {
    if (!grouping) {
      return null;
    }
    const colSepEnd = start + colSep.length;
    const rowSepEnd = start + rowSep.length;
    if (input.slice(start, rowSepEnd) === rowSep) {
      return {
        type: "sep.row",
        value: rowSep,
        end: rowSepEnd
      };
    }
    if (input.slice(start, colSepEnd) === colSep) {
      return {
        type: "sep.col",
        value: colSep,
        end: colSepEnd
      };
    }
    return null;
  }

  const INFIX_MAP = new Map([["^", "sup"], ["/", "frac"], ["_", "sub"], [".^", "over"], ["._", "under"]]);

  /** @type {Set<string>} */
  const COLLISIONS = new Set();
  for (const map of [KNOWN_COMMANDS, KNOWN_IDENTS, KNOWN_OPS, KNOWN_PARENS_CLOSE, KNOWN_PARENS_OPEN, KNOWN_PREFIX]) {
    for (const key of map.keys()) {
      if ("^/_".includes(key[0])) {
        COLLISIONS.add(key);
      }
    }
  }

  /** @type {import("./index.js").Scanner} */
  function infixScanner(char, input, {
    start
  }) {
    if (char === ".") {
      const next = input[start + 1];
      const infix = INFIX_MAP.get(`.${next}`);
      if (infix) {
        for (const key of COLLISIONS) {
          if (key === input.slice(start + 1, start + 1 + key.length)) {
            return null;
          }
        }
        return {
          type: "infix",
          value: infix,
          end: start + 2
        };
      }
    }
    const infix = INFIX_MAP.get(char);
    if (infix) {
      for (const key of COLLISIONS) {
        if (key === input.slice(start, start + key.length)) {
          return null;
        }
      }
      return {
        type: "infix",
        value: infix,
        end: start + 1
      };
    }
    return null;
  }

  /** @type {import("./index.js").Scanner} */
  function newlineScanner(char, input, {
    start,
    grouping
  }) {
    if (char !== "\n") {
      return null;
    }
    let i = start;
    let nextChar = char;
    let value = "";
    while (nextChar === "\n") {
      value += nextChar;
      i += 1;
      nextChar = input[i];
    }
    return {
      type: grouping ? "sep.row" : "space",
      value,
      end: i
    };
  }

  /** @type {import("./index.js").Scanner} */
  function numberScanner(char, input, {
    start
  }, {
    decimalMark
  }) {
    if (!isNumeric(char)) {
      return null;
    }
    let nextChar = char;
    let value = "";
    while (isNumeric(nextChar)) {
      value += nextChar;
      [nextChar] = input.slice(start + value.length);
    }
    if (nextChar === decimalMark) {
      [nextChar] = input.slice(start + value.length + decimalMark.length);
      if (isNumeric(nextChar)) {
        value += decimalMark;
        while (isNumeric(nextChar)) {
          value += nextChar;
          [nextChar] = input.slice(start + value.length);
        }
      }
    }
    return {
      type: "number",
      value,
      end: start + value.length
    };
  }

  /** @type {import("./index.js").Scanner} */
  function octothorpeScanner(char, input, {
    start
  }) {
    if (char !== "#") {
      return null;
    }
    const {
      end,
      value
    } = handlePrefixed("#", start, input);
    return {
      type: "number",
      value,
      end
    };
  }

  /**
   * @typedef {import("./index.js").Token} Token
   */


  /**
   * @param {string} partial
   * @returns {boolean}
   */
  function opsPotential(partial) {
    for (const [op] of KNOWN_OPS) {
      if (op.startsWith(partial)) {
        return true;
      }
    }
    return false;
  }

  /** @type {import("./index.js").Scanner} */
  function operatorScanner(char, input, {
    start,
    grouping
  }) {
    let value = char;
    if (opsPotential(value)) {
      let [nextChar] = input.slice(start + value.length);
      let nextValue = value + nextChar;
      while (nextChar && opsPotential(nextValue)) {
        value += nextChar;
        [nextChar] = input.slice(start + value.length);
        nextValue = value + nextChar;
      }
    }
    const known = KNOWN_OPS.get(value);
    if (known) {
      return {
        type: known.sep && grouping ? "sep.col" : "operator",
        value: known.value,
        attrs: known.attrs,
        end: start + value.length
      };
    }
    const command = KNOWN_COMMANDS.get(value);
    if (command) {
      return {
        type: "command",
        end: start + value.length,
        ...command
      };
    }
    if (!isOperational(char)) {
      return null;
    }

    /** @type {Token & { end: number }} */
    const token = {
      type: "operator",
      value: char,
      end: start + char.length
    };
    const isStretchy = char === "|" && input.at(start - 1) === " " && input.at(start + 1) === " ";
    if (isStretchy) {
      token.attrs = {
        stretchy: true
      };
    }
    return token;
  }

  /**
   * @param {string} partial
   * @returns {boolean}
   */
  function parenClosePotential(partial) {
    for (const [paren] of KNOWN_PARENS_CLOSE) {
      if (paren.startsWith(partial)) {
        return true;
      }
    }
    return false;
  }

  /** @type {import("./index.js").Scanner} */
  function parenCloseScanner(char, input, {
    grouping,
    start
  }) {
    if (!grouping) {
      return null;
    }
    let value = char;
    if (parenClosePotential(value)) {
      let [nextChar] = input.slice(start + value.length);
      let nextValue = value + nextChar;
      while (nextChar && parenClosePotential(nextValue)) {
        value += nextChar;
        [nextChar] = input.slice(start + value.length);
        nextValue = value + nextChar;
      }
    }
    {
      const [nextChar] = input.slice(start + value.length);
      const nextValue = value + nextChar;
      if (KNOWN_OPS.has(nextValue)) {
        return null;
      }
    }
    const known = KNOWN_PARENS_CLOSE.get(value);
    if (known) {
      return {
        type: "paren.close",
        value: known.value,
        end: start + value.length
      };
    }
    if (isPunctClose(char)) {
      return {
        type: "paren.close",
        value: char,
        end: start + char.length
      };
    }
    return null;
  }

  /**
   * @param {string} partial
   * @returns {boolean}
   */
  function parenOpenPotential(partial) {
    for (const [paren] of KNOWN_PARENS_OPEN) {
      if (paren.startsWith(partial)) {
        return true;
      }
    }
    return false;
  }

  /** @type {import("./index.js").Scanner} */
  function parenOpenScanner(char, input, {
    start
  }) {
    let value = char;
    if (parenOpenPotential(value)) {
      let [nextChar] = input.slice(start + value.length);
      let nextValue = value + nextChar;
      while (nextChar && parenOpenPotential(nextValue)) {
        value += nextChar;
        [nextChar] = input.slice(start + value.length);
        nextValue = value + nextChar;
      }
    }
    {
      const [nextChar] = input.slice(start + value.length);
      const nextValue = value + nextChar;
      if (KNOWN_OPS.has(nextValue)) {
        return null;
      }
    }
    const known = KNOWN_PARENS_OPEN.get(value);
    if (known) {
      return {
        ...known,
        type: "paren.open",
        end: start + value.length
      };
    }
    if (isPunctOpen(char)) {
      return {
        type: "paren.open",
        value: char,
        end: start + char.length
      };
    }
    return null;
  }

  /** @type {import("./index.js").Scanner} */
  function quoteScanner(char, input, {
    start
  }) {
    if (char !== '"') {
      return null;
    }
    const {
      end,
      value
    } = handleQuote(start, input);
    return {
      type: "text",
      value,
      end
    };
  }

  /** @type {import("./index.js").Scanner} */
  function spaceScanner(char, input, {
    start
  }) {
    if (char !== " ") {
      return null;
    }
    let i = start;
    let nextChar = char;
    let value = "";
    while (nextChar === " ") {
      value += nextChar;
      i += 1;
      nextChar = input[i];
    }
    return {
      type: "space",
      value,
      end: i
    };
  }

  /**
   * @typedef {import("./index.js").Scanner} Scanner
   * @type {(
   *   ...args: Parameters<Scanner>
   * ) => NonNullable<ReturnType<Scanner>>}
   */
  function unhandledScanner(char, _input, {
    start
  }) {
    return {
      type: "ident",
      value: char,
      end: start + char.length
    };
  }

  /**
   * @typedef {Required<import("../index.js").TokenizerOptions>} Options
   * @typedef {import("../index.js").State} State
   * @typedef {import("../index.js").Token} Token
   * @typedef {(
   *   char: string,
   *   input: string,
   *   state: State,
   *   options: Options,
   * ) => (Token & { end: number }) | null} Scanner
   * @type {Scanner[]}
   */
  var scanners = [spaceScanner, alphaScanner, numberScanner, backtickScanner, quoteScanner, octothorpeScanner, backslashScanner, newlineScanner, infixScanner, groupSepScanner, parenOpenScanner, parenCloseScanner, operatorScanner];

  /**
   * @typedef {"command"
   *   | "ident"
   *   | "infix"
   *   | "number"
   *   | "operator"
   *   | "paren.close"
   *   | "paren.open"
   *   | "prefix"
   *   | "sep.col"
   *   | "sep.row"
   *   | "space"
   *   | "text"} TokenType
   *
   * @typedef {object} Token
   * @property {TokenType} type
   * @property {string} value
   * @property {string} [accent]
   * @property {number} [arity]
   * @property {string} [name]
   * @property {boolean} [split] - Can be broken up into smaller units.
   * @property {boolean} [sep] - Can function as a group seperator.
   * @property {Record<string, string | number | boolean | null | undefined>} [attrs]
   * @property {Token[]} [extraTokensAfter]
   *
   * @typedef {object} State
   * @property {number} start
   * @property {boolean} grouping
   *
   * @typedef {object} TokenizerOptions
   * @property {string} [decimalMark="."] - Decimal separator. Default is `"."`
   * @property {string} [colSep=","] - Column separator e.g. for arrays and
   *   matrices. Default is `","`
   * @property {string} [rowSep=";"] - Row separator e.g. for matrices. Default is
   *   `";"`
   *
   * @param {Required<TokenizerOptions>} options
   * @returns {(input: string) => Generator<Token>}
   */
  function createTokenizer(options) {
    /**
     * @param {string} input
     * @param {State} state
     * @returns {Token & { end: number }}
     */
    function nextToken(input, state) {
      const [char] = input.slice(state.start);
      for (const scan of scanners) {
        const token = scan(char, input, state, options);
        if (token) {
          return token;
        }
      }
      return unhandledScanner(char, input, state);
    }

    /**
     * @param {string} input
     * @yields {Token}
     */
    return function* tokenize(input) {
      let pos = 0;
      let nestLevel = 0;
      while (pos < input.length) {
        const {
          type,
          value,
          end,
          split,
          extraTokensAfter,
          ...attrs
        } = nextToken(input, {
          start: pos,
          grouping: nestLevel > 0
        });
        if (split) {
          let char = "";
          for (const codePoint of value) {
            if (isMark(codePoint)) {
              char += codePoint;
            } else if (char) {
              yield {
                type,
                value: char
              };
              char = codePoint;
            } else {
              char = codePoint;
            }
          }
          if (char) {
            yield {
              type,
              value: char
            };
          }
        } else {
          yield {
            type,
            value,
            ...attrs
          };
        }
        if (extraTokensAfter) {
          for (const token of extraTokensAfter) {
            yield token;
          }
        }
        pos = end;
        if (type === "paren.open") {
          nestLevel += 1;
        } else if (type === "paren.close" && nestLevel > 0) {
          nestLevel -= 1;
        }
      }
    };
  }

  /**
   * @typedef {import("../../parser/index.js").FencedGroup} FencedGroup
   */

  /**
   * @param {import("../index.js").Tag | null} tag
   * @returns {boolean}
   */
  function notNull$2(tag) {
    return tag !== null;
  }

  /** @type {import("../index.js").TransformFn<FencedGroup>} */
  function fencedGroup(node, transform) {
    const cols = node.items.map(col => {
      if (col.length === 1) {
        return transform(col[0]);
      }
      return {
        tag: "mrow",
        childNodes: col.map(transform).filter(notNull$2)
      };
    }).reduce((joined, next, i) => {
      joined?.childNodes?.push(next);
      const sep = node.attrs.seps[i];
      if (sep) {
        const {
          value,
          attrs = {}
        } = sep;
        joined?.childNodes?.push({
          tag: "mo",
          textContent: value,
          attrs: {
            ...attrs,
            separator: true
          }
        });
      }
      return joined;
    }, {
      tag: "mrow",
      childNodes: []
    });
    const {
      open,
      close
    } = node.attrs;
    if (cols && !open.value && !close?.value) {
      return cols;
    }
    const childNodes = [];
    if (open.value) {
      childNodes.push({
        tag: "mo",
        textContent: open.value,
        attrs: {
          ...open.attrs,
          fence: "true"
        }
      });
    }
    if (cols?.childNodes?.length === 1) {
      childNodes.push(cols.childNodes[0]);
    } else {
      childNodes.push(cols);
    }
    if (close?.value) {
      childNodes.push({
        tag: "mo",
        textContent: close.value,
        attrs: {
          ...close.attrs,
          fence: "true"
        }
      });
    }
    return {
      tag: "mrow",
      childNodes
    };
  }

  /**
   * @typedef {import("../../parser/index.js").Literal} Literal
   * @typedef {import("../../parser/index.js").SpaceLiteral} SpaceLiteral
   */

  /**
   * @param {string} tag
   * @returns {import("../index.js").TransformFn<
   *   Exclude<Literal, SpaceLiteral>
   * >}
   */
  function literal(tag) {
    return node => {
      return {
        tag,
        attrs: node.attrs,
        textContent: node.value
      };
    };
  }

  /**
   * @typedef {import("../../parser/index.js").Node} Node
   * @typedef {import("../../parser/index.js").MatrixGroup} MatrixGroup
   * @typedef {import("../index.js").Tag} Tag
   */

  /**
   * @param {Tag | null} tag
   * @returns {boolean}
   */
  function notNull$1(tag) {
    return tag !== null;
  }

  /** @type {import("../index.js").TransformFn<MatrixGroup>} */
  function matrixGroup(node, transform) {
    const childNodes = [];
    const colCount = node.items.reduce((max, row) => Math.max(max, row.length), 0);
    const {
      open,
      close
    } = node.attrs;

    // auto start-align tables with no close param.
    const aligns = !close?.value ? Array.from({
      length: colCount
    }).fill("start") : [];
    if (open.value) {
      childNodes.push({
        tag: "mo",
        textContent: open.value,
        attrs: {
          ...open.attrs,
          fence: "true"
        }
      });
    }
    childNodes.push({
      tag: "mtable",
      childNodes: node.items.map(row => ({
        tag: "mtr",
        childNodes: row.map((col, i) => ({
          tag: "mtd",
          attrs: aligns[i] ? {
            style: `text-align: ${aligns[i]}`
          } : {},
          childNodes: col.map(transform).filter(notNull$1)
        }))
      }))
    });
    if (close?.value) {
      childNodes.push({
        tag: "mo",
        textContent: close.value,
        attrs: {
          ...close.attrs,
          fence: "true"
        }
      });
    }
    return {
      tag: "mrow",
      childNodes
    };
  }

  /**
   * @typedef {import("../../parser/index.js").MultiScripts} MultiScripts
   * @type {import("../index.js").TransformFn<MultiScripts>}
   */
  function multiscripts(node, transform) {
    const postscripts = node.post.flatMap(pair => pair.map(nodes => {
      if (nodes.length === 1) {
        return transform(nodes[0]);
      }
      return {
        tag: "mrow",
        childNodes: nodes.map(transform)
      };
    }));
    const childNodes = [transform(node.base), ...postscripts];
    if (node.pre && node.pre.length > 0) {
      childNodes.push({
        tag: "mprescripts"
      });
      const prescripts = node.pre.flatMap(pair => pair.map(nodes => {
        if (nodes.length === 1) {
          return transform(nodes[0]);
        }
        return {
          tag: "mrow",
          childNodes: nodes.map(transform)
        };
      }));
      childNodes.push(...prescripts);
    }
    return {
      tag: "mmultiscripts",
      childNodes
    };
  }

  /**
   * @typedef {import("../../parser/index.js").BinaryOperation} BinaryOperation
   * @typedef {import("../../parser/index.js").TernaryOperation} TernaryOperation
   * @type {import("../index.js").TransformFn<
   *   BinaryOperation | TernaryOperation
   * >}
   */
  function operation(node, transform) {
    const {
      open,
      close,
      ...attrs
    } = node.attrs || {};
    const opNode = {
      tag: `m${node.name}`,
      childNodes: node.items.map(transform),
      attrs
    };
    if (open || close) {
      return {
        tag: "mrow",
        childNodes: [{
          tag: "mo",
          textContent: `${open}`,
          attrs: {
            fence: true
          }
        }, opNode, {
          tag: "mo",
          textContent: `${close}`,
          attrs: {
            fence: true
          }
        }]
      };
    }
    return opNode;
  }

  /**
   * @typedef {import("../../parser/index.js").Sentence} Sentence
   * @type {import("../index.js").TransformFn<Sentence>}
   */
  function sentence(node, transform, options) {
    /** @type {Record<string, string>} */
    const attrs = {};
    if (options.dir) {
      attrs.dir = options.dir;
    }
    if (options.display) {
      attrs.display = options.display;
    }
    return {
      tag: "math",
      attrs,
      childNodes: node.body.map(transform).filter(x => x !== null)
    };
  }

  /**
   * @typedef {import("../../parser/index.js").SpaceLiteral} SpaceLiteral
   * @type {import("../index.js").TransformFn<SpaceLiteral>}
   */
  function spaceLiteral(node) {
    if (node.attrs.width === "0ex") {
      // No 0 width whitespace.
      return null;
    }
    return {
      tag: "mspace",
      attrs: node.attrs
    };
  }

  /** @typedef {import("../index.js").Tag} Tag */

  /**
   * @param {string} name
   * @param {string} value
   * @returns {string}
   */
  function withCustomProperty(name, value) {
    return `var(--mathup-${name}-${value}, ${value})`;
  }

  /**
   * @param {Map<string, string> | undefined} styles
   * @param {Tag | null} tag
   * @returns {Tag | null}
   */
  function applyStyles(styles, tag) {
    if (!styles || !tag) {
      return tag;
    }

    /** @type {Map<string, string>} */
    const combinedStyles = new Map();
    for (const [name, value] of styles) {
      if (name === "background") {
        const currentValue = combinedStyles.get("background");
        const backgroundValue = withCustomProperty(name, value);
        if (currentValue) {
          combinedStyles.set("background", `${currentValue},${backgroundValue}`);
        } else {
          combinedStyles.set("background", backgroundValue);
        }
      } else {
        combinedStyles.set(name, withCustomProperty(name, value));
      }
    }
    let style = tag.attrs?.style ?? "";
    for (const [name, value] of combinedStyles) {
      style += `${name}:${value};`;
    }
    if (!style) {
      return tag;
    }
    return {
      ...tag,
      attrs: tag.attrs ? {
        ...tag.attrs,
        style
      } : {
        style
      }
    };
  }

  /**
   * @typedef {import("../../parser/index.js").Node} Node
   * @typedef {import("../../parser/index.js").Term} Term
   * @typedef {import("../index.js").Tag} Tag
   */

  /**
   * @template T
   * @param {T} node
   * @returns {boolean}
   */
  function notNull(node) {
    return node !== null;
  }

  /** @type {import("../index.js").TransformFn<Term>} */
  function term(node, transform) {
    if (node.items.length === 1 && notNull(node.items[0])) {
      return applyStyles(node.styles, transform(node.items[0]));
    }
    return applyStyles(node.styles, {
      tag: "mrow",
      childNodes: node.items.map(transform).filter(notNull)
    });
  }

  // See tables at https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols

  /**
   * @param {(char: string) => string} fn
   * @param {string} string
   * @returns {string}
   */
  function mapChars(fn, string) {
    let mapped = "";
    for (const char of string) {
      mapped += fn(char);
    }
    return mapped;
  }

  /**
   * @param {[[number, number], number][]} ranges
   * @param {Map<string, string>} [defaults]
   * @returns {(char: string) => string}
   */
  function createCharMapping(ranges, defaults) {
    return char => {
      if (defaults) {
        const found = defaults.get(char);
        if (found) {
          return found;
        }
      }
      const codePoint = char.codePointAt(0);
      if (!codePoint) {
        return char;
      }
      for (const [[min, max], offset] of ranges) {
        if (min <= codePoint && codePoint <= max) {
          return String.fromCodePoint(codePoint + offset);
        }
      }
      return char;
    };
  }

  /**
   * @param {[number, number]} range
   * @returns {(offset: number) => [[number, number], number]}
   */
  function createRange([min, max]) {
    return offset => [[min, max], offset - min];
  }
  const digitRange = createRange([0x30, 0x39]);
  const latinCapitalLetterRange = createRange([0x41, 0x5a]);
  const latinSmallLetterRange = createRange([0x61, 0x7a]);
  const greekCapitalLetterRange = createRange([0x0391, 0x03a9]);
  const greekSmallLetterRange = createRange([0x03b1, 0x03c9]);
  const bold = createCharMapping([digitRange(0x1d7ce), latinCapitalLetterRange(0x1d400), latinSmallLetterRange(0x1d41a), greekCapitalLetterRange(0x1d6a8), greekSmallLetterRange(0x1d6c2)], new Map([["ϴ", "𝚹"], ["∇", "𝛁"], ["∂", "𝛛"], ["ϵ", "𝛜"], ["ϑ", "𝛝"], ["ϰ", "𝛞"], ["ϕ", "𝛟"], ["ϱ", "𝛠"], ["ϖ", "𝛡"], ["Ϝ", "𝟊"], ["ϝ", "𝟋"]]));
  const italic = createCharMapping([latinCapitalLetterRange(0x1d434), latinSmallLetterRange(0x1d44e), greekCapitalLetterRange(0x1d6e2), greekSmallLetterRange(0x1d6fc)], new Map([["h", "ℎ"], ["ı", "𝚤"], ["ȷ", "𝚥"], ["ϴ", "𝛳"], ["∇", "𝛻"], ["∂", "𝜕"], ["ϵ", "𝜖 "], ["ϑ", "𝜗"], ["ϰ", "𝜘"], ["ϕ", "𝜙"], ["ϱ", "𝜚"], ["ϖ", "𝜛"]]));
  const boldItalic = createCharMapping([latinCapitalLetterRange(0x1d468), latinSmallLetterRange(0x1d482), greekCapitalLetterRange(0x1d71c), greekSmallLetterRange(0x1d736)], new Map([["ϴ", "𝜭"], ["∇", "𝜵"], ["∂", "𝝏"], ["ϵ", "𝝐"], ["ϑ", "𝝑"], ["ϰ", "𝝒"], ["ϕ", "𝝓"], ["ϱ", "𝝔"], ["ϖ", "𝝕"]]));
  const doubleStruck = createCharMapping([digitRange(0x1d7d8), latinCapitalLetterRange(0x1d538), latinSmallLetterRange(0x1d552)], new Map([["C", "ℂ"], ["H", "ℍ"], ["N", "ℕ"], ["P", "ℙ"], ["Q", "ℚ"], ["R", "ℝ"], ["Z", "ℤ"], ["π", "ℼ"], ["γ", "ℽ"], ["Π", "ℿ"], ["∑", "⅀"]]));
  const italicDoubleStruck = createCharMapping([], new Map([["D", "ⅅ"], ["d", "ⅆ"], ["i", "ⅈ"], ["j", "ⅉ"]]));
  const fraktur = createCharMapping([latinCapitalLetterRange(0x1d504), latinSmallLetterRange(0x1d51e)], new Map([["C", "ℭ"], ["H", "ℌ"], ["I", "ℑ"], ["R", "ℜ"], ["Z", "ℨ"]]));
  const boldFraktur = createCharMapping([latinCapitalLetterRange(0x1d56c), latinSmallLetterRange(0x1d586)]);
  const monospace = createCharMapping([digitRange(0x1d7f6), latinCapitalLetterRange(0x1d670), latinSmallLetterRange(0x1d68a)]);
  const sansSerif = createCharMapping([digitRange(0x1d7e2), latinCapitalLetterRange(0x1d5a0), latinSmallLetterRange(0x1d5ba)]);
  const boldSansSerif = createCharMapping([digitRange(0x1d7ec), latinCapitalLetterRange(0x1d5d4), latinSmallLetterRange(0x1d5ee), greekCapitalLetterRange(0x1d756), greekSmallLetterRange(0x1d770)], new Map([["ϴ", "𝝧"], ["∇", "𝝯"], ["∂", "𝞉"], ["ϵ", "𝞊"], ["ϑ", "𝞋"], ["ϰ", "𝞌"], ["ϕ", "𝞍"], ["ϱ", "𝞎"], ["ϖ", "𝞏"]]));
  const italicSansSerif = createCharMapping([latinCapitalLetterRange(0x1d608), latinSmallLetterRange(0x1d622)]);
  const boldItalicSansSerif = createCharMapping([latinCapitalLetterRange(0x1d63c), latinSmallLetterRange(0x1d656), greekCapitalLetterRange(0x1d790), greekSmallLetterRange(0x1d7aa)], new Map([["ϴ", "𝞡"], ["∇", "𝞩"], ["∂", "𝟃"], ["ϵ", "𝟄"], ["ϑ", "𝟅"], ["ϰ", "𝟆"], ["ϕ", "𝟇"], ["ϱ", "𝟈"], ["ϖ", "𝟉"]]));
  const script = createCharMapping([latinCapitalLetterRange(0x1d49c), latinSmallLetterRange(0x1d4b6)], new Map([["B", "ℬ"], ["E", "ℰ"], ["F", "ℱ"], ["H", "ℋ"], ["I", "ℐ"], ["L", "ℒ"], ["M", "ℳ"], ["R", "ℛ"], ["e", "ℯ"], ["g", "ℊ"], ["o", "ℴ"]]));
  const boldScript = createCharMapping([latinCapitalLetterRange(0x1d4d0), latinSmallLetterRange(0x1d4ea)]);

  /**
   * @param {string} family
   * @returns {string}
   */
  function fontFamilyStyleValue(family) {
    return `var(--mathup-font-family-${family}, ${family})`;
  }

  /**
   * @typedef {import("../../parser/index.js").LiteralAttrs} LiteralAttrs
   * @param {LiteralAttrs} attrs
   * @param {string[]} transforms
   * @returns {LiteralAttrs}
   */
  function textLiteralAttrs(attrs, transforms) {
    /** @type {Map<string, string>} */
    const styles = new Map();
    if (transforms.includes("bold")) {
      styles.set("font-weight", "bold");
    }
    if (transforms.includes("italic")) {
      styles.set("font-style", "italic");
    }

    // serif is default font-family, so no need to check for `rm`.
    if (transforms.includes("sans-serif")) {
      styles.set("font-family", fontFamilyStyleValue("sans-serif"));
    } else if (transforms.includes("monospace")) {
      styles.set("font-family", fontFamilyStyleValue("monospace"));
    }
    if (styles.size === 0) {
      return attrs;
    }
    let style = "";
    for (const [name, value] of styles) {
      style += `${name}:${value};`;
    }
    return {
      ...attrs,
      style
    };
  }

  /**
   * @param {string} text
   * @param {string[]} transforms
   * @returns {string}
   */
  function textLiteralValue(text, transforms) {
    if (transforms.includes("double-struck")) {
      return mapChars(doubleStruck, text);
    }
    if (transforms.includes("script")) {
      return mapChars(script, text);
    }
    if (transforms.includes("fraktur")) {
      return mapChars(fraktur, text);
    }
    return text;
  }

  /**
   * @param {string} text
   * @param {string[]} transforms
   * @returns {string}
   */
  function literalValue(text, transforms) {
    if (transforms.includes("bold")) {
      if (transforms.includes("italic")) {
        if (transforms.includes("sans-serif")) {
          return mapChars(boldItalicSansSerif, text);
        }
        return mapChars(boldItalic, text);
      }
      if (transforms.includes("script")) {
        return mapChars(boldScript, text);
      }
      if (transforms.includes("fraktur")) {
        return mapChars(boldFraktur, text);
      }
      if (transforms.includes("sans-serif")) {
        return mapChars(boldSansSerif, text);
      }
      return mapChars(bold, text);
    }
    if (transforms.includes("italic")) {
      if (transforms.includes("double-struck")) {
        return mapChars(italicDoubleStruck, text);
      }
      if (transforms.includes("sans-serif")) {
        return mapChars(italicSansSerif, text);
      }
      return mapChars(italic, text);
    }
    if (transforms.includes("sans-serif")) {
      return mapChars(sansSerif, text);
    }
    if (transforms.includes("monospace")) {
      return mapChars(monospace, text);
    }
    return textLiteralValue(text, transforms);
  }

  /**
   * @typedef {import("../../parser/index.js").Node} ASTNode
   * @typedef {import("../../parser/index.js").UnaryOperation} UnaryOperation
   * @typedef {import("../../parser/index.js").Literal} Literal
   * @typedef {import("../../parser/index.js").SpaceLiteral} SpaceLiteral
   */

  /**
   * @template {ASTNode} Node
   * @overload
   * @param {Node} childNode
   * @param {UnaryOperation} node
   * @returns {Node}
   */

  /**
   * @template {ASTNode} Node
   * @overload
   * @param {Node[]} childNode
   * @param {UnaryOperation} node
   * @returns {Node[]}
   */

  /**
   * @template {ASTNode} Node
   * @overload
   * @param {Node[][]} childNode
   * @param {UnaryOperation} node
   * @returns {Node[][]}
   */

  /**
   * @template {ASTNode} Node
   * @param {Node | Node[] | Node[][]} childNode
   * @param {UnaryOperation} node
   * @returns {Node | Node[] | Node[][]}
   */
  function handleCommand(childNode, node) {
    if (Array.isArray(childNode)) {
      return childNode.map(grandChild => handleCommand(/** @type {Node} */grandChild, node));
    }
    if (childNode.type === "TextLiteral") {
      const {
        attrs = {},
        transforms = []
      } = node;
      return {
        ...childNode,
        attrs: textLiteralAttrs(attrs, transforms),
        value: textLiteralValue(childNode.value, transforms)
      };
    }
    if (childNode.type === "IdentLiteral" && childNode.value.length === 1 && node.transforms && node.transforms.length === 1 && node.transforms[0] === "normal") {
      // Disable auto-italic idents
      return {
        ...childNode,
        attrs: {
          ...(childNode.attrs ?? {}),
          mathvariant: "normal"
        }
      };
    }
    if (childNode.type === "IdentLiteral" || childNode.type === "NumberLiteral" || childNode.type === "OperatorLiteral") {
      const {
        transforms = []
      } = node;
      return {
        ...childNode,
        value: literalValue(childNode.value, transforms)
      };
    }
    if (childNode.type === "MultiScripts") {
      return {
        ...childNode,
        base: handleCommand(childNode.base, node)
      };
    }
    if ("items" in childNode) {
      return {
        ...childNode,
        items: childNode.items.map(grandChild => handleCommand(/** @type {Node} */grandChild, node))
      };
    }
    return childNode;
  }

  /** @type {import("../index.js").TransformFn<UnaryOperation>} */
  function unaryOperation(node, transform) {
    if (node.name === "command") {
      return applyStyles(node.styles, transform(handleCommand(node.items[0], node)));
    }
    if (node.name === "fence") {
      const {
        open,
        close
      } = node.attrs || {};

      /** @type {(import("../index.js").Tag | null)[]} */
      const childNodes = [{
        tag: "mo",
        attrs: {
          fence: true
        },
        textContent: `${open}`
      }, node.items.length === 1 ? transform(node.items[0]) : {
        tag: "mrow",
        childNodes: node.items.map(transform)
      }, {
        tag: "mo",
        attrs: {
          fence: true
        },
        textContent: `${close}`
      }];
      return {
        tag: "mrow",
        childNodes
      };
    }
    const tag = `m${node.name}`;
    let [operand] = node.items;
    if (operand.type === "Term" && operand.items.length === 1) {
      // There is no need of a singleton term.
      [operand] = operand.items;
    }
    if (node.accent) {
      const attrs = node.attrs ? {
        ...node.attrs
      } : {};
      if (node.name === "over") {
        attrs.accent = true;

        // Remove the dot from i and j to make way for the accent.
        if (operand.type === "IdentLiteral") {
          if (operand.value === "i") {
            operand.value = "ı";
          } else if (operand.value === "j") {
            operand.value = "ȷ";
          }
        }
      } else if (node.name === "under") {
        attrs.accentunder = true;
      }
      const childNodes = [transform(operand), {
        tag: "mo",
        textContent: node.accent
      }];
      return {
        tag,
        childNodes,
        attrs
      };
    }
    return {
      tag,
      childNodes: [transform(operand)],
      attrs: node.attrs
    };
  }

  /** @typedef {import("../index.js").TransformFn} TransformFn */
  var transforms = new Map([["BinaryOperation", (/** @type {TransformFn} */operation)], ["FencedGroup", (/** @type {TransformFn} */fencedGroup)], ["IdentLiteral", (/** @type {TransformFn} */literal("mi"))], ["MatrixGroup", (/** @type {TransformFn} */matrixGroup)], ["MultiScripts", (/** @type {TransformFn} */multiscripts)], ["NumberLiteral", (/** @type {TransformFn} */literal("mn"))], ["OperatorLiteral", (/** @type {TransformFn} */literal("mo"))], ["Sentence", (/** @type {TransformFn} */sentence)], ["SpaceLiteral", (/** @type {TransformFn} */spaceLiteral)], ["Term", (/** @type {TransformFn} */term)], ["TernaryOperation", (/** @type {TransformFn} */operation)], ["TextLiteral", (/** @type {TransformFn} */literal("mtext"))], ["UnaryOperation", (/** @type {TransformFn} */unaryOperation)]]);

  /**
   * @typedef {import("../parser/index.js").Node} ASTNode
   * @typedef {object} TransformerOptions
   * @property {"ltr" | "rtl" | null} [dir=null] - The reading direction of the
   *   output. Default is `null`
   * @property {"block" | "inline" | null} [display=null] - Whether the expression
   *   should be in display mode. Default is `null` Default is `null` Default is
   *   `null` Default is `null`
   * @typedef {object} Tag
   * @property {string} tag
   * @property {Record<string, string | number | boolean | undefined | null>} [attrs]
   * @property {(Tag | null)[]} [childNodes]
   * @property {string} [textContent]
   */

  /**
   * @template {ASTNode} [Node=ASTNode] Default is `ASTNode`
   * @typedef {(
   *   node: Node,
   *   transform: (node: ASTNode) => Tag | null,
   *   options: Required<TransformerOptions>,
   * ) => Tag | null} TransformFn
   */

  /**
   * @param {Required<TransformerOptions>} options
   * @returns {(node: ASTNode) => Tag | null}
   */
  function transformer(options) {
    return function transform(node) {
      const transformNode = transforms.get(node.type);
      if (!transformNode) {
        return null;
      }
      return transformNode(node, transform, options);
    };
  }

  /**
   * @typedef {import("./tokenizer/index.js").TokenizerOptions} TokenizerOptions
   * @typedef {import("./transformer/index.js").TransformerOptions} TransformerOptions
   * @typedef {import("./renders/index.js").RenderOptions} RenderOptions
   * @typedef {TokenizerOptions & TransformerOptions & RenderOptions} Options
   *
   * @typedef {object} Result
   * @property {() => string} toString
   * @property {() => Element | DocumentFragment} toDOM
   * @property {(root: Element) => void} updateDOM
   *
   * @param {Options} options
   * @returns {(input: string) => Result}
   */
  function compiler({
    bare = false,
    dir = null,
    display = null,
    decimalMark = ".",
    colSep = decimalMark === "," ? ";" : ",",
    rowSep = colSep === ";" ? ";;" : ";"
  }) {
    const tokenizerOptions = {
      decimalMark,
      colSep,
      rowSep
    };
    const transformerOptions = {
      dir,
      display
    };
    const renderOptions = {
      bare
    };

    /**
     * @param {string} input - Mathup expression
     * @returns {Result}
     */
    return function compile(input) {
      const tokenize = createTokenizer(tokenizerOptions);
      const tokens = tokenize(input);
      const ast = parse([...tokens]);
      const transform = transformer(transformerOptions);
      const domTree = transform(ast);
      if (!domTree) {
        throw new Error("Failed parsing AST");
      }
      return {
        toDOM() {
          return toDOM(domTree, renderOptions);
        },
        toString() {
          return toString(domTree, renderOptions);
        },
        updateDOM(root) {
          return updateDOM(root, domTree, renderOptions);
        }
      };
    };
  }

  /**
   * @typedef {import("./compiler/index.js").Result} Mathup
   * @typedef {import("./compiler/index.js").Options} Options
   * @param {string} input
   * @param {Options} [options]
   * @returns {Mathup}
   */
  function mathup(input, options = {}) {
    const compile = compiler(options);
    return compile(input);
  }

  return mathup;

})();
//# sourceMappingURL=mathup.iife.js.map