// Some imports
import fallbackMathJaxLangData from "./locales/en.json";
import { FONT_FAMILY, SVG_NS } from "../../../../constants";
import { getFontString, getFontFamilyString, isRTL } from "../../../../utils";
import {
  getApproxLineHeight,
  getBoundTextElement,
  getContainerElement,
  getTextWidth,
  measureText,
  wrapText,
} from "../../../../element/textElement";
import {
  hasBoundTextElement,
  isTextElement,
} from "../../../../element/typeChecks";
import {
  ExcalidrawElement,
  ExcalidrawTextElement,
  NonDeleted,
} from "../../../../element/types";
import { newElementWith } from "../../../../element/mutateElement";
import { getElementAbsoluteCoords } from "../../../../element/bounds";

// Imports for actions
import { t, registerAuxLangData } from "../../../../i18n";
import { Action } from "../../../../actions/types";
import { AppState } from "../../../../types";
import {
  changeProperty,
  getFormValue,
} from "../../../../actions/actionProperties";
import { getSelectedElements } from "../../../../scene";
import {
  getNonDeletedElements,
  redrawTextBoundingBox,
} from "../../../../element";
import { ButtonSelect } from "../../../../components/ButtonSelect";

// Subtype imports
import {
  SubtypeLoadedCb,
  SubtypeMethods,
  SubtypePrepFn,
} from "../../../../subtypes";
import { mathSubtypeIcon } from "./icon";
import { getMathSubtypeRecord } from "./types";
import { SubtypeButton } from "../../../../components/Subtypes";
import { getMaxContainerWidth } from "../../../../element/newElement";

const mathSubtype = getMathSubtypeRecord().subtype;
const FONT_FAMILY_MATH = FONT_FAMILY.Helvetica;
type MathProps = Record<"useTex" | "mathOnly", boolean>;

type ExcalidrawMathElement = ExcalidrawTextElement &
  Readonly<{
    subtype: typeof mathSubtype;
  }>;

const isMathElement = (
  element: ExcalidrawElement | null,
): element is ExcalidrawMathElement => {
  return (
    isTextElement(element) &&
    "subtype" in element &&
    element.subtype === mathSubtype
  );
};

class GetMathProps {
  private useTex: boolean = true;
  private mathOnly: boolean = false;
  getUseTex = (appState?: AppState): boolean => {
    const mathProps =
      appState?.customData && appState.customData[`${mathSubtype}`];
    if (mathProps !== undefined) {
      this.useTex = mathProps.useTex !== undefined ? mathProps.useTex : true;
    }
    return this.useTex;
  };

  getMathOnly = (appState?: AppState): boolean => {
    const mathProps =
      appState?.customData && appState.customData[`${mathSubtype}`];
    if (mathProps !== undefined) {
      this.mathOnly =
        mathProps.mathOnly !== undefined ? mathProps.mathOnly : false;
    }
    return this.mathOnly;
  };

  ensureMathProps = (props: ExcalidrawElement["customData"]): MathProps => {
    const mathProps: MathProps = {
      useTex:
        props !== undefined && props.useTex !== undefined
          ? props.useTex
          : this.useTex,
      mathOnly:
        props !== undefined && props.mathOnly !== undefined
          ? props.mathOnly
          : this.mathOnly,
    };
    return mathProps;
  };
}
const getMathProps = new GetMathProps();

const getStartDelimiter = (useTex: boolean): string => {
  return useTex ? "\\(" : "`";
};

const getEndDelimiter = (useTex: boolean): string => {
  return useTex ? "\\)" : "`";
};

const mathJax = {} as {
  adaptor: any;
  amHtml: any;
  texHtml: any;
  visitor: any;
  mmlSvg: any;
  mmlSre: any;
  amFixes: any;
};

let stopLoadingMathJax = false;
let mathJaxLoaded = false;
let mathJaxLoading = false;
let mathJaxLoadedCallback: SubtypeLoadedCb | undefined;

// Configure use or non-use of speech-rule-engine
const useSRE = false;

let errorSvg: string;
let errorAria: string;

const loadMathJax = async () => {
  const shouldLoad =
    !mathJaxLoaded &&
    !mathJaxLoading &&
    (mathJax.adaptor === undefined ||
      mathJax.amHtml === undefined ||
      mathJax.texHtml === undefined ||
      mathJax.visitor === undefined ||
      mathJax.mmlSvg === undefined ||
      mathJax.amFixes === undefined ||
      (useSRE && mathJax.mmlSre === undefined));
  if (!shouldLoad && !mathJaxLoaded) {
    stopLoadingMathJax = true;
  }
  if (!mathJaxLoaded) {
    mathJaxLoading = true;

    // MathJax components we use
    const AsciiMath = (await import("mathjax-full/js/input/asciimath"))
      .AsciiMath;
    const TeX = (await import("mathjax-full/js/input/tex")).TeX;
    const SVG = (await import("mathjax-full/js/output/svg")).SVG;
    const liteAdaptor = (await import("mathjax-full/js/adaptors/liteAdaptor"))
      .liteAdaptor;
    const RegisterHTMLHandler = (await import("mathjax-full/js/handlers/html"))
      .RegisterHTMLHandler;

    // Components for MathJax accessibility
    const MathML = (await import("mathjax-full/js/input/mathml")).MathML;
    const SerializedMmlVisitor = (
      await import("mathjax-full/js/core/MmlTree/SerializedMmlVisitor")
    ).SerializedMmlVisitor;
    const Sre = useSRE
      ? (await import("mathjax-full/js/a11y/sre")).Sre
      : undefined;

    // Import some TeX packages
    await import("mathjax-full/js/input/tex/ams/AmsConfiguration");
    await import(
      "mathjax-full/js/input/tex/boldsymbol/BoldsymbolConfiguration"
    );

    // Set the following to "true" to import the "mhchem" and "physics" packages.
    const includeMhchemPhysics = false;
    if (includeMhchemPhysics) {
      await import("mathjax-full/js/input/tex/mhchem/MhchemConfiguration");
      await import("mathjax-full/js/input/tex/physics/PhysicsConfiguration");
    }
    const texPackages = includeMhchemPhysics
      ? ["base", "ams", "boldsymbol", "mhchem", "physics"]
      : ["base", "ams", "boldsymbol"];

    // Types needed to lazy-load MathJax
    const LiteElement = (await import("mathjax-full/js/adaptors/lite/Element"))
      .LiteElement;
    const LiteText = (await import("mathjax-full/js/adaptors/lite/Text"))
      .LiteText;
    const LiteDocument = (
      await import("mathjax-full/js/adaptors/lite/Document")
    ).LiteDocument;

    // Configure AsciiMath to use the "display" option.  See
    // https://github.com/mathjax/MathJax/issues/2520#issuecomment-1128831182.
    const MathJax = (
      await require("mathjax-full/js/input/asciimath/mathjax2/legacy/MathJax")
    ).MathJax;
    mathJax.amFixes = MathJax.InputJax.AsciiMath.AM.Augment;

    // Load the document creator last
    const mathjax = (await import("mathjax-full/js/mathjax")).mathjax;

    type E = typeof LiteElement;
    type T = typeof LiteText;
    type D = typeof LiteDocument;
    if (stopLoadingMathJax) {
      stopLoadingMathJax = false;
      return;
    }

    // Set up shared components
    mathJax.adaptor = liteAdaptor();
    mathJax.visitor = new SerializedMmlVisitor();
    RegisterHTMLHandler(mathJax.adaptor);

    // Set up input components
    const asciimath = new AsciiMath<E | T, T, D>({});
    const tex = new TeX<E | T, T, D>({ packages: texPackages });

    // Set up output components
    const mml = new MathML<E | T, T, D>();
    const svg = new SVG<E | T, T, D>({ fontCache: "local" });

    // AsciiMath input
    mathJax.amHtml = mathjax.document("", { InputJax: asciimath });

    // LaTeX input
    mathJax.texHtml = mathjax.document("", { InputJax: tex });

    // Capture the MathML for accessibility purposes
    mathJax.mmlSvg = mathjax.document("", {
      InputJax: mml,
      OutputJax: svg,
    });

    const mathJaxReady = function () {
      mathJax.mmlSre = Sre;

      // Error indicator
      const errorMML = mathJax.visitor.visitTree(
        mathJax.texHtml.convert("ERR", { display: false }),
      );
      errorSvg = mathJax.adaptor.outerHTML(mathJax.mmlSvg.convert(errorMML));
      errorAria = useSRE ? mathJax.mmlSre.toSpeech(errorMML) : "ERR";

      // Finalize loading MathJax
      mathJaxLoaded = true;
      if (mathJaxLoadedCallback !== undefined) {
        mathJaxLoadedCallback(isMathElement);
      }
    };

    if (useSRE) {
      // Set up a custom loader to use our local mathmaps
      const custom = (locale: string) => {
        return new Promise<string>((resolve, reject) => {
          try {
            const mathmap = JSON.stringify(
              require(`mathjax-full/es5/sre/mathmaps/${locale}.json`),
            );
            resolve(mathmap);
          } catch (e) {
            reject("");
          }
        });
      };
      global.SREfeature = { custom };

      // Configure MathJax for accessibility
      Sre?.setupEngine({ speech: "shallow", custom: true }).then(() => {
        mathJaxReady();
      });
    } else {
      mathJaxReady();
    }
  }
};

// Round `x` to `n` decimal places
const roundDec = (x: number, n: number) => {
  const powOfTen = Math.pow(10, n);
  return Math.round(x * powOfTen) / powOfTen;
};

const splitMath = (text: string, mathProps: MathProps) => {
  const startDelimiter = getStartDelimiter(mathProps.useTex);
  const endDelimiter = getEndDelimiter(mathProps.useTex);
  let curIndex = 0;
  let oldIndex = 0;
  const array = [];

  const mathFirst = text.indexOf(startDelimiter, 0) === 0;
  let inText = !mathFirst;
  if (!inText) {
    array.push("");
  }
  while (oldIndex >= 0 && curIndex >= 0) {
    oldIndex =
      curIndex +
      (inText
        ? curIndex > 0
          ? endDelimiter.length
          : 0
        : startDelimiter.length);
    curIndex = text.indexOf(inText ? startDelimiter : endDelimiter, oldIndex);
    if (curIndex >= oldIndex || (curIndex < 0 && oldIndex > 0)) {
      inText = !inText;
      array.push(
        text.substring(oldIndex, curIndex >= 0 ? curIndex : text.length),
      );
    }
  }
  if (array.length === 0 && !mathFirst) {
    array[0] = text;
  }

  return array;
};

const joinMath = (
  text: string[],
  mathProps: MathProps,
  isMathJaxLoaded: boolean,
) => {
  const startDelimiter = getStartDelimiter(mathProps.useTex);
  const endDelimiter = getEndDelimiter(mathProps.useTex);
  let inText = true;
  let joined = "";
  for (let index = 0; index < text.length; index++) {
    const space = index > 0 ? " " : "";
    joined +=
      mathProps.mathOnly && isMathJaxLoaded
        ? `${space}${text[index]}`
        : inText
        ? text[index]
        : startDelimiter + text[index] + endDelimiter;
    inText = !inText;
  }
  return joined;
};

const getMathNewline = (mathProps: MathProps) => {
  return mathProps.useTex && mathProps.mathOnly ? "\\newline" : "\n";
};

// This lets math input run across multiple newlines.
// Basically, replace with a space each newline between the delimiters.
// Do so unless it's AsciiMath in math-only mode.
const consumeMathNewlines = (
  text: string,
  mathProps: MathProps,
  isMathJaxLoaded: boolean,
) => {
  if (!isMathJaxLoaded) {
    return text;
  }
  const tempText = splitMath(text.replace(/\r\n?/g, "\n"), mathProps);
  if (mathProps.useTex || !mathProps.mathOnly) {
    for (let i = 0; i < tempText.length; i++) {
      if (i % 2 === 1 || mathProps.mathOnly) {
        tempText[i] = tempText[i].replace(/\n/g, " ");
      }
    }
  }
  return joinMath(tempText, mathProps, isMathJaxLoaded);
};

// Cache the SVGs from MathJax
const mathJaxSvgCacheAM = {} as {
  [key: string]: { svg: string; aria: string };
};
const mathJaxSvgCacheTex = {} as {
  [key: string]: { svg: string; aria: string };
};
// Cache the results of getMetrics()
const metricsCache = {} as {
  [key: string]: {
    markupMetrics: Array<{
      x: number;
      y: number;
      width: number;
      height: number;
    }>[];
    lineMetrics: Array<{ width: number; height: number; baseline: number }>;
    imageMetrics: { width: number; height: number; baseline: number };
  };
};
// Cache the SVGs for renderSvgMathElement()
const svgCache = {} as { [key: string]: string };
// Cache the rendered MathJax images for renderMathElement()
const imageCache = {} as { [key: string]: HTMLImageElement };

const textAsMjxContainer = (
  text: string,
  isMathJaxLoaded: boolean,
): Element | null => {
  // Put the content in a div to check whether it is SVG or Text
  const container = document.createElement("div");
  container.innerHTML = text;
  // The mjx-container has child nodes, while Text nodes do not.
  // Check for the "viewBox" attribute to determine if it's SVG.
  const childIsSvg =
    isMathJaxLoaded &&
    container.childNodes &&
    container.childNodes.length > 0 &&
    container.childNodes[0].hasChildNodes() &&
    container.childNodes[0].childNodes[0].nodeName === "svg";
  // Conditionally return the mjx-container
  return childIsSvg ? (container.children[0] as Element) : null;
};

const math2Svg = (
  text: string,
  mathProps: MathProps,
  isMathJaxLoaded: boolean,
): { svg: string; aria: string } => {
  const useTex = mathProps.useTex;
  const key = `${mathProps.mathOnly}${text}`;
  if (
    isMathJaxLoaded &&
    (useTex ? mathJaxSvgCacheTex[key] : mathJaxSvgCacheAM[key])
  ) {
    return useTex ? mathJaxSvgCacheTex[key] : mathJaxSvgCacheAM[key];
  }
  loadMathJax();
  try {
    const userOptions = { display: mathProps.mathOnly };
    // For some reason this needs to be set before each call to the AsciiMath
    // input jax to render display/inline correctly.
    if (isMathJaxLoaded && !useTex) {
      mathJax.amFixes({ displaystyle: mathProps.mathOnly });
    }
    // Intermediate MathML
    const mmlString = isMathJaxLoaded
      ? mathJax.visitor.visitTree(
          useTex
            ? mathJax.texHtml.convert(text, userOptions)
            : mathJax.amHtml.convert(text, userOptions),
        )
      : text;
    // For rendering
    const htmlString = isMathJaxLoaded
      ? mathJax.adaptor.outerHTML(mathJax.mmlSvg.convert(mmlString))
      : text;
    // For accessibility
    const ariaString = isMathJaxLoaded
      ? useSRE
        ? mathJax.mmlSre.toSpeech(mmlString)
        : text
      : mmlString;
    if (isMathJaxLoaded) {
      if (useTex) {
        mathJaxSvgCacheTex[key] = { svg: htmlString, aria: ariaString };
      } else {
        mathJaxSvgCacheAM[key] = { svg: htmlString, aria: ariaString };
      }
    }
    return { svg: htmlString, aria: ariaString };
  } catch {
    if (isMathJaxLoaded) {
      return { svg: errorSvg, aria: errorAria };
    }
    return { svg: text, aria: text };
  }
};

const markupText = (
  text: string,
  mathProps: MathProps,
  isMathJaxLoaded: boolean,
) => {
  const lines = consumeMathNewlines(text, mathProps, isMathJaxLoaded).split(
    isMathJaxLoaded ? getMathNewline(mathProps) : "\n",
  );
  const markup = [] as Array<string>[];
  const aria = [] as Array<string>[];
  for (let index = 0; index < lines.length; index++) {
    markup.push([]);
    aria.push([]);
    if (!isMathJaxLoaded) {
      // Run lines[index] through math2Svg so loadMathJax() gets called
      const math = math2Svg(lines[index], mathProps, isMathJaxLoaded);
      markup[index].push(math.svg);
      aria[index].push(math.aria);
      continue;
    }
    // Don't split by the delimiter in math-only mode
    const lineArray =
      mathProps.mathOnly || !isMathJaxLoaded
        ? [lines[index]]
        : splitMath(lines[index], mathProps);
    for (let i = 0; i < lineArray.length; i++) {
      // Don't guard the following as "isMathJaxLoaded && i % 2 === 1"
      // in order to ensure math2Svg() actually gets called, and thus
      // loadMathJax().
      if (i % 2 === 1 || mathProps.mathOnly) {
        const math = math2Svg(lineArray[i], mathProps, isMathJaxLoaded);
        markup[index].push(math.svg);
        aria[index].push(math.aria);
      } else {
        markup[index].push(lineArray[i]);
        aria[index].push(lineArray[i]);
      }
    }
    if (lineArray.length === 0) {
      markup[index].push("");
      aria[index].push("");
    }
  }
  return { markup, aria };
};

const getCacheKey = (
  text: string,
  fontSize: number,
  strokeColor: String,
  textAlign: string,
  opacity: Number,
  mathProps: MathProps,
) => {
  const key = `${text}, ${fontSize}, ${strokeColor}, ${textAlign}, ${opacity}, ${mathProps.useTex}, ${mathProps.mathOnly}`;
  return key;
};

const measureMarkup = (
  markup: Array<string | Element>,
  fontSize: number,
  mathProps: MathProps,
  isMathJaxLoaded: boolean,
  maxWidth?: number | null,
) => {
  const font = getFontString({ fontSize, fontFamily: FONT_FAMILY_MATH });
  const container = document.createElement("div");
  container.style.position = "absolute";
  container.style.whiteSpace = "pre";
  container.style.font = font;
  container.style.minHeight = "1em";

  if (maxWidth) {
    const lineHeight = getApproxLineHeight(font);
    container.style.maxWidth = `${String(maxWidth)}px`;
    container.style.overflow = "hidden";
    container.style.wordBreak = "break-word";
    container.style.lineHeight = `${String(lineHeight)}px`;
    container.style.whiteSpace = "pre-wrap";
  }
  document.body.appendChild(container);

  // Sanitize possible HTML entered
  for (let i = 0; i < markup.length; i++) {
    // This should be an mjx-container
    if ((markup[i] as Element).hasChildNodes) {
      // Append as HTML
      container.appendChild(markup[i] as Element);
    } else {
      // Append as text
      container.append(markup[i]);
    }
  }

  // Now creating 1px sized item that will be aligned to baseline
  // to calculate baseline shift
  const span = document.createElement("span");
  span.style.display = "inline-block";
  span.style.overflow = "hidden";
  span.style.width = "1px";
  span.style.height = "1px";
  container.appendChild(span);
  // Baseline is important for positioning text on canvas
  const baseline = span.offsetTop + span.offsetHeight;
  const width = container.offsetWidth + 1;
  const height = container.offsetHeight;

  const containerRect = container.getBoundingClientRect();
  // Compute for each SVG or Text child node of line (the last
  // child is the span element for the baseline).
  const childMetrics = [];
  let nextX = 0;
  for (let i = 0; i < container.childNodes.length - 1; i++) {
    // The mjx-container has child nodes, while Text nodes do not
    const childIsSvg =
      isMathJaxLoaded &&
      ((container.childNodes[i].hasChildNodes() &&
        container.childNodes[i].childNodes[0].nodeName === "svg") ||
        mathProps.mathOnly);
    // The mjx-container element or the Text node
    const child = container.childNodes[i] as HTMLElement | Text;
    if (isMathJaxLoaded && (mathProps.mathOnly || childIsSvg)) {
      // The svg element
      const grandchild = (child as HTMLElement).firstChild as HTMLElement;
      const grandchildRect = grandchild.getBoundingClientRect();
      childMetrics.push({
        x: grandchildRect.x - containerRect.x,
        y: grandchildRect.y - containerRect.y,
        width: grandchildRect.width,
        height: grandchildRect.height,
      });
      // Set the x value for the next Text node
      nextX = grandchildRect.x + grandchildRect.width - containerRect.x;
    } else {
      // The Text node
      const grandchild = child as Text;
      const text = grandchild.textContent ?? "";
      if (text !== "") {
        const textMetrics = measureText(text, font, maxWidth);
        childMetrics.push({
          x: nextX,
          y: baseline,
          width: textMetrics.width,
          height: textMetrics.height,
        });
      }
    }
  }
  if (childMetrics.length === 0) {
    // Avoid crashes in getMetrics()
    childMetrics.push({ x: 0, y: 0, width: 0, height: 0 });
  }
  document.body.removeChild(container);
  return { width, height, baseline, childMetrics };
};

const getMetrics = (
  markup: string[][],
  fontSize: number,
  mathProps: MathProps,
  isMathJaxLoaded: boolean,
  maxWidth?: number | null,
) => {
  let key = `${fontSize} ${mathProps.useTex} ${mathProps.mathOnly} ${isMathJaxLoaded} ${maxWidth}`;
  for (let index = 0; index < markup.length; index++) {
    for (let i = 0; i < markup[index].length; i++) {
      key += markup[index][i];
    }
    if (index < markup.length - 1) {
      key += "\n";
    }
  }
  const cKey = key;
  if (isMathJaxLoaded && metricsCache[cKey]) {
    return metricsCache[cKey];
  }
  const markupMetrics = [] as Array<{
    x: number;
    y: number;
    width: number;
    height: number;
  }>[];
  const lineMetrics = [] as Array<{
    width: number;
    height: number;
    baseline: number;
  }>;
  let imageWidth = 0;
  let imageHeight = 0;
  for (let index = 0; index < markup.length; index++) {
    // We pass an array of mjx-containers and strings
    const lineMarkup = [] as Array<string | Element>;
    for (let i = 0; i < markup[index].length; i++) {
      const mjx = textAsMjxContainer(markup[index][i], isMathJaxLoaded);
      lineMarkup.push(mjx !== null ? mjx : markup[index][i]);
    }

    // Use the browser's measurements by temporarily attaching
    // the rendered line to the document.body.
    const { width, height, baseline, childMetrics } = measureMarkup(
      lineMarkup,
      fontSize,
      mathProps,
      isMathJaxLoaded,
      maxWidth,
    );

    markupMetrics.push(childMetrics);
    lineMetrics.push({ width, height, baseline });
    imageWidth = Math.max(imageWidth, width);
    imageHeight += height;
  }
  const lastLineMetrics = lineMetrics[lineMetrics.length - 1];
  const imageMetrics = {
    width: imageWidth,
    height: imageHeight,
    baseline: imageHeight - lastLineMetrics.height + lastLineMetrics.baseline,
  };
  const metrics = { markupMetrics, lineMetrics, imageMetrics };
  if (isMathJaxLoaded) {
    metricsCache[cKey] = metrics;
  }
  return metrics;
};

const renderMath = (
  text: string,
  fontSize: number,
  textAlign: string,
  mathProps: MathProps,
  isMathJaxLoaded: boolean,
  doSetupChild: (
    childIsSvg: boolean,
    svg: SVGSVGElement | null,
    text: string,
    rtl: boolean,
    childRtl: boolean,
    lineHeight: number,
  ) => void,
  doRenderChild: (x: number, y: number, width: number, height: number) => void,
  parentWidth?: number,
): string => {
  const mathLines = consumeMathNewlines(text, mathProps, isMathJaxLoaded).split(
    isMathJaxLoaded ? getMathNewline(mathProps) : "\n",
  );
  const { markup, aria } = markupText(text, mathProps, isMathJaxLoaded);
  const metrics = getMetrics(markup, fontSize, mathProps, isMathJaxLoaded);
  const width = parentWidth ?? metrics.imageMetrics.width;

  let y = 0;
  for (let index = 0; index < markup.length; index++) {
    const lineMetrics = metrics.lineMetrics[index];
    const lineMarkupMetrics = metrics.markupMetrics[index];
    const rtl = isRTL(mathLines[index]);
    const x =
      textAlign === "right"
        ? width - lineMetrics.width
        : textAlign === "left"
        ? 0
        : (width - lineMetrics.width) / 2;
    // Drop any empty strings from this line to match childMetrics
    const content = markup[index].filter((value) => value !== "");
    for (let i = 0; i < content.length; i += 1) {
      const mjx = textAsMjxContainer(
        content[mathProps.mathOnly ? 0 : i],
        isMathJaxLoaded,
      );
      // If we got an mjx-container, then assume it contains an SVG child
      const childIsSvg = mjx !== null;
      const svg = childIsSvg ? (mjx.children[0] as SVGSVGElement) : null;
      const childRtl = childIsSvg
        ? false
        : isRTL(content[mathProps.mathOnly ? 0 : i]);
      // Set up the child for rendering
      const height = lineMetrics.height;
      doSetupChild(childIsSvg, svg, content[i], rtl, childRtl, height);
      // Don't offset when we have an empty string.
      const nullContent = content.length > 0 && content[i] === "";
      const childX = nullContent ? 0 : lineMarkupMetrics[i].x;
      const childY = nullContent ? 0 : lineMarkupMetrics[i].y;
      const childWidth = nullContent ? 0 : lineMarkupMetrics[i].width;
      const childHeight = nullContent ? 0 : lineMarkupMetrics[i].height;
      // Now render the child
      doRenderChild(x + childX, y + childY, childWidth, childHeight);
    }
    y += lineMetrics.height;
  }
  let ariaText = "";
  for (let i = 0; i < aria.length; i++) {
    if (i > 0) {
      ariaText = `${ariaText} `;
    }
    for (let j = 0; j < aria[i].length; j++) {
      ariaText = `${ariaText}${aria[i][j]}`;
    }
  }
  return ariaText;
};

const getImageMetrics = (
  text: string,
  fontSize: number,
  mathProps: MathProps,
  isMathJaxLoaded: boolean,
  maxWidth?: number | null,
) => {
  const markup = markupText(text, mathProps, isMathJaxLoaded).markup;
  return getMetrics(markup, fontSize, mathProps, isMathJaxLoaded, maxWidth)
    .imageMetrics;
};

const getSelectedMathElements = (
  elements: readonly ExcalidrawElement[],
  appState: Readonly<AppState>,
): NonDeleted<ExcalidrawMathElement>[] => {
  const selectedElements = getSelectedElements(
    getNonDeletedElements(elements),
    appState,
  );
  if (appState.editingElement) {
    selectedElements.push(appState.editingElement);
  }
  const eligibleElements = selectedElements.filter(
    (element, index, eligibleElements) =>
      isMathElement(element) ||
      (hasBoundTextElement(element) &&
        isMathElement(getBoundTextElement(element))),
  ) as NonDeleted<ExcalidrawMathElement>[];
  return eligibleElements;
};

// Be sure customData is defined with proper values for ExcalidrawMathElements
const ensureMathElement = (element: Partial<ExcalidrawElement>) => {
  if (!isMathElement(element as Required<ExcalidrawElement>)) {
    return;
  }
  const mathProps = getMathProps.ensureMathProps(element.customData);
  (element as any).customData = {
    ...element.customData,
    useTex: mathProps.useTex,
    mathOnly: mathProps.mathOnly,
  } as ExcalidrawElement["customData"];
};

const cleanMathElementUpdate = function (updates) {
  const oldUpdates = {};
  for (const key in updates) {
    if (key !== "fontFamily") {
      (oldUpdates as any)[key] = (updates as any)[key];
    }
    if (key === "customData") {
      const mathProps = getMathProps.ensureMathProps((updates as any)[key]);
      (updates as any)[key] = {
        ...(updates as any)[key],
        useTex: mathProps.useTex,
        mathOnly: mathProps.mathOnly,
      };
    } else {
      (updates as any)[key] = (updates as any)[key];
    }
  }
  (updates as any).fontFamily = FONT_FAMILY_MATH;
  return oldUpdates;
} as SubtypeMethods["clean"];

const measureMathElement = function (element, next, maxWidth) {
  ensureMathElement(element);
  const isMathJaxLoaded = mathJaxLoaded;
  const fontSize = next?.fontSize ?? element.fontSize;
  const text = next?.text ?? element.text;
  const customData = next?.customData ?? element.customData;
  const mathProps = getMathProps.ensureMathProps(customData);
  const noMaxWidth = mathProps.mathOnly;
  const cWidth = noMaxWidth ? undefined : maxWidth;
  const metrics = getImageMetrics(
    text,
    fontSize,
    mathProps,
    isMathJaxLoaded,
    cWidth,
  );
  const { height, baseline } = metrics;
  const width = noMaxWidth ? maxWidth ?? metrics.width : metrics.width;
  return { width, height, baseline };
} as SubtypeMethods["measureText"];

const renderMathElement = function (element, context, renderCb) {
  ensureMathElement(element);
  const isMathJaxLoaded = mathJaxLoaded;
  const _element = element as NonDeleted<ExcalidrawMathElement>;
  const text = _element.text;
  const fontSize = _element.fontSize;
  const strokeColor = _element.strokeColor;
  const textAlign = _element.textAlign;
  const opacity = _element.opacity / 100;
  const mathProps = getMathProps.ensureMathProps(_element.customData);

  let _childIsSvg: boolean;
  let _text: string;
  let _svg: SVGSVGElement;

  const doSetupChild: (
    childIsSvg: boolean,
    svg: SVGSVGElement | null,
    text: string,
    rtl: boolean,
    childRtl: boolean,
  ) => void = function (childIsSvg, svg, text, rtl, childRtl) {
    _childIsSvg = childIsSvg;
    _text = text;

    if (_childIsSvg) {
      _svg = svg!;
    } else {
      context.save();
      context.canvas.setAttribute("dir", childRtl ? "rtl" : "ltr");
      context.font = getFontString({ fontSize, fontFamily: FONT_FAMILY_MATH });
      context.fillStyle = _element.strokeColor;
      context.textAlign = _element.textAlign as CanvasTextAlign;
    }
  };

  const doRenderChild: (
    x: number,
    y: number,
    width: number,
    height: number,
  ) => void = function (x, y, width, height) {
    if (_childIsSvg) {
      const key = getCacheKey(
        _text,
        fontSize,
        strokeColor,
        "left",
        1,
        mathProps,
      );

      const _x = Math.round(x);
      const _y = Math.round(y);
      const imgKey = `${key}, ${width}, ${height}`;
      if (
        isMathJaxLoaded &&
        imageCache[imgKey] &&
        imageCache[imgKey] !== undefined
      ) {
        const img = imageCache[imgKey];
        const [width, height] = [img.naturalWidth, img.naturalHeight];
        context.drawImage(img, _x, _y, width, height);
      } else {
        const img = new Image();
        _svg.setAttribute("width", `${width}`);
        _svg.setAttribute("height", `${height}`);
        _svg.setAttribute("color", `${strokeColor}`);
        const svgString = _svg.outerHTML;
        const svg = new Blob([svgString], {
          type: "image/svg+xml;charset=utf-8",
        });
        const transformMatrix = context.getTransform();
        const reader = new FileReader();
        reader.addEventListener(
          "load",
          () => {
            img.onload = function () {
              const [width, height] = [img.naturalWidth, img.naturalHeight];
              context.save();
              context.setTransform(transformMatrix);
              context.globalAlpha = opacity;
              context.drawImage(img, _x, _y, width, height);
              context.restore();
              if (isMathJaxLoaded) {
                imageCache[imgKey] = img;
              }
              if (renderCb) {
                renderCb();
              }
            };
            img.src = reader.result as string;
          },
          false,
        );
        reader.readAsDataURL(svg);
      }
    } else {
      const childOffset =
        textAlign === "center"
          ? (width - 1) / 2
          : textAlign === "right"
          ? width - 1
          : 0;
      context.fillText(_text, x + childOffset, y);
      context.restore();
    }
  };
  const container = getContainerElement(_element);
  const parentWidth = container ? getMaxContainerWidth(container) : undefined;

  const offsetX =
    (_element.width - (container ? parentWidth! : _element.width)) *
    (textAlign === "right" ? 1 : textAlign === "center" ? 1 / 2 : 0);

  context.save();
  context.translate(offsetX, 0);
  element.customData!.ariaLabel = renderMath(
    text,
    fontSize,
    textAlign,
    mathProps,
    isMathJaxLoaded,
    doSetupChild,
    doRenderChild,
    parentWidth,
  );
  context.restore();
} as SubtypeMethods["render"];

const renderSvgMathElement = function (svgRoot, root, element, opt) {
  ensureMathElement(element);
  const isMathJaxLoaded = mathJaxLoaded;

  const _element = element as NonDeleted<ExcalidrawMathElement>;
  const mathProps = getMathProps.ensureMathProps(_element.customData);
  const text = _element.text;
  const fontSize = _element.fontSize;
  const strokeColor = _element.strokeColor;
  const textAlign = _element.textAlign;
  const opacity = _element.opacity / 100;
  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
  const cx = (x2 - x1) / 2 - (element.x - x1);
  const cy = (y2 - y1) / 2 - (element.y - y1);
  const degree = (180 * element.angle) / Math.PI;

  const node = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
  node.setAttribute(
    "transform",
    `translate(${opt?.offsetX || 0} ${
      opt?.offsetY || 0
    }) rotate(${degree} ${cx} ${cy})`,
  );

  const key = getCacheKey(
    text,
    fontSize,
    strokeColor,
    textAlign,
    opacity,
    mathProps,
  );
  if (isMathJaxLoaded && svgCache[key]) {
    const cachedDiv = svgRoot.ownerDocument!.createElement("div");
    cachedDiv.innerHTML = svgCache[key];
    node.appendChild(cachedDiv.firstElementChild!);
    root.appendChild(node);
    return;
  }

  const tempSvg = svgRoot.ownerDocument!.createElementNS(SVG_NS, "svg");
  const groupNode = tempSvg.ownerDocument.createElementNS(SVG_NS, "g");

  const font = getFontFamilyString({ fontFamily: FONT_FAMILY_MATH });
  groupNode.setAttribute("font-family", `${font}`);
  groupNode.setAttribute("font-size", `${fontSize}px`);
  groupNode.setAttribute("color", `${strokeColor}`);
  if (opacity !== 1) {
    groupNode.setAttribute("stroke-opacity", `${opacity}`);
    groupNode.setAttribute("fill-opacity", `${opacity}`);
  }
  tempSvg.appendChild(groupNode);

  const container = getContainerElement(_element);
  const parentWidth = container ? getMaxContainerWidth(container) : undefined;

  const offsetX =
    (_element.width - (container ? parentWidth! : _element.width)) *
    (textAlign === "right" ? 1 : textAlign === "center" ? 1 / 2 : 0);

  const { width, height } = getImageMetrics(
    text,
    fontSize,
    mathProps,
    isMathJaxLoaded,
    parentWidth,
  );

  let childNode = {} as SVGSVGElement | SVGTextElement;
  const doSetupChild: (
    childIsSvg: boolean,
    svg: SVGSVGElement | null,
    text: string,
    rtl: boolean,
    childRtl: boolean,
    lineHeight: number,
  ) => void = function (childIsSvg, svg, text, rtl, childRtl, lineHeight) {
    if (childIsSvg && text !== "") {
      childNode = svg!;

      // Scale the viewBox to have a centered height of 1.2 * lineHeight
      const rect = childNode.viewBox.baseVal;
      const scale = (1.2 * lineHeight) / rect.height;
      const goffset = roundDec(0.12 * lineHeight, 3);
      const gx = roundDec(rect.x * scale, 3) + goffset;
      const gy = roundDec(rect.y * scale, 3) + goffset;
      const gwidth = roundDec(rect.width * scale, 3);
      const gheight = roundDec(rect.height * scale, 3);
      childNode.setAttribute(
        "viewBox",
        `${-goffset} ${-goffset} ${gwidth} ${gheight}`,
      );

      // Set the transform on the svg's group node(s)
      for (let i = 0; i < childNode.childNodes.length; i++) {
        if (childNode.childNodes[i].nodeName === "g") {
          const group = childNode.childNodes[i] as SVGGElement;
          const transform = group.getAttribute("transform");
          group.setAttribute(
            "transform",
            `translate(${-gx} ${-gy}) scale(${scale}) ${transform}`,
          );
        }
      }
    } else {
      const textNode = tempSvg.ownerDocument.createElementNS(
        "http://www.w3.org/2000/svg",
        "text",
      );
      textNode.setAttribute("style", "white-space: pre;");
      textNode.setAttribute("fill", `${strokeColor}`);
      textNode.setAttribute("direction", `${childRtl ? "rtl" : "ltr"}`);
      textNode.setAttribute("text-anchor", `${childRtl ? "end" : "start"}`);
      textNode.textContent = text;
      childNode = textNode;
    }
  };
  const doRenderChild: (x: number, y: number) => void = function (x, y) {
    childNode.setAttribute("x", `${x + offsetX}`);
    childNode.setAttribute("y", `${y}`);
    groupNode.appendChild(childNode);
  };
  element.customData!.ariaLabel = renderMath(
    text,
    fontSize,
    textAlign,
    mathProps,
    isMathJaxLoaded,
    doSetupChild,
    doRenderChild,
    parentWidth,
  );

  tempSvg.setAttribute("version", "1.1");
  tempSvg.setAttribute("viewBox", `0 0 ${width} ${height}`);
  tempSvg.setAttribute("width", `${width}`);
  tempSvg.setAttribute("height", `${height}`);
  if (isMathJaxLoaded) {
    svgCache[key] = tempSvg.outerHTML;
  }
  node.appendChild(tempSvg);
  root.appendChild(node);
} as SubtypeMethods["renderSvg"];

const wrappedMathCache: { [key: string]: string } = {};

const wrapMathElement = function (element, containerWidth, next) {
  ensureMathElement(element);
  const isMathJaxLoaded = mathJaxLoaded;
  const fontSize =
    next?.fontSize !== undefined ? next.fontSize : element.fontSize;
  const text = next?.text !== undefined ? next.text : element.originalText;
  const customData = next?.customData ?? element.customData;
  const mathProps = getMathProps.ensureMathProps(customData);

  const font = getFontString({ fontSize, fontFamily: FONT_FAMILY_MATH });

  // Use regular text-wrapping for math-only mode
  if (mathProps.mathOnly) {
    return wrapText(text, font, containerWidth);
  }

  const maxWidth = containerWidth;

  const markup = markupText(text, mathProps, isMathJaxLoaded).markup;
  const metrics = getMetrics(markup, fontSize, mathProps, isMathJaxLoaded);

  const lines = consumeMathNewlines(text, mathProps, isMathJaxLoaded).split(
    isMathJaxLoaded ? getMathNewline(mathProps) : "\n",
  );
  const wrappedLines: string[] = [];
  const spaceWidth = getTextWidth(" ", font);
  for (let index = 0; index < lines.length; index++) {
    const mathLineKey = `${containerWidth} ${fontSize} ${mathProps.useTex} ${mathProps.mathOnly} ${lines[index]}`;
    if (wrappedMathCache[mathLineKey] !== undefined) {
      wrappedLines.push(...wrappedMathCache[mathLineKey].split("\n"));
      continue;
    }
    const currLineNum = wrappedLines.length;
    const lineArray = splitMath(lines[index], mathProps).filter(
      (value) => value !== "",
    );
    const markupArray = markup[index].filter((value) => value !== "");
    let curWidth = 0;
    wrappedLines.push("");
    // The following two boolean variables are to handle edge cases
    let nlByText = false;
    let nlByMath = false;
    for (let i = 0; i < lineArray.length; i++) {
      const isSvg =
        textAsMjxContainer(markupArray[i], isMathJaxLoaded) !== null;
      if (isSvg) {
        const lineItem =
          getStartDelimiter(mathProps.useTex) +
          lineArray[i] +
          getEndDelimiter(mathProps.useTex);
        const itemWidth = metrics.markupMetrics[index][i].width;
        if (itemWidth > maxWidth) {
          // If the math svg is greater than maxWidth, make its source
          // text be a new line and start on the next line.  Don't try
          // to split the math rendering into multiple lines.
          if (nlByText) {
            wrappedLines.pop();
            nlByText = false;
          }
          wrappedLines.push(lineItem);
          wrappedLines.push("");
          curWidth = 0;
          nlByMath = true;
        } else if (curWidth <= maxWidth && curWidth + itemWidth > maxWidth) {
          // If the math svg would push us past maxWidth, start a
          // new line and continue on that new line.  Store the math
          // svg's width in curWidth.
          wrappedLines.push(lineItem);
          curWidth = itemWidth;
          nlByMath = false;
        } else {
          // If the math svg would not push us past maxWidth, then
          // just append its source text to the current line.  Add
          // the math svg's width to curWidth.
          wrappedLines[wrappedLines.length - 1] += lineItem;
          curWidth += itemWidth;
          nlByMath = false;
        }
      } else {
        // Don't have spaces at the start of a wrapped line.  But
        // allow them at the start of new lines from the originalText.
        const lineItem =
          curWidth > 0 || i === 0 ? lineArray[i] : lineArray[i].trimStart();
        // Append words one-by-one until we would be over maxWidth;
        // then let wrapText() take effect.
        const words = lineItem.split(" ");
        let wordsIndex = 0;
        while (curWidth <= maxWidth && wordsIndex < words.length) {
          const wordWidth = getTextWidth(words[wordsIndex], font);
          if (nlByMath && wordWidth + spaceWidth > maxWidth) {
            wrappedLines.pop();
            nlByMath = false;
          }
          if (curWidth + wordWidth + spaceWidth <= maxWidth) {
            wrappedLines[wrappedLines.length - 1] += words[wordsIndex];
            curWidth += wordWidth;
            wordsIndex++;
            // Only append a space if we wouldn't go over maxWidth
            // and we haven't appended the last word yet.
            if (
              curWidth + spaceWidth <= maxWidth &&
              wordsIndex < words.length
            ) {
              wrappedLines[wrappedLines.length - 1] += " ";
              curWidth += spaceWidth;
            } else {
              break;
            }
          } else {
            break;
          }
        }
        const toWrap = words.slice(wordsIndex).join(" ").trimStart();
        if (toWrap !== "") {
          wrappedLines.push(
            ...wrapText(toWrap, font, containerWidth).split("\n"),
          );
          // Set curWidth to the width of the last wrapped line
          curWidth = getTextWidth(wrappedLines[wrappedLines.length - 1], font);
          // This means wrapText() caused us to start a new line
          if (curWidth === 0) {
            nlByText = true;
          }
        }
      }
    }
    wrappedMathCache[mathLineKey] = wrappedLines.slice(currLineNum).join("\n");
  }

  if (wrappedLines[0] === "") {
    wrappedLines.splice(0, 1);
  }
  return wrappedLines.join("\n");
} as SubtypeMethods["wrapText"];

const ensureMathJaxLoaded = async function (callback) {
  await loadMathJax();
  if (callback) {
    callback();
  }
} as SubtypeMethods["ensureLoaded"];

const enableActionChangeMathProps = (
  elements: readonly ExcalidrawElement[],
  appState: AppState,
) => {
  const eligibleElements = getSelectedMathElements(elements, appState);

  let enabled = false;
  eligibleElements.forEach((element) => {
    if (
      isMathElement(element) ||
      (hasBoundTextElement(element) &&
        isMathElement(getBoundTextElement(element)))
    ) {
      enabled = true;
    }
  });

  if (
    appState.activeTool.type === "text" &&
    appState.activeSubtypes &&
    appState.activeSubtypes.includes(mathSubtype)
  ) {
    enabled = true;
  }
  return enabled;
};

const createMathActions = () => {
  const mathActions: Action[] = [];
  const actionUseTexTrue: Action = {
    name: "useTexTrue",
    perform: (elements, appState) => {
      const mathOnly = getMathProps.getMathOnly(appState);
      const customData = appState.customData ?? {};
      customData[`${mathSubtype}`] = { useTex: true, mathOnly };
      return {
        elements,
        appState: { ...appState, customData },
        commitToHistory: true,
      };
    },
    contextItemLabel: (elements, appState) =>
      getMathProps.getUseTex(appState)
        ? "labels.useTexTrueActive"
        : "labels.useTexTrueInactive",
    predicate: (...rest) => rest.length < 5 || rest[4]?.subtype === mathSubtype,
    trackEvent: false,
  };
  const actionUseTexFalse: Action = {
    name: "useTexFalse",
    perform: (elements, appState) => {
      const mathOnly = getMathProps.getMathOnly(appState);
      const customData = appState.customData ?? {};
      customData[`${mathSubtype}`] = { useTex: false, mathOnly };
      return {
        elements,
        appState: { ...appState, customData },
        commitToHistory: true,
      };
    },
    contextItemLabel: (elements, appState) =>
      !getMathProps.getUseTex(appState)
        ? "labels.useTexFalseActive"
        : "labels.useTexFalseInactive",
    predicate: (...rest) => rest.length < 5 || rest[4]?.subtype === mathSubtype,
    trackEvent: false,
  };
  const actionResetUseTex: Action = {
    name: "resetUseTex",
    perform: (elements, appState) => {
      const useTex = getMathProps.getUseTex(appState);
      const modElements = changeProperty(
        elements,
        appState,
        (oldElement) => {
          if (
            isMathElement(oldElement) &&
            (oldElement.customData === undefined ||
              oldElement.customData.useTex !== useTex)
          ) {
            const newElement: ExcalidrawTextElement = newElementWith(
              oldElement,
              {
                customData: getMathProps.ensureMathProps({
                  useTex: useTex as boolean,
                  mathOnly: oldElement.customData?.mathOnly,
                }),
              },
            );
            redrawTextBoundingBox(newElement, getContainerElement(oldElement));
            return newElement;
          }

          return oldElement;
        },
        true,
      );

      return {
        elements: modElements,
        commitToHistory: true,
      };
    },
    keyTest: (event) => event.shiftKey && event.code === "KeyR",
    contextItemLabel: "labels.resetUseTex",
    predicate: (elements, appState) => {
      const useTex = getMathProps.getUseTex(appState);
      const mathElements = getSelectedMathElements(elements, appState);
      return mathElements.some((el) => {
        const e = isMathElement(el) ? el : getBoundTextElement(el)!;
        return e.customData === undefined || e.customData.useTex !== useTex;
      });
    },
    trackEvent: false,
  };
  const actionChangeMathOnly: Action = {
    name: "changeMathOnly",
    perform: (elements, appState, mathOnly: boolean | null) => {
      if (mathOnly === null) {
        mathOnly = getFormValue(elements, appState, (element) => {
          const el = hasBoundTextElement(element)
            ? getBoundTextElement(element)
            : element;
          return isMathElement(el) && el.customData?.mathOnly;
        });
        if (mathOnly === null) {
          mathOnly = getMathProps.getMathOnly(appState);
        }
      }
      const modElements = changeProperty(
        elements,
        appState,
        (oldElement) => {
          if (isMathElement(oldElement)) {
            const customData = getMathProps.ensureMathProps({
              useTex: oldElement.customData?.useTex,
              mathOnly: mathOnly as boolean,
            });
            const newElement: ExcalidrawTextElement = newElementWith(
              oldElement,
              { customData },
            );
            redrawTextBoundingBox(newElement, getContainerElement(oldElement));
            return newElement;
          }

          return oldElement;
        },
        true,
      );

      const useTex = getMathProps.getUseTex(appState);
      const customData = appState.customData ?? {};
      customData[`${mathSubtype}`] = { useTex, mathOnly };
      return {
        elements: modElements,
        appState: { ...appState, customData },
        commitToHistory: true,
      };
    },
    PanelComponent: ({ elements, appState, updateData }) => (
      <fieldset>
        <legend>{t("labels.changeMathOnly")}</legend>
        <ButtonSelect
          group="mathOnly"
          options={[
            {
              value: false,
              text: t("labels.mathOnlyFalse"),
            },
            {
              value: true,
              text: t("labels.mathOnlyTrue"),
            },
          ]}
          value={getFormValue(
            elements,
            appState,
            (element) => {
              const el = hasBoundTextElement(element)
                ? getBoundTextElement(element)
                : element;
              return isMathElement(el)
                ? getMathProps.ensureMathProps(el.customData).mathOnly
                : null;
            },
            getMathProps.getMathOnly(appState),
          )}
          onChange={(value) => updateData(value)}
        />
      </fieldset>
    ),
    predicate: (...rest) =>
      rest[4] === undefined && enableActionChangeMathProps(rest[0], rest[1]),
    trackEvent: false,
  };
  const actionMath = SubtypeButton(mathSubtype, "text", mathSubtypeIcon, "M");
  mathActions.push(actionUseTexTrue);
  mathActions.push(actionUseTexFalse);
  mathActions.push(actionResetUseTex);
  mathActions.push(actionChangeMathOnly);
  mathActions.push(actionMath);
  return mathActions;
};

export const prepareMathSubtype = function (
  addSubtypeAction,
  addLangData,
  onSubtypeLoaded,
) {
  // Set the callback first just in case anything in this method
  // calls loadMathJax().
  mathJaxLoadedCallback = onSubtypeLoaded;

  const methods = {} as SubtypeMethods;
  methods.clean = cleanMathElementUpdate;
  methods.ensureLoaded = ensureMathJaxLoaded;
  methods.measureText = measureMathElement;
  methods.render = renderMathElement;
  methods.renderSvg = renderSvgMathElement;
  methods.wrapText = wrapMathElement;
  const getLangData = async (langCode: string): Promise<Object | undefined> => {
    try {
      const condData = await import(
        /* webpackChunkName: "locales/[request]" */ `./locales/${langCode}.json`
      );
      if (condData) {
        return condData;
      }
    } catch (e) {}
    return undefined;
  };
  addLangData(fallbackMathJaxLangData, getLangData);
  registerAuxLangData(fallbackMathJaxLangData, getLangData);

  const actions = createMathActions();
  actions.forEach((action) => addSubtypeAction(action));
  // Call loadMathJax() here if we want to be sure it's loaded.

  return { actions, methods };
} as SubtypePrepFn;
