Merge remote-tracking branch 'upstream/HEAD' into text-import-export

This commit is contained in:
RumovZ 2022-05-16 11:50:19 +02:00
commit 6d93a578f3
52 changed files with 544 additions and 562 deletions

View file

@ -1,7 +1,9 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
"[python]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
"files.watcherExclude": {
"**/.git/objects/**": true,

View file

@ -15,6 +15,7 @@ editing-customize-card-templates = Customize Card Templates
editing-customize-fields = Customize Fields
editing-cut = Cut
editing-double-click-image = double-click image
editing-double-click-to-expand = (double-click to expand)
editing-edit-current = Edit Current
editing-edit-html = Edit HTML
editing-fields = Fields
@ -38,6 +39,7 @@ editing-outdent = Decrease indent
editing-paste = Paste
editing-record-audio = Record audio
editing-remove-formatting = Remove formatting
editing-restore-original-size = Restore original size
editing-select-remove-formatting = Select formatting to remove
editing-show-duplicates = Show Duplicates
editing-subscript = Subscript

View file

@ -76,7 +76,6 @@
"lodash-es": "^4.17.21",
"marked": "^4.0.0",
"mathjax": "^3.1.2",
"mathjax-full": "^3.2.0",
"protobufjs": "^6.10.2"
},
"resolutions": {

View file

@ -41,13 +41,21 @@ copy_files_into_group(
package = "//ts/reviewer",
)
copy_files_into_group(
name = "mathjax",
srcs = [
"mathjax.js",
],
package = "//ts/mathjax",
)
filegroup(
name = "js",
srcs = [
"aqt",
"mathjax.js",
"reviewer",
"editor",
"mathjax",
"//qt/aqt/data/web/js/vendor",
],
visibility = ["//qt:__subpackages__"],
@ -68,6 +76,5 @@ eslint_test(
)
exports_files([
"mathjax.js",
"tsconfig.json",
])

View file

@ -1,28 +0,0 @@
window.MathJax = {
tex: {
displayMath: [["\\[", "\\]"]],
processRefs: false,
processEnvironments: false,
processEscapes: false,
packages: {
"[+]": ["noerrors", "mhchem"],
},
},
startup: {
typeset: false,
pageReady: () => {
return MathJax.startup.defaultPageReady();
},
},
options: {
renderActions: {
addMenu: [],
checkLoading: [],
},
ignoreHtmlClass: "tex2jax_ignore",
processHtmlClass: "tex2jax_process",
},
loader: {
load: ["[tex]/noerrors", "[tex]/mhchem"],
},
};

View file

@ -155,7 +155,10 @@ class Editor:
self.web.stdHtml(
"",
css=[f"css/{file}.css"],
js=[f"js/{file}.js"],
js=[
"js/mathjax.js",
f"js/{file}.js",
],
context=self,
default_css=False,
)

View file

@ -149,6 +149,14 @@ class AVPlayer:
self._enqueued = tags[:]
self._play_next_if_idle()
def append_tags(self, tags: list[AVTag]) -> None:
"""Append provided tags to the queue, then start playing them if the current player is idle."""
self._enqueued.extend(tags)
self._play_next_if_idle()
def queue_is_empty(self) -> bool:
return bool(self._enqueued)
def stop_and_clear_queue(self) -> None:
self._enqueued = []
self._stop_if_playing()

View file

@ -21,8 +21,8 @@ _ts_deps = [
"//ts/lib",
"//ts/domlib",
"//ts/sveltelib",
"//ts/mathjax:mathjax_ts",
"@npm//mathjax",
"@npm//mathjax-full",
"@npm//svelte",
]

View file

@ -0,0 +1,11 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<style lang="scss">
:global([data-editor-shrink]:not([data-editor-shrink="false"])) {
max-width: var(--editor-shrink-max-width);
max-height: var(--editor-shrink-max-height);
width: auto;
}
</style>

View file

@ -5,3 +5,4 @@ import "./editable-base.css";
/* only imported for the CSS */
import "./ContentEditable.svelte";
import "./Mathjax.svelte";
import "./ResizableImage.svelte";

View file

@ -1,8 +1,6 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import "mathjax/es5/tex-svg-full";
import { placeCaretAfter, placeCaretBefore } from "../domlib/place-caret";
import { on } from "../lib/events";
import type { DecoratedElement, DecoratedElementConstructor } from "./decorated";
@ -15,6 +13,13 @@ const mathjaxTagPattern =
const mathjaxBlockDelimiterPattern = /\\\[(.*?)\\\]/gsu;
const mathjaxInlineDelimiterPattern = /\\\((.*?)\\\)/gsu;
function trimBreaks(text: string): string {
return text
.replace(/<br[ ]*\/?>/gsu, "\n")
.replace(/^\n*/, "")
.replace(/\n*$/, "");
}
export const Mathjax: DecoratedElementConstructor = class Mathjax
extends HTMLElement
implements DecoratedElement
@ -25,9 +30,10 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
const stored = undecorated.replace(
mathjaxTagPattern,
(_match: string, block: string | undefined, text: string) => {
const trimmed = trimBreaks(text);
return typeof block === "string" && block !== "false"
? `\\[${text}\\]`
: `\\(${text}\\)`;
? `\\[${trimmed}\\]`
: `\\(${trimmed}\\)`;
},
);
@ -37,10 +43,12 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
static toUndecorated(stored: string): string {
return stored
.replace(mathjaxBlockDelimiterPattern, (_match: string, text: string) => {
return `<${Mathjax.tagName} block="true">${text}</${Mathjax.tagName}>`;
const trimmed = trimBreaks(text);
return `<${Mathjax.tagName} block="true">${trimmed}</${Mathjax.tagName}>`;
})
.replace(mathjaxInlineDelimiterPattern, (_match: string, text: string) => {
return `<${Mathjax.tagName}>${text}</${Mathjax.tagName}>`;
const trimmed = trimBreaks(text);
return `<${Mathjax.tagName}>${trimmed}</${Mathjax.tagName}>`;
});
}

View file

@ -5,6 +5,8 @@
@typescript-eslint/no-explicit-any: "off",
*/
import "mathjax/es5/tex-svg";
import { mathIcon } from "./icons";
const parser = new DOMParser();

View file

@ -5,6 +5,7 @@
{ "path": "../components" },
{ "path": "../lib" },
{ "path": "../domlib" },
{ "path": "../sveltelib" }
{ "path": "../sveltelib" },
{ "path": "../mathjax" }
]
}

View file

@ -3,19 +3,18 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { get } from "svelte/store";
import ButtonGroup from "../../components/ButtonGroup.svelte";
import IconButton from "../../components/IconButton.svelte";
import Shortcut from "../../components/Shortcut.svelte";
import * as tr from "../../lib/ftl";
import { isApplePlatform } from "../../lib/platform";
import { getPlatformString } from "../../lib/shortcuts";
import { wrapInternal } from "../../lib/wrap";
import { context as noteEditorContext } from "../NoteEditor.svelte";
import type { RichTextInputAPI } from "../rich-text-input";
import { editingInputIsRichText } from "../rich-text-input";
import ButtonGroup from "../components/ButtonGroup.svelte";
import IconButton from "../components/IconButton.svelte";
import Shortcut from "../components/Shortcut.svelte";
import * as tr from "../lib/ftl";
import { isApplePlatform } from "../lib/platform";
import { getPlatformString } from "../lib/shortcuts";
import { clozeIcon, incrementClozeIcon } from "./icons";
import { context as noteEditorContext } from "./NoteEditor.svelte";
import { editingInputIsRichText } from "./rich-text-input";
const { focusedInput, fields } = noteEditorContext.get();
@ -48,20 +47,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
return Math.max(1, highest);
}
$: richTextAPI = $focusedInput as RichTextInputAPI;
const dispatch = createEventDispatcher();
async function onIncrementCloze(): Promise<void> {
const richText = await richTextAPI.element;
const highestCloze = getCurrentHighestCloze(true);
wrapInternal(richText, `{{c${highestCloze}::`, "}}", false);
dispatch("surround", {
prefix: `{{c${highestCloze}::`,
suffix: "}}",
});
}
async function onSameCloze(): Promise<void> {
const richText = await richTextAPI.element;
const highestCloze = getCurrentHighestCloze(false);
wrapInternal(richText, `{{c${highestCloze}::`, "}}", false);
dispatch("surround", {
prefix: `{{c${highestCloze}::`,
suffix: "}}",
});
}
$: disabled = !editingInputIsRichText($focusedInput);

View file

@ -7,7 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import contextProperty from "../sveltelib/context-property";
export interface EditingInputAPI {
export interface FocusableInputAPI {
readonly name: string;
focusable: boolean;
/**
@ -22,6 +22,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
refocus(): void;
}
export interface EditingInputAPI extends FocusableInputAPI {
/**
* Check whether blurred target belongs to an editing input.
* The editing area can then restore focus to this input.
*
* @returns An editing input api that is associated with the event target.
*/
getInputAPI(target: EventTarget): Promise<FocusableInputAPI | null>;
}
export interface EditingAreaAPI {
content: Writable<string>;
editingInputs: Writable<EditingInputAPI[]>;
@ -36,7 +46,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</script>
<script lang="ts">
import { setContext as svelteSetContext } from "svelte";
import { setContext as svelteSetContext, tick } from "svelte";
import { writable } from "svelte/store";
import { fontFamilyKey, fontSizeKey } from "../lib/context-keys";
@ -116,12 +126,39 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
}
// prevents editor field being entirely deselected when
// closing active field
function trapFocusOnBlurOut(event: FocusEvent): void {
if (!event.relatedTarget && editingInputs.every((input) => !input.focusable)) {
// Prevents editor field being entirely deselected when
// closing active field.
async function trapFocusOnBlurOut(event: FocusEvent): Promise<void> {
if (event.relatedTarget) {
return;
}
event.preventDefault();
const oldInputElement = event.target;
await tick();
let focusableInput: FocusableInputAPI | null = null;
const focusableInputs = editingInputs.filter(
(input: EditingInputAPI): boolean => input.focusable,
);
if (oldInputElement) {
for (const input of focusableInputs) {
focusableInput = await input.getInputAPI(oldInputElement);
if (focusableInput) {
break;
}
}
}
if (focusableInput || (focusableInput = focusableInputs[0])) {
focusableInput.focus();
} else {
focusTrap.focus();
event.preventDefault();
}
}

View file

@ -4,53 +4,32 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { getContext } from "svelte";
import { afterUpdate, createEventDispatcher, onMount } from "svelte";
import { createEventDispatcher, onMount } from "svelte";
import type { Readable } from "svelte/store";
import { directionKey } from "../lib/context-keys";
let dimensions: HTMLDivElement;
let overflowFix = 0;
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
function updateOverflow(dimensions: HTMLDivElement): void {
const boundingClientRect = dimensions.getBoundingClientRect();
const overflow =
$direction === "ltr"
? boundingClientRect.x
: window.innerWidth - boundingClientRect.x - boundingClientRect.width;
overflowFix = Math.min(0, overflowFix + overflow, overflow);
}
afterUpdate(() => updateOverflow(dimensions));
function updateOverflowAsync(dimensions: HTMLDivElement): void {
setTimeout(() => updateOverflow(dimensions));
}
const dispatch = createEventDispatcher();
onMount(() => dispatch("mount"));
</script>
<div
bind:this={dimensions}
class="image-handle-dimensions"
class:is-rtl={$direction === "rtl"}
style="--overflow-fix: {overflowFix}px"
use:updateOverflowAsync
>
<div class="image-handle-dimensions" class:is-rtl={$direction === "rtl"}>
<slot />
</div>
<style lang="scss">
div {
position: absolute;
width: fit-content;
pointer-events: none;
user-select: none;
left: 0;
right: 0;
bottom: 3px;
margin-left: auto;
margin-right: auto;
font-size: 13px;
color: white;
@ -59,16 +38,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
border-radius: 5px;
padding: 0 5px;
bottom: 3px;
right: 3px;
margin-left: 3px;
margin-right: var(--overflow-fix, 0);
&.is-rtl {
right: auto;
left: 3px;
margin-right: 3px;
margin-left: var(--overflow-fix, 0);
}
pointer-events: none;
user-select: none;
}
</style>

View file

@ -324,8 +324,26 @@ the AddCards dialog) should be implemented in the user of this component.
{#if cols[index] === "dupe"}
<DuplicateLink />
{/if}
<RichTextBadge bind:off={richTextsHidden[index]} />
<PlainTextBadge bind:off={plainTextsHidden[index]} />
<RichTextBadge
bind:off={richTextsHidden[index]}
on:toggle={() => {
richTextsHidden[index] = !richTextsHidden[index];
if (!richTextsHidden[index]) {
richTextInputs[index].api.refocus();
}
}}
/>
<PlainTextBadge
bind:off={plainTextsHidden[index]}
on:toggle={() => {
plainTextsHidden[index] = !plainTextsHidden[index];
if (!plainTextsHidden[index]) {
plainTextInputs[index].api.refocus();
}
}}
/>
<slot name="field-state" {field} {index} />
</svelte:fragment>
@ -339,7 +357,7 @@ the AddCards dialog) should be implemented in the user of this component.
}}
bind:this={richTextInputs[index]}
>
<ImageHandle />
<ImageHandle maxWidth={250} maxHeight={125} />
<MathjaxHandle />
</RichTextInput>

View file

@ -3,7 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onMount } from "svelte";
import { createEventDispatcher, onMount } from "svelte";
import Badge from "../components/Badge.svelte";
import * as tr from "../lib/ftl";
@ -13,13 +13,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const editorField = editorFieldContext.get();
const keyCombination = "Control+Shift+X";
const dispatch = createEventDispatcher();
export let off = false;
$: icon = off ? htmlOff : htmlOn;
function toggle() {
off = !off;
dispatch("toggle");
}
function shortcut(target: HTMLElement): () => void {

View file

@ -3,14 +3,18 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { createEventDispatcher } from "svelte";
import Badge from "../components/Badge.svelte";
import * as tr from "../lib/ftl";
import { richTextOff, richTextOn } from "./icons";
export let off: boolean;
function toggle(): void {
off = !off;
const dispatch = createEventDispatcher();
function toggle() {
dispatch("toggle");
}
$: icon = off ? richTextOff : richTextOn;

View file

@ -9,9 +9,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { MatchType } from "../../domlib/surround";
import * as tr from "../../lib/ftl";
import { getPlatformString } from "../../lib/shortcuts";
import { removeStyleProperties } from "../../lib/styling";
import { context as noteEditorContext } from "../NoteEditor.svelte";
import { editingInputIsRichText } from "../rich-text-input";
import { removeEmptyStyle, Surrounder } from "../surround";
import { Surrounder } from "../surround";
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
import { boldIcon } from "./icons";
@ -25,9 +26,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const fontWeight = element.style.fontWeight;
if (fontWeight === "bold" || Number(fontWeight) >= 400) {
return match.clear((): void => {
element.style.removeProperty("font-weight");
if (removeEmptyStyle(element) && element.className.length === 0) {
if (
removeStyleProperties(element, "font-weight") &&
element.className.length === 0
) {
match.remove();
}
});

View file

@ -56,9 +56,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import Item from "../../components/Item.svelte";
import StickyContainer from "../../components/StickyContainer.svelte";
import BlockButtons from "./BlockButtons.svelte";
import ClozeButtons from "./ClozeButtons.svelte";
import InlineButtons from "./InlineButtons.svelte";
import NotetypeButtons from "./NotetypeButtons.svelte";
import RichTextClozeButtons from "./RichTextClozeButtons.svelte";
import TemplateButtons from "./TemplateButtons.svelte";
export let size: number;
@ -108,7 +108,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</Item>
<Item id="cloze">
<ClozeButtons />
<RichTextClozeButtons />
</Item>
</DynamicallySlottable>
</ButtonToolbar>

View file

@ -11,9 +11,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} from "../../domlib/surround";
import { bridgeCommand } from "../../lib/bridgecommand";
import * as tr from "../../lib/ftl";
import { removeStyleProperties } from "../../lib/styling";
import { context as noteEditorContext } from "../NoteEditor.svelte";
import { editingInputIsRichText } from "../rich-text-input";
import { removeEmptyStyle, Surrounder } from "../surround";
import { Surrounder } from "../surround";
import ColorPicker from "./ColorPicker.svelte";
import type { RemoveFormat } from "./EditorToolbar.svelte";
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
@ -45,9 +46,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
match.setCache(value);
match.clear((): void => {
element.style.removeProperty("background-color");
if (removeEmptyStyle(element) && element.className.length === 0) {
if (
removeStyleProperties(element, "background-color") &&
element.className.length === 0
) {
match.remove();
}
});

View file

@ -9,9 +9,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { MatchType } from "../../domlib/surround";
import * as tr from "../../lib/ftl";
import { getPlatformString } from "../../lib/shortcuts";
import { removeStyleProperties } from "../../lib/styling";
import { context as noteEditorContext } from "../NoteEditor.svelte";
import { editingInputIsRichText } from "../rich-text-input";
import { removeEmptyStyle, Surrounder } from "../surround";
import { Surrounder } from "../surround";
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
import { italicIcon } from "./icons";
@ -24,9 +25,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
if (["italic", "oblique"].includes(element.style.fontStyle)) {
return match.clear((): void => {
element.style.removeProperty("font-style");
if (removeEmptyStyle(element) && element.className.length === 0) {
if (
removeStyleProperties(element, "font-style") &&
element.className.length === 0
) {
return match.remove();
}
});

View file

@ -0,0 +1,23 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { wrapInternal } from "../../lib/wrap";
import ClozeButtons from "../ClozeButtons.svelte";
import { context as noteEditorContext } from "../NoteEditor.svelte";
import type { RichTextInputAPI } from "../rich-text-input";
const { focusedInput } = noteEditorContext.get();
$: richTextAPI = $focusedInput as RichTextInputAPI;
async function onSurround({ detail }): Promise<void> {
const richText = await richTextAPI.element;
const { prefix, suffix } = detail;
wrapInternal(richText, prefix, suffix, false);
}
</script>
<ClozeButtons on:surround={onSurround} />

View file

@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script context="module" lang="ts">
import type { MatchType } from "../../domlib/surround";
import { removeEmptyStyle } from "../surround";
import { removeStyleProperties } from "../../lib/styling";
const surroundElement = document.createElement("sub");
@ -15,9 +15,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
if (element.style.verticalAlign === "sub") {
return match.clear((): void => {
element.style.removeProperty("vertical-align");
if (removeEmptyStyle(element) && element.className.length === 0) {
if (
removeStyleProperties(element, "vertical-align") &&
element.className.length === 0
) {
return match.remove();
}
});

View file

@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script context="module" lang="ts">
import type { MatchType } from "../../domlib/surround";
import { removeEmptyStyle } from "../surround";
import { removeStyleProperties } from "../../lib/styling";
const surroundElement = document.createElement("sup");
@ -15,9 +15,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
if (element.style.verticalAlign === "super") {
return match.clear((): void => {
element.style.removeProperty("vertical-align");
if (removeEmptyStyle(element) && element.className.length === 0) {
if (
removeStyleProperties(element, "vertical-align") &&
element.className.length === 0
) {
return match.remove();
}
});

View file

@ -13,10 +13,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { bridgeCommand } from "../../lib/bridgecommand";
import * as tr from "../../lib/ftl";
import { getPlatformString } from "../../lib/shortcuts";
import { removeStyleProperties } from "../../lib/styling";
import { withFontColor } from "../helpers";
import { context as noteEditorContext } from "../NoteEditor.svelte";
import { editingInputIsRichText } from "../rich-text-input";
import { removeEmptyStyle, Surrounder } from "../surround";
import { Surrounder } from "../surround";
import ColorPicker from "./ColorPicker.svelte";
import type { RemoveFormat } from "./EditorToolbar.svelte";
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
@ -59,9 +60,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
match.setCache(value);
match.clear((): void => {
element.style.removeProperty("color");
if (removeEmptyStyle(element) && element.className.length === 0) {
if (
removeStyleProperties(element, "color") &&
element.className.length === 0
) {
match.remove();
}
});

View file

@ -24,8 +24,6 @@ export { default as underlineIcon } from "bootstrap-icons/icons/type-underline.s
export const arrowIcon =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="transparent" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2 5l6 6 6-6"/></svg>';
export { default as incrementClozeIcon } from "../../icons/contain-plus.svg";
export { default as clozeIcon } from "@mdi/svg/svg/contain.svg";
export { default as functionIcon } from "@mdi/svg/svg/function-variant.svg";
export { default as paperclipIcon } from "@mdi/svg/svg/paperclip.svg";
export { default as micIcon } from "bootstrap-icons/icons/mic.svg";

View file

@ -3,3 +3,4 @@
export type { EditorToolbarAPI } from "./EditorToolbar.svelte";
export { default as EditorToolbar, editorToolbar } from "./EditorToolbar.svelte";
export { default as ClozeButtons } from "./EditorToolbar.svelte";

View file

@ -3,8 +3,10 @@
/// <reference types="../lib/image-import" />
export { default as incrementClozeIcon } from "../icons/contain-plus.svg";
export { default as alertIcon } from "@mdi/svg/svg/alert.svg";
export { default as htmlOn } from "@mdi/svg/svg/code-tags.svg";
export { default as clozeIcon } from "@mdi/svg/svg/contain.svg";
export { default as richTextOff } from "@mdi/svg/svg/eye-off-outline.svg";
export { default as richTextOn } from "@mdi/svg/svg/eye-outline.svg";
export { default as stickyOff } from "@mdi/svg/svg/pin-off-outline.svg";

View file

@ -7,6 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ButtonDropdown from "../../components/ButtonDropdown.svelte";
import WithDropdown from "../../components/WithDropdown.svelte";
import * as tr from "../../lib/ftl";
import HandleBackground from "../HandleBackground.svelte";
import HandleControl from "../HandleControl.svelte";
import HandleLabel from "../HandleLabel.svelte";
@ -14,16 +15,33 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { context } from "../rich-text-input";
import FloatButtons from "./FloatButtons.svelte";
import SizeSelect from "./SizeSelect.svelte";
import WithImageConstrained from "./WithImageConstrained.svelte";
const { container, styles } = context.get();
export let maxWidth: number;
export let maxHeight: number;
const sheetPromise = styles
.addStyleTag("imageOverlay")
.then((styleObject) => styleObject.element.sheet!);
const { container } = context.get();
$: {
container.style.setProperty("--editor-shrink-max-width", `${maxWidth}px`);
container.style.setProperty("--editor-shrink-max-height", `${maxHeight}px`);
}
let activeImage: HTMLImageElement | null = null;
/**
* For element dataset attributes which work like the contenteditable attribute
*/
function isDatasetAttributeFlagSet(
element: HTMLElement | SVGElement,
attribute: string,
): boolean {
return attribute in element.dataset && element.dataset[attribute] !== "false";
}
$: isSizeConstrained = activeImage
? isDatasetAttributeFlagSet(activeImage, "editorShrink")
: false;
async function resetHandle(): Promise<void> {
activeImage = null;
await tick();
@ -166,6 +184,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
activeImage!.width = width;
}
function toggleActualSize(): void {
if (isSizeConstrained) {
delete activeImage!.dataset.editorShrink;
} else {
activeImage!.dataset.editorShrink = "true";
}
isSizeConstrained = !isSizeConstrained;
}
function clearActualSize(): void {
activeImage!.removeAttribute("width");
}
onDestroy(() => {
resizeObserver.disconnect();
container.removeEventListener("click", maybeShowHandle);
@ -173,6 +205,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
container.removeEventListener("key", resetHandle);
container.removeEventListener("paste", resetHandle);
});
let shrinkingDisabled: boolean;
$: shrinkingDisabled =
Number(actualWidth) <= maxWidth && Number(actualHeight) <= maxHeight;
let restoringDisabled: boolean;
$: restoringDisabled = !activeImage?.hasAttribute("width") ?? true;
const widthObserver = new MutationObserver(
() => (restoringDisabled = !activeImage!.hasAttribute("width")),
);
$: activeImage
? widthObserver.observe(activeImage, {
attributes: true,
attributeFilter: ["width"],
})
: widthObserver.disconnect();
</script>
<WithDropdown
@ -183,62 +233,69 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let:createDropdown
let:dropdownObject
>
{#await sheetPromise then sheet}
<WithImageConstrained
{sheet}
{#if activeImage}
<HandleSelection
bind:updateSelection
{container}
{activeImage}
maxWidth={250}
maxHeight={125}
on:update={() => {
updateSizesWithDimensions();
dropdownObject.update();
}}
let:toggleActualSize
let:active
image={activeImage}
on:mount={(event) => createDropdown(event.detail.selection)}
>
{#if activeImage}
<HandleSelection
bind:updateSelection
{container}
image={activeImage}
on:mount={(event) => createDropdown(event.detail.selection)}
>
<HandleBackground on:dblclick={toggleActualSize} />
<HandleBackground
on:dblclick={() => {
if (shrinkingDisabled) {
return;
}
toggleActualSize();
updateSizesWithDimensions();
dropdownObject.update();
}}
/>
<HandleLabel on:mount={updateDimensions}>
<span>{actualWidth}&times;{actualHeight}</span>
{#if customDimensions}
<span>(Original: {naturalWidth}&times;{naturalHeight})</span
>
{/if}
</HandleLabel>
<HandleLabel on:mount={updateDimensions}>
{#if isSizeConstrained}
<span>{tr.editingDoubleClickToExpand()}</span>
{:else}
<span>{actualWidth}&times;{actualHeight}</span>
{#if customDimensions}
<span>(Original: {naturalWidth}&times;{naturalHeight})</span>
{/if}
{/if}
</HandleLabel>
<HandleControl
{active}
activeSize={8}
offsetX={5}
offsetY={5}
on:pointerclick={(event) => {
if (active) {
setPointerCapture(event);
}
}}
on:pointermove={(event) => {
resize(event);
updateSizesWithDimensions();
dropdownObject.update();
}}
/>
</HandleSelection>
<ButtonDropdown on:click={updateSizesWithDimensions}>
<FloatButtons
image={activeImage}
on:update={dropdownObject.update}
/>
<SizeSelect {active} on:click={toggleActualSize} />
</ButtonDropdown>
{/if}
</WithImageConstrained>
{/await}
<HandleControl
active={!isSizeConstrained}
activeSize={8}
offsetX={5}
offsetY={5}
on:pointerclick={(event) => {
if (!isSizeConstrained) {
setPointerCapture(event);
}
}}
on:pointermove={(event) => {
resize(event);
updateSizesWithDimensions();
dropdownObject.update();
}}
/>
</HandleSelection>
<ButtonDropdown on:click={updateSizesWithDimensions}>
<FloatButtons image={activeImage} on:update={dropdownObject.update} />
<SizeSelect
{shrinkingDisabled}
{restoringDisabled}
{isSizeConstrained}
on:imagetoggle={() => {
toggleActualSize();
updateSizesWithDimensions();
dropdownObject.update();
}}
on:imageclear={() => {
clearActualSize();
updateSizesWithDimensions();
dropdownObject.update();
}}
/>
</ButtonDropdown>
{/if}
</WithDropdown>

View file

@ -3,29 +3,38 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { getContext } from "svelte";
import { createEventDispatcher, getContext } from "svelte";
import type { Readable } from "svelte/store";
import ButtonGroup from "../../components/ButtonGroup.svelte";
import IconButton from "../../components/IconButton.svelte";
import { directionKey } from "../../lib/context-keys";
import * as tr from "../../lib/ftl";
import { sizeActual, sizeMinimized } from "./icons";
import { sizeActual, sizeClear, sizeMinimized } from "./icons";
export let active: boolean;
export let isSizeConstrained: boolean;
export let shrinkingDisabled: boolean;
export let restoringDisabled: boolean;
$: icon = active ? sizeActual : sizeMinimized;
$: icon = isSizeConstrained ? sizeMinimized : sizeActual;
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
const dispatch = createEventDispatcher();
</script>
<ButtonGroup size={1.6}>
<IconButton
{active}
disabled={shrinkingDisabled}
flipX={$direction === "rtl"}
tooltip="{tr.editingActualSize()} ({tr.editingDoubleClickImage()})"
on:click
--border-left-radius="5px"
--border-right-radius="5px">{@html icon}</IconButton
on:click={() => dispatch("imagetoggle")}
--border-left-radius="5px">{@html icon}</IconButton
>
<IconButton
disabled={restoringDisabled}
tooltip={tr.editingRestoreOriginalSize()}
on:click={() => dispatch("imageclear")}
--border-right-radius="5px">{@html sizeClear}</IconButton
>
</ButtonGroup>

View file

@ -1,203 +0,0 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { createEventDispatcher, onDestroy } from "svelte";
import { nodeIsElement } from "../../lib/dom";
export let activeImage: HTMLImageElement | null;
export let container: HTMLElement;
export let sheet: CSSStyleSheet;
let active: boolean = false;
$: {
const index = images.indexOf(activeImage!);
if (index >= 0) {
const rule = sheet.cssRules[index] as CSSStyleRule;
active = rule.cssText.endsWith("{ }");
} else {
activeImage = null;
}
}
export let maxWidth: number;
export let maxHeight: number;
$: restrictionAspectRatio = maxWidth / maxHeight;
const dispatch = createEventDispatcher();
function createPathRecursive(tokens: string[], element: Element): string[] {
const tagName = element.tagName.toLowerCase();
if (!element.parentElement) {
const nth =
Array.prototype.indexOf.call(
(element.parentNode! as Document | ShadowRoot).children,
element,
) + 1;
return [`${tagName}:nth-child(${nth})`, ...tokens];
} else {
const nth =
Array.prototype.indexOf.call(element.parentElement.children, element) +
1;
return createPathRecursive(
[`${tagName}:nth-child(${nth})`, ...tokens],
element.parentElement,
);
}
}
function createPath(element: Element): string {
return createPathRecursive([], element).join(" > ");
}
const images: HTMLImageElement[] = [];
$: for (const [index, image] of images.entries()) {
const rule = sheet.cssRules[index] as CSSStyleRule;
rule.selectorText = createPath(image);
}
function filterImages(nodes: HTMLCollection | Node[]): HTMLImageElement[] {
const result: HTMLImageElement[] = [];
for (const node of nodes) {
if (!nodeIsElement(node)) {
continue;
}
if (node.tagName === "IMG" && !(node as HTMLElement).dataset.anki) {
result.push(node as HTMLImageElement);
} else {
result.push(...filterImages(node.children));
}
}
return result;
}
function setImageRule(image: HTMLImageElement, rule: CSSStyleRule): void {
const aspectRatio = image.naturalWidth / image.naturalHeight;
if (restrictionAspectRatio - aspectRatio > 1) {
// restricted by height
rule.style.setProperty("width", "auto", "important");
const width = Number(image.getAttribute("width")) || image.width;
const height = Number(image.getAttribute("height")) || width / aspectRatio;
rule.style.setProperty(
"height",
height < maxHeight ? `${height}px` : "auto",
"important",
);
} else {
// square or restricted by width
const width = Number(image.getAttribute("width")) || image.width;
rule.style.setProperty(
"width",
width < maxWidth ? `${width}px` : "auto",
"important",
);
rule.style.setProperty("height", "auto", "important");
}
rule.style.setProperty("max-width", `min(${maxWidth}px, 100%)`, "important");
rule.style.setProperty("max-height", `${maxHeight}px`, "important");
}
function resetImageRule(rule: CSSStyleRule): void {
rule.style.removeProperty("width");
rule.style.removeProperty("height");
rule.style.removeProperty("max-width");
rule.style.removeProperty("max-height");
}
function addImage(image: HTMLImageElement): void {
if (!container.contains(image)) {
return;
}
images.push(image);
const index = sheet.insertRule(
`${createPath(image)} {}`,
sheet.cssRules.length,
);
const rule = sheet.cssRules[index] as CSSStyleRule;
setImageRule(image, rule);
}
function addImageOnLoad(image: HTMLImageElement): void {
if (image.complete && image.naturalWidth > 0 && image.naturalHeight > 0) {
addImage(image);
} else {
image.addEventListener("load", () => addImage(image));
}
}
function removeImage(image: HTMLImageElement): void {
const index = images.indexOf(image);
if (index < 0) {
return;
}
images.splice(index, 1);
sheet.deleteRule(index);
}
const mutationObserver = new MutationObserver((mutations) => {
const addedImages = mutations.flatMap((mutation) =>
filterImages([...mutation.addedNodes]),
);
for (const image of addedImages) {
addImageOnLoad(image);
}
const removedImages = mutations.flatMap((mutation) =>
filterImages([...mutation.removedNodes]),
);
for (const image of removedImages) {
removeImage(image);
}
});
mutationObserver.observe(container, {
childList: true,
subtree: true,
});
for (const image of filterImages([...container.childNodes])) {
addImageOnLoad(image);
}
onDestroy(() => mutationObserver.disconnect());
export function toggleActualSize() {
const index = images.indexOf(activeImage!);
if (index === -1) {
return;
}
const rule = sheet.cssRules[index] as CSSStyleRule;
active = !active;
if (active) {
resetImageRule(rule);
} else {
setImageRule(activeImage!, rule);
}
dispatch("update", active);
}
</script>
{#if activeImage}
<slot {toggleActualSize} {active} />
{/if}

View file

@ -4,5 +4,6 @@
export { default as floatLeftIcon } from "@mdi/svg/svg/format-float-left.svg";
export { default as floatNoneIcon } from "@mdi/svg/svg/format-float-none.svg";
export { default as floatRightIcon } from "@mdi/svg/svg/format-float-right.svg";
export { default as sizeClear } from "@mdi/svg/svg/image-remove.svg";
export { default as sizeActual } from "@mdi/svg/svg/image-size-select-actual.svg";
export { default as sizeMinimized } from "@mdi/svg/svg/image-size-select-large.svg";

View file

@ -10,6 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import IconButton from "../../components/IconButton.svelte";
import { hasBlockAttribute } from "../../lib/dom";
import * as tr from "../../lib/ftl";
import ClozeButtons from "../ClozeButtons.svelte";
import { blockIcon, deleteIcon, inlineIcon } from "./icons";
export let element: Element;
@ -48,6 +49,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
>
</ButtonGroup>
<ClozeButtons on:surround />
<ButtonGroup>
<IconButton
tooltip={tr.actionsDelete()}

View file

@ -99,6 +99,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/>
</div>
<slot editor={codeMirror} />
<style lang="scss">
.mathjax-editor {
:global(.CodeMirror) {

View file

@ -47,21 +47,28 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
on:blur={() => dispatch("reset")}
on:moveoutstart
on:moveoutend
/>
let:editor={mathjaxEditor}
>
<Shortcut
keyCombination={acceptShortcut}
on:action={() => dispatch("moveoutend")}
/>
<Shortcut
keyCombination={acceptShortcut}
on:action={() => dispatch("moveoutend")}
/>
<MathjaxButtons
{element}
on:delete={() => {
placeCaretAfter(element);
element.remove();
dispatch("reset");
}}
on:surround={async ({ detail }) => {
const editor = await mathjaxEditor.editor;
const { prefix, suffix } = detail;
<MathjaxButtons
{element}
on:delete={() => {
placeCaretAfter(element);
element.remove();
dispatch("reset");
}}
/>
editor.replaceSelection(prefix + editor.getSelection() + suffix);
}}
/>
</MathjaxEditor>
</DropdownMenu>
</div>

View file

@ -6,7 +6,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { registerPackage } from "../../lib/runtime-require";
import lifecycleHooks from "../../sveltelib/lifecycle-hooks";
import type { CodeMirrorAPI } from "../CodeMirror.svelte";
import type { EditingInputAPI } from "../EditingArea.svelte";
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
export interface PlainTextInputAPI extends EditingInputAPI {
name: "plain-text";
@ -39,6 +39,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import removeProhibitedTags from "./remove-prohibited";
import { storedToUndecorated, undecoratedToStored } from "./transform";
export let hidden: boolean;
const configuration = {
mode: htmlanki,
...baseOptions,
@ -46,7 +48,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
};
const { focusedInput } = noteEditorContext.get();
const { editingInputs, content } = editingAreaContext.get();
const code = writable($content);
@ -70,13 +71,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
moveCaretToEnd();
}
export let hidden = false;
function toggle(): boolean {
hidden = !hidden;
return hidden;
}
async function getInputAPI(target: EventTarget): Promise<FocusableInputAPI | null> {
const editor = (await codeMirror.editor) as any;
if (target === editor.display.input.textarea) {
return api;
}
return null;
}
export const api: PlainTextInputAPI = {
name: "plain-text",
focus,
@ -84,6 +93,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
moveCaretToEnd,
refocus,
toggle,
getInputAPI,
codeMirror,
};

View file

@ -10,7 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { InputHandlerAPI } from "../../sveltelib/input-handler";
import useInputHandler from "../../sveltelib/input-handler";
import { pageTheme } from "../../sveltelib/theme";
import type { EditingInputAPI } from "../EditingArea.svelte";
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
import type CustomStyles from "./CustomStyles.svelte";
export interface RichTextInputAPI extends EditingInputAPI {
@ -92,6 +92,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
return hidden;
}
const className = "rich-text-editable";
let richTextDiv: HTMLElement;
async function getInputAPI(target: EventTarget): Promise<FocusableInputAPI | null> {
if (target === richTextDiv) {
return api;
}
return null;
}
export const api: RichTextInputAPI = {
name: "rich-text",
element: richTextPromise,
@ -99,6 +110,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
refocus,
focusable: !hidden,
toggle,
getInputAPI,
moveCaretToEnd,
preventResubscription,
inputHandler,
@ -155,7 +167,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let:stylesDidLoad
>
<div
class="rich-text-editable"
bind:this={richTextDiv}
class={className}
class:hidden
class:night-mode={$pageTheme.isDark}
use:attachShadow

View file

@ -243,19 +243,6 @@ export class Surrounder {
}
}
/**
* @returns True, if element has no style attribute (anymore).
*/
export function removeEmptyStyle(element: HTMLElement | SVGElement): boolean {
if (element.style.cssText.length === 0) {
element.removeAttribute("style");
// Calling `.hasAttribute` right after `.removeAttribute` might return true.
return true;
}
return false;
}
registerPackage("anki/surround", {
Surrounder,
});

View file

@ -346,7 +346,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
function selectAllTags() {
tagTypes.forEach((tag) => (tag.selected = true));
for (const tag of tagTypes) {
tag.selected = true;
}
tagTypes = tagTypes;
}
@ -451,7 +454,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
splitTag(index, detail.start, detail.end)}
on:tagadd={() => insertTagKeepFocus(index)}
on:tagdelete={() => deleteTagAt(index)}
on:tagselectall={selectAllTags}
on:tagselectall={async () => {
if (tagTypes.length <= 1) {
// Noop if no other tags exist
return;
}
activeInput.blur();
// Ensure blur events are processed first
await tick();
selectAllTags();
}}
on:tagjoinprevious={() => joinWithPreviousTag(index)}
on:tagjoinnext={() => joinWithNextTag(index)}
on:tagmoveprevious={() => moveToPreviousTag(index)}

View file

@ -231,13 +231,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
name = last;
}
async function onSelectAll(event: KeyboardEvent) {
function onSelectAll(event: KeyboardEvent) {
if (name.length === 0) {
input.blur();
await tick(); // ensure blur events are processed before tagselectall
dispatch("tagselectall");
event.preventDefault();
event.stopPropagation();
dispatch("tagselectall");
}
}

View file

@ -39,7 +39,7 @@ export function gatherData(data: Stats.GraphsResponse): GraphData {
};
let haveBacklog = false;
const due = (data.cards as Cards.Card[])
.filter((c: Cards.Card) => c.queue >= 0)
.filter((c: Cards.Card) => c.queue > 0)
.map((c: Cards.Card) => {
// - testing just odue fails on day 1
// - testing just odid fails on lapsed cards that

View file

@ -54,7 +54,7 @@ function filterSpan(element: Element): void {
const tagsAllowedBasic: TagsAllowed = {
BR: allowNone,
IMG: allow(["SRC"]),
IMG: allow(["SRC", "ALT"]),
DIV: allowNone,
P: allowNone,
SUB: allowNone,

31
ts/lib/styling.ts Normal file
View file

@ -0,0 +1,31 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/**
* @returns True, if element has no style attribute (anymore).
*/
function removeEmptyStyle(element: HTMLElement | SVGElement): boolean {
if (element.style.cssText.length === 0) {
element.removeAttribute("style");
// Calling `.hasAttribute` right after `.removeAttribute` might return true.
return true;
}
return false;
}
/**
* Will remove the style attribute, if all properties were removed.
*
* @returns True, if element has no style attributes anymore
*/
export function removeStyleProperties(
element: HTMLElement | SVGElement,
...props: string[]
): boolean {
for (const prop of props) {
element.style.removeProperty(prop);
}
return removeEmptyStyle(element);
}

View file

@ -410,14 +410,6 @@
"path": "node_modules/commander",
"licenseFile": "node_modules/commander/LICENSE"
},
"commander@9.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"
},
"concat-map@0.0.1": {
"licenses": "MIT",
"repository": "https://github.com/substack/node-concat-map",
@ -875,14 +867,6 @@
"path": "node_modules/eslint",
"licenseFile": "node_modules/eslint/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"
},
"espree@7.3.1": {
"licenses": "BSD-2-Clause",
"repository": "https://github.com/eslint/espree",
@ -1437,25 +1421,12 @@
"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.2.0": {
"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"
},
"minimatch@3.1.2": {
"licenses": "ISC",
"repository": "https://github.com/isaacs/minimatch",
@ -1474,12 +1445,6 @@
"path": "node_modules/minimist",
"licenseFile": "node_modules/minimist/LICENSE"
},
"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"
},
"ms@2.0.0": {
"licenses": "MIT",
"repository": "https://github.com/zeit/ms",
@ -1765,12 +1730,6 @@
"path": "node_modules/slice-ansi",
"licenseFile": "node_modules/slice-ansi/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"
},
"sprintf-js@1.0.3": {
"licenses": "BSD-3-Clause",
"repository": "https://github.com/alexei/sprintf.js",
@ -1858,7 +1817,7 @@
"path": "node_modules/supports-preserve-symlinks-flag",
"licenseFile": "node_modules/supports-preserve-symlinks-flag/LICENSE"
},
"svelte@3.46.4": {
"svelte@3.48.0": {
"licenses": "MIT",
"repository": "https://github.com/sveltejs/svelte",
"publisher": "Rich Harris",
@ -1948,13 +1907,6 @@
"path": "node_modules/which",
"licenseFile": "node_modules/which/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"
},
"word-wrap@1.2.3": {
"licenses": "MIT",
"repository": "https://github.com/jonschlinkert/word-wrap",
@ -1972,15 +1924,6 @@
"path": "node_modules/wrappy",
"licenseFile": "node_modules/wrappy/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"
},
"yallist@4.0.0": {
"licenses": "ISC",
"repository": "https://github.com/isaacs/yallist",

24
ts/mathjax/BUILD.bazel Normal file
View file

@ -0,0 +1,24 @@
load("//ts:prettier.bzl", "prettier_test")
load("//ts:eslint.bzl", "eslint_test")
load("//ts:esbuild.bzl", "esbuild")
load("//ts:typescript.bzl", "typescript")
typescript(name = "mathjax_ts")
_esbuild_deps = [
":mathjax_ts",
]
esbuild(
name = "mathjax",
entry_point = "index.ts",
visibility = ["//visibility:public"],
deps = _esbuild_deps,
)
# Tests
################
prettier_test()
eslint_test()

31
ts/mathjax/index.ts Normal file
View file

@ -0,0 +1,31 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/// <reference types="./mathjax-types" />
const packages = ["noerrors", "mathtools", "mhchem"];
function packagesForLoading(packages: string[]): string[] {
return packages.map((value: string): string => `[tex]/${value}`);
}
window.MathJax = {
tex: {
displayMath: [["\\[", "\\]"]],
processEscapes: false,
processEnvironments: false,
processRefs: false,
packages: {
"[+]": packages,
},
},
loader: {
load: packagesForLoading(packages),
paths: {
mathjax: "/_anki/js/vendor/mathjax",
},
},
startup: {
typeset: false,
},
};

15
ts/mathjax/mathjax-types.d.ts vendored Normal file
View file

@ -0,0 +1,15 @@
// 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",
*/
export {};
declare global {
interface Window {
// Mathjax does not provide a full type
MathJax: { [name: string]: any };
}
}

5
ts/mathjax/tsconfig.json Normal file
View file

@ -0,0 +1,5 @@
{
"extends": "../tsconfig.json",
"include": ["*"],
"references": []
}

View file

@ -11,6 +11,7 @@
{ "path": "html-filter" },
{ "path": "reviewer" },
{ "path": "lib" },
{ "path": "mathjax" },
{ "path": "domlib" },
{ "path": "sveltelib" },
{ "path": "icons" }

View file

@ -1699,11 +1699,6 @@ 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 "9.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-9.1.0.tgz#a6b263b2327f2e188c6402c42623327909f2dbec"
integrity sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -2402,11 +2397,6 @@ 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"
@ -3743,16 +3733,6 @@ marked@^4.0.0, marked@^4.0.10:
resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.12.tgz#2262a4e6fd1afd2f13557726238b69a48b982f7d"
integrity sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==
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.2.0"
resolved "https://registry.yarnpkg.com/mathjax/-/mathjax-3.2.0.tgz#ea7c317f8c81776ecfc83b294fc313046bf7f726"
@ -3773,11 +3753,6 @@ merge2@^1.3.0, merge2@^1.4.1:
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"
@ -3820,11 +3795,6 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
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@^0.5.1:
version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
@ -4626,15 +4596,6 @@ spdx-satisfies@^5.0.1:
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"
@ -4784,9 +4745,9 @@ svelte2tsx@^0.4.6:
pascal-case "^3.1.1"
svelte@^3.25.0:
version "3.46.4"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.46.4.tgz#0c46bc4a3e20a2617a1b7dc43a722f9d6c084a38"
integrity sha512-qKJzw6DpA33CIa+C/rGp4AUdSfii0DOTCzj/2YpSKKayw5WGSS624Et9L1nU1k2OVRS9vaENQXp2CVZNU+xvIg==
version "3.48.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.48.0.tgz#f98c866d45e155bad8e1e88f15f9c03cd28753d3"
integrity sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ==
symbol-tree@^3.2.4:
version "3.2.4"
@ -5084,11 +5045,6 @@ 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"
@ -5136,11 +5092,6 @@ xmlcreate@^2.0.4:
resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be"
integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==
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"