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:
Henrik Giesel 2022-02-25 02:14:26 +01:00 committed by GitHub
parent 7c27159149
commit 50e36bc312
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 307 additions and 192 deletions

View File

@ -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;
}
} }

View File

@ -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(/&lt;/g, "{\\lt}").replace(/&gt;/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;
} }

View File

@ -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">

View File

@ -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>

View File

@ -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;
} }

View File

@ -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();
}

View File

@ -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>

View File

@ -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}

View File

@ -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>

View 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;

View File

@ -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"

View File

@ -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) {