Fix plain text (#1689)
* Move remove-prohibited-tags logic from PlainTextInput * Make CodeMirror export a promise * Offer lifecycle hooks for PlainTextInput for easily accessing code mirror instance * Fix </> breaking Mathjax * remove debug statement (dae)
This commit is contained in:
parent
7c27159149
commit
50e36bc312
@ -38,13 +38,37 @@ export interface DecoratedElementConstructor
|
|||||||
toUndecorated(stored: string): string;
|
toUndecorated(stored: string): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CustomElementArray<
|
export class CustomElementArray extends Array<DecoratedElementConstructor> {
|
||||||
T extends CustomElementConstructor & WithTagName,
|
push(...elements: DecoratedElementConstructor[]): number {
|
||||||
> extends Array<T> {
|
|
||||||
push(...elements: T[]): number {
|
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
customElements.define(element.tagName, element);
|
customElements.define(element.tagName, element);
|
||||||
}
|
}
|
||||||
return super.push(...elements);
|
return super.push(...elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms any decorated elements in input HTML from undecorated to stored state.
|
||||||
|
*/
|
||||||
|
toStored(html: string): string {
|
||||||
|
let result = html;
|
||||||
|
|
||||||
|
for (const element of this) {
|
||||||
|
result = element.toStored(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms any decorated elements in input HTML from stored to undecorated state.
|
||||||
|
*/
|
||||||
|
toUndecorated(html: string): string {
|
||||||
|
let result = html;
|
||||||
|
|
||||||
|
for (const element of this) {
|
||||||
|
result = element.toUndecorated(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,14 @@ const mathjaxTagPattern =
|
|||||||
const mathjaxBlockDelimiterPattern = /\\\[(.*?)\\\]/gsu;
|
const mathjaxBlockDelimiterPattern = /\\\[(.*?)\\\]/gsu;
|
||||||
const mathjaxInlineDelimiterPattern = /\\\((.*?)\\\)/gsu;
|
const mathjaxInlineDelimiterPattern = /\\\((.*?)\\\)/gsu;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the user enters the Mathjax with delimiters, "<" and ">" will
|
||||||
|
* be first translated to entities.
|
||||||
|
*/
|
||||||
|
function translateEntitiesToMathjax(value: string) {
|
||||||
|
return value.replace(/</g, "{\\lt}").replace(/>/g, "{\\gt}");
|
||||||
|
}
|
||||||
|
|
||||||
export const Mathjax: DecoratedElementConstructor = class Mathjax
|
export const Mathjax: DecoratedElementConstructor = class Mathjax
|
||||||
extends HTMLElement
|
extends HTMLElement
|
||||||
implements DecoratedElement
|
implements DecoratedElement
|
||||||
@ -22,7 +30,7 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
|
|||||||
static tagName = "anki-mathjax";
|
static tagName = "anki-mathjax";
|
||||||
|
|
||||||
static toStored(undecorated: string): string {
|
static toStored(undecorated: string): string {
|
||||||
return undecorated.replace(
|
const stored = undecorated.replace(
|
||||||
mathjaxTagPattern,
|
mathjaxTagPattern,
|
||||||
(_match: string, block: string | undefined, text: string) => {
|
(_match: string, block: string | undefined, text: string) => {
|
||||||
return typeof block === "string" && block !== "false"
|
return typeof block === "string" && block !== "false"
|
||||||
@ -30,20 +38,20 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
|
|||||||
: `\\(${text}\\)`;
|
: `\\(${text}\\)`;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return stored;
|
||||||
}
|
}
|
||||||
|
|
||||||
static toUndecorated(stored: string): string {
|
static toUndecorated(stored: string): string {
|
||||||
return stored
|
return stored
|
||||||
.replace(
|
.replace(mathjaxBlockDelimiterPattern, (_match: string, text: string) => {
|
||||||
mathjaxBlockDelimiterPattern,
|
const escaped = translateEntitiesToMathjax(text);
|
||||||
(_match: string, text: string) =>
|
return `<${Mathjax.tagName} block="true">${escaped}</${Mathjax.tagName}>`;
|
||||||
`<${Mathjax.tagName} block="true">${text}</${Mathjax.tagName}>`,
|
})
|
||||||
)
|
.replace(mathjaxInlineDelimiterPattern, (_match: string, text: string) => {
|
||||||
.replace(
|
const escaped = translateEntitiesToMathjax(text);
|
||||||
mathjaxInlineDelimiterPattern,
|
return `<${Mathjax.tagName}>${escaped}</${Mathjax.tagName}>`;
|
||||||
(_match: string, text: string) =>
|
});
|
||||||
`<${Mathjax.tagName}>${text}</${Mathjax.tagName}>`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
block = false;
|
block = false;
|
||||||
@ -76,6 +84,9 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "data-mathjax":
|
case "data-mathjax":
|
||||||
|
if (!newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.component?.$set({ mathjax: newValue });
|
this.component?.$set({ mathjax: newValue });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -3,92 +3,84 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
import { CodeMirror as CodeMirrorLib } from "./code-mirror";
|
import type CodeMirrorLib from "codemirror";
|
||||||
|
|
||||||
export interface CodeMirrorAPI {
|
export interface CodeMirrorAPI {
|
||||||
readonly editor: CodeMirrorLib.EditorFromTextArea;
|
readonly editor: Promise<CodeMirrorLib.Editor>;
|
||||||
|
setOption<T extends keyof CodeMirrorLib.EditorConfiguration>(
|
||||||
|
key: T,
|
||||||
|
value: CodeMirrorLib.EditorConfiguration[T],
|
||||||
|
): Promise<void>;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
import { createEventDispatcher, getContext, onMount } from "svelte";
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
|
|
||||||
import { directionKey } from "../lib/context-keys";
|
import { directionKey } from "../lib/context-keys";
|
||||||
import storeSubscribe from "../sveltelib/store-subscribe";
|
import { promiseWithResolver } from "../lib/promise";
|
||||||
import { pageTheme } from "../sveltelib/theme";
|
import { pageTheme } from "../sveltelib/theme";
|
||||||
import { darkCodeMirrorTheme, lightCodeMirrorTheme } from "./code-mirror";
|
import {
|
||||||
|
darkTheme,
|
||||||
|
lightTheme,
|
||||||
|
openCodeMirror,
|
||||||
|
setupCodeMirror,
|
||||||
|
} from "./code-mirror";
|
||||||
|
|
||||||
export let configuration: CodeMirrorLib.EditorConfiguration;
|
export let configuration: CodeMirrorLib.EditorConfiguration;
|
||||||
export let code: Writable<string>;
|
export let code: Writable<string>;
|
||||||
|
|
||||||
const direction = getContext<Writable<"ltr" | "rtl">>(directionKey);
|
|
||||||
const defaultConfiguration = {
|
const defaultConfiguration = {
|
||||||
direction: $direction,
|
|
||||||
rtlMoveVisually: true,
|
rtlMoveVisually: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let codeMirror: CodeMirrorLib.EditorFromTextArea;
|
const [editorPromise, resolve] = promiseWithResolver<CodeMirrorLib.Editor>();
|
||||||
$: codeMirror?.setOption("direction", $direction);
|
|
||||||
|
|
||||||
function setValue(content: string): void {
|
/**
|
||||||
codeMirror.setValue(content);
|
* Convenience function for editor.setOption.
|
||||||
|
*/
|
||||||
|
function setOption<T extends keyof CodeMirrorLib.EditorConfiguration>(
|
||||||
|
key: T,
|
||||||
|
value: CodeMirrorLib.EditorConfiguration[T],
|
||||||
|
): void {
|
||||||
|
editorPromise.then((editor) => editor.setOption(key, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { subscribe, unsubscribe } = storeSubscribe(code, setValue, false);
|
const direction = getContext<Writable<"ltr" | "rtl">>(directionKey);
|
||||||
|
|
||||||
|
$: setOption("direction", $direction);
|
||||||
|
$: setOption("theme", $pageTheme.isDark ? darkTheme : lightTheme);
|
||||||
|
|
||||||
|
let apiPartial: Partial<CodeMirrorAPI>;
|
||||||
|
export { apiPartial as api };
|
||||||
|
|
||||||
|
Object.assign(apiPartial, {
|
||||||
|
editor: editorPromise,
|
||||||
|
setOption,
|
||||||
|
});
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
function openCodeMirror(textarea: HTMLTextAreaElement): void {
|
onMount(() =>
|
||||||
codeMirror = CodeMirrorLib.fromTextArea(textarea, {
|
editorPromise.then((editor) => {
|
||||||
...defaultConfiguration,
|
setupCodeMirror(editor, code);
|
||||||
...configuration,
|
editor.on("change", () => dispatch("change", editor.getValue()));
|
||||||
});
|
editor.on("focus", () => dispatch("focus"));
|
||||||
|
editor.on("blur", () => dispatch("blur"));
|
||||||
// TODO passing in the tabindex option does not do anything: bug?
|
}),
|
||||||
codeMirror.getInputField().tabIndex = 0;
|
|
||||||
|
|
||||||
let ranges: CodeMirrorLib.Range[] | null = null;
|
|
||||||
|
|
||||||
codeMirror.on("change", () => dispatch("change", codeMirror.getValue()));
|
|
||||||
codeMirror.on("mousedown", () => {
|
|
||||||
ranges = null;
|
|
||||||
});
|
|
||||||
codeMirror.on("focus", () => {
|
|
||||||
if (ranges) {
|
|
||||||
try {
|
|
||||||
codeMirror.setSelections(ranges);
|
|
||||||
} catch {
|
|
||||||
ranges = null;
|
|
||||||
codeMirror.setCursor(codeMirror.lineCount(), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unsubscribe();
|
|
||||||
dispatch("focus");
|
|
||||||
});
|
|
||||||
codeMirror.on("blur", () => {
|
|
||||||
ranges = codeMirror.listSelections();
|
|
||||||
subscribe();
|
|
||||||
dispatch("blur");
|
|
||||||
});
|
|
||||||
|
|
||||||
subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
$: codeMirror?.setOption(
|
|
||||||
"theme",
|
|
||||||
$pageTheme.isDark ? darkCodeMirrorTheme : lightCodeMirrorTheme,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const api = Object.create(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
editor: { get: () => codeMirror },
|
|
||||||
},
|
|
||||||
) as CodeMirrorAPI;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="code-mirror">
|
<div class="code-mirror">
|
||||||
<textarea tabindex="-1" hidden use:openCodeMirror />
|
<textarea
|
||||||
|
tabindex="-1"
|
||||||
|
hidden
|
||||||
|
use:openCodeMirror={{
|
||||||
|
configuration: { ...configuration, ...defaultConfiguration },
|
||||||
|
resolve,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -3,17 +3,13 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
import {
|
import { CustomElementArray } from "../editable/decorated";
|
||||||
CustomElementArray,
|
|
||||||
DecoratedElementConstructor,
|
|
||||||
} from "../editable/decorated";
|
|
||||||
import contextProperty from "../sveltelib/context-property";
|
import contextProperty from "../sveltelib/context-property";
|
||||||
|
|
||||||
const decoratedElements = new CustomElementArray<DecoratedElementConstructor>();
|
const decoratedElements = new CustomElementArray();
|
||||||
|
|
||||||
const key = Symbol("decoratedElements");
|
const key = Symbol("decoratedElements");
|
||||||
const [context, setContextProperty] =
|
const [context, setContextProperty] = contextProperty<CustomElementArray>(key);
|
||||||
contextProperty<CustomElementArray<DecoratedElementConstructor>>(key);
|
|
||||||
|
|
||||||
export { context };
|
export { context };
|
||||||
</script>
|
</script>
|
||||||
|
@ -149,7 +149,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
// TODO this is a hack, because it requires the NoteEditor to know implementation details of the PlainTextInput.
|
// TODO this is a hack, because it requires the NoteEditor to know implementation details of the PlainTextInput.
|
||||||
// It should be refactored once we work on our own Undo stack
|
// It should be refactored once we work on our own Undo stack
|
||||||
for (const pi of plainTextInputs) {
|
for (const pi of plainTextInputs) {
|
||||||
pi.api.getEditor().clearHistory();
|
pi.api.codeMirror.editor.then((editor) => editor.clearHistory());
|
||||||
}
|
}
|
||||||
noteId = ntid;
|
noteId = ntid;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,9 @@ import "codemirror/addon/edit/closetag";
|
|||||||
import "codemirror/addon/display/placeholder";
|
import "codemirror/addon/display/placeholder";
|
||||||
|
|
||||||
import CodeMirror from "codemirror";
|
import CodeMirror from "codemirror";
|
||||||
|
import type { Readable } from "svelte/store";
|
||||||
|
|
||||||
|
import storeSubscribe from "../sveltelib/store-subscribe";
|
||||||
|
|
||||||
export { CodeMirror };
|
export { CodeMirror };
|
||||||
|
|
||||||
@ -29,11 +32,11 @@ export const htmlanki = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const lightCodeMirrorTheme = "default";
|
export const lightTheme = "default";
|
||||||
export const darkCodeMirrorTheme = "monokai";
|
export const darkTheme = "monokai";
|
||||||
|
|
||||||
export const baseOptions: CodeMirror.EditorConfiguration = {
|
export const baseOptions: CodeMirror.EditorConfiguration = {
|
||||||
theme: lightCodeMirrorTheme,
|
theme: lightTheme,
|
||||||
lineWrapping: true,
|
lineWrapping: true,
|
||||||
matchTags: { bothTags: true },
|
matchTags: { bothTags: true },
|
||||||
autoCloseTags: true,
|
autoCloseTags: true,
|
||||||
@ -53,3 +56,73 @@ export function focusAndCaretAfter(editor: CodeMirror.Editor): void {
|
|||||||
editor.focus();
|
editor.focus();
|
||||||
editor.setCursor(editor.lineCount(), 0);
|
editor.setCursor(editor.lineCount(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OpenCodeMirrorOptions {
|
||||||
|
configuration: CodeMirror.EditorConfiguration;
|
||||||
|
resolve(editor: CodeMirror.EditorFromTextArea): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openCodeMirror(
|
||||||
|
textarea: HTMLTextAreaElement,
|
||||||
|
{ configuration, resolve }: Partial<OpenCodeMirrorOptions>,
|
||||||
|
): { update: (options: Partial<OpenCodeMirrorOptions>) => void; destroy: () => void } {
|
||||||
|
const editor = CodeMirror.fromTextArea(textarea, configuration);
|
||||||
|
resolve?.(editor);
|
||||||
|
|
||||||
|
return {
|
||||||
|
update({ configuration }: Partial<OpenCodeMirrorOptions>): void {
|
||||||
|
for (const key in configuration) {
|
||||||
|
editor.setOption(
|
||||||
|
key as keyof CodeMirror.EditorConfiguration,
|
||||||
|
configuration[key],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroy(): void {
|
||||||
|
editor.toTextArea();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the contract with the code store and location restoration.
|
||||||
|
*/
|
||||||
|
export function setupCodeMirror(
|
||||||
|
editor: CodeMirror.Editor,
|
||||||
|
code: Readable<string>,
|
||||||
|
): void {
|
||||||
|
const { subscribe, unsubscribe } = storeSubscribe(
|
||||||
|
code,
|
||||||
|
(value: string): void => editor.setValue(value),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO passing in the tabindex option does not do anything: bug?
|
||||||
|
editor.getInputField().tabIndex = 0;
|
||||||
|
|
||||||
|
let ranges: CodeMirror.Range[] | null = null;
|
||||||
|
|
||||||
|
editor.on("focus", () => {
|
||||||
|
if (ranges) {
|
||||||
|
try {
|
||||||
|
editor.setSelections(ranges);
|
||||||
|
} catch {
|
||||||
|
ranges = null;
|
||||||
|
editor.setCursor(editor.lineCount(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.on("mousedown", () => {
|
||||||
|
// Prevent focus restoring location
|
||||||
|
ranges = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.on("blur", () => {
|
||||||
|
ranges = editor.listSelections();
|
||||||
|
subscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
subscribe();
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type CodeMirrorLib from "codemirror";
|
||||||
import { createEventDispatcher, onMount } from "svelte";
|
import { createEventDispatcher, onMount } from "svelte";
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
|
|
||||||
@ -14,14 +15,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
import CodeMirror from "../CodeMirror.svelte";
|
import CodeMirror from "../CodeMirror.svelte";
|
||||||
|
|
||||||
export let code: Writable<string>;
|
export let code: Writable<string>;
|
||||||
|
|
||||||
export let acceptShortcut: string;
|
export let acceptShortcut: string;
|
||||||
export let newlineShortcut: string;
|
export let newlineShortcut: string;
|
||||||
|
|
||||||
const configuration = {
|
const configuration = {
|
||||||
...Object.assign({}, baseOptions, {
|
...Object.assign({}, baseOptions, {
|
||||||
extraKeys: {
|
extraKeys: {
|
||||||
...(baseOptions.extraKeys as CodeMirror.KeyMap),
|
...(baseOptions.extraKeys as CodeMirrorLib.KeyMap),
|
||||||
[acceptShortcut]: noop,
|
[acceptShortcut]: noop,
|
||||||
[newlineShortcut]: noop,
|
[newlineShortcut]: noop,
|
||||||
},
|
},
|
||||||
@ -37,28 +37,35 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let codeMirror: CodeMirrorAPI = {} as CodeMirrorAPI;
|
let codeMirror = {} as CodeMirrorAPI;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() =>
|
||||||
focusAndCaretAfter(codeMirror.editor);
|
codeMirror.editor.then((editor) => {
|
||||||
|
focusAndCaretAfter(editor);
|
||||||
|
|
||||||
if (selectAll) {
|
if (selectAll) {
|
||||||
codeMirror.editor.execCommand("selectAll");
|
editor.execCommand("selectAll");
|
||||||
}
|
}
|
||||||
|
|
||||||
let direction: "start" | "end" | undefined = undefined;
|
let direction: "start" | "end" | undefined = undefined;
|
||||||
|
|
||||||
codeMirror.editor.on("keydown", (_instance, event: KeyboardEvent) => {
|
editor.on(
|
||||||
|
"keydown",
|
||||||
|
(_instance: CodeMirrorLib.Editor, event: KeyboardEvent): void => {
|
||||||
if (event.key === "ArrowLeft") {
|
if (event.key === "ArrowLeft") {
|
||||||
direction = "start";
|
direction = "start";
|
||||||
} else if (event.key === "ArrowRight") {
|
} else if (event.key === "ArrowRight") {
|
||||||
direction = "end";
|
direction = "end";
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
codeMirror.editor.on(
|
editor.on(
|
||||||
"beforeSelectionChange",
|
"beforeSelectionChange",
|
||||||
(instance, obj: CodeMirror.EditorSelectionChange) => {
|
(
|
||||||
|
instance: CodeMirrorLib.Editor,
|
||||||
|
obj: CodeMirrorLib.EditorSelectionChange,
|
||||||
|
): void => {
|
||||||
const { anchor } = obj.ranges[0];
|
const { anchor } = obj.ranges[0];
|
||||||
|
|
||||||
if (anchor["hitSide"]) {
|
if (anchor["hitSide"]) {
|
||||||
@ -76,7 +83,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
direction = undefined;
|
direction = undefined;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape characters which are technically legal in Mathjax, but confuse HTML.
|
||||||
|
*/
|
||||||
|
export function escapeSomeEntities(value: string): string {
|
||||||
|
return value.replace(/</g, "{\\lt}").replace(/>/g, "{\\gt}");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mathjax-editor">
|
<div class="mathjax-editor">
|
||||||
@ -84,7 +99,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
{code}
|
{code}
|
||||||
{configuration}
|
{configuration}
|
||||||
bind:api={codeMirror}
|
bind:api={codeMirror}
|
||||||
on:change={({ detail }) => code.set(detail)}
|
on:change={({ detail }) => code.set(escapeSomeEntities(detail))}
|
||||||
on:blur
|
on:blur
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -132,13 +132,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithDropdown
|
<WithDropdown drop="down" autoOpen autoClose={false} distance={4} let:createDropdown>
|
||||||
drop="down"
|
|
||||||
autoOpen={true}
|
|
||||||
autoClose={false}
|
|
||||||
distance={4}
|
|
||||||
let:createDropdown
|
|
||||||
>
|
|
||||||
{#if activeImage && mathjaxElement}
|
{#if activeImage && mathjaxElement}
|
||||||
<MathjaxMenu
|
<MathjaxMenu
|
||||||
element={mathjaxElement}
|
element={mathjaxElement}
|
||||||
|
@ -3,17 +3,27 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
import type { CodeMirror as CodeMirrorType } from "../code-mirror";
|
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 } from "../EditingArea.svelte";
|
||||||
|
|
||||||
export interface PlainTextInputAPI extends EditingInputAPI {
|
export interface PlainTextInputAPI extends EditingInputAPI {
|
||||||
name: "plain-text";
|
name: "plain-text";
|
||||||
moveCaretToEnd(): void;
|
moveCaretToEnd(): void;
|
||||||
toggle(): boolean;
|
toggle(): boolean;
|
||||||
getEditor(): CodeMirrorType.Editor;
|
codeMirror: CodeMirrorAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parsingInstructions: string[] = [];
|
export const parsingInstructions: string[] = [];
|
||||||
|
|
||||||
|
const [lifecycle, instances, setupLifecycleHooks] =
|
||||||
|
lifecycleHooks<PlainTextInputAPI>();
|
||||||
|
|
||||||
|
registerPackage("anki/PlainTextInput", {
|
||||||
|
lifecycle,
|
||||||
|
instances,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -22,11 +32,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
import { pageTheme } from "../../sveltelib/theme";
|
import { pageTheme } from "../../sveltelib/theme";
|
||||||
import { baseOptions, gutterOptions, htmlanki } from "../code-mirror";
|
import { baseOptions, gutterOptions, htmlanki } from "../code-mirror";
|
||||||
import type { CodeMirrorAPI } from "../CodeMirror.svelte";
|
|
||||||
import CodeMirror from "../CodeMirror.svelte";
|
import CodeMirror from "../CodeMirror.svelte";
|
||||||
import { context as decoratedElementsContext } from "../DecoratedElements.svelte";
|
import { context as decoratedElementsContext } from "../DecoratedElements.svelte";
|
||||||
import { context as editingAreaContext } from "../EditingArea.svelte";
|
import { context as editingAreaContext } from "../EditingArea.svelte";
|
||||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||||
|
import removeProhibitedTags from "./remove-prohibited";
|
||||||
|
|
||||||
export let hidden = false;
|
export let hidden = false;
|
||||||
|
|
||||||
@ -42,62 +52,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
const decoratedElements = decoratedElementsContext.get();
|
const decoratedElements = decoratedElementsContext.get();
|
||||||
const code = writable($content);
|
const code = writable($content);
|
||||||
|
|
||||||
function adjustInputHTML(html: string): string {
|
|
||||||
for (const component of decoratedElements) {
|
|
||||||
html = component.toUndecorated(html);
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parser = new DOMParser();
|
|
||||||
|
|
||||||
function removeTag(element: HTMLElement, tagName: string): void {
|
|
||||||
for (const elem of element.getElementsByTagName(tagName)) {
|
|
||||||
elem.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDummyDoc(html: string): string {
|
|
||||||
return (
|
|
||||||
"<html><head></head><body>" +
|
|
||||||
parsingInstructions.join("") +
|
|
||||||
html +
|
|
||||||
"</body>"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseAsHTML(html: string): string {
|
|
||||||
const doc = parser.parseFromString(createDummyDoc(html), "text/html");
|
|
||||||
const body = doc.body;
|
|
||||||
|
|
||||||
removeTag(body, "script");
|
|
||||||
removeTag(body, "link");
|
|
||||||
removeTag(body, "style");
|
|
||||||
|
|
||||||
return doc.body.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
function adjustOutputHTML(html: string): string {
|
|
||||||
for (const component of decoratedElements) {
|
|
||||||
html = component.toStored(html);
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
let codeMirror: CodeMirrorAPI;
|
|
||||||
|
|
||||||
function focus(): void {
|
function focus(): void {
|
||||||
codeMirror?.editor.focus();
|
codeMirror.editor.then((editor) => editor.focus());
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveCaretToEnd(): void {
|
function moveCaretToEnd(): void {
|
||||||
codeMirror?.editor.setCursor(codeMirror.editor.lineCount(), 0);
|
codeMirror.editor.then((editor) => editor.setCursor(editor.lineCount(), 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
function refocus(): void {
|
function refocus(): void {
|
||||||
(codeMirror?.editor as any).display.input.blur();
|
codeMirror.editor.then((editor) => (editor as any).display.input.blur());
|
||||||
focus();
|
focus();
|
||||||
moveCaretToEnd();
|
moveCaretToEnd();
|
||||||
}
|
}
|
||||||
@ -107,9 +71,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
return hidden;
|
return hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEditor(): CodeMirrorType.Editor {
|
let codeMirror = {} as CodeMirrorAPI;
|
||||||
return codeMirror?.editor;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
name: "plain-text",
|
name: "plain-text",
|
||||||
@ -118,7 +80,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
moveCaretToEnd,
|
moveCaretToEnd,
|
||||||
refocus,
|
refocus,
|
||||||
toggle,
|
toggle,
|
||||||
getEditor,
|
codeMirror,
|
||||||
} as PlainTextInputAPI;
|
} as PlainTextInputAPI;
|
||||||
|
|
||||||
function pushUpdate(): void {
|
function pushUpdate(): void {
|
||||||
@ -126,8 +88,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
$editingInputs = $editingInputs;
|
$editingInputs = $editingInputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function refresh() {
|
function refresh(): void {
|
||||||
codeMirror.editor.refresh();
|
codeMirror.editor.then((editor) => editor.refresh());
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
@ -136,18 +98,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
pushUpdate();
|
pushUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function storedToUndecorated(html: string): string {
|
||||||
|
return decoratedElements.toUndecorated(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function undecoratedToStored(html: string): string {
|
||||||
|
return decoratedElements.toStored(html);
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
$editingInputs.push(api);
|
$editingInputs.push(api);
|
||||||
$editingInputs = $editingInputs;
|
$editingInputs = $editingInputs;
|
||||||
|
|
||||||
const unsubscribeFromEditingArea = content.subscribe((value: string): void => {
|
const unsubscribeFromEditingArea = content.subscribe((value: string): void => {
|
||||||
const adjusted = adjustInputHTML(value);
|
code.set(storedToUndecorated(value));
|
||||||
code.set(adjusted);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const unsubscribeToEditingArea = code.subscribe((value: string): void => {
|
const unsubscribeToEditingArea = code.subscribe((value: string): void => {
|
||||||
const parsed = parseAsHTML(value);
|
content.set(removeProhibitedTags(undecoratedToStored(value)));
|
||||||
content.set(adjustOutputHTML(parsed));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -155,6 +123,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
unsubscribeToEditingArea();
|
unsubscribeToEditingArea();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setupLifecycleHooks(api);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -167,7 +137,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
{configuration}
|
{configuration}
|
||||||
{code}
|
{code}
|
||||||
bind:api={codeMirror}
|
bind:api={codeMirror}
|
||||||
on:change={({ detail: html }) => code.set(parseAsHTML(html))}
|
on:change={({ detail: html }) => code.set(removeProhibitedTags(html))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
38
ts/editor/plain-text-input/remove-prohibited.ts
Normal file
38
ts/editor/plain-text-input/remove-prohibited.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
|
||||||
|
function createDummyDoc(html: string): string {
|
||||||
|
return (
|
||||||
|
"<html><head></head><body>" +
|
||||||
|
// parsingInstructions.join("") +
|
||||||
|
html +
|
||||||
|
"</body>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTag(element: HTMLElement, tagName: string): void {
|
||||||
|
for (const elem of element.getElementsByTagName(tagName)) {
|
||||||
|
elem.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prohibitedTags = ["script", "link", "style"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The use cases for using those tags in the field html are slim to none.
|
||||||
|
* We want to make it easier to possibly display cards in an iframe in the future.
|
||||||
|
*/
|
||||||
|
function removeProhibitedTags(html: string): string {
|
||||||
|
const doc = parser.parseFromString(createDummyDoc(html), "text/html");
|
||||||
|
const body = doc.body;
|
||||||
|
|
||||||
|
for (const tag of prohibitedTags) {
|
||||||
|
removeTag(body, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc.body.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default removeProhibitedTags;
|
@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
type AnkiPackages =
|
type AnkiPackages =
|
||||||
| "anki/NoteEditor"
|
| "anki/NoteEditor"
|
||||||
|
| "anki/PlainTextInput"
|
||||||
| "anki/TemplateButtons"
|
| "anki/TemplateButtons"
|
||||||
| "anki/packages"
|
| "anki/packages"
|
||||||
| "anki/bridgecommand"
|
| "anki/bridgecommand"
|
||||||
|
@ -38,7 +38,8 @@ function lifecycleHooks<T>(): [LifecycleHooks<T>, T[], SetLifecycleHooksAction<T
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
instances.push(api);
|
// onMount seems to be called in reverse order
|
||||||
|
instances.unshift(api);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
for (const cleanup of cleanups) {
|
for (const cleanup of cleanups) {
|
||||||
|
Loading…
Reference in New Issue
Block a user