Add support for LaTeX inline and block expressions. (#4446)
* Add support for LaTeX inline and block expressions. (#4364) Co-authored-by: Tom Moor <tom@getoutline.com> * tsc * Show heading markers when LaTeX block is being edited * Tab to space, name katex chunk * Fork htmldiff, add support for math nodes Co-authored-by: luisbc92 <luiscarlos.banuelos@gmail.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
AttachmentIcon,
|
||||
ClockIcon,
|
||||
CalendarIcon,
|
||||
MathIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
@@ -124,6 +125,12 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] {
|
||||
shortcut: "^ ⇧ \\",
|
||||
keywords: "script",
|
||||
},
|
||||
{
|
||||
name: "math_block",
|
||||
title: dictionary.mathBlock,
|
||||
icon: <MathIcon />,
|
||||
keywords: "math katex latex",
|
||||
},
|
||||
{
|
||||
name: "hr",
|
||||
title: dictionary.hr,
|
||||
|
||||
@@ -69,6 +69,8 @@ export default function useDictionary() {
|
||||
strong: t("Bold"),
|
||||
subheading: t("Subheading"),
|
||||
table: t("Table"),
|
||||
mathInline: t("Math inline (LaTeX)"),
|
||||
mathBlock: t("Math block (LaTeX)"),
|
||||
tip: t("Tip"),
|
||||
tipNotice: t("Tip notice"),
|
||||
showDiagram: t("Show diagram"),
|
||||
|
||||
@@ -339,6 +339,14 @@ function KeyboardShortcuts() {
|
||||
shortcut: <Key>{"```"}</Key>,
|
||||
label: t("Code block"),
|
||||
},
|
||||
{
|
||||
shortcut: (
|
||||
<>
|
||||
<Key>$$</Key> <Key>Space</Key>
|
||||
</>
|
||||
),
|
||||
label: t("LaTeX block"),
|
||||
},
|
||||
{
|
||||
shortcut: <Key>{":::"}</Key>,
|
||||
label: t("Info notice"),
|
||||
@@ -359,6 +367,10 @@ function KeyboardShortcuts() {
|
||||
shortcut: "`code`",
|
||||
label: t("Inline code"),
|
||||
},
|
||||
{
|
||||
shortcut: "$latex$",
|
||||
label: t("Inline LaTeX"),
|
||||
},
|
||||
{
|
||||
shortcut: "==highlight==",
|
||||
label: t("Highlight"),
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"@babel/plugin-transform-regenerator": "^7.10.4",
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"@babel/preset-react": "^7.16.0",
|
||||
"@benrbray/prosemirror-math": "^0.2.2",
|
||||
"@bull-board/api": "^4.2.2",
|
||||
"@bull-board/koa": "^4.6.2",
|
||||
"@dnd-kit/core": "^6.0.5",
|
||||
@@ -113,6 +114,7 @@
|
||||
"json-loader": "0.5.7",
|
||||
"jsonwebtoken": "^8.5.0",
|
||||
"jszip": "^3.10.1",
|
||||
"katex": "^0.16.3",
|
||||
"kbar": "0.1.0-beta.28",
|
||||
"koa": "^2.13.4",
|
||||
"koa-body": "^4.2.0",
|
||||
@@ -246,6 +248,7 @@
|
||||
"@types/ioredis": "^4.28.1",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/katex": "^0.14.0",
|
||||
"@types/koa": "^2.13.4",
|
||||
"@types/koa-compress": "^4.0.3",
|
||||
"@types/koa-helmet": "^6.0.4",
|
||||
@@ -309,6 +312,7 @@
|
||||
"babel-plugin-transform-typescript-metadata": "^0.3.2",
|
||||
"babel-plugin-tsconfig-paths-module-resolver": "^1.0.3",
|
||||
"concurrently": "^7.4.0",
|
||||
"css-loader": "5.2.6",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.6",
|
||||
"eslint": "^7.32.0",
|
||||
@@ -335,6 +339,7 @@
|
||||
"prettier": "^2.0.5",
|
||||
"react-refresh": "^0.14.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"style-loader": "2.0.0",
|
||||
"terser-webpack-plugin": "^4.1.0",
|
||||
"typescript": "^4.7.4",
|
||||
"url-loader": "^4.1.1",
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
} from "@getoutline/y-prosemirror";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { escapeRegExp } from "lodash";
|
||||
import diff from "node-htmldiff";
|
||||
import { Node, DOMSerializer } from "prosemirror-model";
|
||||
import * as React from "react";
|
||||
import { renderToString } from "react-dom/server";
|
||||
@@ -19,6 +18,7 @@ import { parser, schema } from "@server/editor";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import Document from "@server/models/Document";
|
||||
import type Revision from "@server/models/Revision";
|
||||
import diff from "@server/utils/diff";
|
||||
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
||||
import { getSignedUrl } from "@server/utils/s3";
|
||||
import Attachment from "../Attachment";
|
||||
|
||||
1118
server/utils/diff.ts
Normal file
1118
server/utils/diff.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,109 @@ export type Props = {
|
||||
theme: DefaultTheme;
|
||||
};
|
||||
|
||||
const mathStyle = (props: Props) => `
|
||||
/* Based on https://github.com/benrbray/prosemirror-math/blob/master/style/math.css */
|
||||
|
||||
.math-node {
|
||||
min-width: 1em;
|
||||
min-height: 1em;
|
||||
font-size: 0.95em;
|
||||
font-family: ${props.theme.fontFamilyMono};
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.math-node.empty-math .math-render::before {
|
||||
content: "(empty math)";
|
||||
color: ${props.theme.brand.red};
|
||||
}
|
||||
|
||||
.math-node .math-render.parse-error::before {
|
||||
content: "(math error)";
|
||||
color: ${props.theme.brand.red};
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.math-node.ProseMirror-selectednode {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.math-node .math-src {
|
||||
display: none;
|
||||
color: ${props.theme.codeStatement};
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.math-node.ProseMirror-selectednode .math-src {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.math-node.ProseMirror-selectednode .math-render {
|
||||
display: none;
|
||||
}
|
||||
|
||||
math-inline {
|
||||
display: inline; white-space: nowrap;
|
||||
|
||||
}
|
||||
|
||||
math-inline .math-render {
|
||||
display: inline-block;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
math-inline .math-src .ProseMirror {
|
||||
display: inline;
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${props.theme.codeBorder};
|
||||
padding: 3px 4px;
|
||||
margin: 0px 3px;
|
||||
font-family: ${props.theme.fontFamilyMono};
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
math-block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
math-block .math-render {
|
||||
display: block;
|
||||
}
|
||||
|
||||
math-block.ProseMirror-selectednode {
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${props.theme.codeBorder};
|
||||
background: ${props.theme.codeBackground};
|
||||
padding: 0.75em 1em;
|
||||
font-family: ${props.theme.fontFamilyMono};
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
math-block .math-src .ProseMirror {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
math-block .katex-display {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p::selection, p > *::selection {
|
||||
background-color: #c0c0c0;
|
||||
}
|
||||
|
||||
.katex-html *::selection {
|
||||
background-color: none !important;
|
||||
}
|
||||
|
||||
.math-node.math-select .math-render {
|
||||
background-color: #c0c0c0ff;
|
||||
}
|
||||
|
||||
math-inline.math-select .math-render {
|
||||
padding-top: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
const style = (props: Props) => `
|
||||
flex-grow: ${props.grow ? 1 : 0};
|
||||
justify-content: start;
|
||||
@@ -329,6 +432,7 @@ h6:not(.placeholder):before {
|
||||
content: "H6";
|
||||
}
|
||||
|
||||
.ProseMirror[contenteditable="true"]:focus-within,
|
||||
.ProseMirror-focused {
|
||||
h1,
|
||||
h2,
|
||||
@@ -1254,6 +1358,7 @@ table {
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror[contenteditable="true"]:focus-within,
|
||||
.ProseMirror-focused .block-menu-trigger,
|
||||
.block-menu-trigger:active,
|
||||
.block-menu-trigger:focus {
|
||||
@@ -1344,6 +1449,7 @@ del[data-operation-index] {
|
||||
|
||||
const EditorContainer = styled.div<Props>`
|
||||
${style};
|
||||
${mathStyle};
|
||||
`;
|
||||
|
||||
export default EditorContainer;
|
||||
|
||||
86
shared/editor/nodes/Math.ts
Normal file
86
shared/editor/nodes/Math.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
mathBackspaceCmd,
|
||||
insertMathCmd,
|
||||
makeInlineMathInputRule,
|
||||
REGEX_INLINE_MATH_DOLLARS,
|
||||
mathSchemaSpec,
|
||||
} from "@benrbray/prosemirror-math";
|
||||
import { PluginSimple } from "markdown-it";
|
||||
import {
|
||||
chainCommands,
|
||||
deleteSelection,
|
||||
selectNodeBackward,
|
||||
joinBackward,
|
||||
} from "prosemirror-commands";
|
||||
import {
|
||||
NodeSpec,
|
||||
NodeType,
|
||||
Schema,
|
||||
Node as ProsemirrorNode,
|
||||
} from "prosemirror-model";
|
||||
import { EditorState, Plugin } from "prosemirror-state";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import MathPlugin from "../plugins/Math";
|
||||
import mathRule from "../rules/math";
|
||||
import { Dispatch } from "../types";
|
||||
import Node from "./Node";
|
||||
|
||||
export default class Math extends Node {
|
||||
get name() {
|
||||
return "math_inline";
|
||||
}
|
||||
|
||||
get schema(): NodeSpec {
|
||||
return mathSchemaSpec.nodes.math_inline;
|
||||
}
|
||||
|
||||
commands({ type }: { type: NodeType }) {
|
||||
return () => (state: EditorState, dispatch: Dispatch) => {
|
||||
dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView());
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
inputRules({ schema }: { schema: Schema }) {
|
||||
return [
|
||||
makeInlineMathInputRule(
|
||||
REGEX_INLINE_MATH_DOLLARS,
|
||||
schema.nodes.math_inline
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
keys({ type }: { type: NodeType }) {
|
||||
return {
|
||||
"Mod-Space": insertMathCmd(type),
|
||||
Backspace: chainCommands(
|
||||
deleteSelection,
|
||||
mathBackspaceCmd,
|
||||
joinBackward,
|
||||
selectNodeBackward
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
get plugins(): Plugin[] {
|
||||
return [MathPlugin];
|
||||
}
|
||||
|
||||
get rulePlugins(): PluginSimple[] {
|
||||
return [mathRule];
|
||||
}
|
||||
|
||||
toMarkdown(state: MarkdownSerializerState, node: ProsemirrorNode) {
|
||||
state.write("$");
|
||||
state.text(node.textContent, false);
|
||||
state.write("$");
|
||||
}
|
||||
|
||||
parseMarkdown() {
|
||||
return {
|
||||
node: "math_inline",
|
||||
block: "math_inline",
|
||||
noCloseToken: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
53
shared/editor/nodes/MathBlock.ts
Normal file
53
shared/editor/nodes/MathBlock.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
makeBlockMathInputRule,
|
||||
REGEX_BLOCK_MATH_DOLLARS,
|
||||
mathSchemaSpec,
|
||||
} from "@benrbray/prosemirror-math";
|
||||
import { PluginSimple } from "markdown-it";
|
||||
import { NodeSpec, NodeType, Node as ProsemirrorNode } from "prosemirror-model";
|
||||
import { EditorState } from "prosemirror-state";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import mathRule from "../rules/math";
|
||||
import { Dispatch } from "../types";
|
||||
import Node from "./Node";
|
||||
|
||||
export default class MathBlock extends Node {
|
||||
get name() {
|
||||
return "math_block";
|
||||
}
|
||||
|
||||
get schema(): NodeSpec {
|
||||
return mathSchemaSpec.nodes.math_display;
|
||||
}
|
||||
|
||||
get rulePlugins(): PluginSimple[] {
|
||||
return [mathRule];
|
||||
}
|
||||
|
||||
commands({ type }: { type: NodeType }) {
|
||||
return () => (state: EditorState, dispatch: Dispatch) => {
|
||||
dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView());
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
inputRules({ type }: { type: NodeType }) {
|
||||
return [makeBlockMathInputRule(REGEX_BLOCK_MATH_DOLLARS, type)];
|
||||
}
|
||||
|
||||
toMarkdown(state: MarkdownSerializerState, node: ProsemirrorNode) {
|
||||
state.write("$$\n");
|
||||
state.text(node.textContent, false);
|
||||
state.ensureNewLine();
|
||||
state.write("$$");
|
||||
state.closeBlock(node);
|
||||
}
|
||||
|
||||
parseMarkdown() {
|
||||
return {
|
||||
node: "math_block",
|
||||
block: "math_block",
|
||||
noCloseToken: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import Embed from "../nodes/Embed";
|
||||
import Heading from "../nodes/Heading";
|
||||
import HorizontalRule from "../nodes/HorizontalRule";
|
||||
import ListItem from "../nodes/ListItem";
|
||||
import Math from "../nodes/Math";
|
||||
import MathBlock from "../nodes/MathBlock";
|
||||
import Node from "../nodes/Node";
|
||||
import Notice from "../nodes/Notice";
|
||||
import OrderedList from "../nodes/OrderedList";
|
||||
@@ -36,6 +38,8 @@ const fullPackage: (typeof Node | typeof Mark | typeof Extension)[] = [
|
||||
OrderedList,
|
||||
Embed,
|
||||
ListItem,
|
||||
Math,
|
||||
MathBlock,
|
||||
Attachment,
|
||||
Notice,
|
||||
Heading,
|
||||
|
||||
75
shared/editor/plugins/Math.ts
Normal file
75
shared/editor/plugins/Math.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { MathView } from "@benrbray/prosemirror-math";
|
||||
import { Node as ProseNode } from "prosemirror-model";
|
||||
import { Plugin, PluginKey, PluginSpec } from "prosemirror-state";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
|
||||
export interface IMathPluginState {
|
||||
macros: { [cmd: string]: string };
|
||||
activeNodeViews: MathView[];
|
||||
prevCursorPos: number;
|
||||
}
|
||||
|
||||
const MATH_PLUGIN_KEY = new PluginKey<IMathPluginState>("prosemirror-math");
|
||||
|
||||
export function createMathView(displayMode: boolean) {
|
||||
return (
|
||||
node: ProseNode,
|
||||
view: EditorView,
|
||||
getPos: boolean | (() => number)
|
||||
): MathView => {
|
||||
// dynamically load katex styles and fonts
|
||||
import(
|
||||
/* webpackChunkName: "katex" */
|
||||
"katex/dist/katex.min.css"
|
||||
);
|
||||
|
||||
const pluginState = MATH_PLUGIN_KEY.getState(view.state);
|
||||
if (!pluginState) {
|
||||
throw new Error("no math plugin!");
|
||||
}
|
||||
const nodeViews = pluginState.activeNodeViews;
|
||||
|
||||
// set up NodeView
|
||||
const nodeView = new MathView(
|
||||
node,
|
||||
view,
|
||||
getPos as () => number,
|
||||
{ katexOptions: { displayMode, macros: pluginState.macros } },
|
||||
MATH_PLUGIN_KEY,
|
||||
() => {
|
||||
nodeViews.splice(nodeViews.indexOf(nodeView));
|
||||
}
|
||||
);
|
||||
|
||||
nodeViews.push(nodeView);
|
||||
return nodeView;
|
||||
};
|
||||
}
|
||||
|
||||
const mathPluginSpec: PluginSpec<IMathPluginState> = {
|
||||
key: MATH_PLUGIN_KEY,
|
||||
state: {
|
||||
init() {
|
||||
return {
|
||||
macros: {},
|
||||
activeNodeViews: [],
|
||||
prevCursorPos: 0,
|
||||
};
|
||||
},
|
||||
apply(tr, value, oldState) {
|
||||
return {
|
||||
activeNodeViews: value.activeNodeViews,
|
||||
macros: value.macros,
|
||||
prevCursorPos: oldState.selection.from,
|
||||
};
|
||||
},
|
||||
},
|
||||
props: {
|
||||
nodeViews: {
|
||||
math_inline: createMathView(false),
|
||||
math_block: createMathView(true),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default new Plugin(mathPluginSpec);
|
||||
180
shared/editor/rules/math.ts
Normal file
180
shared/editor/rules/math.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import MarkdownIt from "markdown-it";
|
||||
import StateBlock from "markdown-it/lib/rules_block/state_block";
|
||||
import StateInline from "markdown-it/lib/rules_inline/state_inline";
|
||||
|
||||
// test if potential opening or closing delimiter
|
||||
// assumes that there is a "$" at state.src[pos]
|
||||
function isValidDelimiter(state: StateInline, pos: number) {
|
||||
const max = state.posMax;
|
||||
let canOpen = true,
|
||||
canClose = true;
|
||||
|
||||
const prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1;
|
||||
const nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1;
|
||||
|
||||
// check non-whitespace conditions for open/close, and
|
||||
// check that closing delimiter isn't followed by a number
|
||||
if (
|
||||
prevChar === 0x20 || // " "
|
||||
prevChar === 0x09 || // "\t"
|
||||
(nextChar >= 0x30 && nextChar <= 0x39) // "0" - "9"
|
||||
) {
|
||||
canClose = false;
|
||||
}
|
||||
|
||||
if (nextChar === 0x20 || nextChar === 0x09) {
|
||||
canOpen = false;
|
||||
}
|
||||
|
||||
return { canOpen, canClose };
|
||||
}
|
||||
|
||||
function mathInline(state: StateInline, silent: boolean): boolean {
|
||||
let match, token, res, pos;
|
||||
|
||||
if (state.src[state.pos] !== "$") {
|
||||
return false;
|
||||
}
|
||||
|
||||
res = isValidDelimiter(state, state.pos);
|
||||
if (!res.canOpen) {
|
||||
if (!silent) {
|
||||
state.pending += "$";
|
||||
}
|
||||
state.pos += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// first check for and bypass all properly escaped delimiters
|
||||
// this loop will assume that the first leading backtick can not
|
||||
// be the first character in state.src, which is known since
|
||||
// we have found an opening delimiter already
|
||||
const start = state.pos + 1;
|
||||
match = start;
|
||||
while ((match = state.src.indexOf("$", match)) !== 1) {
|
||||
// found potential $, look for escapes, pos will point to
|
||||
// first non escape when complete
|
||||
pos = match - 1;
|
||||
while (state.src[pos] === "\\") {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
// even number of escapes, potential closing delimiter found
|
||||
if ((match - pos) % 2 === 1) {
|
||||
break;
|
||||
}
|
||||
match += 1;
|
||||
}
|
||||
|
||||
// no closing delimiter found, consume $ and continue
|
||||
if (match === -1) {
|
||||
if (!silent) {
|
||||
state.pending += "$";
|
||||
}
|
||||
state.pos = start;
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if we have empty content (ex. $$) do not parse
|
||||
if (match - start === 0) {
|
||||
if (!silent) {
|
||||
state.pending += "$$";
|
||||
}
|
||||
state.pos = start + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// check for valid closing delimiter
|
||||
res = isValidDelimiter(state, match);
|
||||
if (!res.canClose) {
|
||||
if (!silent) {
|
||||
state.pending += "$";
|
||||
}
|
||||
state.pos = start;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
token = state.push("math_inline", "math", 0);
|
||||
token.markup = "$";
|
||||
token.content = state.src.slice(start, match);
|
||||
}
|
||||
|
||||
state.pos = match + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
function mathDisplay(
|
||||
state: StateBlock,
|
||||
start: number,
|
||||
end: number,
|
||||
silent: boolean
|
||||
) {
|
||||
let firstLine,
|
||||
lastLine,
|
||||
next,
|
||||
lastPos,
|
||||
found = false,
|
||||
pos = state.bMarks[start] + state.tShift[start],
|
||||
max = state.eMarks[start];
|
||||
|
||||
if (pos + 2 > max) {
|
||||
return false;
|
||||
}
|
||||
if (state.src.slice(pos, pos + 2) !== "$$") {
|
||||
return false;
|
||||
}
|
||||
|
||||
pos += 2;
|
||||
firstLine = state.src.slice(pos, max);
|
||||
|
||||
if (silent) {
|
||||
return true;
|
||||
}
|
||||
if (firstLine.trim().slice(-2) === "$$") {
|
||||
// Single line expression
|
||||
firstLine = firstLine.trim().slice(0, -2);
|
||||
found = true;
|
||||
}
|
||||
|
||||
for (next = start; !found; ) {
|
||||
next++;
|
||||
|
||||
if (next >= end) {
|
||||
break;
|
||||
}
|
||||
|
||||
pos = state.bMarks[next] + state.tShift[next];
|
||||
max = state.eMarks[next];
|
||||
|
||||
if (pos < max && state.tShift[next] < state.blkIndent) {
|
||||
// non-empty line with negative indent should stop the list:
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.src.slice(pos, max).trim().slice(-2) === "$$") {
|
||||
lastPos = state.src.slice(0, max).lastIndexOf("$$");
|
||||
lastLine = state.src.slice(pos, lastPos);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
state.line = next + 1;
|
||||
|
||||
const token = state.push("math_block", "math", 0);
|
||||
token.block = true;
|
||||
token.content =
|
||||
(firstLine && firstLine.trim() ? firstLine + "\n" : "") +
|
||||
state.getLines(start + 1, next, state.tShift[start], true) +
|
||||
(lastLine && lastLine.trim() ? lastLine : "");
|
||||
token.map = [start, state.line];
|
||||
token.markup = "$$";
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function markdownMath(md: MarkdownIt) {
|
||||
md.inline.ruler.after("escape", "math_inline", mathInline);
|
||||
md.block.ruler.after("blockquote", "math_block", mathDisplay, {
|
||||
alt: ["paragraph", "reference", "blockquote", "list"],
|
||||
});
|
||||
}
|
||||
@@ -277,6 +277,8 @@
|
||||
"Bold": "Bold",
|
||||
"Subheading": "Subheading",
|
||||
"Table": "Table",
|
||||
"Math inline (LaTeX)": "Math inline (LaTeX)",
|
||||
"Math block (LaTeX)": "Math block (LaTeX)",
|
||||
"Tip": "Tip",
|
||||
"Tip notice": "Tip notice",
|
||||
"Show diagram": "Show diagram",
|
||||
@@ -571,7 +573,9 @@
|
||||
"Numbered list": "Numbered list",
|
||||
"Blockquote": "Blockquote",
|
||||
"Horizontal divider": "Horizontal divider",
|
||||
"LaTeX block": "LaTeX block",
|
||||
"Inline code": "Inline code",
|
||||
"Inline LaTeX": "Inline LaTeX",
|
||||
"Sign In": "Sign In",
|
||||
"Continue with Email": "Continue with Email",
|
||||
"Continue with {{ authProviderName }}": "Continue with {{ authProviderName }}",
|
||||
|
||||
1
shared/typings/styles.d.ts
vendored
Normal file
1
shared/typings/styles.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module "*.css";
|
||||
@@ -17,27 +17,32 @@ module.exports = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.[jt]sx?$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: [
|
||||
path.join(__dirname, 'node_modules')
|
||||
],
|
||||
include: [
|
||||
path.join(__dirname, 'app'),
|
||||
path.join(__dirname, 'shared'),
|
||||
],
|
||||
options: {
|
||||
cacheDirectory: true
|
||||
}
|
||||
test: /\.[jt]sx?$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: [
|
||||
path.join(__dirname, 'node_modules')
|
||||
],
|
||||
include: [
|
||||
path.join(__dirname, 'app'),
|
||||
path.join(__dirname, 'shared'),
|
||||
],
|
||||
options: {
|
||||
cacheDirectory: true
|
||||
}
|
||||
},
|
||||
// inline base64 URLs for <=8k images, direct URLs for the rest
|
||||
{ test: /\.(png|jpg|svg)$/, loader: 'url-loader' },
|
||||
{
|
||||
test: /\.woff$/,
|
||||
loader: 'url-loader?limit=1&mimetype=application/font-woff&name=public/fonts/[name].[ext]',
|
||||
test: /\.(woff|woff2|ttf|eot)$/,
|
||||
loader:
|
||||
'url-loader?limit=1&mimetype=application/font-woff&name=public/fonts/[name].[ext]',
|
||||
},
|
||||
{ test: /\.md/, loader: 'raw-loader' },
|
||||
]
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
||||
|
||||
116
yarn.lock
116
yarn.lock
@@ -1057,6 +1057,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@benrbray/prosemirror-math@^0.2.2":
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@benrbray/prosemirror-math/-/prosemirror-math-0.2.2.tgz#5b2aeb7c8c5a41d2e39317fe1fbd7040fd347cc9"
|
||||
integrity sha512-n+V8MNKaQ9HtA1IASzoBFwthFY55kpu2I+0aF103AbqUw5eM8YlxHeltnLqjnYRVY4/a6A9t9YlBMBQOli5jgw==
|
||||
|
||||
"@braintree/sanitize-url@^6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz#fe364f025ba74f6de6c837a84ef44bdb1d61e68f"
|
||||
@@ -2903,6 +2908,11 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/katex@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.14.0.tgz#b84c0afc3218069a5ad64fe2a95321881021b5fe"
|
||||
integrity sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==
|
||||
|
||||
"@types/keygrip@*":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
|
||||
@@ -5407,7 +5417,7 @@ commander@^6.1.0:
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75"
|
||||
integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==
|
||||
|
||||
commander@^8.3.0:
|
||||
commander@^8.0.0, commander@^8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
|
||||
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
|
||||
@@ -5752,6 +5762,22 @@ css-color-names@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67"
|
||||
integrity sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==
|
||||
|
||||
css-loader@5.2.6:
|
||||
version "5.2.6"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.6.tgz#c3c82ab77fea1f360e587d871a6811f4450cc8d1"
|
||||
integrity sha512-0wyN5vXMQZu6BvjbrPdUJvkCzGEO24HC7IS7nW4llc6BBFC+zwR9CKtYGv63Puzsg10L/o12inMY5/2ByzfD6w==
|
||||
dependencies:
|
||||
icss-utils "^5.1.0"
|
||||
loader-utils "^2.0.0"
|
||||
postcss "^8.2.15"
|
||||
postcss-modules-extract-imports "^3.0.0"
|
||||
postcss-modules-local-by-default "^4.0.0"
|
||||
postcss-modules-scope "^3.0.0"
|
||||
postcss-modules-values "^4.0.0"
|
||||
postcss-value-parser "^4.1.0"
|
||||
schema-utils "^3.0.0"
|
||||
semver "^7.3.5"
|
||||
|
||||
css-rules@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-rules/-/css-rules-1.1.0.tgz#404b8b1f77bd775f6c6902b7a7b534f5c016b07f"
|
||||
@@ -5815,6 +5841,11 @@ css-what@^6.1.0:
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
|
||||
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
|
||||
|
||||
cssesc@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
||||
|
||||
cssom@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36"
|
||||
@@ -8638,6 +8669,11 @@ iconv-lite@0.6, iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
icss-utils@^5.0.0, icss-utils@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
|
||||
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
|
||||
|
||||
idb@^6.1.4:
|
||||
version "6.1.5"
|
||||
resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.5.tgz#dbc53e7adf1ac7c59f9b2bf56e00b4ea4fce8c7b"
|
||||
@@ -9974,6 +10010,13 @@ jws@^3.2.2:
|
||||
jwa "^1.4.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
katex@^0.16.3:
|
||||
version "0.16.3"
|
||||
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.3.tgz#29640560b8fa0403e45f3aa20da5fdbb6d2b83a8"
|
||||
integrity sha512-3EykQddareoRmbtNiNEDgl3IGjryyrp2eg/25fHDEnlHymIDi33bptkMv6K4EOC2LZCybLW/ZkEo6Le+EM9pmA==
|
||||
dependencies:
|
||||
commander "^8.0.0"
|
||||
|
||||
kbar@0.1.0-beta.28:
|
||||
version "0.1.0-beta.28"
|
||||
resolved "https://registry.yarnpkg.com/kbar/-/kbar-0.1.0-beta.28.tgz#35bcf1d45996f5b0cf32f4f70f673c97dc67a1f8"
|
||||
@@ -11117,6 +11160,11 @@ nan@^2.12.1, nan@^2.16.0:
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
|
||||
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
|
||||
|
||||
nanoid@^3.3.4:
|
||||
version "3.3.4"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
||||
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.13"
|
||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
||||
@@ -12106,10 +12154,55 @@ posix-character-classes@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
|
||||
integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
|
||||
|
||||
postcss-value-parser@^4.0.2:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
|
||||
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
|
||||
postcss-modules-extract-imports@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
|
||||
integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
|
||||
|
||||
postcss-modules-local-by-default@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
|
||||
integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
|
||||
dependencies:
|
||||
icss-utils "^5.0.0"
|
||||
postcss-selector-parser "^6.0.2"
|
||||
postcss-value-parser "^4.1.0"
|
||||
|
||||
postcss-modules-scope@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
|
||||
integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
|
||||
dependencies:
|
||||
postcss-selector-parser "^6.0.4"
|
||||
|
||||
postcss-modules-values@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
|
||||
integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==
|
||||
dependencies:
|
||||
icss-utils "^5.0.0"
|
||||
|
||||
postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
|
||||
version "6.0.10"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
|
||||
integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.2.15:
|
||||
version "8.4.18"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.18.tgz#6d50046ea7d3d66a85e0e782074e7203bc7fbca2"
|
||||
integrity sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==
|
||||
dependencies:
|
||||
nanoid "^3.3.4"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
postgres-array@~2.0.0:
|
||||
version "2.0.0"
|
||||
@@ -13906,6 +13999,11 @@ source-list-map@~0.1.7:
|
||||
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106"
|
||||
integrity sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=
|
||||
|
||||
source-map-js@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||
|
||||
source-map-resolve@^0.5.0:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
|
||||
@@ -14296,6 +14394,14 @@ style-data@^2.0.0:
|
||||
mediaquery-text "^1.2.0"
|
||||
pick-util "^1.1.4"
|
||||
|
||||
style-loader@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c"
|
||||
integrity sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==
|
||||
dependencies:
|
||||
loader-utils "^2.0.0"
|
||||
schema-utils "^3.0.0"
|
||||
|
||||
style-value-types@4.1.4:
|
||||
version "4.1.4"
|
||||
resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-4.1.4.tgz#80f37cb4fb024d6394087403dfb275e8bb627e75"
|
||||
|
||||
Reference in New Issue
Block a user