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;
|
||||
}
|
||||
|
||||
export class CustomElementArray<
|
||||
T extends CustomElementConstructor & WithTagName,
|
||||
> extends Array<T> {
|
||||
push(...elements: T[]): number {
|
||||
export class CustomElementArray extends Array<DecoratedElementConstructor> {
|
||||
push(...elements: DecoratedElementConstructor[]): number {
|
||||
for (const element of elements) {
|
||||
customElements.define(element.tagName, element);
|
||||
}
|
||||
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 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
|
||||
extends HTMLElement
|
||||
implements DecoratedElement
|
||||
@ -22,7 +30,7 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
|
||||
static tagName = "anki-mathjax";
|
||||
|
||||
static toStored(undecorated: string): string {
|
||||
return undecorated.replace(
|
||||
const stored = undecorated.replace(
|
||||
mathjaxTagPattern,
|
||||
(_match: string, block: string | undefined, text: string) => {
|
||||
return typeof block === "string" && block !== "false"
|
||||
@ -30,20 +38,20 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
|
||||
: `\\(${text}\\)`;
|
||||
},
|
||||
);
|
||||
|
||||
return stored;
|
||||
}
|
||||
|
||||
static toUndecorated(stored: string): string {
|
||||
return stored
|
||||
.replace(
|
||||
mathjaxBlockDelimiterPattern,
|
||||
(_match: string, text: string) =>
|
||||
`<${Mathjax.tagName} block="true">${text}</${Mathjax.tagName}>`,
|
||||
)
|
||||
.replace(
|
||||
mathjaxInlineDelimiterPattern,
|
||||
(_match: string, text: string) =>
|
||||
`<${Mathjax.tagName}>${text}</${Mathjax.tagName}>`,
|
||||
);
|
||||
.replace(mathjaxBlockDelimiterPattern, (_match: string, text: string) => {
|
||||
const escaped = translateEntitiesToMathjax(text);
|
||||
return `<${Mathjax.tagName} block="true">${escaped}</${Mathjax.tagName}>`;
|
||||
})
|
||||
.replace(mathjaxInlineDelimiterPattern, (_match: string, text: string) => {
|
||||
const escaped = translateEntitiesToMathjax(text);
|
||||
return `<${Mathjax.tagName}>${escaped}</${Mathjax.tagName}>`;
|
||||
});
|
||||
}
|
||||
|
||||
block = false;
|
||||
@ -76,6 +84,9 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
|
||||
break;
|
||||
|
||||
case "data-mathjax":
|
||||
if (!newValue) {
|
||||
return;
|
||||
}
|
||||
this.component?.$set({ mathjax: newValue });
|
||||
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
|
||||
-->
|
||||
<script context="module" lang="ts">
|
||||
import { CodeMirror as CodeMirrorLib } from "./code-mirror";
|
||||
import type CodeMirrorLib from "codemirror";
|
||||
|
||||
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 lang="ts">
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import { createEventDispatcher, getContext, onMount } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
import { directionKey } from "../lib/context-keys";
|
||||
import storeSubscribe from "../sveltelib/store-subscribe";
|
||||
import { promiseWithResolver } from "../lib/promise";
|
||||
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 code: Writable<string>;
|
||||
|
||||
const direction = getContext<Writable<"ltr" | "rtl">>(directionKey);
|
||||
const defaultConfiguration = {
|
||||
direction: $direction,
|
||||
rtlMoveVisually: true,
|
||||
};
|
||||
|
||||
let codeMirror: CodeMirrorLib.EditorFromTextArea;
|
||||
$: codeMirror?.setOption("direction", $direction);
|
||||
const [editorPromise, resolve] = promiseWithResolver<CodeMirrorLib.Editor>();
|
||||
|
||||
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();
|
||||
|
||||
function openCodeMirror(textarea: HTMLTextAreaElement): void {
|
||||
codeMirror = CodeMirrorLib.fromTextArea(textarea, {
|
||||
...defaultConfiguration,
|
||||
...configuration,
|
||||
});
|
||||
|
||||
// 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,
|
||||
onMount(() =>
|
||||
editorPromise.then((editor) => {
|
||||
setupCodeMirror(editor, code);
|
||||
editor.on("change", () => dispatch("change", editor.getValue()));
|
||||
editor.on("focus", () => dispatch("focus"));
|
||||
editor.on("blur", () => dispatch("blur"));
|
||||
}),
|
||||
);
|
||||
|
||||
export const api = Object.create(
|
||||
{},
|
||||
{
|
||||
editor: { get: () => codeMirror },
|
||||
},
|
||||
) as CodeMirrorAPI;
|
||||
</script>
|
||||
|
||||
<div class="code-mirror">
|
||||
<textarea tabindex="-1" hidden use:openCodeMirror />
|
||||
<textarea
|
||||
tabindex="-1"
|
||||
hidden
|
||||
use:openCodeMirror={{
|
||||
configuration: { ...configuration, ...defaultConfiguration },
|
||||
resolve,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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
|
||||
-->
|
||||
<script context="module" lang="ts">
|
||||
import {
|
||||
CustomElementArray,
|
||||
DecoratedElementConstructor,
|
||||
} from "../editable/decorated";
|
||||
import { CustomElementArray } from "../editable/decorated";
|
||||
import contextProperty from "../sveltelib/context-property";
|
||||
|
||||
const decoratedElements = new CustomElementArray<DecoratedElementConstructor>();
|
||||
const decoratedElements = new CustomElementArray();
|
||||
|
||||
const key = Symbol("decoratedElements");
|
||||
const [context, setContextProperty] =
|
||||
contextProperty<CustomElementArray<DecoratedElementConstructor>>(key);
|
||||
const [context, setContextProperty] = contextProperty<CustomElementArray>(key);
|
||||
|
||||
export { context };
|
||||
</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.
|
||||
// It should be refactored once we work on our own Undo stack
|
||||
for (const pi of plainTextInputs) {
|
||||
pi.api.getEditor().clearHistory();
|
||||
pi.api.codeMirror.editor.then((editor) => editor.clearHistory());
|
||||
}
|
||||
noteId = ntid;
|
||||
}
|
||||
|
@ -14,6 +14,9 @@ import "codemirror/addon/edit/closetag";
|
||||
import "codemirror/addon/display/placeholder";
|
||||
|
||||
import CodeMirror from "codemirror";
|
||||
import type { Readable } from "svelte/store";
|
||||
|
||||
import storeSubscribe from "../sveltelib/store-subscribe";
|
||||
|
||||
export { CodeMirror };
|
||||
|
||||
@ -29,11 +32,11 @@ export const htmlanki = {
|
||||
},
|
||||
};
|
||||
|
||||
export const lightCodeMirrorTheme = "default";
|
||||
export const darkCodeMirrorTheme = "monokai";
|
||||
export const lightTheme = "default";
|
||||
export const darkTheme = "monokai";
|
||||
|
||||
export const baseOptions: CodeMirror.EditorConfiguration = {
|
||||
theme: lightCodeMirrorTheme,
|
||||
theme: lightTheme,
|
||||
lineWrapping: true,
|
||||
matchTags: { bothTags: true },
|
||||
autoCloseTags: true,
|
||||
@ -53,3 +56,73 @@ export function focusAndCaretAfter(editor: CodeMirror.Editor): void {
|
||||
editor.focus();
|
||||
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
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type CodeMirrorLib from "codemirror";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
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";
|
||||
|
||||
export let code: Writable<string>;
|
||||
|
||||
export let acceptShortcut: string;
|
||||
export let newlineShortcut: string;
|
||||
|
||||
const configuration = {
|
||||
...Object.assign({}, baseOptions, {
|
||||
extraKeys: {
|
||||
...(baseOptions.extraKeys as CodeMirror.KeyMap),
|
||||
...(baseOptions.extraKeys as CodeMirrorLib.KeyMap),
|
||||
[acceptShortcut]: noop,
|
||||
[newlineShortcut]: noop,
|
||||
},
|
||||
@ -37,46 +37,61 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let codeMirror: CodeMirrorAPI = {} as CodeMirrorAPI;
|
||||
let codeMirror = {} as CodeMirrorAPI;
|
||||
|
||||
onMount(() => {
|
||||
focusAndCaretAfter(codeMirror.editor);
|
||||
onMount(() =>
|
||||
codeMirror.editor.then((editor) => {
|
||||
focusAndCaretAfter(editor);
|
||||
|
||||
if (selectAll) {
|
||||
codeMirror.editor.execCommand("selectAll");
|
||||
}
|
||||
|
||||
let direction: "start" | "end" | undefined = undefined;
|
||||
|
||||
codeMirror.editor.on("keydown", (_instance, event: KeyboardEvent) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
direction = "start";
|
||||
} else if (event.key === "ArrowRight") {
|
||||
direction = "end";
|
||||
if (selectAll) {
|
||||
editor.execCommand("selectAll");
|
||||
}
|
||||
});
|
||||
|
||||
codeMirror.editor.on(
|
||||
"beforeSelectionChange",
|
||||
(instance, obj: CodeMirror.EditorSelectionChange) => {
|
||||
const { anchor } = obj.ranges[0];
|
||||
let direction: "start" | "end" | undefined = undefined;
|
||||
|
||||
if (anchor["hitSide"]) {
|
||||
if (instance.getValue().length === 0) {
|
||||
if (direction) {
|
||||
dispatch(`moveout${direction}`);
|
||||
}
|
||||
} else if (anchor.line === 0 && anchor.ch === 0) {
|
||||
dispatch("moveoutstart");
|
||||
} else {
|
||||
dispatch("moveoutend");
|
||||
editor.on(
|
||||
"keydown",
|
||||
(_instance: CodeMirrorLib.Editor, event: KeyboardEvent): void => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
direction = "start";
|
||||
} else if (event.key === "ArrowRight") {
|
||||
direction = "end";
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
direction = undefined;
|
||||
},
|
||||
);
|
||||
});
|
||||
editor.on(
|
||||
"beforeSelectionChange",
|
||||
(
|
||||
instance: CodeMirrorLib.Editor,
|
||||
obj: CodeMirrorLib.EditorSelectionChange,
|
||||
): void => {
|
||||
const { anchor } = obj.ranges[0];
|
||||
|
||||
if (anchor["hitSide"]) {
|
||||
if (instance.getValue().length === 0) {
|
||||
if (direction) {
|
||||
dispatch(`moveout${direction}`);
|
||||
}
|
||||
} else if (anchor.line === 0 && anchor.ch === 0) {
|
||||
dispatch("moveoutstart");
|
||||
} else {
|
||||
dispatch("moveoutend");
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<div class="mathjax-editor">
|
||||
@ -84,7 +99,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
{code}
|
||||
{configuration}
|
||||
bind:api={codeMirror}
|
||||
on:change={({ detail }) => code.set(detail)}
|
||||
on:change={({ detail }) => code.set(escapeSomeEntities(detail))}
|
||||
on:blur
|
||||
/>
|
||||
</div>
|
||||
|
@ -132,13 +132,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
});
|
||||
</script>
|
||||
|
||||
<WithDropdown
|
||||
drop="down"
|
||||
autoOpen={true}
|
||||
autoClose={false}
|
||||
distance={4}
|
||||
let:createDropdown
|
||||
>
|
||||
<WithDropdown drop="down" autoOpen autoClose={false} distance={4} let:createDropdown>
|
||||
{#if activeImage && mathjaxElement}
|
||||
<MathjaxMenu
|
||||
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
|
||||
-->
|
||||
<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";
|
||||
|
||||
export interface PlainTextInputAPI extends EditingInputAPI {
|
||||
name: "plain-text";
|
||||
moveCaretToEnd(): void;
|
||||
toggle(): boolean;
|
||||
getEditor(): CodeMirrorType.Editor;
|
||||
codeMirror: CodeMirrorAPI;
|
||||
}
|
||||
|
||||
export const parsingInstructions: string[] = [];
|
||||
|
||||
const [lifecycle, instances, setupLifecycleHooks] =
|
||||
lifecycleHooks<PlainTextInputAPI>();
|
||||
|
||||
registerPackage("anki/PlainTextInput", {
|
||||
lifecycle,
|
||||
instances,
|
||||
});
|
||||
</script>
|
||||
|
||||
<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 { baseOptions, gutterOptions, htmlanki } from "../code-mirror";
|
||||
import type { CodeMirrorAPI } from "../CodeMirror.svelte";
|
||||
import CodeMirror from "../CodeMirror.svelte";
|
||||
import { context as decoratedElementsContext } from "../DecoratedElements.svelte";
|
||||
import { context as editingAreaContext } from "../EditingArea.svelte";
|
||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||
import removeProhibitedTags from "./remove-prohibited";
|
||||
|
||||
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 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 {
|
||||
codeMirror?.editor.focus();
|
||||
codeMirror.editor.then((editor) => editor.focus());
|
||||
}
|
||||
|
||||
function moveCaretToEnd(): void {
|
||||
codeMirror?.editor.setCursor(codeMirror.editor.lineCount(), 0);
|
||||
codeMirror.editor.then((editor) => editor.setCursor(editor.lineCount(), 0));
|
||||
}
|
||||
|
||||
function refocus(): void {
|
||||
(codeMirror?.editor as any).display.input.blur();
|
||||
codeMirror.editor.then((editor) => (editor as any).display.input.blur());
|
||||
focus();
|
||||
moveCaretToEnd();
|
||||
}
|
||||
@ -107,9 +71,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
return hidden;
|
||||
}
|
||||
|
||||
function getEditor(): CodeMirrorType.Editor {
|
||||
return codeMirror?.editor;
|
||||
}
|
||||
let codeMirror = {} as CodeMirrorAPI;
|
||||
|
||||
export const api = {
|
||||
name: "plain-text",
|
||||
@ -118,7 +80,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
moveCaretToEnd,
|
||||
refocus,
|
||||
toggle,
|
||||
getEditor,
|
||||
codeMirror,
|
||||
} as PlainTextInputAPI;
|
||||
|
||||
function pushUpdate(): void {
|
||||
@ -126,8 +88,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
$editingInputs = $editingInputs;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
codeMirror.editor.refresh();
|
||||
function refresh(): void {
|
||||
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();
|
||||
}
|
||||
|
||||
function storedToUndecorated(html: string): string {
|
||||
return decoratedElements.toUndecorated(html);
|
||||
}
|
||||
|
||||
function undecoratedToStored(html: string): string {
|
||||
return decoratedElements.toStored(html);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
$editingInputs.push(api);
|
||||
$editingInputs = $editingInputs;
|
||||
|
||||
const unsubscribeFromEditingArea = content.subscribe((value: string): void => {
|
||||
const adjusted = adjustInputHTML(value);
|
||||
code.set(adjusted);
|
||||
code.set(storedToUndecorated(value));
|
||||
});
|
||||
|
||||
const unsubscribeToEditingArea = code.subscribe((value: string): void => {
|
||||
const parsed = parseAsHTML(value);
|
||||
content.set(adjustOutputHTML(parsed));
|
||||
content.set(removeProhibitedTags(undecoratedToStored(value)));
|
||||
});
|
||||
|
||||
return () => {
|
||||
@ -155,6 +123,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
unsubscribeToEditingArea();
|
||||
};
|
||||
});
|
||||
|
||||
setupLifecycleHooks(api);
|
||||
</script>
|
||||
|
||||
<div
|
||||
@ -167,7 +137,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
{configuration}
|
||||
{code}
|
||||
bind:api={codeMirror}
|
||||
on:change={({ detail: html }) => code.set(parseAsHTML(html))}
|
||||
on:change={({ detail: html }) => code.set(removeProhibitedTags(html))}
|
||||
/>
|
||||
</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 =
|
||||
| "anki/NoteEditor"
|
||||
| "anki/PlainTextInput"
|
||||
| "anki/TemplateButtons"
|
||||
| "anki/packages"
|
||||
| "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 () => {
|
||||
for (const cleanup of cleanups) {
|
||||
|
Loading…
Reference in New Issue
Block a user