anki/ts/editable/mathjax-element.ts
Damien Elmes 75723d7c9c
Add option in math dropdown to toggle MathJax rendering (#2014)
* Add option in math dropdown to toggle MathJax rendering

Closes #1942

* Hackily redraw the page when toggling MathJax

* Add Fluent string
2022-08-18 12:06:06 +10:00

199 lines
5.8 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { placeCaretAfter, placeCaretBefore } from "../domlib/place-caret";
import { on } from "../lib/events";
import type { DecoratedElement, DecoratedElementConstructor } from "./decorated";
import { FrameElement, frameElement } from "./frame-element";
import Mathjax_svelte from "./Mathjax.svelte";
const mathjaxTagPattern =
/<anki-mathjax(?:[^>]*?block="(.*?)")?[^>]*?>(.*?)<\/anki-mathjax>/gsu;
const mathjaxBlockDelimiterPattern = /\\\[(.*?)\\\]/gsu;
const mathjaxInlineDelimiterPattern = /\\\((.*?)\\\)/gsu;
function trimBreaks(text: string): string {
return text
.replace(/<br[ ]*\/?>/gsu, "\n")
.replace(/^\n*/, "")
.replace(/\n*$/, "");
}
export const mathjaxConfig = {
enabled: true,
};
export const Mathjax: DecoratedElementConstructor = class Mathjax
extends HTMLElement
implements DecoratedElement
{
static tagName = "anki-mathjax";
static toStored(undecorated: string): string {
const stored = undecorated.replace(
mathjaxTagPattern,
(_match: string, block: string | undefined, text: string) => {
const trimmed = trimBreaks(text);
return typeof block === "string" && block !== "false"
? `\\[${trimmed}\\]`
: `\\(${trimmed}\\)`;
},
);
return stored;
}
static toUndecorated(stored: string): string {
if (!mathjaxConfig.enabled) {
return stored;
}
return stored
.replace(mathjaxBlockDelimiterPattern, (_match: string, text: string) => {
const trimmed = trimBreaks(text);
return `<${Mathjax.tagName} block="true">${trimmed}</${Mathjax.tagName}>`;
})
.replace(mathjaxInlineDelimiterPattern, (_match: string, text: string) => {
const trimmed = trimBreaks(text);
return `<${Mathjax.tagName}>${trimmed}</${Mathjax.tagName}>`;
});
}
block = false;
frame?: FrameElement;
component?: Mathjax_svelte;
static get observedAttributes(): string[] {
return ["block", "data-mathjax"];
}
connectedCallback(): void {
this.decorate();
this.addEventListeners();
}
disconnectedCallback(): void {
this.removeEventListeners();
}
attributeChangedCallback(name: string, old: string, newValue: string): void {
if (newValue === old) {
return;
}
switch (name) {
case "block":
this.block = newValue !== "false";
this.component?.$set({ block: this.block });
this.frame?.setAttribute("block", String(this.block));
break;
case "data-mathjax":
if (!newValue) {
return;
}
this.component?.$set({ mathjax: newValue });
break;
}
}
decorate(): void {
if (this.hasAttribute("decorated")) {
this.undecorate();
}
if (this.parentElement?.tagName === FrameElement.tagName.toUpperCase()) {
this.frame = this.parentElement as FrameElement;
} else {
frameElement(this, this.block);
/* Framing will place this element inside of an anki-frame element,
* causing the connectedCallback to be called again.
* If we'd continue decorating at this point, we'd loose all the information */
return;
}
this.dataset.mathjax = this.innerHTML;
this.innerHTML = "";
this.style.whiteSpace = "normal";
this.component = new Mathjax_svelte({
target: this,
props: {
mathjax: this.dataset.mathjax,
block: this.block,
fontSize: 20,
},
});
if (this.hasAttribute("focusonmount")) {
let position: [number, number] | undefined = undefined;
if (this.getAttribute("focusonmount")!.length > 0) {
position = this.getAttribute("focusonmount")!
.split(",")
.map(Number) as [number, number];
}
this.component.moveCaretAfter(position);
}
this.setAttribute("contentEditable", "false");
this.setAttribute("decorated", "true");
}
undecorate(): void {
if (this.parentElement?.tagName === FrameElement.tagName.toUpperCase()) {
this.parentElement.replaceWith(this);
}
this.innerHTML = this.dataset.mathjax ?? "";
delete this.dataset.mathjax;
this.removeAttribute("style");
this.removeAttribute("focusonmount");
if (this.block) {
this.setAttribute("block", "true");
} else {
this.removeAttribute("block");
}
this.removeAttribute("contentEditable");
this.removeAttribute("decorated");
}
removeMoveInStart?: () => void;
removeMoveInEnd?: () => void;
addEventListeners(): void {
this.removeMoveInStart = on(
this,
"moveinstart" as keyof HTMLElementEventMap,
() => this.component!.selectAll(),
);
this.removeMoveInEnd = on(this, "moveinend" as keyof HTMLElementEventMap, () =>
this.component!.selectAll(),
);
}
removeEventListeners(): void {
this.removeMoveInStart?.();
this.removeMoveInStart = undefined;
this.removeMoveInEnd?.();
this.removeMoveInEnd = undefined;
}
placeCaretBefore(): void {
if (this.frame) {
placeCaretBefore(this.frame);
}
}
placeCaretAfter(): void {
if (this.frame) {
placeCaretAfter(this.frame);
}
}
};