Skip to content

Commit

Permalink
Initial math
Browse files Browse the repository at this point in the history
  • Loading branch information
allisonking committed Feb 26, 2025
1 parent e159696 commit f6b52e0
Show file tree
Hide file tree
Showing 14 changed files with 240 additions and 37 deletions.
2 changes: 2 additions & 0 deletions packages/context-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@
"vitest": "catalog:"
},
"dependencies": {
"@benrbray/prosemirror-math": "^1.0.0",
"@nytimes/react-prosemirror": "^1.0.0",
"@prosemirror-adapter/react": "^0.4.0",
"deepmerge": "^4.3.1",
"fuzzy": "^0.1.3",
"install": "^0.13.0",
"katex": "^0.16.18",
"lucide-react": "^0.469.0",
"prosemirror-autocomplete": "^0.4.3",
"prosemirror-commands": "^1.6.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/context-editor/src/ContextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import { baseSchema } from "./schemas";

import "prosemirror-view/style/prosemirror.css";
import "prosemirror-gapcursor/style/gapcursor.css";
// For math
import "@benrbray/prosemirror-math/dist/prosemirror-math.css";
import "katex/dist/katex.min.css";

import SuggestPanel from "./components/SuggestPanel";

Expand Down
2 changes: 1 addition & 1 deletion packages/context-editor/src/commands/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Node, NodeType } from "prosemirror-model";
import { NodeSelection } from "prosemirror-state";

import type { Attrs, ToggleCommandFn, ToggleOptions } from "./types";
import { createTypeToggle } from "./utils";
import { createTypeToggle } from "./util";

const nodeMatchesTypeAndAttrs = (node: Node, type: NodeType, attrs?: Attrs) => {
if (node.type === type) {
Expand Down
2 changes: 1 addition & 1 deletion packages/context-editor/src/commands/marks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { MarkType } from "prosemirror-model";
import { toggleMark as pmToggleMark } from "prosemirror-commands";

import type { ToggleOptions } from "./types";
import { createTypeToggle } from "./utils";
import { createTypeToggle } from "./util";

export const markIsActive = (options: ToggleOptions<MarkType>) => {
const { type, state } = options;
Expand Down
58 changes: 58 additions & 0 deletions packages/context-editor/src/commands/math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Command } from "prosemirror-state";

import { EditorState, NodeSelection } from "prosemirror-state";

import type { Dispatch } from "./types";
import { createCommandSpec } from "./util";

const toggleKind: Command = (state: EditorState, dispatch?: Dispatch) => {
const { node } = state.selection as NodeSelection;
const canRun = node && (node.type.name === "math_inline" || node.type.name === "math_display");
if (!canRun) {
return false;
}
const isDisplay = node.type.name === "math_display";
if (dispatch) {
const {
schema: {
nodes: { math_display: displayType, math_inline: inlineType },
},
} = state;
const swapNodeType = isDisplay ? inlineType : displayType;
const transaction = state.tr.replaceSelectionWith(
swapNodeType.create({}, node.content),
true
);
dispatch(transaction);
}
return true;
};

const toggleLabel: Command = (state: EditorState, dispatch?: Dispatch) => {
const { node, $anchor } = state.selection as NodeSelection;
const canRun = node && node.type.name === "math_display";
if (!canRun) return false;
if (dispatch) {
const transaction = state.tr.setNodeMarkup(
$anchor.pos,
node.type,
{
hideLabel: !node.attrs.hideLabel,
},
node.marks
);
dispatch(transaction);
}
return true;
};

export const mathToggleKind = createCommandSpec((dispatch, state) => ({
run: () => toggleKind(state, dispatch),
canRun: toggleKind(state),
isActive: false,
}));
export const mathToggleLabel = createCommandSpec((dispatch, state) => ({
run: () => toggleLabel(state, dispatch),
canRun: toggleLabel(state),
isActive: false,
}));
90 changes: 59 additions & 31 deletions packages/context-editor/src/components/MenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ReactNode } from "react";

import React from "react";
import { usePluginViewContext } from "@prosemirror-adapter/react";
import { Quote } from "lucide-react";
import { Bold, Italic, Quote, SquareFunction } from "lucide-react";

import { Button } from "ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "ui/select";
Expand All @@ -20,33 +20,45 @@ import {
paragraphToggle,
} from "../commands/blocks";
import { emToggle, strongToggle } from "../commands/marks";
import { insertNodeIntoEditor } from "../utils/nodes";

type MenuItem = {
type MenuItemBase = {
key: string;
name?: string;
name: string;
icon: ReactNode;
command: CommandSpec;
};

type MenuItem = MenuItemBase & ({ command: CommandSpec } | { insertNodeType: string });
type ParagraphSelectorItem = MenuItemBase & { command: CommandSpec };

const menuItems: MenuItem[] = [
{
key: "strong",
icon: "B",
name: "Bold",
icon: <Bold />,
command: strongToggle,
},
{
key: "em",
icon: <span className="italic">I</span>,
name: "Italic",
icon: <Italic />,
command: emToggle,
},
{
key: "blockquote",
name: "Blockquote",
icon: <Quote />,
command: blockquoteToggle,
},
{
key: "math",
name: "Math",
icon: <SquareFunction />,
insertNodeType: "math_inline",
},
];

const paragraphTypeItems: MenuItem[] = [
const paragraphTypeItems: ParagraphSelectorItem[] = [
{
key: "paragraph",
name: "Paragraph",
Expand Down Expand Up @@ -115,11 +127,11 @@ const ParagraphDropdown = () => {
>
<SelectTrigger className="w-fit border-none">
<SelectValue placeholder="Paragraph">
{activeType ? activeType.name || activeType.key : "Paragraph"}
{activeType ? activeType.name : "Paragraph"}
</SelectValue>
</SelectTrigger>
<SelectContent className="bg-white">
{paragraphTypeItems.map(({ key, icon, command }) => {
{paragraphTypeItems.map(({ key, icon }) => {
return (
<SelectItem key={key} value={key}>
{icon}
Expand All @@ -135,31 +147,47 @@ export const MenuBar = () => {
const { view } = usePluginViewContext();
return (
<div
className="flex items-center rounded border bg-slate-50"
className="flex items-center gap-1 rounded border bg-slate-50"
role="toolbar"
aria-label="Formatting tools"
>
{menuItems.map((menuItem) => {
const { key, icon, command } = menuItem;
const { run, canRun, isActive } = command(view)(view.state);
return (
<Button
key={key}
onClick={() => {
view.focus();
run();
}}
variant="ghost"
size="sm"
disabled={!canRun}
type="button"
className={cn("w-6 rounded-none", { "bg-slate-300": isActive })}
>
{icon}
</Button>
);
})}
<ParagraphDropdown />
<div className="border-r-2">
<ParagraphDropdown />
</div>
<div className="flex gap-1">
{menuItems.map((menuItem) => {
const { key, name, icon } = menuItem;
let canRun = true;
// TODO: this isn't right
let isActive = false;
let run: () => void;
if ("command" in menuItem) {
const { command } = menuItem;
({ run, canRun, isActive } = command(view)(view.state));
} else {
run = () => insertNodeIntoEditor(view, menuItem.insertNodeType);
}
return (
<Button
key={key}
onClick={() => {
view.focus();
run();
}}
variant="ghost"
size="sm"
disabled={!canRun}
type="button"
className={cn("w-6 rounded-none", {
"bg-slate-300 hover:bg-slate-400": isActive,
})}
title={name}
>
{icon}
</Button>
);
})}
</div>
</div>
);
};
2 changes: 2 additions & 0 deletions packages/context-editor/src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { mathPlugin } from "@benrbray/prosemirror-math";
import { exampleSetup } from "prosemirror-example-setup";
import { Schema } from "prosemirror-model";

Expand All @@ -24,6 +25,7 @@ export const basePlugins = (
structureDecorations(),
attributePanel(panelPosition, setPanelPosition),
onChange(),
mathPlugin,
inputRules(schema),
];
};
12 changes: 12 additions & 0 deletions packages/context-editor/src/plugins/inputRules.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { MarkType, NodeType } from "prosemirror-model";
import type { EditorState } from "prosemirror-state";

import {
makeBlockMathInputRule,
makeInlineMathInputRule,
REGEX_BLOCK_MATH_DOLLARS,
REGEX_INLINE_MATH_DOLLARS,
} from "@benrbray/prosemirror-math";
import { InputRule, inputRules, wrappingInputRule } from "prosemirror-inputrules";
import { Fragment, Schema } from "prosemirror-model";

Expand Down Expand Up @@ -40,6 +46,10 @@ const applyMarkRule = (markType: MarkType, regex: RegExp) => {
);
};
const blockQuoteRule = (nodeType: NodeType) => wrappingInputRule(/^\s*>\s$/, nodeType);
const inlineMathRule = (nodeType: NodeType) =>
makeInlineMathInputRule(REGEX_INLINE_MATH_DOLLARS, nodeType);
const blockMathRule = (nodeType: NodeType) =>
makeBlockMathInputRule(REGEX_BLOCK_MATH_DOLLARS, nodeType);

export default (schema: Schema) => {
const rules = [
Expand All @@ -56,6 +66,8 @@ export default (schema: Schema) => {
applyMarkRule(schema.marks.strong, boldRegex),
applyMarkRule(schema.marks.em, italicsRegex),
blockQuoteRule(schema.nodes.blockquote),
inlineMathRule(schema.nodes.math_inline),
blockMathRule(schema.nodes.math_display),
];
return inputRules({ rules });
};
1 change: 1 addition & 0 deletions packages/context-editor/src/schemas/blockquote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default {
getAttrs: (node) => {
return {
id: (node as Element).getAttribute("id"),
class: (node as Element).getAttribute("class"),
};
},
},
Expand Down
6 changes: 2 additions & 4 deletions packages/context-editor/src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@ import blockquote from "./blockquote";
import inlineCode from "./code";
import contextAtom from "./contextAtom";
import contextDoc from "./contextDoc";
// import { marks, nodes } from "prosemirror-schema-basic";

/* Nodes */
import doc from "./doc";
import em from "./em";
import heading from "./heading";
import math from "./math";
import paragraph from "./paragraph";
/* Marks */
import strong from "./strong";
import text from "./text";

Expand All @@ -24,6 +21,7 @@ export const baseSchema = new Schema({
contextDoc,
contextAtom,
blockquote,
...math,
},
marks: {
strong,
Expand Down
52 changes: 52 additions & 0 deletions packages/context-editor/src/schemas/math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { DOMOutputSpec, MarkSpec, NodeSpec } from "prosemirror-model";

const mathInline = {
attrs: {
id: { default: null },
class: { default: null },
},
content: "text*",
group: "inline math",
inline: true,
atom: true,
parseDOM: [
{
tag: "math-inline",
getAttrs: (node) => {
return {
id: (node as Element).getAttribute("id"),
class: (node as Element).getAttribute("class"),
};
},
},
],
toDOM: () => ["math-inline", { class: "math-node" }, 0],
} satisfies NodeSpec;

const mathDisplay = {
attrs: {
id: { default: null },
class: { default: null },
},
group: "block math",
content: "text*",
atom: true,
code: true,
parseDOM: [
{
tag: "math-display",
getAttrs: (node) => {
return {
id: (node as Element).getAttribute("id"),
class: (node as Element).getAttribute("class"),
};
},
},
],
toDOM: () => ["math-display", { class: "math-node" }, 0],
} satisfies NodeSpec;

export default {
math_inline: mathInline,
math_display: mathDisplay,
};
15 changes: 15 additions & 0 deletions packages/context-editor/src/utils/nodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { EditorView } from "prosemirror-view";

import { Node } from "prosemirror-model";

export const insertNodeIntoEditor = (view: EditorView, nodeType: string, attrs?: Node["attrs"]) => {
const { schema, tr } = view.state;
const nodeSchema = schema.nodes[nodeType];
if (nodeSchema.spec.onInsert) {
nodeSchema.spec.onInsert(view, attrs);
} else {
const node = nodeSchema.create(attrs);
const transaction = tr.replaceSelectionWith(node);
view.dispatch(transaction);
}
};
Loading

0 comments on commit f6b52e0

Please sign in to comment.