From 6a17bc03e77bfecdd726359b60f0223973930919 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Thu, 27 Jul 2023 12:31:42 +0500 Subject: [PATCH] editor: add new POC for math nodes --- .../src/extensions/code-block/code-block.ts | 38 +- .../src/extensions/code-block/highlighter.ts | 5 +- packages/editor/src/extensions/math/block.tsx | 183 ++++++++++ .../editor/src/extensions/math/component.tsx | 88 +++++ .../editor/src/extensions/math/math-block.ts | 345 ++++++++++++++++-- .../editor/src/extensions/math/math-inline.ts | 28 +- .../extensions/math/plugin/renderers/katex.ts | 11 +- .../src/extensions/react/react-node-view.tsx | 4 +- .../react/selection-based-react-node-view.tsx | 18 +- packages/editor/styles/styles.css | 35 -- 10 files changed, 652 insertions(+), 103 deletions(-) create mode 100644 packages/editor/src/extensions/math/block.tsx create mode 100644 packages/editor/src/extensions/math/component.tsx diff --git a/packages/editor/src/extensions/code-block/code-block.ts b/packages/editor/src/extensions/code-block/code-block.ts index cee1d2fd38..4bb14b2a55 100644 --- a/packages/editor/src/extensions/code-block/code-block.ts +++ b/packages/editor/src/extensions/code-block/code-block.ts @@ -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({ 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({ 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({ 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({ 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 { diff --git a/packages/editor/src/extensions/code-block/highlighter.ts b/packages/editor/src/extensions/code-block/highlighter.ts index f3ee92df01..e417636d68 100644 --- a/packages/editor/src/extensions/code-block/highlighter.ts +++ b/packages/editor/src/extensions/code-block/highlighter.ts @@ -109,7 +109,9 @@ export function HighlighterPlugin({ name: string; defaultLanguage: string | null | undefined; }) { - const HIGHLIGHTER_PLUGIN_KEY = new PluginKey("highlighter"); + const HIGHLIGHTER_PLUGIN_KEY = new PluginKey( + `${name}-highlighter` + ); const HIGHLIGHTED_BLOCKS: Set = new Set(); return new Plugin({ @@ -293,6 +295,7 @@ function updateSelection( } const position = toCaretPosition( + name, newState.selection, isDocChanged ? toCodeLines(node.textContent, pos) : undefined ); diff --git a/packages/editor/src/extensions/math/block.tsx b/packages/editor/src/extensions/math/block.tsx new file mode 100644 index 0000000000..23e9eff870 --- /dev/null +++ b/packages/editor/src/extensions/math/block.tsx @@ -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 . +*/ + +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 +) { + const { editor, node, forwardRef, getPos } = props; + const { indentLength, indentType, caretPosition } = node.attrs; + const toolbarRef = useRef(null); + const { enabled, start } = useTimer(1000); + const isActive = editor.isActive(MathBlock.name); + const elementRef = useRef(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 ( + <> + + + + {caretPosition ? ( + + Line {caretPosition.line}, Column {caretPosition.column}{" "} + {caretPosition.selected + ? `(${caretPosition.selected} selected)` + : ""} + + ) : null} + + + + + + {node.textContent?.length > 0 ? ( + + ) : null} + + + + + ); +} diff --git a/packages/editor/src/extensions/math/component.tsx b/packages/editor/src/extensions/math/component.tsx new file mode 100644 index 0000000000..133502d4a1 --- /dev/null +++ b/packages/editor/src/extensions/math/component.tsx @@ -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 . +*/ + +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(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 ( + <> + + + + + + ); +} diff --git a/packages/editor/src/extensions/math/math-block.ts b/packages/editor/src/extensions/math/math-block.ts index 7d878a2423..68fc41e155 100644 --- a/packages/editor/src/extensions/math/math-block.ts +++ b/packages/editor/src/extensions/math/math-block.ts @@ -17,9 +17,25 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -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 { @@ -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, "

"); + }, + // 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)}`; +} diff --git a/packages/editor/src/extensions/math/math-inline.ts b/packages/editor/src/extensions/math/math-inline.ts index 64e6944064..f7af6d1718 100644 --- a/packages/editor/src/extensions/math/math-inline.ts +++ b/packages/editor/src/extensions/math/math-inline.ts @@ -18,7 +18,8 @@ along with this program. If not, see . */ import { Node, mergeAttributes } from "@tiptap/core"; -import { mathPlugin } from "./plugin"; +import { createSelectionBasedNodeView } from "../react"; +import { InlineMathComponent } from "./component"; declare module "@tiptap/core" { interface Commands { @@ -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("(? document.createElement("span") + }); }, addInputRules() { diff --git a/packages/editor/src/extensions/math/plugin/renderers/katex.ts b/packages/editor/src/extensions/math/plugin/renderers/katex.ts index 345044741f..941b33b278 100644 --- a/packages/editor/src/extensions/math/plugin/renderers/katex.ts +++ b/packages/editor/src/extensions/math/plugin/renderers/katex.ts @@ -17,16 +17,19 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +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 = { diff --git a/packages/editor/src/extensions/react/react-node-view.tsx b/packages/editor/src/extensions/react/react-node-view.tsx index c8cd89c38c..251beb0d19 100644 --- a/packages/editor/src/extensions/react/react-node-view.tsx +++ b/packages/editor/src/extensions/react/react-node-view.tsx @@ -126,7 +126,9 @@ export class ReactNodeView

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` ); diff --git a/packages/editor/src/extensions/react/selection-based-react-node-view.tsx b/packages/editor/src/extensions/react/selection-based-react-node-view.tsx index ba8029f835..f78e9d66ba 100644 --- a/packages/editor/src/extensions/react/selection-based-react-node-view.tsx +++ b/packages/editor/src/extensions/react/selection-based-react-node-view.tsx @@ -20,7 +20,7 @@ along with this program. If not, see . 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 = diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css index c69292a2fb..0a6f2b4be9 100644 --- a/packages/editor/styles/styles.css +++ b/packages/editor/styles/styles.css @@ -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 {