Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { BlockNoteEditor, createExtension, createStore } from "@blocknote/core";
import {
InputRule,
inputRules as inputRulesPlugin,
} from "@handlewithcare/prosemirror-inputrules";
import { Selection } from "prosemirror-state";

import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
import {
createExtension,
createStore,
} from "../../../../editor/BlockNoteExtension.js";

/**
* Inline-content counterpart of {@link SourceBlockWithPreviewExtension}. Drives
* the source popup for inline content with a preview.
Expand Down Expand Up @@ -71,27 +72,6 @@ export const SourceInlineContentWithPreviewExtension = createExtension(
ArrowUp: moveSelectionOut("before"),
ArrowDown: moveSelectionOut("after"),
},
// Cannot use `inputRules` field as it only allows for converting matched content to blocks.
prosemirrorPlugins: [
inputRulesPlugin({
rules: [/\$([^$]+)\$$/, /\\\((.+?)\\\)$/].map(
(find) =>
new InputRule(find, (state, match, start, end) => {
const source = match[1]?.trim();
const nodeType = state.schema.nodes[inlineContentType];
if (!source || !nodeType) {
return null;
}

return state.tr.replaceRangeWith(
start,
end,
nodeType.create(null, state.schema.text(source)),
);
}),
),
}),
],
mount: ({ dom, signal }) => {
// The popup is open exactly when the selection is inside the inline
// content, so we just track which inline content (if any) holds it.
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from "./Video/block.js";
export { EMPTY_CELL_HEIGHT, EMPTY_CELL_WIDTH } from "./Table/TableExtension.js";
export * from "./Code/helpers/extensions/CodeKeyboardShortcutsExtension.js";
export * from "./Code/helpers/extensions/SourceBlockWithPreviewExtension.js";
export * from "./Code/helpers/extensions/SourceInlineContentWithPreviewExtension.js";
export * from "./Code/helpers/parse/parsePreCode.js";
export * from "./Code/helpers/render/createSourceBlock.js";
export * from "./Code/helpers/render/createSourceBlockWithPreview.js";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,104 +1,26 @@
import { SourceBlockWithPreviewExtension } from "@blocknote/core";
import {
ReactCustomBlockRenderProps,
useExtension,
useExtensionState,
SourceBlockWithPreview,
} from "@blocknote/react";
import { MouseEvent } from "react";

import { MathBlockConfig } from "../../createMathBlockConfig.js";
import { getMathPlainTextContent } from "../../../shared/getMathPlainTextContent.js";
import { AddSourceButton } from "../../../shared/react/render/AddSourceButton.js";
import { useLatexToMathMLString } from "../../../shared/react/render/useLatexToMathML.js";

export const MathBlockPreviewWithPopup = (
props: ReactCustomBlockRenderProps<MathBlockConfig>,
) => {
const { block, editor, contentRef } = props;

const source = getMathPlainTextContent(block.content);

const { store } = useExtension(SourceBlockWithPreviewExtension, { editor });
const popupOpen = useExtensionState(SourceBlockWithPreviewExtension, {
editor,
selector: (state) => state.popupOpen === block.id,
});
const selected = useExtensionState(SourceBlockWithPreviewExtension, {
editor,
selector: (state) => state.selected === block.id,
});

const source = getMathPlainTextContent(props.block.content);
const { mathMLString, error } = useLatexToMathMLString(source);

// Opens the popup when clicking the preview.
const handlePreviewMouseDown = (event: MouseEvent) => {
if (!editor.isEditable) {
return;
}

store.setState((state) => ({ ...state, popupOpen: block.id }));

event.preventDefault();
event.stopPropagation();

editor.setTextCursorPosition(block.id, "end");
editor.focus();
};

// Closes the popup when clicking the "OK" button.
const handleOkButtonMouseDown = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();

store.setState((state) => ({ ...state, popupOpen: undefined }));
};

return (
<div
className={
"bn-preview-with-source-popup" +
(selected ? " ProseMirror-selectednode" : "")
}
data-open={popupOpen ? "true" : "false"}
>
<div
className="bn-preview-container"
contentEditable={false}
onMouseDown={handlePreviewMouseDown}
>
{source.length > 0 ? (
<span dangerouslySetInnerHTML={{ __html: mathMLString }} />
) : (
<AddSourceButton
text={editor.dictionary.code_block.add_source_button_text}
/>
)}
</div>
<div className="bn-source-block-popup">
<div className="bn-code-block-source-popup-body">
<pre>
<code ref={contentRef} />
</pre>
<div
className="bn-code-block-source-popup-ok-button-wrapper"
contentEditable={false}
>
<button
className="bn-code-block-source-popup-ok-button"
onMouseDown={handleOkButtonMouseDown}
>
OK
</button>
</div>
</div>
<div
className="bn-code-block-source-error"
contentEditable={false}
style={{ display: error ? "block" : "none" }}
>
{error}
</div>
</div>
</div>
<SourceBlockWithPreview
block={props.block}
editor={props.editor}
contentRef={props.contentRef}
source={source}
preview={<span dangerouslySetInnerHTML={{ __html: mathMLString }} />}
error={error}
/>
);
};
3 changes: 1 addition & 2 deletions packages/math-block/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ export * from "./block/vanilla/createMathBlockSpec.js";
export * from "./block/vanilla/render/createMathBlockPreviewWithPopup.js";
export * from "./block/vanilla/toExternalHTML/createBlockMathMLElement.js";
export * from "./inlineContent/mathInlineContentConfig.js";
export * from "./inlineContent/SourceInlineContentWithPreviewExtension.js";
export * from "./inlineContent/MathInlineInputRulesExtension.js";
export * from "./inlineContent/react/createReactMathInlineContentSpec.js";
export * from "./inlineContent/react/render/MathInlinePreviewWithPopup.js";
export * from "./inlineContent/react/toExternalHTML/InlineMathMLElement.js";
export * from "./shared/getMathPlainTextContent.js";
export * from "./shared/latexToHTMLString.js";
export * from "./shared/react/render/AddSourceButton.js";
export * from "./shared/react/render/useLatexToMathML.js";
export * from "./shared/vanilla/toExternalHTML/latexToMathMLElement.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createExtension } from "@blocknote/core";
import {
InputRule,
inputRules as inputRulesPlugin,
} from "@handlewithcare/prosemirror-inputrules";

import { mathInlineContentConfig } from "./mathInlineContentConfig.js";

/**
* Converts text wrapped in LaTeX inline-math delimiters into inline math
* content as it's typed:
* - `$...$` (TeX inline math)
* - `\(...\)` (LaTeX inline math)
*
* The delimiters are removed and the enclosed source becomes the inline math's
* content.
*/
export const MathInlineInputRulesExtension = createExtension({
key: "math-inline-input-rules",
// Cannot use the `inputRules` field as it only allows for converting matched
// content to blocks.
prosemirrorPlugins: [
inputRulesPlugin({
rules: [/\$([^$]+)\$$/, /\\\((.+?)\\\)$/].map(
(find) =>
new InputRule(find, (state, match, start, end) => {
const source = match[1]?.trim();
const nodeType = state.schema.nodes[mathInlineContentConfig.type];
if (!source || !nodeType) {
return null;
}

return state.tr.replaceRangeWith(
start,
end,
nodeType.create(null, state.schema.text(source)),
);
}),
),
}),
],
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SourceInlineContentWithPreviewExtension } from "@blocknote/core";
import { createReactInlineContentSpec } from "@blocknote/react";

import { SourceInlineContentWithPreviewExtension } from "../SourceInlineContentWithPreviewExtension.js";
import { MathInlineInputRulesExtension } from "../MathInlineInputRulesExtension.js";
import { mathInlineContentConfig } from "../mathInlineContentConfig.js";
import { MathInlinePreviewWithPopup } from "./render/MathInlinePreviewWithPopup.js";
import { InlineMathMLElement } from "./toExternalHTML/InlineMathMLElement.js";
Expand All @@ -23,7 +24,8 @@ export const createReactInlineMathSpec = () =>
[
SourceInlineContentWithPreviewExtension({
key: INLINE_MATH_PREVIEW_KEY,
inlineContentType: "inlineMath",
inlineContentType: mathInlineContentConfig.type,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're changing this here we should probably do the same for the math block

}),
MathInlineInputRulesExtension,
],
);
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { StyleSchema } from "@blocknote/core";
import {
ReactCustomInlineContentRenderProps,
useExtension,
useExtensionState,
SourceInlineContentWithPreview,
} from "@blocknote/react";
import { TextSelection } from "prosemirror-state";
import { MouseEvent } from "react";

import { MathInlineContentConfig } from "../../mathInlineContentConfig.js";
import { SourceInlineContentWithPreviewExtension } from "../../SourceInlineContentWithPreviewExtension.js";
import { getMathPlainTextContent } from "../../../shared/getMathPlainTextContent.js";
import { AddSourceButton } from "../../../shared/react/render/AddSourceButton.js";
import { useLatexToMathMLString } from "../../../shared/react/render/useLatexToMathML.js";

export const MathInlinePreviewWithPopup = (
Expand All @@ -19,109 +14,18 @@ export const MathInlinePreviewWithPopup = (
StyleSchema
>,
) => {
const { inlineContent, editor, contentRef, node, getPos } = props;
const pos = getPos();

const source = getMathPlainTextContent(inlineContent.content);

const { store } = useExtension(SourceInlineContentWithPreviewExtension, {
editor,
});
// The popup is open exactly when the selection is inside this inline content,
// which is the same condition that marks it as selected.
const selected = useExtensionState(SourceInlineContentWithPreviewExtension, {
editor,
selector: (state) => state.selected === pos,
});

const source = getMathPlainTextContent(props.inlineContent.content);
const { mathMLString, error } = useLatexToMathMLString(source, true);

// Opens the popup when clicking the preview.
const handlePreviewMouseDown = (event: MouseEvent) => {
if (!editor.isEditable || !pos) {
return;
}

store.setState({ selected: pos });

event.preventDefault();
event.stopPropagation();

const view = editor.prosemirrorView!;
view.dispatch(
view.state.tr.setSelection(
TextSelection.create(view.state.tr.doc, pos + node.nodeSize - 1),
),
);
editor.focus();
};

// Closes the popup by moving the selection to just after the inline content.
const handleOkButtonMouseDown = (event: MouseEvent) => {
if (!editor.isEditable || !pos) {
return;
}

event.preventDefault();
event.stopPropagation();

const view = editor.prosemirrorView!;
view.dispatch(
view.state.tr.setSelection(
TextSelection.create(view.state.tr.doc, pos + node.nodeSize),
),
);
editor.focus();
};

return (
<span
// The source is hidden, so highlight the whole inline content while the
// cursor is in it.
className={
"bn-preview-with-source-popup" +
(selected ? " ProseMirror-selectednode" : "")
}
data-open={selected ? "true" : "false"}
>
<span
className="bn-preview-container"
contentEditable={false}
onMouseDown={handlePreviewMouseDown}
>
{source.length > 0 ? (
<span dangerouslySetInnerHTML={{ __html: mathMLString }} />
) : (
<AddSourceButton
text={editor.dictionary.code_block.add_source_button_text}
/>
)}
</span>
<div className="bn-source-block-popup">
<div className="bn-code-block-source-popup-body">
<pre>
<code ref={contentRef} />
</pre>
<div
className="bn-code-block-source-popup-ok-button-wrapper"
contentEditable={false}
>
<button
className="bn-code-block-source-popup-ok-button"
onMouseDown={handleOkButtonMouseDown}
>
OK
</button>
</div>
</div>
<div
className="bn-code-block-source-error"
contentEditable={false}
style={{ display: error ? "block" : "none" }}
>
{error}
</div>
</div>
</span>
<SourceInlineContentWithPreview
editor={props.editor}
contentRef={props.contentRef}
node={props.node}
getPos={props.getPos}
source={source}
preview={<span dangerouslySetInnerHTML={{ __html: mathMLString }} />}
error={error}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Shown in place of the preview when the math content has no source yet.
// Shown in place of the preview when the source content is empty.
export const AddSourceButton = (props: { text: string }) => (
<div className="bn-add-source-code-button" contentEditable={false}>
<div className="bn-add-source-code-button-icon">
Expand Down
Loading
Loading