Formalize "Decorated components API"

This commit is contained in:
Henrik Giesel 2021-08-05 20:54:25 +02:00
parent 9fb0ce973b
commit 6a1fae53df
4 changed files with 107 additions and 53 deletions

45
ts/editable/decorated.ts Normal file
View file

@ -0,0 +1,45 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/**
* decorated elements know three states:
* - stored, which is stored in the DB, e.g. `\(\alpha + \beta\)`
* - undecorated, which is displayed to the user in Codable, e.g. `<anki-mathjax>\alpha + \beta</anki-mathjax>`
* - decorated, which is displayed to the user in Editable, e.g. `<anki-mathjax data-mathjax="\alpha + \beta"><img src="data:..."></anki-mathjax>`
*/
export interface DecoratedElement extends HTMLElement {
/**
* Transforms itself from undecorated to decorated state.
* Should be called in connectedCallback.
*/
decorate(): void;
/**
* Transforms itself from decorated to undecorated state.
*/
undecorate(): void;
}
export interface DecoratedElementConstructor extends CustomElementConstructor {
prototype: DecoratedElement;
tagName: string;
/**
* Transforms elements in input HTML from undecorated to stored state.
*/
toStored(undecorated: string): string;
/**
* Transforms elements in input HTML from stored to undecorated state.
*/
toUndecorated(stored: string): string;
}
class DefineArray extends Array {
push(...elements: DecoratedElementConstructor[]) {
for (const element of elements) {
customElements.define(element.tagName, element);
}
return super.push(...elements);
}
}
export const decoratedComponents: DecoratedElementConstructor[] = new DefineArray();

View file

@ -1,6 +1,8 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { DecoratedElement } from "./decorated";
import { decoratedComponents } from "./decorated";
import { bridgeCommand } from "lib/bridgecommand";
import { elementIsBlock, getBlockElement } from "lib/dom";
// import { inCodable } from "./toolbar";
@ -25,13 +27,6 @@ function containsInlineContent(element: Element): boolean {
return true;
}
interface DecoratedElement extends HTMLElement {
decorate(): void;
undecorate(): void;
}
const decoratedTags = ["anki-mathjax"];
export class Editable extends HTMLElement {
set fieldHTML(content: string) {
this.innerHTML = content;
@ -44,8 +39,8 @@ export class Editable extends HTMLElement {
get fieldHTML(): string {
const clone = this.cloneNode(true) as Element;
for (const component of decoratedTags) {
for (const element of clone.getElementsByTagName(component)) {
for (const component of decoratedComponents) {
for (const element of clone.getElementsByTagName(component.tagName)) {
(element as DecoratedElement).undecorate();
}
}

View file

@ -1,4 +1,7 @@
import "mathjax/es5/tex-svg-full";
import type { DecoratedElement, DecoratedElementConstructor } from "./decorated";
import { decoratedComponents } from "./decorated";
import { nodeIsElement } from "lib/dom";
import Mathjax_svelte from "./Mathjax.svelte";
@ -37,9 +40,44 @@ function moveNodesInsertedBeforeEndToAfterEnd(element: Element): () => void {
return () => observer.disconnect();
}
class Mathjax extends HTMLElement {
block: boolean = false;
disconnect: () => void = () => {};
const mathjaxTagPattern =
/<anki-mathjax(?:[^>]*?block="(.*?)")?[^>]*?>(.*?)<\/anki-mathjax>/gsu;
const mathjaxBlockDelimiterPattern = /\\\[(.*?)\\\]/gsu;
const mathjaxInlineDelimiterPattern = /\\\((.*?)\\\)/gsu;
export const Mathjax: DecoratedElementConstructor = class Mathjax
extends HTMLElement
implements DecoratedElement
{
static tagName = "anki-mathjax";
static toStored(undecorated: string): string {
return undecorated.replace(
mathjaxTagPattern,
(_match: string, block: string | undefined, text: string) => {
return typeof block === "string" && block !== "false"
? `\\[${text}\\]`
: `\\(${text}\\)`;
}
);
}
static toUndecorated(stored: string): string {
return stored
.replace(
mathjaxBlockDelimiterPattern,
(_match: string, text: string) =>
`<anki-mathjax block="true">${text}</anki-mathjax>`
)
.replace(
mathjaxInlineDelimiterPattern,
(_match: string, text: string) => `<anki-mathjax>${text}</anki-mathjax>`
);
}
block = false;
disconnect: () => void = () => {/* noop */};
constructor() {
super();
@ -82,40 +120,6 @@ class Mathjax extends HTMLElement {
this.removeAttribute("block");
}
}
}
};
customElements.define("anki-mathjax", Mathjax);
const mathjaxTagPattern =
/<anki-mathjax(?:[^>]*?block="(.*?)")?[^>]*?>(.*?)<\/anki-mathjax>/gsu;
export function toMathjaxDelimiters(html: string): string {
return html.replace(
mathjaxTagPattern,
(_match: string, block: string | undefined, text: string) => {
console.log("delim", _match, block, "text", text);
return typeof block === "string" && block !== "false"
? `\\[${text}\\]`
: `\\(${text}\\)`;
}
);
}
const mathjaxBlockDelimiterPattern = /\\\[(.*?)\\\]/gsu;
const mathjaxInlineDelimiterPattern = /\\\((.*?)\\\)/gsu;
export function toMathjaxTags(html: string): string {
return html
.replace(
mathjaxBlockDelimiterPattern,
(_match: string, text: string) => (
console.log(text), `<anki-mathjax block="true">${text}</anki-mathjax>`
)
)
.replace(
mathjaxInlineDelimiterPattern,
(_match: string, text: string) => (
console.log(text), `<anki-mathjax>${text}</anki-mathjax>`
)
);
}
decoratedComponents.push(Mathjax);

View file

@ -17,7 +17,7 @@ import { bridgeCommand } from "./lib";
import { onInput, onKey, onKeyUp } from "./input-handlers";
import { onFocus, onBlur } from "./focus-handlers";
import { nightModeKey } from "components/context-keys";
import { toMathjaxTags, toMathjaxDelimiters } from "editable/mathjax-component";
import { decoratedComponents } from "editable/decorated";
function onCutOrCopy(): void {
bridgeCommand("cutOrCopy");
@ -93,13 +93,23 @@ export class EditingArea extends HTMLDivElement {
}
set fieldHTML(content: string) {
this.imageHandle.then(
() => (this.activeInput.fieldHTML = toMathjaxTags(content))
);
this.imageHandle.then(() => {
let result = content;
for (const component of decoratedComponents) {
result = component.toUndecorated(result);
}
this.activeInput.fieldHTML = result;
});
}
get fieldHTML(): string {
return toMathjaxDelimiters(this.activeInput.fieldHTML);
let result = this.activeInput.fieldHTML;
for (const component of decoratedComponents) {
result = component.toStored(result);
}
return result;
}
connectedCallback(): void {