anki/ts/editable/mathjax.ts
Damien Elmes ae18ba2a05 Switch editor to full MathJax package to fix broken autoloads
Re: https://forums.ankiweb.net/t/anki-2-1-53-release-candidate/20122/2

Autoloads in MathJax are asynchronous, and the caller is expected to
use asynchronous APIs when they are in use [1]. The editor uses the synchronous
tex2svg(), which throws a "MathJax retry" error when an autoload package
has not yet loaded. Attempting to use the package before it has loaded
appears to break future invocations as well, so the package fails to work
at all until a new webview is created.

The following HTML will reproduce the issue when added to a single card
in a new profile:

```
<strong>6&nbsp; &nbsp;&nbsp;</strong>Every combination of <anki-mathjax>\boldsymbol{v}=(1,-2,1)</anki-mathjax> and <anki-mathjax>\boldsymbol{w}=(0,1,-1)</anki-mathjax> has components that add to&nbsp; _____.<br>
```

Ideally we'd switch the MathJax rendering to be asynchronous, but that
didn't work well when I tried it in #1862. For now I've just switched
to the full package, which adds about 130KB to the final minified JS
(2.76MB), and likely slows down editor loading somewhat.

[1] https://github.com/mathjax/MathJax/issues/2557#issuecomment-727655089
2022-05-21 14:01:38 +10:00

88 lines
2.5 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/* eslint
@typescript-eslint/no-explicit-any: "off",
*/
import "mathjax/es5/tex-svg-full";
import { mathIcon } from "./icons";
const parser = new DOMParser();
function getCSS(nightMode: boolean, fontSize: number): string {
const color = nightMode ? "white" : "black";
/* color is set for Maths, fill for the empty icon */
return `svg { color: ${color}; fill: ${color}; font-size: ${fontSize}px; };`;
}
function getStyle(css: string): HTMLStyleElement {
const style = document.createElement("style") as HTMLStyleElement;
style.appendChild(document.createTextNode(css));
return style;
}
function getEmptyIcon(style: HTMLStyleElement): [string, string] {
const icon = parser.parseFromString(mathIcon, "image/svg+xml");
const svg = icon.children[0];
svg.insertBefore(style, svg.children[0]);
return [svg.outerHTML, "MathJax"];
}
export function convertMathjax(
input: string,
nightMode: boolean,
fontSize: number,
): [string, string] {
input = revealClozeAnswers(input);
const style = getStyle(getCSS(nightMode, fontSize));
if (input.trim().length === 0) {
return getEmptyIcon(style);
}
let output: Element;
try {
output = globalThis.MathJax.tex2svg(input);
} catch (e) {
return ["Mathjax Error", String(e)];
}
const svg = output.children[0] as SVGElement;
if ((svg as any).viewBox.baseVal.height === 16) {
return getEmptyIcon(style);
}
let title = "";
if (svg.innerHTML.includes("data-mjx-error")) {
svg.querySelector("rect")?.setAttribute("fill", "yellow");
svg.querySelector("text")?.setAttribute("color", "red");
title = svg.querySelector("title")?.innerHTML ?? "";
} else {
svg.insertBefore(style, svg.children[0]);
}
return [svg.outerHTML, title];
}
/**
* Escape characters which are technically legal in Mathjax, but confuse HTML.
*/
export function escapeSomeEntities(value: string): string {
return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
export function unescapeSomeEntities(value: string): string {
return value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
}
function revealClozeAnswers(input: string): string {
// one-line version of regex in cloze.rs
const regex = /\{\{c(\d+)::(.*?)(?:::(.*?))?\}\}/gis;
return input.replace(regex, "[$2]");
}