mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 04:00:59 +01:00
editor: add new POC for math nodes
This commit is contained in:
@@ -36,7 +36,7 @@ import stripIndent from "strip-indent";
|
||||
import { nanoid } from "nanoid";
|
||||
import Languages from "./languages.json";
|
||||
|
||||
interface Indent {
|
||||
export interface Indent {
|
||||
type: "tab" | "space";
|
||||
amount: number;
|
||||
}
|
||||
@@ -349,7 +349,7 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
if (this.options.exitOnTripleEnter && exitOnTripleEnter(editor, $from))
|
||||
return true;
|
||||
|
||||
const indentation = parseIndentation($from.parent);
|
||||
const indentation = parseIndentation($from.parent, this.name);
|
||||
|
||||
if (indentation) return indentOnEnter(editor, $from, indentation);
|
||||
return false;
|
||||
@@ -420,7 +420,7 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
return false;
|
||||
}
|
||||
|
||||
const indentation = parseIndentation($from.parent);
|
||||
const indentation = parseIndentation($from.parent, this.name);
|
||||
if (!indentation) return false;
|
||||
|
||||
const indentToken = indent(indentation);
|
||||
@@ -452,7 +452,7 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
if ($from.parent.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
const indentation = parseIndentation($from.parent);
|
||||
const indentation = parseIndentation($from.parent, this.name);
|
||||
if (!indentation) return false;
|
||||
|
||||
const { lines } = $from.parent.attrs as CodeBlockAttributes;
|
||||
@@ -518,7 +518,7 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
|
||||
const indent = fixIndentation(
|
||||
text,
|
||||
parseIndentation(view.state.selection.$from.parent)
|
||||
parseIndentation(view.state.selection.$from.parent, this.name)
|
||||
);
|
||||
|
||||
const { tr } = view.state;
|
||||
@@ -584,11 +584,12 @@ export type CaretPosition = {
|
||||
from: number;
|
||||
};
|
||||
export function toCaretPosition(
|
||||
name: string,
|
||||
selection: Selection,
|
||||
lines?: CodeLine[]
|
||||
): CaretPosition | undefined {
|
||||
const { $from, $to, $head } = selection;
|
||||
if ($from.parent.type.name !== CodeBlock.name) return;
|
||||
if ($from.parent.type.name !== name) return;
|
||||
lines = lines || getLines($from.parent);
|
||||
|
||||
for (const line of lines) {
|
||||
@@ -611,7 +612,7 @@ export function getLines(node: ProsemirrorNode) {
|
||||
return lines || [];
|
||||
}
|
||||
|
||||
function exitOnTripleEnter(editor: Editor, $from: ResolvedPos) {
|
||||
export function exitOnTripleEnter(editor: Editor, $from: ResolvedPos) {
|
||||
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
|
||||
const endsWithDoubleNewline = $from.parent.textContent.endsWith("\n\n");
|
||||
|
||||
@@ -630,7 +631,11 @@ function exitOnTripleEnter(editor: Editor, $from: ResolvedPos) {
|
||||
.run();
|
||||
}
|
||||
|
||||
function indentOnEnter(editor: Editor, $from: ResolvedPos, options: Indent) {
|
||||
export function indentOnEnter(
|
||||
editor: Editor,
|
||||
$from: ResolvedPos,
|
||||
options: Indent
|
||||
) {
|
||||
const { indentation, newline } = getNewline($from, options) || {};
|
||||
if (!newline) return false;
|
||||
|
||||
@@ -657,7 +662,7 @@ function getNewline($from: ResolvedPos, options: Indent) {
|
||||
};
|
||||
}
|
||||
|
||||
type CodeLine = {
|
||||
export type CodeLine = {
|
||||
index: number;
|
||||
from: number;
|
||||
to: number;
|
||||
@@ -697,7 +702,7 @@ export function toCodeLines(code: string, pos: number): CodeLine[] {
|
||||
return positions;
|
||||
}
|
||||
|
||||
function getSelectedLines(lines: CodeLine[], selection: Selection) {
|
||||
export function getSelectedLines(lines: CodeLine[], selection: Selection) {
|
||||
const { $from, $to } = selection;
|
||||
return lines.filter(
|
||||
(line) =>
|
||||
@@ -707,8 +712,11 @@ function getSelectedLines(lines: CodeLine[], selection: Selection) {
|
||||
);
|
||||
}
|
||||
|
||||
function parseIndentation(node: ProsemirrorNode): Indent | undefined {
|
||||
if (node.type.name !== CodeBlock.name) return undefined;
|
||||
export function parseIndentation(
|
||||
node: ProsemirrorNode,
|
||||
name: string
|
||||
): Indent | undefined {
|
||||
if (node.type.name !== name) return undefined;
|
||||
|
||||
const { indentType, indentLength } = node.attrs;
|
||||
return {
|
||||
@@ -725,12 +733,12 @@ function inRange(x: number, a: number, b: number) {
|
||||
return x >= a && x <= b;
|
||||
}
|
||||
|
||||
function indent(options: Indent) {
|
||||
export function indent(options: Indent) {
|
||||
const char = options.type === "space" ? " " : "\t";
|
||||
return char.repeat(options.amount);
|
||||
}
|
||||
|
||||
function compareCaretPosition(
|
||||
export function compareCaretPosition(
|
||||
prev: CaretPosition | undefined,
|
||||
next: CaretPosition | undefined
|
||||
): boolean {
|
||||
@@ -744,7 +752,7 @@ function compareCaretPosition(
|
||||
/**
|
||||
* Persist selection between transaction steps
|
||||
*/
|
||||
function withSelection(
|
||||
export function withSelection(
|
||||
tr: Transaction,
|
||||
callback: (tr: Transaction) => void
|
||||
): boolean {
|
||||
|
||||
@@ -109,7 +109,9 @@ export function HighlighterPlugin({
|
||||
name: string;
|
||||
defaultLanguage: string | null | undefined;
|
||||
}) {
|
||||
const HIGHLIGHTER_PLUGIN_KEY = new PluginKey<HighlighterState>("highlighter");
|
||||
const HIGHLIGHTER_PLUGIN_KEY = new PluginKey<HighlighterState>(
|
||||
`${name}-highlighter`
|
||||
);
|
||||
const HIGHLIGHTED_BLOCKS: Set<string> = new Set();
|
||||
|
||||
return new Plugin<HighlighterState>({
|
||||
@@ -293,6 +295,7 @@ function updateSelection(
|
||||
}
|
||||
|
||||
const position = toCaretPosition(
|
||||
name,
|
||||
newState.selection,
|
||||
isDocChanged ? toCodeLines(node.textContent, pos) : undefined
|
||||
);
|
||||
|
||||
183
packages/editor/src/extensions/math/block.tsx
Normal file
183
packages/editor/src/extensions/math/block.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Box, Flex, Text } from "@theme-ui/components";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Button } from "../../components/button";
|
||||
import { useTimer } from "../../hooks/use-timer";
|
||||
import { SelectionBasedReactNodeViewProps } from "../react/types";
|
||||
import { MathBlock, MathBlockAttributes } from "./math-block";
|
||||
import { loadKatex } from "./plugin/renderers/katex";
|
||||
|
||||
export function MathBlockComponent(
|
||||
props: SelectionBasedReactNodeViewProps<MathBlockAttributes>
|
||||
) {
|
||||
const { editor, node, forwardRef, getPos } = props;
|
||||
const { indentLength, indentType, caretPosition } = node.attrs;
|
||||
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||
const { enabled, start } = useTimer(1000);
|
||||
const isActive = editor.isActive(MathBlock.name);
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) return;
|
||||
(async function () {
|
||||
const pos = getPos();
|
||||
const node = editor.current?.state.doc.nodeAt(pos);
|
||||
const text = node?.textContent;
|
||||
|
||||
if (text && elementRef.current) {
|
||||
const katex = await loadKatex();
|
||||
|
||||
elementRef.current.innerHTML = katex.renderToString(text, {
|
||||
displayMode: true,
|
||||
globalGroup: true,
|
||||
throwOnError: false
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, [isActive]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
borderRadius: "default",
|
||||
overflow: "hidden",
|
||||
...(isActive ? {} : { height: "1px", width: 0, visibility: "hidden" })
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
ref={forwardRef}
|
||||
as="pre"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
css={{}}
|
||||
sx={{
|
||||
"div, span.token, span.line-number-widget, span.line-number::before":
|
||||
{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "code",
|
||||
whiteSpace: "pre", // TODO !important
|
||||
tabSize: 1
|
||||
},
|
||||
position: "relative",
|
||||
lineHeight: "20px",
|
||||
bg: "codeBg",
|
||||
color: "static",
|
||||
overflowX: "auto",
|
||||
display: "flex",
|
||||
px: 2,
|
||||
pt: 2,
|
||||
pb: 2
|
||||
}}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<Flex
|
||||
ref={toolbarRef}
|
||||
contentEditable={false}
|
||||
sx={{
|
||||
bg: "codeBg",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
borderTop: "1px solid var(--codeBorder)"
|
||||
}}
|
||||
>
|
||||
{caretPosition ? (
|
||||
<Text variant={"subBody"} sx={{ mr: 1, color: "codeFg" }}>
|
||||
Line {caretPosition.line}, Column {caretPosition.column}{" "}
|
||||
{caretPosition.selected
|
||||
? `(${caretPosition.selected} selected)`
|
||||
: ""}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
variant={"icon"}
|
||||
sx={{
|
||||
p: 1,
|
||||
opacity: "1 !important",
|
||||
":hover": { bg: "codeSelection" }
|
||||
}}
|
||||
title="Toggle indentation mode"
|
||||
disabled={!editor.isEditable}
|
||||
onClick={() => {
|
||||
if (!editor.isEditable) return;
|
||||
editor.commands.changeCodeBlockIndentation({
|
||||
type: indentType === "space" ? "tab" : "space",
|
||||
amount: indentLength
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text variant={"subBody"} sx={{ color: "codeFg" }}>
|
||||
{indentType === "space" ? "Spaces" : "Tabs"}: {indentLength}
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={"icon"}
|
||||
sx={{
|
||||
opacity: "1 !important",
|
||||
p: 1,
|
||||
bg: "transparent",
|
||||
":hover": { bg: "codeSelection" }
|
||||
}}
|
||||
disabled={true}
|
||||
>
|
||||
<Text
|
||||
variant={"subBody"}
|
||||
spellCheck={false}
|
||||
sx={{ color: "codeFg" }}
|
||||
>
|
||||
Latex
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
{node.textContent?.length > 0 ? (
|
||||
<Button
|
||||
variant={"icon"}
|
||||
sx={{
|
||||
opacity: "1 !important",
|
||||
p: 1,
|
||||
mr: 1,
|
||||
bg: "transparent",
|
||||
":hover": { bg: "codeSelection" }
|
||||
}}
|
||||
onClick={() => {
|
||||
editor.commands.copyToClipboard(node.textContent);
|
||||
start();
|
||||
}}
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<Text
|
||||
variant={"subBody"}
|
||||
spellCheck={false}
|
||||
sx={{ color: "codeFg" }}
|
||||
>
|
||||
{enabled ? "Copied" : "Copy"}
|
||||
</Text>
|
||||
</Button>
|
||||
) : null}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Box contentEditable={false} ref={elementRef} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
88
packages/editor/src/extensions/math/component.tsx
Normal file
88
packages/editor/src/extensions/math/component.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Text } from "@theme-ui/components";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { SelectionBasedReactNodeViewProps } from "../react";
|
||||
import { loadKatex } from "./plugin/renderers/katex";
|
||||
import { MathInline } from "./math-inline";
|
||||
|
||||
const HIDDEN_STYLES = {
|
||||
visibility: "hidden" as const,
|
||||
width: 0,
|
||||
height: 0,
|
||||
display: "inline-block" as const,
|
||||
position: "absolute" as const
|
||||
};
|
||||
|
||||
const VISIBLE_STYLES = {
|
||||
visibility: "visible" as const
|
||||
};
|
||||
|
||||
export function InlineMathComponent(props: SelectionBasedReactNodeViewProps) {
|
||||
const { editor, getPos, forwardRef } = props;
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const isActive = editor.isActive(MathInline.name);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) return;
|
||||
(async function () {
|
||||
const pos = getPos();
|
||||
const node = editor.current?.state.doc.nodeAt(pos);
|
||||
const text = node?.textContent;
|
||||
|
||||
if (text && elementRef.current) {
|
||||
const katex = await loadKatex();
|
||||
|
||||
elementRef.current.innerHTML = katex.renderToString(text, {
|
||||
displayMode: false,
|
||||
globalGroup: true,
|
||||
throwOnError: false
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, [isActive]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text
|
||||
as="code"
|
||||
sx={{
|
||||
...(isActive
|
||||
? {
|
||||
...VISIBLE_STYLES,
|
||||
":before, :after": { content: `"$$"`, color: "fontTertiary" }
|
||||
}
|
||||
: HIDDEN_STYLES)
|
||||
}}
|
||||
>
|
||||
<Text as="span" ref={forwardRef} />
|
||||
</Text>
|
||||
<Text
|
||||
as="span"
|
||||
contentEditable={false}
|
||||
ref={elementRef}
|
||||
sx={{
|
||||
...(!isActive ? VISIBLE_STYLES : HIDDEN_STYLES),
|
||||
".katex": { fontSize: "1em" }
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -17,9 +17,25 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { insertMathNode } from "./plugin";
|
||||
import { NodeSelection } from "prosemirror-state";
|
||||
import { Node, mergeAttributes, textblockTypeInputRule } from "@tiptap/core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { Node as ProsemirrorNode } from "prosemirror-model";
|
||||
import { HighlighterPlugin } from "../code-block/highlighter";
|
||||
import {
|
||||
CaretPosition,
|
||||
CodeLine,
|
||||
Indent,
|
||||
compareCaretPosition,
|
||||
exitOnTripleEnter,
|
||||
getSelectedLines,
|
||||
indent,
|
||||
indentOnEnter,
|
||||
parseIndentation,
|
||||
withSelection
|
||||
} from "../code-block";
|
||||
import { createSelectionBasedNodeView } from "../react";
|
||||
import { MathBlockComponent } from "./block";
|
||||
import { findParentNodeClosestToPos } from "@tiptap/core";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
@@ -29,19 +45,278 @@ declare module "@tiptap/core" {
|
||||
}
|
||||
}
|
||||
|
||||
export type MathBlockAttributes = {
|
||||
language: string;
|
||||
|
||||
indentType: Indent["type"];
|
||||
indentLength: number;
|
||||
lines: CodeLine[];
|
||||
caretPosition?: CaretPosition;
|
||||
};
|
||||
|
||||
// simple inputrule for block math
|
||||
const REGEX_BLOCK_MATH_DOLLARS = /\$\$\$\s+$/; //new RegExp("\$\$\s+$", "i");
|
||||
export const MathBlock = Node.create({
|
||||
name: "mathBlock",
|
||||
group: "block math",
|
||||
content: "text*", // important!
|
||||
atom: true, // important!
|
||||
// atom: true, // important!
|
||||
code: true,
|
||||
draggable: false,
|
||||
marks: "",
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
language: {
|
||||
default: "latex",
|
||||
rendered: false
|
||||
},
|
||||
id: {
|
||||
default: undefined,
|
||||
rendered: false,
|
||||
parseHTML: () => createMathBlockId()
|
||||
},
|
||||
caretPosition: {
|
||||
default: undefined,
|
||||
rendered: false
|
||||
},
|
||||
lines: {
|
||||
default: [],
|
||||
rendered: false
|
||||
},
|
||||
indentType: {
|
||||
default: "space",
|
||||
parseHTML: (element) => {
|
||||
const indentType = element.dataset.indentType;
|
||||
return indentType;
|
||||
},
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.indentType) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
"data-indent-type": attributes.indentType
|
||||
};
|
||||
}
|
||||
},
|
||||
indentLength: {
|
||||
default: 2,
|
||||
parseHTML: (element) => {
|
||||
const indentLength = element.dataset.indentLength;
|
||||
return indentLength;
|
||||
},
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.indentLength) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
"data-indent-length": attributes.indentLength
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Mod-a": ({ editor }) => {
|
||||
const { $anchor } = this.editor.state.selection;
|
||||
if ($anchor.parent.type.name !== this.name) {
|
||||
return false;
|
||||
}
|
||||
const codeblock = findParentNodeClosestToPos(
|
||||
$anchor,
|
||||
(node) => node.type.name === this.type.name
|
||||
);
|
||||
|
||||
if (!codeblock) return false;
|
||||
return editor.commands.setTextSelection({
|
||||
from: codeblock.pos + 1,
|
||||
to: codeblock.pos + codeblock.node.nodeSize - 1
|
||||
});
|
||||
},
|
||||
// remove code block when at start of document or code block is empty
|
||||
Backspace: ({ editor }) => {
|
||||
const { empty, $anchor } = editor.state.selection;
|
||||
|
||||
const currentNode = $anchor.parent;
|
||||
const nextNode = editor.state.doc.nodeAt($anchor.pos + 1);
|
||||
const isCodeBlock = (node: ProsemirrorNode | null) =>
|
||||
node && node.type.name === this.name;
|
||||
const isAtStart = $anchor.pos === 1;
|
||||
|
||||
if (!empty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
isAtStart ||
|
||||
(isCodeBlock(currentNode) && !currentNode.textContent.length)
|
||||
) {
|
||||
return this.editor.commands.deleteNode(this.type);
|
||||
}
|
||||
// on android due to composition issues with various keyboards,
|
||||
// sometimes backspace is detected one node behind. We need to
|
||||
// manually handle this case.
|
||||
else if (
|
||||
nextNode &&
|
||||
isCodeBlock(nextNode) &&
|
||||
!nextNode.textContent.length
|
||||
) {
|
||||
return this.editor.commands.command(({ tr }) => {
|
||||
tr.delete($anchor.pos + 1, $anchor.pos + 1 + nextNode.nodeSize);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// exit node on triple enter
|
||||
Enter: ({ editor }) => {
|
||||
const { state } = editor;
|
||||
const { selection } = state;
|
||||
const { $from, empty } = selection;
|
||||
|
||||
if (!empty || $from.parent.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.options.exitOnTripleEnter && exitOnTripleEnter(editor, $from))
|
||||
return true;
|
||||
|
||||
const indentation = parseIndentation($from.parent, this.name);
|
||||
|
||||
if (indentation) return indentOnEnter(editor, $from, indentation);
|
||||
return false;
|
||||
},
|
||||
|
||||
// exit node on arrow up
|
||||
ArrowUp: ({ editor }) => {
|
||||
if (!this.options.exitOnArrowUp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { state } = editor;
|
||||
const { selection } = state;
|
||||
const { $anchor, empty } = selection;
|
||||
|
||||
if (!empty || $anchor.parent.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAtStart = $anchor.pos === 1;
|
||||
if (!isAtStart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor.commands.insertContentAt(0, "<p></p>");
|
||||
},
|
||||
// exit node on arrow down
|
||||
ArrowDown: ({ editor }) => {
|
||||
if (!this.options.exitOnArrowDown) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { state } = editor;
|
||||
const { selection, doc } = state;
|
||||
const { $from, empty } = selection;
|
||||
|
||||
if (!empty || $from.parent.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
|
||||
|
||||
if (!isAtEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const after = $from.after();
|
||||
|
||||
if (after === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nodeAfter = doc.nodeAt(after);
|
||||
|
||||
if (nodeAfter) {
|
||||
editor.commands.setNodeSelection($from.before());
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor.commands.exitCode();
|
||||
},
|
||||
"Shift-Tab": ({ editor }) => {
|
||||
const { state } = editor;
|
||||
const { selection } = state;
|
||||
const { $from } = selection;
|
||||
|
||||
if ($from.parent.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const indentation = parseIndentation($from.parent, this.name);
|
||||
if (!indentation) return false;
|
||||
|
||||
const indentToken = indent(indentation);
|
||||
|
||||
const { lines } = $from.parent.attrs as MathBlockAttributes;
|
||||
const selectedLines = getSelectedLines(lines, selection);
|
||||
|
||||
return editor
|
||||
.chain()
|
||||
.command(({ tr }) =>
|
||||
withSelection(tr, (tr) => {
|
||||
for (const line of selectedLines) {
|
||||
if (line.text(indentToken.length) !== indentToken) continue;
|
||||
|
||||
tr.delete(
|
||||
tr.mapping.map(line.from),
|
||||
tr.mapping.map(line.from + indentation.amount)
|
||||
);
|
||||
}
|
||||
})
|
||||
)
|
||||
.run();
|
||||
},
|
||||
Tab: ({ editor }) => {
|
||||
const { state } = editor;
|
||||
const { selection } = state;
|
||||
const { $from } = selection;
|
||||
|
||||
if ($from.parent.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
const indentation = parseIndentation($from.parent, this.name);
|
||||
if (!indentation) return false;
|
||||
|
||||
const { lines } = $from.parent.attrs as MathBlockAttributes;
|
||||
const selectedLines = getSelectedLines(lines, selection);
|
||||
return editor
|
||||
.chain()
|
||||
.command(({ tr }) =>
|
||||
withSelection(tr, (tr) => {
|
||||
const indentToken = indent(indentation);
|
||||
|
||||
if (selectedLines.length === 1)
|
||||
return tr.insertText(indentToken, $from.pos);
|
||||
|
||||
for (const line of selectedLines) {
|
||||
tr.insertText(indentToken, tr.mapping.map(line.from));
|
||||
}
|
||||
})
|
||||
)
|
||||
.run();
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "div[class*='math-block']" // important!
|
||||
tag: "div[class*='math-block']", // important!
|
||||
preserveWhitespace: "full"
|
||||
}
|
||||
];
|
||||
},
|
||||
@@ -58,38 +333,50 @@ export const MathBlock = Node.create({
|
||||
return {
|
||||
insertMathBlock:
|
||||
() =>
|
||||
({ state, dispatch, view }) => {
|
||||
return insertMathNode(this.type)(state, dispatch, view);
|
||||
({ commands }) => {
|
||||
return commands.setNode(this.name, {
|
||||
id: createMathBlockId()
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
{
|
||||
textblockTypeInputRule({
|
||||
find: REGEX_BLOCK_MATH_DOLLARS,
|
||||
handler: ({ state, range }) => {
|
||||
const { from: start, to: end } = range;
|
||||
const $start = state.doc.resolve(start);
|
||||
if (
|
||||
!$start
|
||||
.node(-1)
|
||||
.canReplaceWith(
|
||||
$start.index(-1),
|
||||
$start.indexAfter(-1),
|
||||
this.type
|
||||
)
|
||||
)
|
||||
return null;
|
||||
const tr = state.tr
|
||||
.delete(start, end)
|
||||
.setBlockType(start, start, this.type, null);
|
||||
|
||||
tr.setSelection(
|
||||
NodeSelection.create(tr.doc, tr.mapping.map($start.pos - 1))
|
||||
);
|
||||
type: this.type,
|
||||
getAttributes: {
|
||||
id: createMathBlockId()
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [HighlighterPlugin({ name: this.name, defaultLanguage: "latex" })];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return createSelectionBasedNodeView(MathBlockComponent, {
|
||||
contentDOMFactory: () => {
|
||||
const content = document.createElement("div");
|
||||
content.classList.add("node-content-wrapper");
|
||||
content.style.whiteSpace = "pre";
|
||||
// caret is not visible if content element width is 0px
|
||||
content.style.minWidth = "20px";
|
||||
return { dom: content };
|
||||
},
|
||||
shouldUpdate: ({ attrs: prev }, { attrs: next }) => {
|
||||
return (
|
||||
compareCaretPosition(prev.caretPosition, next.caretPosition) ||
|
||||
prev.indentType !== next.indentType
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function createMathBlockId() {
|
||||
return `mathBlock-${nanoid(12)}`;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { mathPlugin } from "./plugin";
|
||||
import { createSelectionBasedNodeView } from "../react";
|
||||
import { InlineMathComponent } from "./component";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
@@ -28,25 +29,15 @@ declare module "@tiptap/core" {
|
||||
}
|
||||
}
|
||||
// simple input rule for inline math
|
||||
const REGEX_INLINE_MATH_DOLLARS = /\$\$(.+)\$\$/; //new RegExp("\$(.+)\$", "i");
|
||||
// negative lookbehind regex notation allows for escaped \$ delimiters
|
||||
// (requires browser supporting ECMA2018 standard -- currently only Chrome / FF)
|
||||
// (see https://javascript.info/regexp-lookahead-lookbehind)
|
||||
// const REGEX_INLINE_MATH_DOLLARS_ESCAPED: RegExp = (() => {
|
||||
// // attempt to create regex with negative lookbehind
|
||||
// try {
|
||||
// return new RegExp("(?<!\\\\)\\$(.+)(?<!\\\\)\\$");
|
||||
// } catch (e) {
|
||||
// return REGEX_INLINE_MATH_DOLLARS;
|
||||
// }
|
||||
// })();
|
||||
const REGEX_INLINE_MATH_DOLLARS = /\$\$(.+)\$\$/;
|
||||
|
||||
export const MathInline = Node.create({
|
||||
name: "mathInline",
|
||||
group: "inline math",
|
||||
group: "inline",
|
||||
content: "text*", // important!
|
||||
marks: "",
|
||||
inline: true, // important!
|
||||
atom: true, // important!
|
||||
draggable: false,
|
||||
code: true,
|
||||
|
||||
parseHTML() {
|
||||
@@ -79,8 +70,11 @@ export const MathInline = Node.create({
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [mathPlugin];
|
||||
addNodeView() {
|
||||
return createSelectionBasedNodeView(InlineMathComponent, {
|
||||
contentDOMFactory: true,
|
||||
wrapperFactory: () => document.createElement("span")
|
||||
});
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
|
||||
@@ -17,16 +17,19 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type katex from "katex";
|
||||
import { MathRenderer } from "./types";
|
||||
|
||||
async function loadKatex() {
|
||||
const { default: katex } = await import("katex");
|
||||
|
||||
let Katex: typeof katex;
|
||||
export async function loadKatex() {
|
||||
if (Katex) return Katex;
|
||||
const { default: _katex } = await import("katex");
|
||||
// Chemistry formulas support
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore TODO: maybe rewrite this in typescript?
|
||||
await import("katex/contrib/mhchem/mhchem");
|
||||
return katex;
|
||||
Katex = _katex;
|
||||
return Katex;
|
||||
}
|
||||
|
||||
export const KatexRenderer: MathRenderer = {
|
||||
|
||||
@@ -126,7 +126,9 @@ export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
|
||||
getContentDOM(): ContentDOM {
|
||||
if (!this.options.contentDOMFactory) return;
|
||||
if (this.options.contentDOMFactory === true) {
|
||||
const content = document.createElement("div");
|
||||
const content = document.createElement(
|
||||
this.node.isInline ? "span" : "div"
|
||||
);
|
||||
content.classList.add(
|
||||
`${this.node.type.name.toLowerCase()}-content-wrapper`
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import { DecorationSet } from "prosemirror-view";
|
||||
import { Node as PMNode } from "prosemirror-model";
|
||||
import { Selection, NodeSelection } from "prosemirror-state";
|
||||
import { Selection, NodeSelection, TextSelection } from "prosemirror-state";
|
||||
import {
|
||||
stateKey as SelectionChangePluginKey,
|
||||
ReactNodeViewState
|
||||
@@ -234,6 +234,22 @@ export class SelectionBasedNodeView<
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
const newTextSelection = this.isSelectionInsideNode(
|
||||
selection.from,
|
||||
selection.to
|
||||
);
|
||||
const oldTextSelection = this.isSelectionInsideNode(
|
||||
oldSelection.from,
|
||||
oldSelection.to
|
||||
);
|
||||
|
||||
if (
|
||||
(newTextSelection && !oldTextSelection) ||
|
||||
(oldTextSelection && !newTextSelection)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const movedInToSelection =
|
||||
|
||||
@@ -395,41 +395,6 @@ span:focus .fake-cursor {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* -- Inline Math --------------------------------------- */
|
||||
|
||||
.math-inline {
|
||||
display: inline;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.math-inline .math-render {
|
||||
display: inline-block;
|
||||
/* font-size: 0.85em; */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.math-inline .math-src .ProseMirror {
|
||||
display: inline;
|
||||
/* Necessary to fix FireFox bug with contenteditable, https://bugzilla.mozilla.org/show_bug.cgi?id=1252108 */
|
||||
border-right: 1px solid transparent;
|
||||
border-left: 1px solid transparent;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
|
||||
Liberation Mono, monospace !important;
|
||||
}
|
||||
|
||||
.math-inline.ProseMirror-selectednode {
|
||||
background-color: var(--bgSecondary);
|
||||
padding: 3px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.math-inline .math-src::after,
|
||||
.math-inline .math-src::before {
|
||||
content: "$$";
|
||||
color: var(--disabled);
|
||||
}
|
||||
|
||||
/* -- Block Math ---------------------------------------- */
|
||||
|
||||
.math-block {
|
||||
|
||||
Reference in New Issue
Block a user