Skip to content

feat: math block#2857

Open
matthewlipski wants to merge 7 commits into
mainfrom
code-block-previews
Open

feat: math block#2857
matthewlipski wants to merge 7 commits into
mainfrom
code-block-previews

Conversation

@matthewlipski

@matthewlipski matthewlipski commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR adds a LaTeX math block. To facilitate that, changes have been made to the code block and how syntax highlighting is handled.

Code Block & Syntax Highlighting Changes

Code Block

The core code block remains unchanged functionally. It's just been made more composable as helpers for render, parse, extensions, etc, have been extracted to separate files.

Additionally, helper functions have been added to render previews for source code.

Syntax Highlighting

Syntax highlighting configuration has been moved out of the code block entirely and into a new syntaxHighlighting editor option. This has the following fields:

  • createHighlighter: The same field as used to be in the code block options. Moved here as syntax highlighting can now be applied to multiple block types rather than just the code block.
  • highlightBlock: Function which tells the editor for which blocks to apply syntax highlighting. Returning a language string will apply highlighting on that block for that language, while returning undefined will do nothing.

This is done in order to only have a single highlighter plugin from prosemirror-highlight, rather than having an instance per block. The SyntaxHighlightingExtension is now loaded by the editor whenever the syntaxHighlighting editor option is passed.

Math Block

A math block has been added, which renders an equation from LaTeX, stored in its inline content. It uses the following dependencies:

  • temml: LateX to MathML for rendering & external HTML export.
  • mathml-to-latex: MathML to LaTeX for external HTML parsing.

The math block has been added as a separate package.

Rationale

We have wanted to implement a math block for a while. While existing PRs exist for that, this one has a broader scope which includes the above code block/syntax highlighting changes.

Changes

See above.

Impact

N/A

Testing

Added unit tests for SyntaxHighlighting extension & math block.

Screenshots/Video

TODO

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Additional Notes

Summary by CodeRabbit

  • New Features
    • Added math block support: LaTeX rendered via KaTeX to MathML, editable LaTeX source popup, and MathML-to-LaTeX paste/export.
    • Code blocks now support an inline source popup for editable previews and improved keyboard navigation for entering/exiting the source.
    • Syntax highlighting can be configured at the editor level to consistently highlight code-related blocks.
  • Documentation
    • Updated Code Block syntax highlighting docs and refreshed theming/custom-code-block examples.
    • Added a new playground example for math blocks.

@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blocknote Error Error Jun 18, 2026 7:17pm
blocknote-website Error Error Jun 18, 2026 7:17pm

Request Review

@matthewlipski matthewlipski requested a review from nperez0111 June 16, 2026 10:21
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc8f0ece-12d8-4b55-a174-f8f2b9e333f0

📥 Commits

Reviewing files that changed from the base of the PR and between de7b1df and 719fd7a.

📒 Files selected for processing (5)
  • docs/package.json
  • packages/core/package.json
  • packages/core/src/editor/BlockNoteEditor.ts
  • packages/core/src/editor/managers/ExtensionManager/extensions.ts
  • packages/core/src/extensions/index.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/core/src/extensions/index.ts
  • packages/core/src/editor/managers/ExtensionManager/extensions.ts
  • docs/package.json
  • packages/core/package.json
  • packages/core/src/editor/BlockNoteEditor.ts

📝 Walkthrough

Walkthrough

This PR moves Shiki syntax highlighting out of CodeBlockOptions into a new editor-level syntaxHighlighting option, refactors codeBlock internals into helper modules (parse, render, keyboard, navigation), introduces a createPreviewWithSourcePopup for blocks with custom previews, and adds a new @blocknote/math-block package providing KaTeX-rendered math blocks with MathML export/import.

Changes

Code Block Refactor and SyntaxHighlighting Extension

Layer / File(s) Summary
Code block and node-view type contracts
packages/core/src/blocks/Code/CodeBlockOptions.ts, packages/core/src/schema/blocks/types.ts, packages/core/src/schema/blocks/createSpec.ts, packages/core/src/index.ts
CodeBlockPreview and CodeBlockOptions are extracted into a dedicated module with getLanguageId; BlockImplementation.render return type gains an optional update callback for in-place ProseMirror node reconciliation; types are re-exported from the core index.
Editor-level syntax highlighting pipeline
packages/core/src/extensions/SyntaxHighlighting/SyntaxHighlighting.ts, packages/core/src/extensions/SyntaxHighlighting/SyntaxHighlighting.test.ts, packages/core/src/extensions/SyntaxHighlighting/shiki.ts, packages/core/src/editor/BlockNoteEditor.ts, packages/core/src/editor/managers/ExtensionManager/extensions.ts, packages/core/src/extensions/index.ts, packages/code-block/src/index.ts, packages/code-block/src/index.test.ts, packages/code-block/package.json, packages/core/package.json
SyntaxHighlightingOptions, SyntaxHighlightingExtension, and lazyShikiPlugin are introduced; highlighting is now configured via BlockNoteEditorOptions.syntaxHighlighting and registered conditionally at editor init; createHighlighter is exported as a standalone constant from @blocknote/code-block and removed from codeBlockOptions; @floating-ui/dom and katex dependencies added.
Code block parse and render helpers
packages/core/src/blocks/Code/helpers/parse/parsePreCode.ts, packages/core/src/blocks/Code/helpers/render/createSourceBlock.ts, packages/core/src/blocks/Code/helpers/render/createPreviewWithSourcePopup.ts, packages/core/src/blocks/Code/helpers/render/createCodeBlockWrapper.ts, packages/core/src/blocks/Code/helpers/toExternalHTML/createPreCode.ts, packages/core/src/editor/Block.css
PRE/CODE parsing, source-only DOM rendering with optional language select, preview-with-source-popup rendering via FloatingUI autoUpdate, and a wrapper factory that selects between those two paths are introduced as separate helpers; CSS for the popup, preview area, and error state is added.
Code block keyboard and navigation extensions
packages/core/src/blocks/Code/helpers/extensions/createCodeKeyboardShortcutsExtension.ts, packages/core/src/blocks/Code/helpers/extensions/createPreviewSourceNavigationExtension.ts
createCodeKeyboardShortcutsExtension implements Delete/Tab/Enter/Shift-Enter behaviors and fenced code input rules; createPreviewSourceNavigationExtension handles Arrow-key movement across block types with table/column edge traversal and Enter/Escape NodeSelection toggling.
Block spec integration, barrel exports, and example/doc updates
packages/core/src/blocks/Code/block.ts, packages/core/src/blocks/Code/block.test.ts, packages/core/src/blocks/index.ts, examples/04-theming/06-code-block/src/App.tsx, examples/04-theming/07-custom-code-block/src/App.tsx, docs/content/docs/features/blocks/code-blocks.mdx
createCodeBlockSpec is rewired to delegate all inline logic to helper modules; helper modules are added to the barrel export; theming examples and docs are updated to pass createHighlighter via editor-level syntaxHighlighting.

New @blocknote/math-block Package

Layer / File(s) Summary
Math block package scaffolding and build configuration
packages/math-block/package.json, packages/math-block/LICENSE, packages/math-block/.gitignore, packages/math-block/tsconfig.json, packages/math-block/vite.config.ts, packages/math-block/vitestSetup.ts, packages/math-block/src/vite-env.d.ts
Full package manifest, MPL-2.0 license, TypeScript config, Vite library build with conditional monorepo aliases, and Vitest setup with globalThis.window initialization are added.
Math block schema, parse/render, and conversion helpers
packages/math-block/src/block.ts, packages/math-block/src/helpers/getMathSource.ts, packages/math-block/src/helpers/parse/parseMathML.ts, packages/math-block/src/helpers/render/createMathPreview.ts, packages/math-block/src/helpers/toExternalHTML/createMathML.ts, packages/math-block/src/index.ts
createMathBlockSpec wires parseMathML/parseMathMLContent, createPreviewWithSourcePopup+createMathPreview, and createMathML; preview uses KaTeX two-pass rendering; MathML export annotates with application/x-tex; all helpers are re-exported from the package index.
Math block behavior and conversion tests
packages/math-block/src/block.test.ts
Comprehensive tests cover Arrow-key navigation from paragraphs, adjacent math blocks, image blocks, and table cells; preview-source selection decoration; formatting toolbar suppression; columnList nested navigation; inline node-selected navigation; and LaTeX↔MathML round-trip conversion.
Math block example app and playground/docs wiring
examples/06-custom-schema/09-math-block/*, playground/src/examples.gen.tsx, docs/package.json
A new custom-schema math-block example app is added with full scaffolding; registered in generated playground examples; @blocknote/math-block added as a docs workspace dependency.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant BlockNoteEditor
  participant SyntaxHighlightingExtension
  participant lazyShikiPlugin
  participant createPreviewWithSourcePopup

  User->>BlockNoteEditor: click math/code preview
  BlockNoteEditor->>createPreviewWithSourcePopup: mousedown on preview dom
  createPreviewWithSourcePopup->>BlockNoteEditor: set cursor to block start, show popup
  BlockNoteEditor->>SyntaxHighlightingExtension: selection change triggers highlight
  SyntaxHighlightingExtension->>lazyShikiPlugin: extract language from block attrs via highlightBlock
  lazyShikiPlugin-->>BlockNoteEditor: apply decorations
  User->>BlockNoteEditor: Escape key
  BlockNoteEditor->>createPreviewWithSourcePopup: NodeSelection on block-content node, hide popup
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested reviewers

  • nperez0111

Poem

🐇 Hop hop, the math block arrives today,
LaTeX rendered in KaTeX's bright display!
Shiki now lives at the editor's side,
With helpers and helpers and helpers inside.
The rabbit exclaims with a curious cheer—
"Preview popups and floating UIs are here!" 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: math block' clearly and concisely describes the primary feature being added in this pull request.
Description check ✅ Passed The PR description covers most required sections: summary, rationale, changes, impact, testing, and a completed checklist. While the Screenshots/Video section is marked TODO, all critical sections are present and substantive.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch code-block-previews

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://TypeCellOS.github.io/BlockNote/pr-preview/pr-2857/

Built to branch gh-pages at 2026-06-18 19:25 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (1)
packages/math-block/src/helpers/toExternalHTML/createMathML.ts (1)

8-17: Consider explicitly pinning the Temml trust mode.

At lines 8–13, while Temml's default trust value is false (which is secure), explicitly setting trust: false makes the security intent clear and protects against potential upstream default changes.

Suggested change
  const mathml = temml.renderToString(getMathSource(block), {
    displayMode: true,
    annotate: true,
+   trust: false,
    // Export gracefully renders invalid LaTeX rather than throwing.
    throwOnError: false,
  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/math-block/src/helpers/toExternalHTML/createMathML.ts` around lines
8 - 17, The temml.renderToString() call on line 8 does not explicitly set the
trust property in its options object. Add trust: false to the options passed to
temml.renderToString() alongside the existing displayMode, annotate, and
throwOnError properties to explicitly document the security intent and protect
against potential upstream default changes to Temml's trust setting.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/06-custom-schema/09-math-block/index.html`:
- Line 1: Add the HTML5 DOCTYPE declaration as the very first line of the file
before the <html> tag. Insert `<!doctype html>` on line 1, which will move the
existing `<html lang="en">` tag to line 2. This ensures the browser renders in
standards mode instead of triggering quirks mode, and makes the HTML document
compliant with proper HTML5 structure requirements.

In `@examples/06-custom-schema/09-math-block/main.tsx`:
- Line 4: The import statement for App specifies the wrong file extension (.jsx
instead of .tsx). Change the import path from ./src/App.jsx to ./src/App.tsx to
match the actual file name and allow TypeScript module resolution to succeed
with the allowJs: false configuration.

In `@examples/06-custom-schema/09-math-block/vite.config.ts`:
- Around line 16-27: The vite alias configuration in the conditional block is
using `../../` in the path.resolve calls for both `@blocknote/core` and
`@blocknote/react`, but it should use `../../../` to resolve the packages from the
correct directory level. Update each path string passed to path.resolve (for
"`@blocknote/core`" and "`@blocknote/react`") to include one additional `../` in the
relative path so the source-alias branch activates correctly when the packages
source directory exists.

In `@packages/core/src/blocks/Code/CodeBlockOptions.ts`:
- Around line 72-80: The getLanguageId function performs case-sensitive
comparisons when matching the languageName parameter against language aliases
and IDs, causing inputs like `TS` or `Js` to fail to map to their canonical
lowercase configured IDs. Fix this by normalizing all comparisons to be
case-insensitive: convert the incoming languageName parameter to lowercase, and
convert both the aliases from the supportedLanguages configuration and the id
values to lowercase before performing the comparison checks in the find
callback.

In `@packages/core/src/blocks/Code/helpers/parse/parsePreCode.ts`:
- Around line 19-22: The language-class detection in the code is too broad
because it uses includes("language-") which matches the substring anywhere in
the class name, potentially capturing false positives like classes containing
"language-" in the middle of the string. Replace the includes("language-") check
with startsWith("language-") to ensure only class names that actually begin with
"language-" are considered valid language identifiers, restricting the match to
the beginning of the string only.

In `@packages/core/src/blocks/Code/helpers/render/createCodeBlockWrapper.ts`:
- Around line 10-12: The language lookup on line 11 does not normalize language
aliases before accessing the supportedLanguages map. If the language value is an
alias such as "ts", the lookup will fail and createPreview will be skipped,
causing fallback to source-only rendering. Resolve the language string through a
shared language-ID resolver to normalize it to its canonical form before using
it as the lookup key in supportedLanguages on line 11.

In `@packages/core/src/blocks/Code/helpers/render/createSourceBlock.ts`:
- Around line 8-25: The language value being assigned to select.value is not
validated against the supported languages, which can result in no option being
selected if the stored language is an alias or outdated identifier. Before
assigning select.value in the language dropdown initialization, add logic to
resolve the language variable to a canonical key from
options.supportedLanguages. Check if the language exists as a key in the
supportedLanguages object; if not, fall back to the default language or the
first available option. Only after normalizing the language should you assign it
to select.value.

In `@packages/core/src/editor/Block.css`:
- Around line 482-517: Remove the empty lines that appear before CSS property
declarations in the `.bn-code-block-source-popup` and its related child
selectors (including `.bn-code-block-source-popup > div > select`,
`.bn-code-block-source-popup > div > select > option`, and
`.bn-code-block-source-popup > pre`). These blank lines before declarations are
triggering Stylelint violations. Go through each CSS rule block and delete the
extra blank lines while maintaining proper formatting between distinct rule
blocks.

In `@packages/core/src/extensions/SyntaxHighlighting/shiki.ts`:
- Around line 29-32: The code caches the Shiki highlighter and parser on
globalThis, causing all editor instances to share the same cache regardless of
their individual syntaxHighlighting.createHighlighter configuration. This
violates the editor-level configuration contract. Remove the global caching
mechanism by eliminating the globalThis symbol-keyed properties and instead
store the highlighter and parser at the instance level (as properties on an
editor-specific object or context). Specifically, refactor the
globalThisForShiki type definition and related caching logic at the three
affected sites in packages/core/src/extensions/SyntaxHighlighting/shiki.ts
(lines 29-32, 44-46, and 74-76) to use instance-specific storage instead of
globalThis, ensuring each editor instance maintains its own separate highlighter
and parser cache.

In `@packages/math-block/src/helpers/getMathSource.ts`:
- Around line 8-11: In the getMathSource function's array mapping logic, add a
defensive check before using the `in` operator on the node variable. Since
block.content is of unknown type, the array items could be primitives, null, or
non-objects that would throw a TypeError when used with the `in` operator.
Modify the map callback to first verify that node is an object (using typeof
node === "object" && node !== null or a similar guard) before attempting to
access the "text" property, ensuring the function gracefully handles unexpected
data types without crashing.

In `@packages/math-block/vite.config.ts`:
- Around line 52-56: The vite.config.ts build configuration is currently
including devDependencies in the externals check along with dependencies and
peerDependencies. Remove the spread of pkg.devDependencies from the
Object.keys() call in the external configuration block so that only actual
runtime dependencies (dependencies and peerDependencies) are marked as external.
This ensures that accidental imports of dev-only packages will fail during the
build rather than being shipped unresolved to library consumers.

---

Nitpick comments:
In `@packages/math-block/src/helpers/toExternalHTML/createMathML.ts`:
- Around line 8-17: The temml.renderToString() call on line 8 does not
explicitly set the trust property in its options object. Add trust: false to the
options passed to temml.renderToString() alongside the existing displayMode,
annotate, and throwOnError properties to explicitly document the security intent
and protect against potential upstream default changes to Temml's trust setting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e9b64910-b7e1-4084-b088-4b77233bd8c5

📥 Commits

Reviewing files that changed from the base of the PR and between a28f472 and 6ccc955.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (54)
  • docs/content/docs/features/blocks/code-blocks.mdx
  • docs/package.json
  • examples/04-theming/06-code-block/src/App.tsx
  • examples/04-theming/07-custom-code-block/src/App.tsx
  • examples/06-custom-schema/09-math-block/.bnexample.json
  • examples/06-custom-schema/09-math-block/README.md
  • examples/06-custom-schema/09-math-block/index.html
  • examples/06-custom-schema/09-math-block/main.tsx
  • examples/06-custom-schema/09-math-block/package.json
  • examples/06-custom-schema/09-math-block/src/App.tsx
  • examples/06-custom-schema/09-math-block/tsconfig.json
  • examples/06-custom-schema/09-math-block/vite.config.ts
  • packages/code-block/package.json
  • packages/code-block/src/index.test.ts
  • packages/code-block/src/index.ts
  • packages/core/package.json
  • packages/core/src/blocks/Code/CodeBlockOptions.ts
  • packages/core/src/blocks/Code/block.test.ts
  • packages/core/src/blocks/Code/block.ts
  • packages/core/src/blocks/Code/helpers/extensions/createCodeKeyboardShortcutsExtension.ts
  • packages/core/src/blocks/Code/helpers/extensions/createPreviewSourceNavigationExtension.ts
  • packages/core/src/blocks/Code/helpers/extensions/createPreviewSourceSelectionExtension.ts
  • packages/core/src/blocks/Code/helpers/parse/parsePreCode.ts
  • packages/core/src/blocks/Code/helpers/render/createCodeBlockWrapper.ts
  • packages/core/src/blocks/Code/helpers/render/createPreviewWithSourcePopup.ts
  • packages/core/src/blocks/Code/helpers/render/createSourceBlock.ts
  • packages/core/src/blocks/Code/helpers/toExternalHTML/createPreCode.ts
  • packages/core/src/blocks/Code/shiki.ts
  • packages/core/src/blocks/index.ts
  • packages/core/src/editor/Block.css
  • packages/core/src/editor/BlockNoteEditor.ts
  • packages/core/src/editor/managers/ExtensionManager/extensions.ts
  • packages/core/src/extensions/SyntaxHighlighting/SyntaxHighlighting.test.ts
  • packages/core/src/extensions/SyntaxHighlighting/SyntaxHighlighting.ts
  • packages/core/src/extensions/SyntaxHighlighting/shiki.ts
  • packages/core/src/extensions/index.ts
  • packages/core/src/index.ts
  • packages/core/src/schema/blocks/createSpec.ts
  • packages/core/src/schema/blocks/types.ts
  • packages/math-block/.gitignore
  • packages/math-block/LICENSE
  • packages/math-block/package.json
  • packages/math-block/src/block.test.ts
  • packages/math-block/src/block.ts
  • packages/math-block/src/helpers/getMathSource.ts
  • packages/math-block/src/helpers/parse/parseMathML.ts
  • packages/math-block/src/helpers/render/createMathPreview.ts
  • packages/math-block/src/helpers/toExternalHTML/createMathML.ts
  • packages/math-block/src/index.ts
  • packages/math-block/src/vite-env.d.ts
  • packages/math-block/tsconfig.json
  • packages/math-block/vite.config.ts
  • packages/math-block/vitestSetup.ts
  • playground/src/examples.gen.tsx
💤 Files with no reviewable changes (1)
  • packages/core/src/blocks/Code/shiki.ts

Comment thread examples/06-custom-schema/09-math-block/index.html
Comment thread examples/06-custom-schema/09-math-block/main.tsx
Comment thread examples/06-custom-schema/09-math-block/vite.config.ts
Comment thread packages/core/src/blocks/Code/CodeBlockOptions.ts
Comment thread packages/core/src/blocks/Code/helpers/parse/parsePreCode.ts
Comment thread packages/core/src/blocks/Code/helpers/render/createSourceBlock.ts Outdated
Comment thread packages/core/src/editor/Block.css
Comment on lines +29 to +32
const globalThisForShiki = globalThis as {
[shikiHighlighterPromiseSymbol]?: Promise<HighlighterGeneric<any, any>>;
[shikiParserSymbol]?: Parser;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid global Shiki cache collisions across editor instances.

Line 44 and Line 74 cache the highlighter/parser on globalThis, so the first editor instance can silently control highlighting behavior for later editors with different syntaxHighlighting.createHighlighter options. This violates the editor-level configuration contract and can produce incorrect themes/language availability in multi-editor pages.

Suggested fix
-  const globalThisForShiki = globalThis as {
-    [shikiHighlighterPromiseSymbol]?: Promise<HighlighterGeneric<any, any>>;
-    [shikiParserSymbol]?: Parser;
-  };
+  let highlighterPromise: Promise<HighlighterGeneric<any, any>> | undefined;

   let highlighter: HighlighterGeneric<any, any> | undefined;
   let parser: Parser | undefined;
@@
     if (!highlighter) {
-      globalThisForShiki[shikiHighlighterPromiseSymbol] =
-        globalThisForShiki[shikiHighlighterPromiseSymbol] ||
-        options.createHighlighter();
+      highlighterPromise = highlighterPromise || options.createHighlighter();

-      return globalThisForShiki[shikiHighlighterPromiseSymbol].then(
+      return highlighterPromise.then(
         (createdHighlighter) => {
           highlighter = createdHighlighter;
         },
       );
     }
@@
     if (!parser) {
-      parser =
-        globalThisForShiki[shikiParserSymbol] ||
-        createParser(highlighter as any);
-      globalThisForShiki[shikiParserSymbol] = parser;
+      parser = createParser(highlighter as any);
     }

Also applies to: 44-46, 74-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/extensions/SyntaxHighlighting/shiki.ts` around lines 29 -
32, The code caches the Shiki highlighter and parser on globalThis, causing all
editor instances to share the same cache regardless of their individual
syntaxHighlighting.createHighlighter configuration. This violates the
editor-level configuration contract. Remove the global caching mechanism by
eliminating the globalThis symbol-keyed properties and instead store the
highlighter and parser at the instance level (as properties on an
editor-specific object or context). Specifically, refactor the
globalThisForShiki type definition and related caching logic at the three
affected sites in packages/core/src/extensions/SyntaxHighlighting/shiki.ts
(lines 29-32, 44-46, and 74-76) to use instance-specific storage instead of
globalThis, ensuring each editor instance maintains its own separate highlighter
and parser cache.

Comment on lines +8 to +11
if (Array.isArray(block.content)) {
return block.content
.map((node) => ("text" in node ? node.text : ""))
.join("");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard non-object inline nodes before using the in operator.

Line 10 can throw at runtime when an array item is not an object ("text" in node on primitives/null). Since block.content is unknown, this path should be defensive instead of crashing preview/export.

Proposed fix
   if (Array.isArray(block.content)) {
     return block.content
-      .map((node) => ("text" in node ? node.text : ""))
+      .map((node) =>
+        node && typeof node === "object" && "text" in node
+          ? String((node as { text?: unknown }).text ?? "")
+          : "",
+      )
       .join("");
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (Array.isArray(block.content)) {
return block.content
.map((node) => ("text" in node ? node.text : ""))
.join("");
if (Array.isArray(block.content)) {
return block.content
.map((node) =>
node && typeof node === "object" && "text" in node
? String((node as { text?: unknown }).text ?? "")
: "",
)
.join("");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/math-block/src/helpers/getMathSource.ts` around lines 8 - 11, In the
getMathSource function's array mapping logic, add a defensive check before using
the `in` operator on the node variable. Since block.content is of unknown type,
the array items could be primitives, null, or non-objects that would throw a
TypeError when used with the `in` operator. Modify the map callback to first
verify that node is an object (using typeof node === "object" && node !== null
or a similar guard) before attempting to access the "text" property, ensuring
the function gracefully handles unexpected data types without crashing.

Comment on lines +52 to +56
Object.keys({
...pkg.dependencies,
...((pkg as any).peerDependencies || {}),
...pkg.devDependencies,
}).some((dep) => source === dep || source.startsWith(dep + "/"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not externalize devDependencies in the published library build.

Including pkg.devDependencies in external can ship unresolved runtime imports to consumers if a dev-only package is imported by mistake. Keep externals to dependencies/peerDependencies so declaration mistakes fail fast during packaging.

Proposed fix
               Object.keys({
                 ...pkg.dependencies,
                 ...((pkg as any).peerDependencies || {}),
-                ...pkg.devDependencies,
               }).some((dep) => source === dep || source.startsWith(dep + "/"))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Object.keys({
...pkg.dependencies,
...((pkg as any).peerDependencies || {}),
...pkg.devDependencies,
}).some((dep) => source === dep || source.startsWith(dep + "/"))
Object.keys({
...pkg.dependencies,
...((pkg as any).peerDependencies || {}),
}).some((dep) => source === dep || source.startsWith(dep + "/"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/math-block/vite.config.ts` around lines 52 - 56, The vite.config.ts
build configuration is currently including devDependencies in the externals
check along with dependencies and peerDependencies. Remove the spread of
pkg.devDependencies from the Object.keys() call in the external configuration
block so that only actual runtime dependencies (dependencies and
peerDependencies) are marked as external. This ensures that accidental imports
of dev-only packages will fail during the build rather than being shipped
unresolved to library consumers.

@YousefED YousefED left a comment

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.

Exciting!

Looking at the code from a high level (no user testing or deep dive into the functions), I have the following high-level questions:

  • Should we use React or Vanilla for the components? (especially thinking about customizability)
  • Curious if the current code design (with the keyboard handlers) also scales to interfaces where the editor is not in a pop-up (e.g.: Notion / TypeCell style editors that can collapse)

**Syntax Highlighting**

BlockNote also provides a generic set of options for syntax highlighting in the `@blocknote/code-block` package, which support a wide range of languages:
Syntax highlighting is handled by a separate editor extension, configured at the editor level via the `syntaxHighlighting` option (not on the code block itself), so it can highlight any block's content:

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.

Curious if we can find a better way to do this. This will make it difficult to create easily installable block-plugins later I think

const { block } = editor.getTextCursorPosition();
if (block.type === blockType) {
// TODO should probably only tab when at a line start or already tabbed in
tr.insertText(" ");

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.

spaces or \t? or configurable?

@@ -0,0 +1,170 @@
import {

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.

do we prefer react / vanilla for these components? (cc @nperez0111 )

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.

first thought; vanilla makes it pretty complex to customize them in a block-view style manner right?

editor: BlockNoteEditor<any>,
) => {
dom: HTMLElement;
ignoreMutation?: (mutation: ViewMutationRecord) => boolean;

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 a user wants to implement their own preview, would they need to implement ignoreMutation? It's a very prosemirror-style (magic 🤯) API. Can we figure an API design that abstracts this out? (maybe same for dom and destroy actually)

@YousefED YousefED left a comment

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.

Here's some feedback after a super early round of user-testing:

  • pressing up after “s” doesn’t go to the first popup:
Image
  • esc should close popup

  • formattingtoolbar appears when highlighting text.

  • I’m allowed to “bold” things. I think we should see if we can make sure at a lower level code blocks cannot allow formatted text. This probably also caused the issue with the exporters recently (remember?)

  • preview opens when dragging block

  • keyboard is buggy when two math blocks sit above/below combined with the “gapcursor”

  • this is a bit misaligned:

Image

This might also be nice for testing:

  • slashmenu support in demos
  • collaboration example

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/math-block/src/block.test.ts (1)

762-762: ⚡ Quick win

Prefer semantic MathML assertions over exact full-string equality.

This assertion is fragile to harmless serialization differences. Assert the exported node structure and application/x-tex annotation content instead of matching one exact serialized string.

Suggested test hardening
- expect(serializedMathML).toBe(
-   `"<math xmlns="http://www.w3.org/1998/Math/MathML" ... </math>"`,
- );
+ const parsed = new DOMParser().parseFromString(serializedMathML, "text/html");
+ const math = parsed.querySelector("math");
+ expect(math).not.toBeNull();
+ const tex = math
+   ?.querySelector('annotation[encoding="application/x-tex"]')
+   ?.textContent
+   ?.trim();
+ expect(tex).toBe("a^2 + b^2 = c^2");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/math-block/src/block.test.ts` at line 762, The test assertion at
line 762 performs brittle exact full-string matching of the entire serialized
MathML output, which is fragile to harmless serialization differences. Instead
of comparing the complete serialized string, extract and assert specific
structural elements: validate the exported node structure and specifically check
the application/x-tex annotation content value rather than relying on
full-string equality to make the test more resilient to formatting or
serialization changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/core/src/blocks/Code/helpers/extensions/createPreviewSourceNavigationExtension.ts`:
- Around line 175-218: The Enter and Escape handlers are missing
scrollIntoView() calls after setting the selection, creating an inconsistency
with the arrow handlers (lines 127, 147) which do include these calls. Add
tr.scrollIntoView() after each tr.setSelection() call in both the Enter handler
and the Escape handler to ensure the viewport scrolls to keep the focused block
visible when the selection changes.

---

Nitpick comments:
In `@packages/math-block/src/block.test.ts`:
- Line 762: The test assertion at line 762 performs brittle exact full-string
matching of the entire serialized MathML output, which is fragile to harmless
serialization differences. Instead of comparing the complete serialized string,
extract and assert specific structural elements: validate the exported node
structure and specifically check the application/x-tex annotation content value
rather than relying on full-string equality to make the test more resilient to
formatting or serialization changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49fca915-9fa2-4f91-9469-351dd45a6d53

📥 Commits

Reviewing files that changed from the base of the PR and between ae49edc and de7b1df.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • tests/src/unit/core/schema/__snapshots__/blocks.json is excluded by !**/__snapshots__/**
📒 Files selected for processing (14)
  • packages/core/src/blocks/Code/CodeBlockOptions.ts
  • packages/core/src/blocks/Code/helpers/extensions/createPreviewSourceNavigationExtension.ts
  • packages/core/src/blocks/Code/helpers/parse/parsePreCode.ts
  • packages/core/src/blocks/Code/helpers/render/createCodeBlockWrapper.ts
  • packages/core/src/blocks/Code/helpers/render/createPreviewWithSourcePopup.ts
  • packages/core/src/blocks/Code/helpers/render/createSourceBlock.ts
  • packages/core/src/blocks/index.ts
  • packages/core/src/editor/Block.css
  • packages/math-block/package.json
  • packages/math-block/src/block.test.ts
  • packages/math-block/src/block.ts
  • packages/math-block/src/helpers/parse/parseMathML.ts
  • packages/math-block/src/helpers/render/createMathPreview.ts
  • packages/math-block/src/helpers/toExternalHTML/createMathML.ts
💤 Files with no reviewable changes (2)
  • packages/core/src/blocks/index.ts
  • packages/math-block/src/helpers/parse/parseMathML.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/core/src/blocks/Code/helpers/parse/parsePreCode.ts
  • packages/core/src/blocks/Code/helpers/render/createCodeBlockWrapper.ts
  • packages/core/src/blocks/Code/helpers/render/createSourceBlock.ts
  • packages/math-block/src/block.ts
  • packages/core/src/blocks/Code/CodeBlockOptions.ts
  • packages/core/src/editor/Block.css
  • packages/core/src/blocks/Code/helpers/render/createPreviewWithSourcePopup.ts

Comment on lines +175 to +218
Enter: ({ editor }) =>
editor.transact((tr) => {
const blockInfo = getBlockInfoFromTransaction(tr);
if (
!blockInfo.isBlockContainer ||
blockInfo.blockNoteType !== blockType
) {
return false;
}

if (tr.selection instanceof NodeSelection) {
tr.setSelection(
TextSelection.create(
tr.doc,
blockInfo.blockContent.beforePos + 1,
),
);
} else {
tr.setSelection(
NodeSelection.create(tr.doc, blockInfo.blockContent.beforePos),
);
}

return true;
}),
// Closes the source code popup by setting the selection on the whole block content node.
Escape: ({ editor }) =>
editor.transact((tr) => {
const blockInfo = getBlockInfoFromTransaction(tr);

if (
!blockInfo.isBlockContainer ||
blockInfo.blockNoteType !== blockType ||
tr.selection instanceof NodeSelection
) {
return false;
}

tr.setSelection(
NodeSelection.create(tr.doc, blockInfo.blockContent.beforePos),
);

return true;
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing scrollIntoView() after selection changes in Enter/Escape handlers.

The arrow handlers (lines 127, 147) call .scrollIntoView() after setting selection, but Enter and Escape do not. This inconsistency could cause the selection to move without scrolling the viewport, potentially leaving the focused block off-screen.

Proposed fix
         if (tr.selection instanceof NodeSelection) {
           tr.setSelection(
             TextSelection.create(
               tr.doc,
               blockInfo.blockContent.beforePos + 1,
             ),
-          );
+          ).scrollIntoView();
         } else {
           tr.setSelection(
             NodeSelection.create(tr.doc, blockInfo.blockContent.beforePos),
-          );
+          ).scrollIntoView();
         }

         return true;
       }),
     // Closes the source code popup by setting the selection on the whole block content node.
     Escape: ({ editor }) =>
       editor.transact((tr) => {
         const blockInfo = getBlockInfoFromTransaction(tr);

         if (
           !blockInfo.isBlockContainer ||
           blockInfo.blockNoteType !== blockType ||
           tr.selection instanceof NodeSelection
         ) {
           return false;
         }

         tr.setSelection(
           NodeSelection.create(tr.doc, blockInfo.blockContent.beforePos),
-        );
+        ).scrollIntoView();

         return true;
       }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Enter: ({ editor }) =>
editor.transact((tr) => {
const blockInfo = getBlockInfoFromTransaction(tr);
if (
!blockInfo.isBlockContainer ||
blockInfo.blockNoteType !== blockType
) {
return false;
}
if (tr.selection instanceof NodeSelection) {
tr.setSelection(
TextSelection.create(
tr.doc,
blockInfo.blockContent.beforePos + 1,
),
);
} else {
tr.setSelection(
NodeSelection.create(tr.doc, blockInfo.blockContent.beforePos),
);
}
return true;
}),
// Closes the source code popup by setting the selection on the whole block content node.
Escape: ({ editor }) =>
editor.transact((tr) => {
const blockInfo = getBlockInfoFromTransaction(tr);
if (
!blockInfo.isBlockContainer ||
blockInfo.blockNoteType !== blockType ||
tr.selection instanceof NodeSelection
) {
return false;
}
tr.setSelection(
NodeSelection.create(tr.doc, blockInfo.blockContent.beforePos),
);
return true;
}),
Enter: ({ editor }) =>
editor.transact((tr) => {
const blockInfo = getBlockInfoFromTransaction(tr);
if (
!blockInfo.isBlockContainer ||
blockInfo.blockNoteType !== blockType
) {
return false;
}
if (tr.selection instanceof NodeSelection) {
tr.setSelection(
TextSelection.create(
tr.doc,
blockInfo.blockContent.beforePos + 1,
),
).scrollIntoView();
} else {
tr.setSelection(
NodeSelection.create(tr.doc, blockInfo.blockContent.beforePos),
).scrollIntoView();
}
return true;
}),
// Closes the source code popup by setting the selection on the whole block content node.
Escape: ({ editor }) =>
editor.transact((tr) => {
const blockInfo = getBlockInfoFromTransaction(tr);
if (
!blockInfo.isBlockContainer ||
blockInfo.blockNoteType !== blockType ||
tr.selection instanceof NodeSelection
) {
return false;
}
tr.setSelection(
NodeSelection.create(tr.doc, blockInfo.blockContent.beforePos),
).scrollIntoView();
return true;
}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/core/src/blocks/Code/helpers/extensions/createPreviewSourceNavigationExtension.ts`
around lines 175 - 218, The Enter and Escape handlers are missing
scrollIntoView() calls after setting the selection, creating an inconsistency
with the arrow handlers (lines 127, 147) which do include these calls. Add
tr.scrollIntoView() after each tr.setSelection() call in both the Enter handler
and the Escape handler to ensure the viewport scrolls to keep the focused block
visible when the selection changes.

@matthewlipski

Copy link
Copy Markdown
Collaborator Author

Exciting!

Looking at the code from a high level (no user testing or deep dive into the functions), I have the following high-level questions:

  • Should we use React or Vanilla for the components? (especially thinking about customizability)
  • Curious if the current code design (with the keyboard handlers) also scales to interfaces where the editor is not in a pop-up (e.g.: Notion / TypeCell style editors that can collapse)
  • No strong preference for this, though admittedly React would be cleaner and these days I don't know if anyone is using BlockNote outside React.
  • Maybe worth extending the custom block API with handling when the text cursor moves in to/out of the block, as imo these are the biggest pain points in my experience with writing the keyboard handling for the math block. Would have to be a bit limited though without exposing ProseMirror APIs.

@pkg-pr-new

pkg-pr-new Bot commented Jun 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/ariakit@2857

@blocknote/code-block

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/code-block@2857

@blocknote/core

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@2857

@blocknote/mantine

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/mantine@2857

@blocknote/math-block

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/math-block@2857

@blocknote/react

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@2857

@blocknote/server-util

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/server-util@2857

@blocknote/shadcn

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/shadcn@2857

@blocknote/xl-ai

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-ai@2857

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-docx-exporter@2857

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-email-exporter@2857

@blocknote/xl-multi-column

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-multi-column@2857

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-odt-exporter@2857

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-pdf-exporter@2857

commit: 719fd7a

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants