diff --git a/qt/aqt/data/web/css/BUILD.bazel b/qt/aqt/data/web/css/BUILD.bazel
index 6ac2deb4c..724099ac2 100644
--- a/qt/aqt/data/web/css/BUILD.bazel
+++ b/qt/aqt/data/web/css/BUILD.bazel
@@ -21,11 +21,19 @@ copy_files_into_group(
name = "editor",
srcs = [
"editor.css",
- "editable.css",
],
package = "//ts/editor",
)
+
+copy_files_into_group(
+ name = "editable",
+ srcs = [
+ "editable-build.css",
+ ],
+ package = "//ts/editable",
+)
+
copy_files_into_group(
name = "reviewer",
srcs = [
@@ -39,6 +47,7 @@ filegroup(
srcs = [
"css_local",
"editor",
+ "editable",
"reviewer",
],
visibility = ["//qt:__subpackages__"],
diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py
index 6a285ebd4..90c666b40 100644
--- a/qt/aqt/editor.py
+++ b/qt/aqt/editor.py
@@ -80,10 +80,10 @@ audio = (
_html = """
-
+
-
+
"""
diff --git a/ts/change-notetype/NotetypeSelector.svelte b/ts/change-notetype/NotetypeSelector.svelte
index e6de9aab6..22d7733f1 100644
--- a/ts/change-notetype/NotetypeSelector.svelte
+++ b/ts/change-notetype/NotetypeSelector.svelte
@@ -5,7 +5,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-
+
-
@@ -48,4 +48,4 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-
+
diff --git a/ts/components/ButtonDropdown.svelte b/ts/components/ButtonDropdown.svelte
index 4beaaccc0..638e1d210 100644
--- a/ts/components/ButtonDropdown.svelte
+++ b/ts/components/ButtonDropdown.svelte
@@ -19,14 +19,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
diff --git a/ts/components/StickyBottom.svelte b/ts/components/StickyFooter.svelte
similarity index 78%
rename from ts/components/StickyBottom.svelte
rename to ts/components/StickyFooter.svelte
index e62654bb0..e9778096b 100644
--- a/ts/components/StickyBottom.svelte
+++ b/ts/components/StickyFooter.svelte
@@ -7,16 +7,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let className: string = "";
export { className as class };
- export let height: number;
+ export let height: number = 0;
-
+
diff --git a/ts/editable/decorated.ts b/ts/editable/decorated.ts
new file mode 100644
index 000000000..c86601255
--- /dev/null
+++ b/ts/editable/decorated.ts
@@ -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. `\alpha + \beta `
+ * - decorated, which is displayed to the user in Editable, e.g. ` `
+ */
+
+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();
diff --git a/ts/editor/editable.scss b/ts/editable/editable-base.scss
similarity index 94%
rename from ts/editor/editable.scss
rename to ts/editable/editable-base.scss
index 360c9d83c..b324d6906 100644
--- a/ts/editor/editable.scss
+++ b/ts/editable/editable-base.scss
@@ -15,6 +15,10 @@ anki-editable {
}
}
+anki-mathjax {
+ white-space: pre;
+}
+
p {
margin-top: 0;
margin-bottom: 1rem;
diff --git a/ts/editable/editable-container.ts b/ts/editable/editable-container.ts
new file mode 100644
index 000000000..645006b7a
--- /dev/null
+++ b/ts/editable/editable-container.ts
@@ -0,0 +1,96 @@
+// Copyright: Ankitects Pty Ltd and contributors
+// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+/* eslint
+@typescript-eslint/no-non-null-assertion: "off",
+ */
+
+function loadStyleLink(container: Node, href: string): Promise {
+ const rootStyle = document.createElement("link");
+ rootStyle.setAttribute("rel", "stylesheet");
+ rootStyle.setAttribute("href", href);
+
+ let styleResolve: () => void;
+ const stylePromise = new Promise((resolve) => (styleResolve = resolve));
+
+ rootStyle.addEventListener("load", () => styleResolve());
+ container.appendChild(rootStyle);
+
+ return stylePromise;
+}
+
+function loadStyleTag(container: Node): [HTMLStyleElement, Promise] {
+ const style = document.createElement("style");
+ style.setAttribute("rel", "stylesheet");
+
+ let styleResolve: () => void;
+ const stylePromise = new Promise((resolve) => (styleResolve = resolve));
+
+ style.addEventListener("load", () => styleResolve());
+ container.appendChild(style);
+
+ return [style, stylePromise];
+}
+
+export class EditableContainer extends HTMLDivElement {
+ baseStyle: HTMLStyleElement;
+ imageStyle: HTMLStyleElement;
+
+ imagePromise: Promise;
+ stylePromise: Promise;
+
+ baseRule?: CSSStyleRule;
+
+ constructor() {
+ super();
+ const shadow = this.attachShadow({ mode: "open" });
+
+ if (document.documentElement.classList.contains("night-mode")) {
+ this.classList.add("night-mode");
+ }
+
+ const rootPromise = loadStyleLink(shadow, "./_anki/css/editable-build.css");
+ const [baseStyle, basePromise] = loadStyleTag(shadow);
+ const [imageStyle, imagePromise] = loadStyleTag(shadow);
+
+ this.baseStyle = baseStyle;
+ this.imageStyle = imageStyle;
+
+ this.imagePromise = imagePromise;
+ this.stylePromise = Promise.all([
+ rootPromise,
+ basePromise,
+ imagePromise,
+ ]) as unknown as Promise;
+ }
+
+ connectedCallback(): void {
+ const sheet = this.baseStyle.sheet as CSSStyleSheet;
+ const baseIndex = sheet.insertRule("anki-editable {}");
+ this.baseRule = sheet.cssRules[baseIndex] as CSSStyleRule;
+ }
+
+ initialize(color: string): void {
+ this.setBaseColor(color);
+ }
+
+ setBaseColor(color: string): void {
+ if (this.baseRule) {
+ this.baseRule.style.color = color;
+ }
+ }
+
+ setBaseStyling(fontFamily: string, fontSize: string, direction: string): void {
+ if (this.baseRule) {
+ this.baseRule.style.fontFamily = fontFamily;
+ this.baseRule.style.fontSize = fontSize;
+ this.baseRule.style.direction = direction;
+ }
+ }
+
+ isRightToLeft(): boolean {
+ return this.baseRule!.style.direction === "rtl";
+ }
+}
+
+customElements.define("anki-editable-container", EditableContainer, { extends: "div" });
diff --git a/ts/editor/editable.ts b/ts/editable/editable.ts
similarity index 50%
rename from ts/editor/editable.ts
rename to ts/editable/editable.ts
index 38ee75646..101cd20c5 100644
--- a/ts/editor/editable.ts
+++ b/ts/editable/editable.ts
@@ -1,10 +1,24 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-import { bridgeCommand } from "./lib";
-import { elementIsBlock, caretToEnd, getBlockElement } from "./helpers";
-import { inCodable } from "./toolbar";
-import { wrap } from "./wrap";
+/* eslint
+@typescript-eslint/no-non-null-assertion: "off",
+ */
+
+import type { DecoratedElement } from "./decorated";
+import { decoratedComponents } from "./decorated";
+import { bridgeCommand } from "lib/bridgecommand";
+import { elementIsBlock, getBlockElement } from "lib/dom";
+import { wrapInternal } from "lib/wrap";
+
+export function caretToEnd(node: Node): void {
+ const range = document.createRange();
+ range.selectNodeContents(node);
+ range.collapse(false);
+ const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!;
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
function containsInlineContent(element: Element): boolean {
for (const child of element.children) {
@@ -26,26 +40,32 @@ export class Editable extends HTMLElement {
}
get fieldHTML(): string {
- return containsInlineContent(this) && this.innerHTML.endsWith(" ")
- ? this.innerHTML.slice(0, -4) // trim trailing
- : this.innerHTML;
+ const clone = this.cloneNode(true) as Element;
+
+ for (const component of decoratedComponents) {
+ for (const element of clone.getElementsByTagName(component.tagName)) {
+ (element as DecoratedElement).undecorate();
+ }
+ }
+
+ const result =
+ containsInlineContent(clone) && clone.innerHTML.endsWith(" ")
+ ? clone.innerHTML.slice(0, -4) // trim trailing
+ : clone.innerHTML;
+
+ return result;
}
connectedCallback(): void {
this.setAttribute("contenteditable", "");
}
- focus(): void {
- super.focus();
- inCodable.set(false);
- }
-
caretToEnd(): void {
caretToEnd(this);
}
surroundSelection(before: string, after: string): void {
- wrap(before, after);
+ wrapInternal(this.getRootNode() as ShadowRoot, before, after, false);
}
onEnter(event: KeyboardEvent): void {
@@ -63,3 +83,5 @@ export class Editable extends HTMLElement {
event.preventDefault();
}
}
+
+customElements.define("anki-editable", Editable);
diff --git a/ts/editable/icons.ts b/ts/editable/icons.ts
new file mode 100644
index 000000000..a85ecc4b3
--- /dev/null
+++ b/ts/editable/icons.ts
@@ -0,0 +1,4 @@
+// Copyright: Ankitects Pty Ltd and contributors
+// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+export { default as mathIcon } from "./math-integral-box.svg";
diff --git a/ts/editable/index.ts b/ts/editable/index.ts
new file mode 100644
index 000000000..25bf9fa50
--- /dev/null
+++ b/ts/editable/index.ts
@@ -0,0 +1,7 @@
+// Copyright: Ankitects Pty Ltd and contributors
+// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+import "./editable-base.css";
+import "./editable-container";
+import "./editable";
+import "./mathjax-component";
diff --git a/ts/editable/mathjax-component.ts b/ts/editable/mathjax-component.ts
new file mode 100644
index 000000000..e0086d660
--- /dev/null
+++ b/ts/editable/mathjax-component.ts
@@ -0,0 +1,196 @@
+// Copyright: Ankitects Pty Ltd and contributors
+// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+/* eslint
+@typescript-eslint/no-non-null-assertion: "off",
+@typescript-eslint/no-explicit-any: "off",
+ */
+
+import "mathjax/es5/tex-svg-full";
+
+import type { DecoratedElement, DecoratedElementConstructor } from "./decorated";
+import { decoratedComponents } from "./decorated";
+import { nodeIsElement } from "lib/dom";
+import { nightModeKey } from "components/context-keys";
+
+import Mathjax_svelte from "./Mathjax.svelte";
+
+function moveNodeOutOfElement(
+ element: Element,
+ node: Node,
+ placement: "beforebegin" | "afterend"
+): Node {
+ element.removeChild(node);
+
+ let referenceNode: Node;
+
+ if (nodeIsElement(node)) {
+ referenceNode = element.insertAdjacentElement(placement, node)!;
+ } else {
+ element.insertAdjacentText(placement, (node as Text).wholeText);
+ referenceNode =
+ placement === "beforebegin"
+ ? element.previousSibling!
+ : element.nextSibling!;
+ }
+
+ return referenceNode;
+}
+
+function placeCaretAfter(node: Node): void {
+ const range = document.createRange();
+ range.setStartAfter(node);
+ range.collapse(false);
+
+ const selection = document.getSelection()!;
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
+
+function moveNodesInsertedOutside(element: Element, allowedChild: Node): () => void {
+ const observer = new MutationObserver(() => {
+ if (element.childNodes.length === 1) {
+ return;
+ }
+
+ const childNodes = [...element.childNodes];
+ const allowedIndex = childNodes.findIndex((child) => child === allowedChild);
+
+ const beforeChildren = childNodes.slice(0, allowedIndex);
+ const afterChildren = childNodes.slice(allowedIndex + 1);
+
+ // Special treatment for pressing return after mathjax block
+ if (
+ afterChildren.length === 2 &&
+ afterChildren.every((child) => (child as Element).tagName === "BR")
+ ) {
+ const first = afterChildren.pop();
+ element.removeChild(first!);
+ }
+
+ let lastNode: Node | null = null;
+
+ for (const node of beforeChildren) {
+ lastNode = moveNodeOutOfElement(element, node, "beforebegin");
+ }
+
+ for (const node of afterChildren) {
+ lastNode = moveNodeOutOfElement(element, node, "afterend");
+ }
+
+ if (lastNode) {
+ placeCaretAfter(lastNode);
+ }
+ });
+
+ observer.observe(element, { childList: true, characterData: true });
+ return () => observer.disconnect();
+}
+
+const mathjaxTagPattern =
+ /]*?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) =>
+ `${text} `
+ )
+ .replace(
+ mathjaxInlineDelimiterPattern,
+ (_match: string, text: string) => `${text} `
+ );
+ }
+
+ block = false;
+ disconnect: () => void = () => {
+ /* noop */
+ };
+ component?: Mathjax_svelte;
+
+ static get observedAttributes(): string[] {
+ return ["block", "data-mathjax"];
+ }
+
+ connectedCallback(): void {
+ this.decorate();
+ this.disconnect = moveNodesInsertedOutside(this, this.children[0]);
+ }
+
+ disconnectedCallback(): void {
+ this.disconnect();
+ }
+
+ attributeChangedCallback(name: string, _old: string, newValue: string): void {
+ switch (name) {
+ case "block":
+ this.block = newValue !== "false";
+ this.component?.$set({ block: this.block });
+ break;
+ case "data-mathjax":
+ this.component?.$set({ mathjax: newValue });
+ break;
+ }
+ }
+
+ decorate(): void {
+ const mathjax = (this.dataset.mathjax = this.innerText);
+ this.innerHTML = "";
+ this.style.whiteSpace = "normal";
+
+ const context = new Map();
+ context.set(
+ nightModeKey,
+ document.documentElement.classList.contains("night-mode")
+ );
+
+ this.component = new Mathjax_svelte({
+ target: this,
+ props: {
+ mathjax,
+ block: this.block,
+ autofocus: this.hasAttribute("focusonmount"),
+ },
+ context,
+ } as any);
+ }
+
+ undecorate(): void {
+ this.innerHTML = this.dataset.mathjax ?? "";
+ delete this.dataset.mathjax;
+ this.removeAttribute("style");
+ this.removeAttribute("focusonmount");
+
+ this.component?.$destroy();
+ this.component = undefined;
+
+ if (this.block) {
+ this.setAttribute("block", "true");
+ } else {
+ this.removeAttribute("block");
+ }
+ }
+};
+
+decoratedComponents.push(Mathjax);
diff --git a/ts/editable/mathjax.ts b/ts/editable/mathjax.ts
new file mode 100644
index 000000000..b21c43301
--- /dev/null
+++ b/ts/editable/mathjax.ts
@@ -0,0 +1,57 @@
+// Copyright: Ankitects Pty Ltd and contributors
+// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+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] {
+ const style = getStyle(getCSS(nightMode, fontSize));
+
+ if (input.trim().length === 0) {
+ return getEmptyIcon(style);
+ }
+
+ const output = globalThis.MathJax.tex2svg(input);
+ const svg = output.children[0];
+
+ if (svg.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];
+}
diff --git a/ts/editor/BUILD.bazel b/ts/editor/BUILD.bazel
index 2d25e998c..1b49fdefe 100644
--- a/ts/editor/BUILD.bazel
+++ b/ts/editor/BUILD.bazel
@@ -22,18 +22,8 @@ compile_svelte(
visibility = ["//visibility:public"],
deps = [
"//ts/components",
- ],
-)
-
-compile_sass(
- srcs = [
- "editable.scss",
- ],
- group = "editable_scss",
- visibility = ["//visibility:public"],
- deps = [
- "//ts/sass:scrollbar_lib",
- "//ts/sass/codemirror",
+ "@npm//@types/codemirror",
+ "@npm//codemirror",
],
)
@@ -45,8 +35,9 @@ compile_sass(
visibility = ["//visibility:public"],
deps = [
"//ts/sass:base_lib",
- "//ts/sass:buttons_lib",
"//ts/sass:scrollbar_lib",
+ "//ts/sass:buttons_lib",
+ "//ts/sass:button_mixins_lib",
],
)
@@ -71,6 +62,7 @@ ts_library(
"//ts/lib",
"//ts/sveltelib",
"//ts/components",
+ "//ts/editable",
"//ts/html-filter",
"//ts:image_module_support",
"@npm//svelte",
@@ -130,6 +122,10 @@ copy_mdi_icons(
"image-size-select-large.svg",
"image-size-select-actual.svg",
+ # mathjax handle
+ "format-wrap-square.svg",
+ "format-wrap-top-bottom.svg",
+
# tag editor
"tag-outline.svg",
"tag.svg",
@@ -156,8 +152,11 @@ esbuild(
"bootstrap-icons",
"mdi-icons",
"svelte_components",
+ "//ts/editable",
+ "//ts/editable:mdi-icons",
"//ts/components",
"//ts/components:svelte_components",
+ "//ts/editable:svelte_components",
"@npm//protobufjs",
],
)
@@ -192,5 +191,7 @@ svelte_check(
"//ts/sass/bootstrap",
"@npm//@types/bootstrap",
"//ts/components",
+ "@npm//@types/codemirror",
+ "@npm//codemirror",
],
)
diff --git a/ts/editor/EditorToolbar.svelte b/ts/editor/EditorToolbar.svelte
index 5c3728fd8..8c11dd230 100644
--- a/ts/editor/EditorToolbar.svelte
+++ b/ts/editor/EditorToolbar.svelte
@@ -27,7 +27,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-
+
-
@@ -73,4 +73,4 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-
+
diff --git a/ts/editor/FormatBlockButtons.svelte b/ts/editor/FormatBlockButtons.svelte
index d2fc2a140..b7cf587c8 100644
--- a/ts/editor/FormatBlockButtons.svelte
+++ b/ts/editor/FormatBlockButtons.svelte
@@ -14,7 +14,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import OnlyEditable from "./OnlyEditable.svelte";
import CommandIconButton from "./CommandIconButton.svelte";
- import { getCurrentField, getListItem } from "./helpers";
+ import { getListItem } from "lib/dom";
+ import { getCurrentField } from "./helpers";
import {
ulIcon,
olIcon,
@@ -31,7 +32,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
function outdentListItem() {
const currentField = getCurrentField();
- if (getListItem(currentField.editableContainer.shadowRoot!)) {
+ if (getListItem(currentField!.editableContainer.shadowRoot!)) {
document.execCommand("outdent");
} else {
alert("Indent/unindent currently only works with lists.");
@@ -40,7 +41,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
function indentListItem() {
const currentField = getCurrentField();
- if (getListItem(currentField.editableContainer.shadowRoot!)) {
+ if (getListItem(currentField!.editableContainer.shadowRoot!)) {
document.execCommand("indent");
} else {
alert("Indent/unindent currently only works with lists.");
diff --git a/ts/editor/HandleBackground.svelte b/ts/editor/HandleBackground.svelte
index fc6fc4eaf..ad06d50a6 100644
--- a/ts/editor/HandleBackground.svelte
+++ b/ts/editor/HandleBackground.svelte
@@ -5,13 +5,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-
+
diff --git a/ts/editor/MathjaxHandleEditor.svelte b/ts/editor/MathjaxHandleEditor.svelte
new file mode 100644
index 000000000..6dccbfe71
--- /dev/null
+++ b/ts/editor/MathjaxHandleEditor.svelte
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
diff --git a/ts/editor/MathjaxHandleInlineBlock.svelte b/ts/editor/MathjaxHandleInlineBlock.svelte
new file mode 100644
index 000000000..c488c95f9
--- /dev/null
+++ b/ts/editor/MathjaxHandleInlineBlock.svelte
@@ -0,0 +1,38 @@
+
+
+
+
+
+ mathjaxElement.setAttribute("block", "false")}
+ on:click>{@html inlineIcon}
+
+
+
+ mathjaxElement.setAttribute("block", "true")}
+ on:click>{@html blockIcon}
+
+
diff --git a/ts/editor/TagEditor.svelte b/ts/editor/TagEditor.svelte
index 38e478adc..022aafdce 100644
--- a/ts/editor/TagEditor.svelte
+++ b/ts/editor/TagEditor.svelte
@@ -6,8 +6,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { tick } from "svelte";
import { isApplePlatform } from "lib/platform";
import { bridgeCommand } from "lib/bridgecommand";
- import Spacer from "components/Spacer.svelte";
- import StickyBottom from "components/StickyBottom.svelte";
+ import StickyFooter from "components/StickyFooter.svelte";
import TagOptionsBadge from "./TagOptionsBadge.svelte";
import TagEditMode from "./TagEditMode.svelte";
import TagInput from "./TagInput.svelte";
@@ -65,7 +64,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
return response.tags;
}
- const colonAtStartOrEnd = /^:?|:?$/g;
+ const withoutSingleColonAtStartOrEnd = /^:?([^:].*?[^:]):?$/;
function updateSuggestions(): void {
const activeTag = tags[active!];
@@ -76,11 +75,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
if (autocompleteDisabled) {
suggestionsPromise = noSuggestions;
} else {
- const cleanedName = replaceWithColons(activeName).replace(
- colonAtStartOrEnd,
- ""
- );
- suggestionsPromise = fetchSuggestions(cleanedName).then(
+ const withColons = replaceWithColons(activeName);
+ const withoutSingleColons = withoutSingleColonAtStartOrEnd.test(withColons)
+ ? withColons.replace(withoutSingleColonAtStartOrEnd, "$1")
+ : withColons;
+
+ suggestionsPromise = fetchSuggestions(withoutSingleColons).then(
(names: string[]): string[] => {
autocompleteDisabled = names.length === 0;
return names.map(replaceWithUnicodeSeparator);
@@ -390,9 +390,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: shortenTags = shortenTags || assumedRows > 2;
-
-
-
+
{#if !wrap}
SPACER
-
+
";
function parseHTML(html: string): string {
- const doc = parser.parseFromString(html, "text/html");
+ const doc = parser.parseFromString(`${parseStyle}${html}`, "text/html");
return doc.body.innerHTML;
}
export class Codable extends HTMLTextAreaElement {
- codeMirror: CodeMirror | undefined;
+ codeMirror: CodeMirror.EditorFromTextArea | undefined;
get active(): boolean {
return Boolean(this.codeMirror);
@@ -41,14 +31,14 @@ export class Codable extends HTMLTextAreaElement {
set fieldHTML(content: string) {
if (this.active) {
- this.codeMirror.setValue(content);
+ this.codeMirror?.setValue(content);
} else {
this.value = content;
}
}
get fieldHTML(): string {
- return parseHTML(this.active ? this.codeMirror.getValue() : this.value);
+ return parseHTML(this.active ? this.codeMirror!.getValue() : this.value);
}
connectedCallback(): void {
@@ -58,26 +48,27 @@ export class Codable extends HTMLTextAreaElement {
setup(html: string): void {
this.fieldHTML = html;
this.codeMirror = CodeMirror.fromTextArea(this, codeMirrorOptions);
+ this.codeMirror.on("blur", () => inCodable.set(false));
}
teardown(): string {
- this.codeMirror.toTextArea();
+ this.codeMirror!.toTextArea();
this.codeMirror = undefined;
return this.fieldHTML;
}
focus(): void {
- this.codeMirror.focus();
+ this.codeMirror!.focus();
inCodable.set(true);
}
caretToEnd(): void {
- this.codeMirror.setCursor(this.codeMirror.lineCount(), 0);
+ this.codeMirror!.setCursor(this.codeMirror!.lineCount(), 0);
}
surroundSelection(before: string, after: string): void {
- const selection = this.codeMirror.getSelection();
- this.codeMirror.replaceSelection(before + selection + after);
+ const selection = this.codeMirror!.getSelection();
+ this.codeMirror!.replaceSelection(before + selection + after);
}
onEnter(): void {
@@ -88,3 +79,5 @@ export class Codable extends HTMLTextAreaElement {
/* default */
}
}
+
+customElements.define("anki-codable", Codable, { extends: "textarea" });
diff --git a/ts/editor/codeMirror.ts b/ts/editor/codeMirror.ts
new file mode 100644
index 000000000..dc7403415
--- /dev/null
+++ b/ts/editor/codeMirror.ts
@@ -0,0 +1,45 @@
+// Copyright: Ankitects Pty Ltd and contributors
+// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+import CodeMirror from "codemirror";
+import "codemirror/mode/htmlmixed/htmlmixed";
+import "codemirror/mode/stex/stex";
+import "codemirror/addon/fold/foldcode";
+import "codemirror/addon/fold/foldgutter";
+import "codemirror/addon/fold/xml-fold";
+import "codemirror/addon/edit/matchtags.js";
+import "codemirror/addon/edit/closetag.js";
+
+export { CodeMirror };
+
+export const latex = {
+ name: "stex",
+ inMathMode: true,
+};
+
+export const htmlanki = {
+ name: "htmlmixed",
+ tags: {
+ "anki-mathjax": [[null, null, latex]],
+ },
+};
+
+const noop = (): void => {
+ /* noop */
+};
+
+export const baseOptions = {
+ theme: "monokai",
+ lineWrapping: true,
+ matchTags: { bothTags: true },
+ autoCloseTags: true,
+ extraKeys: { Tab: noop, "Shift-Tab": noop },
+ viewportMargin: Infinity,
+ lineWiseCopyCut: false,
+};
+
+export const gutterOptions = {
+ gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
+ lineNumbers: true,
+ foldGutter: true,
+};
diff --git a/ts/editor/editable-container.ts b/ts/editor/editable-container.ts
deleted file mode 100644
index 3448e662e..000000000
--- a/ts/editor/editable-container.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright: Ankitects Pty Ltd and contributors
-// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-
-/* eslint
-@typescript-eslint/no-non-null-assertion: "off",
- */
-
-export class EditableContainer extends HTMLDivElement {
- baseStyle: HTMLStyleElement;
- baseRule?: CSSStyleRule;
- imageStyle?: HTMLStyleElement;
-
- constructor() {
- super();
- const shadow = this.attachShadow({ mode: "open" });
-
- if (document.documentElement.classList.contains("night-mode")) {
- this.classList.add("night-mode");
- }
-
- const rootStyle = document.createElement("link");
- rootStyle.setAttribute("rel", "stylesheet");
- rootStyle.setAttribute("href", "./_anki/css/editable.css");
- shadow.appendChild(rootStyle);
-
- this.baseStyle = document.createElement("style");
- this.baseStyle.setAttribute("rel", "stylesheet");
- this.baseStyle.id = "baseStyle";
- shadow.appendChild(this.baseStyle);
- }
-
- connectedCallback(): void {
- const sheet = this.baseStyle.sheet as CSSStyleSheet;
- const baseIndex = sheet.insertRule("anki-editable {}");
- this.baseRule = sheet.cssRules[baseIndex] as CSSStyleRule;
- }
-
- initialize(color: string): void {
- this.setBaseColor(color);
- }
-
- setBaseColor(color: string): void {
- if (this.baseRule) {
- this.baseRule.style.color = color;
- }
- }
-
- setBaseStyling(fontFamily: string, fontSize: string, direction: string): void {
- if (this.baseRule) {
- this.baseRule.style.fontFamily = fontFamily;
- this.baseRule.style.fontSize = fontSize;
- this.baseRule.style.direction = direction;
- }
- }
-
- isRightToLeft(): boolean {
- return this.baseRule!.style.direction === "rtl";
- }
-}
diff --git a/ts/editor/editing-area.ts b/ts/editor/editing-area.ts
index c1578d76f..127963c2c 100644
--- a/ts/editor/editing-area.ts
+++ b/ts/editor/editing-area.ts
@@ -7,16 +7,18 @@
*/
import ImageHandle from "./ImageHandle.svelte";
+import MathjaxHandle from "./MathjaxHandle.svelte";
-import type { EditableContainer } from "./editable-container";
-import type { Editable } from "./editable";
+import type { EditableContainer } from "editable/editable-container";
+import type { Editable } from "editable/editable";
import type { Codable } from "./codable";
import { updateActiveButtons } from "./toolbar";
import { bridgeCommand } from "./lib";
import { onInput, onKey, onKeyUp } from "./input-handlers";
-import { onFocus, onBlur } from "./focus-handlers";
+import { deferFocusDown, saveFieldIfFieldChanged } from "./focus-handlers";
import { nightModeKey } from "components/context-keys";
+import { decoratedComponents } from "editable/decorated";
function onCutOrCopy(): void {
bridgeCommand("cutOrCopy");
@@ -24,6 +26,7 @@ function onCutOrCopy(): void {
export class EditingArea extends HTMLDivElement {
imageHandle: Promise;
+ mathjaxHandle: MathjaxHandle;
editableContainer: EditableContainer;
editable: Editable;
codable: Codable;
@@ -36,11 +39,9 @@ export class EditingArea extends HTMLDivElement {
is: "anki-editable-container",
}) as EditableContainer;
- const imageStyle = document.createElement("style");
- imageStyle.setAttribute("rel", "stylesheet");
- imageStyle.id = "imageHandleStyle";
-
this.editable = document.createElement("anki-editable") as Editable;
+ this.editableContainer.shadowRoot!.appendChild(this.editable);
+ this.appendChild(this.editableContainer);
const context = new Map();
context.set(
@@ -49,27 +50,32 @@ export class EditingArea extends HTMLDivElement {
);
let imageHandleResolve: (value: ImageHandle) => void;
- this.imageHandle = new Promise((resolve) => {
- imageHandleResolve = resolve;
- });
+ this.imageHandle = new Promise(
+ (resolve) => (imageHandleResolve = resolve)
+ );
- imageStyle.addEventListener("load", () =>
+ this.editableContainer.imagePromise.then(() =>
imageHandleResolve(
new ImageHandle({
target: this,
anchor: this.editableContainer,
props: {
container: this.editable,
- sheet: imageStyle.sheet,
+ sheet: this.editableContainer.imageStyle.sheet,
},
context,
} as any)
)
);
- this.editableContainer.shadowRoot!.appendChild(imageStyle);
- this.editableContainer.shadowRoot!.appendChild(this.editable);
- this.appendChild(this.editableContainer);
+ this.mathjaxHandle = new MathjaxHandle({
+ target: this,
+ anchor: this.editableContainer,
+ props: {
+ container: this.editable,
+ },
+ context,
+ } as any);
this.codable = document.createElement("textarea", {
is: "anki-codable",
@@ -80,7 +86,7 @@ export class EditingArea extends HTMLDivElement {
this.onBlur = this.onBlur.bind(this);
this.onKey = this.onKey.bind(this);
this.onPaste = this.onPaste.bind(this);
- this.showImageHandle = this.showImageHandle.bind(this);
+ this.showHandles = this.showHandles.bind(this);
}
get activeInput(): Editable | Codable {
@@ -92,11 +98,24 @@ export class EditingArea extends HTMLDivElement {
}
set fieldHTML(content: string) {
- this.imageHandle.then(() => (this.activeInput.fieldHTML = content));
+ this.imageHandle.then(() => {
+ let result = content;
+
+ for (const component of decoratedComponents) {
+ result = component.toUndecorated(result);
+ }
+
+ this.activeInput.fieldHTML = result;
+ });
}
get fieldHTML(): string {
- return this.activeInput.fieldHTML;
+ let result = this.activeInput.fieldHTML;
+ for (const component of decoratedComponents) {
+ result = component.toStored(result);
+ }
+
+ return result;
}
connectedCallback(): void {
@@ -109,7 +128,7 @@ export class EditingArea extends HTMLDivElement {
this.addEventListener("copy", onCutOrCopy);
this.addEventListener("oncut", onCutOrCopy);
this.addEventListener("mouseup", updateActiveButtons);
- this.editable.addEventListener("click", this.showImageHandle);
+ this.editable.addEventListener("click", this.showHandles);
}
disconnectedCallback(): void {
@@ -122,12 +141,14 @@ export class EditingArea extends HTMLDivElement {
this.removeEventListener("copy", onCutOrCopy);
this.removeEventListener("oncut", onCutOrCopy);
this.removeEventListener("mouseup", updateActiveButtons);
- this.editable.removeEventListener("click", this.showImageHandle);
+ this.editable.removeEventListener("click", this.showHandles);
}
initialize(color: string, content: string): void {
- this.setBaseColor(color);
- this.fieldHTML = content;
+ this.editableContainer.stylePromise.then(() => {
+ this.setBaseColor(color);
+ this.fieldHTML = content;
+ });
}
setBaseColor(color: string): void {
@@ -179,13 +200,12 @@ export class EditingArea extends HTMLDivElement {
this.activeInput.surroundSelection(before, after);
}
- onFocus(event: FocusEvent): void {
- onFocus(event);
+ onFocus(): void {
+ deferFocusDown(this);
}
onBlur(event: FocusEvent): void {
- this.resetImageHandle();
- onBlur(event);
+ saveFieldIfFieldChanged(this, event.relatedTarget as Element);
}
onEnter(event: KeyboardEvent): void {
@@ -193,33 +213,49 @@ export class EditingArea extends HTMLDivElement {
}
onKey(event: KeyboardEvent): void {
- this.resetImageHandle();
+ this.resetHandles();
onKey(event);
}
onPaste(event: ClipboardEvent): void {
- this.resetImageHandle();
+ this.resetHandles();
this.activeInput.onPaste(event);
}
- resetImageHandle(): void {
- this.imageHandle.then((imageHandle) =>
+ resetHandles(): Promise {
+ const promise = this.imageHandle.then((imageHandle) =>
(imageHandle as any).$set({
activeImage: null,
})
);
+
+ (this.mathjaxHandle as any).$set({
+ activeImage: null,
+ });
+
+ return promise;
}
- showImageHandle(event: MouseEvent): void {
+ async showHandles(event: MouseEvent): Promise {
if (event.target instanceof HTMLImageElement) {
- this.imageHandle.then((imageHandle) =>
- (imageHandle as any).$set({
- activeImage: event.target,
+ const image = event.target as HTMLImageElement;
+ await this.resetHandles();
+
+ if (!image.dataset.anki) {
+ await this.imageHandle.then((imageHandle) =>
+ (imageHandle as any).$set({
+ activeImage: image,
+ isRtl: this.isRightToLeft(),
+ })
+ );
+ } else if (image.dataset.anki === "mathjax") {
+ (this.mathjaxHandle as any).$set({
+ activeImage: image,
isRtl: this.isRightToLeft(),
- })
- );
+ });
+ }
} else {
- this.resetImageHandle();
+ await this.resetHandles();
}
}
@@ -230,7 +266,7 @@ export class EditingArea extends HTMLDivElement {
this.fieldHTML = this.codable.teardown();
this.editable.hidden = false;
} else {
- this.resetImageHandle();
+ this.resetHandles();
this.editable.hidden = true;
this.codable.setup(this.editable.fieldHTML);
}
@@ -254,3 +290,5 @@ export class EditingArea extends HTMLDivElement {
blur();
}
}
+
+customElements.define("anki-editing-area", EditingArea, { extends: "div" });
diff --git a/ts/editor/editor-field.ts b/ts/editor/editor-field.ts
index 692e1e609..dd62c6a48 100644
--- a/ts/editor/editor-field.ts
+++ b/ts/editor/editor-field.ts
@@ -10,7 +10,7 @@ export class EditorField extends HTMLDivElement {
constructor() {
super();
- this.classList.add("editor-field");
+ this.className = "editorfield";
this.labelContainer = document.createElement("div", {
is: "anki-label-container",
@@ -65,3 +65,5 @@ export class EditorField extends HTMLDivElement {
this.editingArea.setBaseStyling(fontFamily, fontSize, direction);
}
}
+
+customElements.define("anki-editor-field", EditorField, { extends: "div" });
diff --git a/ts/editor/fields.scss b/ts/editor/fields.scss
index 51009af22..807df851d 100644
--- a/ts/editor/fields.scss
+++ b/ts/editor/fields.scss
@@ -3,6 +3,17 @@
@use 'base';
@use 'scrollbar';
+@use 'button-mixins';
+
+html,
+body {
+ height: 100%;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+}
.nightMode {
@include scrollbar.night-mode;
@@ -10,13 +21,15 @@
#fields {
display: flex;
- overflow-x: hidden;
flex-direction: column;
+ flex-grow: 1;
+ overflow-x: hidden;
margin: 3px 0;
}
-.editor-field {
+.editorfield {
margin: 3px;
+
border-radius: 5px;
border: 1px solid var(--border-color);
@@ -29,6 +42,7 @@
}
}
+/* editing-area */
.field {
position: relative;
@@ -63,10 +77,6 @@
text-align: center;
background-color: var(--window-bg);
- &.is-inactive {
- display: none;
- }
-
a {
color: var(--link);
}
diff --git a/ts/editor/focus-handlers.ts b/ts/editor/focus-handlers.ts
index 11c317ead..42ac55c65 100644
--- a/ts/editor/focus-handlers.ts
+++ b/ts/editor/focus-handlers.ts
@@ -8,23 +8,34 @@
import { fieldFocused } from "./toolbar";
import type { EditingArea } from "./editing-area";
-import { saveField } from "./change-timer";
+import { saveField } from "./saving";
import { bridgeCommand } from "./lib";
import { getCurrentField } from "./helpers";
-export function onFocus(evt: FocusEvent): void {
- const currentField = evt.currentTarget as EditingArea;
- currentField.focus();
- currentField.caretToEnd();
+export function deferFocusDown(editingArea: EditingArea): void {
+ editingArea.focus();
+ editingArea.caretToEnd();
- bridgeCommand(`focus:${currentField.ord}`);
+ if (editingArea.getSelection().anchorNode === null) {
+ // selection is not inside editable after focusing
+ editingArea.caretToEnd();
+ }
+
+ bridgeCommand(`focus:${editingArea.ord}`);
fieldFocused.set(true);
}
-export function onBlur(evt: FocusEvent): void {
- const previousFocus = evt.currentTarget as EditingArea;
- const currentFieldUnchanged = previousFocus === getCurrentField();
+export function saveFieldIfFieldChanged(
+ editingArea: EditingArea,
+ focusTo: Element | null
+): void {
+ const fieldChanged =
+ editingArea !== getCurrentField() && !editingArea.contains(focusTo);
- saveField(previousFocus, currentFieldUnchanged ? "key" : "blur");
+ saveField(editingArea, fieldChanged ? "blur" : "key");
fieldFocused.set(false);
+
+ if (fieldChanged) {
+ editingArea.resetHandles();
+ }
}
diff --git a/ts/editor/helpers.ts b/ts/editor/helpers.ts
index 174260152..b92b57fd5 100644
--- a/ts/editor/helpers.ts
+++ b/ts/editor/helpers.ts
@@ -1,103 +1,12 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-/* eslint
-@typescript-eslint/no-non-null-assertion: "off",
- */
-
import type { EditingArea } from "./editing-area";
export function getCurrentField(): EditingArea | null {
return document.activeElement?.closest(".field") ?? null;
}
-export function nodeIsElement(node: Node): node is Element {
- return node.nodeType === Node.ELEMENT_NODE;
-}
-
-// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
-const BLOCK_TAGS = [
- "ADDRESS",
- "ARTICLE",
- "ASIDE",
- "BLOCKQUOTE",
- "DETAILS",
- "DIALOG",
- "DD",
- "DIV",
- "DL",
- "DT",
- "FIELDSET",
- "FIGCAPTION",
- "FIGURE",
- "FOOTER",
- "FORM",
- "H1",
- "H2",
- "H3",
- "H4",
- "H5",
- "H6",
- "HEADER",
- "HGROUP",
- "HR",
- "LI",
- "MAIN",
- "NAV",
- "OL",
- "P",
- "PRE",
- "SECTION",
- "TABLE",
- "UL",
-];
-
-export function elementIsBlock(element: Element): boolean {
- return BLOCK_TAGS.includes(element.tagName);
-}
-
-export function caretToEnd(node: Node): void {
- const range = document.createRange();
- range.selectNodeContents(node);
- range.collapse(false);
- const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!;
- selection.removeAllRanges();
- selection.addRange(range);
-}
-
-const getAnchorParent =
- (predicate: (element: Element) => element is T) =>
- (currentField: DocumentOrShadowRoot): T | null => {
- const anchor = currentField.getSelection()?.anchorNode;
-
- if (!anchor) {
- return null;
- }
-
- let anchorParent: T | null = null;
- let element = nodeIsElement(anchor) ? anchor : anchor.parentElement;
-
- while (element) {
- anchorParent = anchorParent || (predicate(element) ? element : null);
- element = element.parentElement;
- }
-
- return anchorParent;
- };
-
-const isListItem = (element: Element): element is HTMLLIElement =>
- window.getComputedStyle(element).display === "list-item";
-const isParagraph = (element: Element): element is HTMLParamElement =>
- element.tagName === "P";
-const isBlockElement = (
- element: Element
-): element is HTMLLIElement & HTMLParamElement =>
- isListItem(element) || isParagraph(element);
-
-export const getListItem = getAnchorParent(isListItem);
-export const getParagraph = getAnchorParent(isParagraph);
-export const getBlockElement = getAnchorParent(isBlockElement);
-
export function appendInParentheses(text: string, appendix: string): string {
return `${text} (${appendix})`;
}
diff --git a/ts/editor/icons.ts b/ts/editor/icons.ts
index 7386dcb2c..a29daecd6 100644
--- a/ts/editor/icons.ts
+++ b/ts/editor/icons.ts
@@ -46,3 +46,6 @@ export { default as floatRightIcon } from "./format-float-right.svg";
export { default as sizeActual } from "./image-size-select-actual.svg";
export { default as sizeMinimized } from "./image-size-select-large.svg";
+
+export { default as inlineIcon } from "./format-wrap-square.svg";
+export { default as blockIcon } from "./format-wrap-top-bottom.svg";
diff --git a/ts/editor/index.ts b/ts/editor/index.ts
index bfa814f97..a8310406c 100644
--- a/ts/editor/index.ts
+++ b/ts/editor/index.ts
@@ -13,28 +13,30 @@ import type EditorToolbar from "./EditorToolbar.svelte";
import type TagEditor from "./TagEditor.svelte";
import { filterHTML } from "html-filter";
-import { updateActiveButtons } from "./toolbar";
import { setupI18n, ModuleName } from "lib/i18n";
import { isApplePlatform } from "lib/platform";
import { registerShortcut } from "lib/shortcuts";
import { bridgeCommand } from "lib/bridgecommand";
+import { updateActiveButtons } from "./toolbar";
+import { saveField } from "./saving";
import "./fields.css";
-import { saveField } from "./change-timer";
-
-import { EditorField } from "./editor-field";
-import { LabelContainer } from "./label-container";
+import "./label-container";
+import "./codable";
+import "./editor-field";
+import type { EditorField } from "./editor-field";
import { EditingArea } from "./editing-area";
-import { EditableContainer } from "./editable-container";
-import { Editable } from "./editable";
-import { Codable } from "./codable";
+import "editable/editable-container";
+import "editable/editable";
+import "editable/mathjax-component";
+
import { initToolbar, fieldFocused } from "./toolbar";
import { initTagEditor } from "./tag-editor";
import { getCurrentField } from "./helpers";
export { setNoteId, getNoteId } from "./note-id";
-export { saveNow } from "./change-timer";
+export { saveNow } from "./saving";
export { wrap, wrapIntoText } from "./wrap";
export { editorToolbar } from "./toolbar";
export { activateStickyShortcuts } from "./label-container";
@@ -50,13 +52,6 @@ declare global {
}
}
-customElements.define("anki-editable", Editable);
-customElements.define("anki-editable-container", EditableContainer, { extends: "div" });
-customElements.define("anki-codable", Codable, { extends: "textarea" });
-customElements.define("anki-editing-area", EditingArea, { extends: "div" });
-customElements.define("anki-label-container", LabelContainer, { extends: "div" });
-customElements.define("anki-editor-field", EditorField, { extends: "div" });
-
if (isApplePlatform()) {
registerShortcut(() => bridgeCommand("paste"), "Control+Shift+V");
}
@@ -157,11 +152,14 @@ export function setBackgrounds(cols: ("dupe" | "")[]): void {
);
document
.getElementById("dupes")!
- .classList.toggle("is-inactive", !cols.includes("dupe"));
+ .classList.toggle("d-none", !cols.includes("dupe"));
}
-export function setClozeHint(cloze_hint: string): void {
- document.getElementById("cloze-hint")!.innerHTML = cloze_hint;
+export function setClozeHint(hint: string): void {
+ const clozeHint = document.getElementById("cloze-hint")!;
+
+ clozeHint.innerHTML = hint;
+ clozeHint.classList.toggle("d-none", hint.length === 0);
}
export function setFonts(fonts: [string, number, boolean][]): void {
diff --git a/ts/editor/input-handlers.ts b/ts/editor/input-handlers.ts
index 3bd61eb8d..55593fa62 100644
--- a/ts/editor/input-handlers.ts
+++ b/ts/editor/input-handlers.ts
@@ -5,10 +5,10 @@
@typescript-eslint/no-non-null-assertion: "off",
*/
+import { nodeIsElement } from "lib/dom";
import { updateActiveButtons } from "./toolbar";
import { EditingArea } from "./editing-area";
-import { nodeIsElement } from "./helpers";
-import { triggerChangeTimer } from "./change-timer";
+import { triggerChangeTimer } from "./saving";
import { registerShortcut } from "lib/shortcuts";
export function onInput(event: Event): void {
diff --git a/ts/editor/label-container.ts b/ts/editor/label-container.ts
index 1491dcb6c..16613b1df 100644
--- a/ts/editor/label-container.ts
+++ b/ts/editor/label-container.ts
@@ -7,7 +7,7 @@ import * as tr from "lib/i18n";
import { registerShortcut } from "lib/shortcuts";
import { bridgeCommand } from "./lib";
import { appendInParentheses } from "./helpers";
-import { saveField } from "./change-timer";
+import { saveField } from "./saving";
import { getCurrentField, forEditorField, i18n } from ".";
import pinIcon from "./pin-angle.svg";
@@ -127,3 +127,5 @@ export class LabelContainer extends HTMLDivElement {
this.toggleSticky();
}
}
+
+customElements.define("anki-label-container", LabelContainer, { extends: "div" });
diff --git a/ts/editor/saving.ts b/ts/editor/saving.ts
new file mode 100644
index 000000000..ebb70e7bd
--- /dev/null
+++ b/ts/editor/saving.ts
@@ -0,0 +1,40 @@
+// Copyright: Ankitects Pty Ltd and contributors
+// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+import type { EditingArea } from "./editing-area";
+
+import { ChangeTimer } from "./change-timer";
+import { getCurrentField } from "./helpers";
+import { bridgeCommand } from "./lib";
+import { getNoteId } from "./note-id";
+
+const saveFieldTimer = new ChangeTimer();
+
+export function triggerChangeTimer(currentField: EditingArea): void {
+ saveFieldTimer.schedule(() => saveField(currentField, "key"), 600);
+}
+
+export function saveField(currentField: EditingArea, type: "blur" | "key"): void {
+ saveFieldTimer.clear();
+ const command = `${type}:${currentField.ord}:${getNoteId()}:${
+ currentField.fieldHTML
+ }`;
+ bridgeCommand(command);
+}
+
+export function saveNow(keepFocus: boolean): void {
+ const currentField = getCurrentField();
+
+ if (!currentField) {
+ return;
+ }
+
+ saveFieldTimer.clear();
+
+ if (keepFocus) {
+ saveField(currentField, "key");
+ } else {
+ // triggers onBlur, which saves
+ currentField.blur();
+ }
+}
diff --git a/ts/editor/wrap.ts b/ts/editor/wrap.ts
index f7c2114a6..12a3f9f3b 100644
--- a/ts/editor/wrap.ts
+++ b/ts/editor/wrap.ts
@@ -5,45 +5,15 @@
@typescript-eslint/no-non-null-assertion: "off",
*/
-import { getCurrentField } from "./helpers";
-import { setFormat } from ".";
-
-function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
- const match = text.match(/^(\s*)([^]*?)(\s*)$/)!;
- return match[1] + front + match[2] + back + match[3];
-}
-
-function moveCursorPastPostfix(selection: Selection, postfix: string): void {
- const range = selection.getRangeAt(0);
- range.setStart(range.startContainer, range.startOffset - postfix.length);
- range.collapse(true);
- selection.removeAllRanges();
- selection.addRange(range);
-}
-
-function wrapInternal(front: string, back: string, plainText: boolean): void {
- const currentField = getCurrentField()!;
- const selection = currentField.getSelection();
- const range = selection.getRangeAt(0);
- const content = range.cloneContents();
- const span = document.createElement("span");
- span.appendChild(content);
-
- if (plainText) {
- const new_ = wrappedExceptForWhitespace(span.innerText, front, back);
- setFormat("inserttext", new_);
- } else {
- const new_ = wrappedExceptForWhitespace(span.innerHTML, front, back);
- setFormat("inserthtml", new_);
- }
-
- if (!span.innerHTML) {
- moveCursorPastPostfix(selection, back);
- }
-}
+import { wrapInternal } from "lib/wrap";
+import { getCurrentField } from ".";
export function wrap(front: string, back: string): void {
- wrapInternal(front, back, false);
+ const editingArea = getCurrentField();
+
+ if (editingArea) {
+ wrapInternal(editingArea.editableContainer.shadowRoot!, front, back, false);
+ }
}
export function wrapCurrent(front: string, back: string): void {
@@ -53,5 +23,9 @@ export function wrapCurrent(front: string, back: string): void {
/* currently unused */
export function wrapIntoText(front: string, back: string): void {
- wrapInternal(front, back, true);
+ const editingArea = getCurrentField();
+
+ if (editingArea) {
+ wrapInternal(editingArea.editableContainer.shadowRoot!, front, back, false);
+ }
}
diff --git a/ts/lib/dom.ts b/ts/lib/dom.ts
new file mode 100644
index 000000000..a3e6a0c95
--- /dev/null
+++ b/ts/lib/dom.ts
@@ -0,0 +1,93 @@
+// Copyright: Ankitects Pty Ltd and contributors
+// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+/* eslint
+@typescript-eslint/no-non-null-assertion: "off",
+ */
+
+export function nodeIsElement(node: Node): node is Element {
+ return node.nodeType === Node.ELEMENT_NODE;
+}
+
+// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
+const BLOCK_TAGS = [
+ "ADDRESS",
+ "ARTICLE",
+ "ASIDE",
+ "BLOCKQUOTE",
+ "DETAILS",
+ "DIALOG",
+ "DD",
+ "DIV",
+ "DL",
+ "DT",
+ "FIELDSET",
+ "FIGCAPTION",
+ "FIGURE",
+ "FOOTER",
+ "FORM",
+ "H1",
+ "H2",
+ "H3",
+ "H4",
+ "H5",
+ "H6",
+ "HEADER",
+ "HGROUP",
+ "HR",
+ "LI",
+ "MAIN",
+ "NAV",
+ "OL",
+ "P",
+ "PRE",
+ "SECTION",
+ "TABLE",
+ "UL",
+];
+
+export function elementIsBlock(element: Element): boolean {
+ return BLOCK_TAGS.includes(element.tagName);
+}
+
+export function caretToEnd(node: Node): void {
+ const range = document.createRange();
+ range.selectNodeContents(node);
+ range.collapse(false);
+ const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!;
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
+
+const getAnchorParent =
+ (predicate: (element: Element) => element is T) =>
+ (currentField: DocumentOrShadowRoot): T | null => {
+ const anchor = currentField.getSelection()?.anchorNode;
+
+ if (!anchor) {
+ return null;
+ }
+
+ let anchorParent: T | null = null;
+ let element = nodeIsElement(anchor) ? anchor : anchor.parentElement;
+
+ while (element) {
+ anchorParent = anchorParent || (predicate(element) ? element : null);
+ element = element.parentElement;
+ }
+
+ return anchorParent;
+ };
+
+const isListItem = (element: Element): element is HTMLLIElement =>
+ window.getComputedStyle(element).display === "list-item";
+const isParagraph = (element: Element): element is HTMLParamElement =>
+ element.tagName === "P";
+const isBlockElement = (
+ element: Element
+): element is HTMLLIElement & HTMLParamElement =>
+ isListItem(element) || isParagraph(element);
+
+export const getListItem = getAnchorParent(isListItem);
+export const getParagraph = getAnchorParent(isParagraph);
+export const getBlockElement = getAnchorParent(isBlockElement);
diff --git a/ts/lib/wrap.ts b/ts/lib/wrap.ts
new file mode 100644
index 000000000..7f97c7c1b
--- /dev/null
+++ b/ts/lib/wrap.ts
@@ -0,0 +1,44 @@
+// Copyright: Ankitects Pty Ltd and contributors
+// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+/* eslint
+@typescript-eslint/no-non-null-assertion: "off",
+ */
+
+function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
+ const match = text.match(/^(\s*)([^]*?)(\s*)$/)!;
+ return match[1] + front + match[2] + back + match[3];
+}
+
+function moveCursorPastPostfix(selection: Selection, postfix: string): void {
+ const range = selection.getRangeAt(0);
+ range.setStart(range.startContainer, range.startOffset - postfix.length);
+ range.collapse(true);
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
+
+export function wrapInternal(
+ root: Document | ShadowRoot,
+ front: string,
+ back: string,
+ plainText: boolean
+): void {
+ const selection = root.getSelection()!;
+ const range = selection.getRangeAt(0);
+ const content = range.cloneContents();
+ const span = document.createElement("span");
+ span.appendChild(content);
+
+ if (plainText) {
+ const new_ = wrappedExceptForWhitespace(span.innerText, front, back);
+ document.execCommand("inserttext", false, new_);
+ } else {
+ const new_ = wrappedExceptForWhitespace(span.innerHTML, front, back);
+ document.execCommand("inserthtml", false, new_);
+ }
+
+ if (!span.innerHTML) {
+ moveCursorPastPostfix(selection, back);
+ }
+}
diff --git a/ts/licenses.json b/ts/licenses.json
index 2f177c860..af0913912 100644
--- a/ts/licenses.json
+++ b/ts/licenses.json
@@ -164,6 +164,14 @@
"path": "node_modules/commander",
"licenseFile": "node_modules/commander/LICENSE"
},
+ "commander@8.1.0": {
+ "licenses": "MIT",
+ "repository": "https://github.com/tj/commander.js",
+ "publisher": "TJ Holowaychuk",
+ "email": "tj@vision-media.ca",
+ "path": "node_modules/speech-rule-engine/node_modules/commander",
+ "licenseFile": "node_modules/speech-rule-engine/node_modules/commander/LICENSE"
+ },
"css-browser-selector@0.6.5": {
"licenses": "CC-BY-SA-2.5",
"repository": "https://github.com/verbatim/css_browser_selector",
@@ -426,6 +434,14 @@
"path": "node_modules/delaunator",
"licenseFile": "node_modules/delaunator/LICENSE"
},
+ "esm@3.2.25": {
+ "licenses": "MIT",
+ "repository": "https://github.com/standard-things/esm",
+ "publisher": "John-David Dalton",
+ "email": "john.david.dalton@gmail.com",
+ "path": "node_modules/esm",
+ "licenseFile": "node_modules/esm/LICENSE"
+ },
"iconv-lite@0.6.3": {
"licenses": "MIT",
"repository": "https://github.com/ashtuchkin/iconv-lite",
@@ -489,12 +505,31 @@
"path": "node_modules/marked",
"licenseFile": "node_modules/marked/LICENSE.md"
},
+ "mathjax-full@3.2.0": {
+ "licenses": "Apache-2.0",
+ "repository": "https://github.com/mathjax/Mathjax-src",
+ "path": "node_modules/mathjax-full",
+ "licenseFile": "node_modules/mathjax-full/LICENSE"
+ },
"mathjax@3.1.4": {
"licenses": "Apache-2.0",
"repository": "https://github.com/mathjax/MathJax",
"path": "node_modules/mathjax",
"licenseFile": "node_modules/mathjax/LICENSE"
},
+ "mhchemparser@4.1.1": {
+ "licenses": "Apache-2.0",
+ "repository": "https://github.com/mhchem/mhchemParser",
+ "publisher": "Martin Hensel",
+ "path": "node_modules/mhchemparser",
+ "licenseFile": "node_modules/mhchemparser/LICENSE.txt"
+ },
+ "mj-context-menu@0.6.1": {
+ "licenses": "Apache-2.0",
+ "repository": "https://github.com/zorkow/context-menu",
+ "path": "node_modules/mj-context-menu",
+ "licenseFile": "node_modules/mj-context-menu/README.md"
+ },
"protobufjs@6.11.2": {
"licenses": "BSD-3-Clause",
"repository": "https://github.com/protobufjs/protobuf.js",
@@ -526,6 +561,28 @@
"url": "https://github.com/ChALkeR",
"path": "node_modules/safer-buffer",
"licenseFile": "node_modules/safer-buffer/LICENSE"
+ },
+ "speech-rule-engine@3.3.3": {
+ "licenses": "Apache-2.0",
+ "repository": "https://github.com/zorkow/speech-rule-engine",
+ "path": "node_modules/speech-rule-engine",
+ "licenseFile": "node_modules/speech-rule-engine/LICENSE"
+ },
+ "wicked-good-xpath@1.3.0": {
+ "licenses": "MIT",
+ "repository": "https://github.com/google/wicked-good-xpath",
+ "publisher": "Google Inc.",
+ "path": "node_modules/wicked-good-xpath",
+ "licenseFile": "node_modules/wicked-good-xpath/LICENSE"
+ },
+ "xmldom-sre@0.1.31": {
+ "licenses": "MIT*",
+ "repository": "https://github.com/zorkow/xmldom",
+ "publisher": "jindw",
+ "email": "jindw@xidea.org",
+ "url": "http://www.xidea.org",
+ "path": "node_modules/xmldom-sre",
+ "licenseFile": "node_modules/xmldom-sre/LICENSE"
}
}
diff --git a/ts/package.json b/ts/package.json
index 1b68e2934..24722dcfc 100644
--- a/ts/package.json
+++ b/ts/package.json
@@ -72,6 +72,7 @@
"lodash-es": "^4.17.21",
"marked": "=2.0.5",
"mathjax": "^3.1.2",
+ "mathjax-full": "^3.2.0",
"protobufjs": "^6.10.2"
},
"resolutions": {
diff --git a/ts/sass/button-mixins.scss b/ts/sass/button-mixins.scss
index c46c1e451..f39f37e9a 100644
--- a/ts/sass/button-mixins.scss
+++ b/ts/sass/button-mixins.scss
@@ -112,7 +112,7 @@ $btn-base-color-night: #666;
}
// should be similar to -webkit-focus-ring-color
-$focus-color: $blue;
+$focus-color: rgba(21 97 174);
@mixin impressed-shadow($intensity) {
box-shadow: inset 0 calc(var(--buttons-size) / 15) calc(var(--buttons-size) / 5)
diff --git a/ts/tsconfig.json b/ts/tsconfig.json
index 0ef8d4f23..29c30547d 100644
--- a/ts/tsconfig.json
+++ b/ts/tsconfig.json
@@ -14,6 +14,7 @@
],
"baseUrl": ".",
"paths": {
+ "editable/*": ["../bazel-bin/ts/editable/*"],
"lib/*": ["../bazel-bin/ts/lib/*"],
"html-filter/*": ["../bazel-bin/ts/html-filter/*"]
/* "sveltelib/*": ["../bazel-bin/ts/sveltelib/*"], */
diff --git a/ts/yarn.lock b/ts/yarn.lock
index d2dd309de..a403e4680 100644
--- a/ts/yarn.lock
+++ b/ts/yarn.lock
@@ -1596,6 +1596,11 @@ commander@7:
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+commander@>=7.0.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-8.1.0.tgz#db36e3e66edf24ff591d639862c6ab2c52664362"
+ integrity sha512-mf45ldcuHSYShkplHHGKWb4TrmwQadxOn7v4WuhDJy0ZVoY5JFajaRDKD0PNe5qXzBX0rhovjTnP6Kz9LETcuA==
+
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -2154,6 +2159,11 @@ eslint@^7.24.0:
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
+esm@^3.2.25:
+ version "3.2.25"
+ resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
+ integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
+
espree@^7.3.0, espree@^7.3.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
@@ -3356,6 +3366,16 @@ marked@=2.0.5, marked@^2.0.3:
resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.5.tgz#2d15c759b9497b0e7b5b57f4c2edabe1002ef9e7"
integrity sha512-yfCEUXmKhBPLOzEC7c+tc4XZdIeTdGoRCZakFMkCxodr7wDXqoapIME4wjcpBPJLNyUnKJ3e8rb8wlAgnLnaDw==
+mathjax-full@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/mathjax-full/-/mathjax-full-3.2.0.tgz#e53269842a943d4df10502937518991268996c5c"
+ integrity sha512-D2EBNvUG+mJyhn+M1C858k0f2Fc4KxXvbEX2WCMXroV10212JwfYqaBJ336ECBSz5X9L5LRoamxb7AJtg3KaJA==
+ dependencies:
+ esm "^3.2.25"
+ mhchemparser "^4.1.0"
+ mj-context-menu "^0.6.1"
+ speech-rule-engine "^3.3.3"
+
mathjax@^3.1.2:
version "3.1.4"
resolved "https://registry.yarnpkg.com/mathjax/-/mathjax-3.1.4.tgz#4e8932d12845c0abae8b7f1976ea98cb505e8420"
@@ -3376,6 +3396,11 @@ merge2@^1.3.0:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+mhchemparser@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/mhchemparser/-/mhchemparser-4.1.1.tgz#a2142fdab37a02ec8d1b48a445059287790becd5"
+ integrity sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA==
+
micromatch@^4.0.2, micromatch@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
@@ -3418,6 +3443,11 @@ minimist@^1.2.0, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+mj-context-menu@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/mj-context-menu/-/mj-context-menu-0.6.1.tgz#a043c5282bf7e1cf3821de07b13525ca6f85aa69"
+ integrity sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==
+
mkdirp@^1.0.3, mkdirp@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
@@ -4094,6 +4124,15 @@ spdx-satisfies@^5.0.0:
spdx-expression-parse "^3.0.0"
spdx-ranges "^2.0.0"
+speech-rule-engine@^3.3.3:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/speech-rule-engine/-/speech-rule-engine-3.3.3.tgz#781ed03cbcf3279f94d1d80241025ea954c6d571"
+ integrity sha512-0exWw+0XauLjat+f/aFeo5T8SiDsO1JtwpY3qgJE4cWt+yL/Stl0WP4VNDWdh7lzGkubUD9lWP4J1ASnORXfyQ==
+ dependencies:
+ commander ">=7.0.0"
+ wicked-good-xpath "^1.3.0"
+ xmldom-sre "^0.1.31"
+
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@@ -4505,6 +4544,11 @@ which@^2.0.1:
dependencies:
isexe "^2.0.0"
+wicked-good-xpath@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz#81b0e95e8650e49c94b22298fff8686b5553cf6c"
+ integrity sha1-gbDpXoZQ5JyUsiKY//hoa1VTz2w=
+
word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
@@ -4554,6 +4598,11 @@ xmlcreate@^2.0.3:
resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.3.tgz#df9ecd518fd3890ab3548e1b811d040614993497"
integrity sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ==
+xmldom-sre@^0.1.31:
+ version "0.1.31"
+ resolved "https://registry.yarnpkg.com/xmldom-sre/-/xmldom-sre-0.1.31.tgz#10860d5bab2c603144597d04bf2c4980e98067f4"
+ integrity sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==
+
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"