Skip to content

Feat/formula editor md example - #2932

Open
Hill-Pham wants to merge 21 commits into
TypeCellOS:mainfrom
Hill-Pham:feat/formula-editor-md-example
Open

Feat/formula editor md example#2932
Hill-Pham wants to merge 21 commits into
TypeCellOS:mainfrom
Hill-Pham:feat/formula-editor-md-example

Conversation

@Hill-Pham

@Hill-Pham Hill-Pham commented Jul 30, 2026

Copy link
Copy Markdown

Summary

Rationale

Changes

Impact

Testing

Screenshots/Video

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

    • Transformed the Markdown example into an editable Vietnamese lesson editor.
    • Added support for inserting and editing inline and block math and chemistry formulas.
    • Added live formula previews, basic and advanced editing modes, formula palettes, and toolbar access.
    • Existing Markdown formulas are automatically imported and rendered as editable formula elements.
    • Added improved slash-menu scrolling within the example.
  • Documentation

    • Updated the example guide, title, and playground description to explain formula editing workflows.
  • Tests

    • Added coverage for formula parsing, rendering, insertion, editing, palette actions, and slash-menu scrolling.

hieuph added 21 commits July 29, 2026 17:39
…kage.json regen

- The formula-confirm button's CSS selector still targeted the old
  data-testid attribute after Task 4 renamed it to data-test, silently
  breaking the confirm button's styling.
- docs/package.json/pnpm-lock.yaml regenerated via `gen:docs` to add
  katex/mathlive/@types/katex deps needed for the docs example preview.
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the TypeCell Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The Markdown interoperability example becomes an editable formula editor with KaTeX and MathLive support. It adds formula schema nodes, Markdown import conversion, toolbar and modal editing, chemistry palettes, localized documentation, slash-menu styling, and end-to-end coverage.

Changes

Formula editor example

Layer / File(s) Summary
Formula model and Markdown rendering
examples/05-interoperability/04-converting-blocks-from-md/src/formula/*, src/markdown/*, src/schema.tsx, *.json
Adds KaTeX rendering, math/chemistry formula nodes, placeholder-based Markdown conversion, initial lesson content, and required packages.
MathLive editor and toolbar flow
examples/05-interoperability/04-converting-blocks-from-md/src/formula/*, src/toolbar/*
Adds the MathLive wrapper, palettes, editor context, modal, formula views, and toolbar insertion/editing behavior.
Editor integration and presentation
examples/05-interoperability/04-converting-blocks-from-md/src/App.tsx, src/styles.css, README.md, index.html, playground/src/examples.gen.tsx
Wires the editable BlockNote view and import pipeline, updates styling and metadata, and documents the Vietnamese formula workflow.
Formula behavior validation
tests/src/end-to-end/formula/formula.test.tsx, tests/src/examples.d.ts
Tests Markdown conversion, KaTeX output, modal insertion and editing, palette behavior, and slash-menu scrolling.

Estimated code review effort: 4 (Complex) | ~60 minutes

Poem

I’m a rabbit with formulas tucked under my ear,
MathLive hops in and makes the symbols clear.
KaTeX paints chemistry bright,
Toolbar buttons guide each write.
Markdown blooms into editable cheer!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is only the template with empty sections, so it lacks the required summary, rationale, changes, testing, and checklist details. Fill in each template section with the feature summary, rationale, changes, impact, testing, screenshots, checklist status, and any notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and matches the main change: adding a formula editor Markdown example.
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.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Warning

⚠️ This pull request shows signs of AI-generated slop (description_diff_mismatch, ai_padded_prose). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@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: 5

🧹 Nitpick comments (2)
tests/src/end-to-end/formula/formula.test.tsx (1)

159-159: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fixed sleep() calls risk CI flakiness; prefer polling on the actual DOM condition.

Several tests (lines 159, 215, 261, 306, 319) use hardcoded sleep(N) to wait for async state to settle instead of the waitForSelector-style polling already used elsewhere in this file. Under CI load these fixed delays can be too short (flaky failures) or unnecessarily slow the suite.

♻️ Example: replace a fixed sleep with a condition-based wait
-    await userEvent.type(textarea, "\\frac{1}{2}");
-    await sleep(150);
+    await userEvent.type(textarea, "\\frac{1}{2}");
+    await vi.waitFor(() => expect(textarea.value).toBe("\\frac{1}{2}"));

Apply the same pattern (wait for the specific expected DOM/value change) at the other sleep() call sites.

Also applies to: 215-215, 261-261, 306-306, 319-319

🤖 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 `@tests/src/end-to-end/formula/formula.test.tsx` at line 159, Replace the
hardcoded sleep calls at the listed test sites with condition-based polling
using the file’s existing waitForSelector-style pattern. Wait for each test’s
specific expected DOM or value change before continuing, preserving the existing
assertions and behavior while removing fixed timing dependencies.
examples/05-interoperability/04-converting-blocks-from-md/src/markdown/postprocessBlocks.ts (1)

50-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Simplify by dropping the shared global-regex .test() step.

matchWholeBlockToken calls the module-level global BLOCK_TOKEN_REGEX.test(), relies on its lastIndex mutation, then manually resets it before re-matching with a separate anchored regex. The anchored match alone is sufficient — no need to touch the shared, stateful regex at all, which removes a subtle footgun for future refactors (e.g. if this helper is ever invoked concurrently/reentrantly).

♻️ Proposed simplification
 function matchWholeBlockToken(
   block: AnyBlock,
   blockMap: Map<string, string>,
 ): string | null {
   if (!Array.isArray(block.content) || block.content.length !== 1) return null;
   const first = block.content[0] as { type?: string; text?: string };
   if (first.type !== "text" || typeof first.text !== "string") return null;
   const text = first.text.trim();
-  if (!BLOCK_TOKEN_REGEX.test(text)) return null;
-  // Reset regex state and re-match to grab the exact token
-  BLOCK_TOKEN_REGEX.lastIndex = 0;
   const m = text.match(/^⟪FML_BLOCK_\d+⟫$/);
   if (!m) return null;
   return blockMap.get(m[0]) ?? null;
 }
🤖 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
`@examples/05-interoperability/04-converting-blocks-from-md/src/markdown/postprocessBlocks.ts`
around lines 50 - 64, In matchWholeBlockToken, remove the shared
BLOCK_TOKEN_REGEX.test() check and its lastIndex reset. Use the existing
anchored match as the sole token validation, then continue returning the
matching blockMap entry.
🤖 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/05-interoperability/04-converting-blocks-from-md/src/App.tsx`:
- Around line 50-73: Update the inline formula editing flow spanning
FormulaInlineView, openEdit, FormulaTarget, and App.tsx's onUpdate so the target
carries stable identity or position information for the specific node. Use that
identifier to replace only the intended block.content entry, rather than
matching every formula by identical latex text; preserve block-level updates
unchanged.

In
`@examples/05-interoperability/04-converting-blocks-from-md/src/formula/FormulaEditorModal.tsx`:
- Around line 61-76: Update isConfirmDisabled in FormulaEditorModal to validate
the sanitized latex value, matching confirm’s sanitizePlaceholders(latex) check.
Keep the existing confirm behavior and ensure placeholder-only templates disable
the Confirm button before submission.
- Around line 80-122: Update the formula modal component around the dialog
container to add aria-modal="true", move initial focus into the modal when it
opens, trap Tab and Shift+Tab within its interactive elements, and close it on
Escape. Restore focus to the triggering element when api.close is invoked, while
preserving the existing backdrop and inner-click behavior.

In
`@examples/05-interoperability/04-converting-blocks-from-md/src/formula/MathFieldWrapper.tsx`:
- Around line 43-47: Update sanitizePlaceholders to remove complete \placeholder
groups with nested balanced braces, replacing the current non-nested regex with
a small scanner that tracks brace depth and skips each placeholder’s entire
argument. Preserve all non-placeholder LaTeX content and continue handling
multiple placeholders in the same input.

In `@examples/05-interoperability/04-converting-blocks-from-md/src/schema.tsx`:
- Around line 28-32: Update the formula editing flow around the inline editor
opener and FormulaTarget to retain a stable inline/range identity alongside the
LaTeX value. Pass that identity through formulaInline and use it in onUpdate to
replace only the targeted formula node, rather than matching every formulaInline
with identical props.latex.

---

Nitpick comments:
In
`@examples/05-interoperability/04-converting-blocks-from-md/src/markdown/postprocessBlocks.ts`:
- Around line 50-64: In matchWholeBlockToken, remove the shared
BLOCK_TOKEN_REGEX.test() check and its lastIndex reset. Use the existing
anchored match as the sole token validation, then continue returning the
matching blockMap entry.

In `@tests/src/end-to-end/formula/formula.test.tsx`:
- Line 159: Replace the hardcoded sleep calls at the listed test sites with
condition-based polling using the file’s existing waitForSelector-style pattern.
Wait for each test’s specific expected DOM or value change before continuing,
preserving the existing assertions and behavior while removing fixed timing
dependencies.
🪄 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 Plus

Run ID: fb8adfe6-18e1-4234-90ca-7bcb1d41471b

📥 Commits

Reviewing files that changed from the base of the PR and between dee7880 and 8166ab3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (27)
  • docs/package.json
  • docs/superpowers/plans/2026-07-29-formula-editor-for-md-example.md
  • docs/superpowers/plans/2026-07-30-formula-editor-wysiwyg-and-slash-menu-fix.md
  • docs/superpowers/specs/2026-07-29-formula-editor-for-md-example-design.md
  • docs/superpowers/specs/2026-07-30-formula-editor-wysiwyg-and-slash-menu-fix-design.md
  • examples/05-interoperability/04-converting-blocks-from-md/.bnexample.json
  • examples/05-interoperability/04-converting-blocks-from-md/README.md
  • examples/05-interoperability/04-converting-blocks-from-md/index.html
  • examples/05-interoperability/04-converting-blocks-from-md/package.json
  • examples/05-interoperability/04-converting-blocks-from-md/src/App.tsx
  • examples/05-interoperability/04-converting-blocks-from-md/src/formula/FormulaBlock.tsx
  • examples/05-interoperability/04-converting-blocks-from-md/src/formula/FormulaEditorModal.tsx
  • examples/05-interoperability/04-converting-blocks-from-md/src/formula/FormulaInline.tsx
  • examples/05-interoperability/04-converting-blocks-from-md/src/formula/MathFieldWrapper.tsx
  • examples/05-interoperability/04-converting-blocks-from-md/src/formula/formulaContext.tsx
  • examples/05-interoperability/04-converting-blocks-from-md/src/formula/katexRenderer.ts
  • examples/05-interoperability/04-converting-blocks-from-md/src/formula/palettes.ts
  • examples/05-interoperability/04-converting-blocks-from-md/src/markdown/initialMarkdown.ts
  • examples/05-interoperability/04-converting-blocks-from-md/src/markdown/postprocessBlocks.ts
  • examples/05-interoperability/04-converting-blocks-from-md/src/markdown/preprocessMarkdown.ts
  • examples/05-interoperability/04-converting-blocks-from-md/src/schema.tsx
  • examples/05-interoperability/04-converting-blocks-from-md/src/styles.css
  • examples/05-interoperability/04-converting-blocks-from-md/src/toolbar/CustomFormattingToolbar.tsx
  • examples/05-interoperability/04-converting-blocks-from-md/src/toolbar/FormulaButton.tsx
  • playground/src/examples.gen.tsx
  • tests/src/end-to-end/formula/formula.test.tsx
  • tests/src/examples.d.ts

Comment on lines +50 to +73
onUpdate(target, latex) {
if (target.kind === "block") {
editor.updateBlock(target.blockId, {
type: "formulaBlock",
props: { latex },
} as any);
} else {
// Replace the inline node at the current selection. The simplest reliable
// path: replace the whole inline content of the containing block by
// rewriting the inline content list.
const block = editor.getTextCursorPosition().block;
if (!Array.isArray(block.content)) return;
const nextContent = block.content.map((node: any) => {
if (
node.type === "formulaInline" &&
node.props?.latex === target.latex
) {
return { ...node, props: { latex } };
}
return node;
});
editor.updateBlock(block, { content: nextContent } as any);
}
},

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Inline formula update can silently overwrite sibling formulas with identical LaTeX.

onUpdate's inline branch matches nodes to replace purely by node.props?.latex === target.latex. If a block contains two or more inline formulas with the same LaTeX (e.g. the same $x$ referenced twice in one paragraph), editing one via FormulaInlineView's "click to edit" path (see the comment in FormulaButton.tsx acknowledging this route bypasses the "sole content" restriction) will rewrite every matching node in that block, not just the one the user intended to edit.

Root cause: FormulaTarget for the "inline" kind carries no positional/identity information (only { kind, latex }), so onUpdate has no way to disambiguate which specific node was targeted when duplicates exist.

Consider threading through a stable identifier (e.g. the node's index within block.content, or a generated id stored in props) from wherever openEdit is invoked for inline formulas, and using that to target the exact node instead of latex-text equality.

🤖 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 `@examples/05-interoperability/04-converting-blocks-from-md/src/App.tsx` around
lines 50 - 73, Update the inline formula editing flow spanning
FormulaInlineView, openEdit, FormulaTarget, and App.tsx's onUpdate so the target
carries stable identity or position information for the specific node. Use that
identifier to replace only the intended block.content entry, rather than
matching every formula by identical latex text; preserve block-level updates
unchanged.

Comment on lines +61 to +76
const isConfirmDisabled = latex.trim().length === 0;

const insertItem = (item: PaletteItem) => {
mfRef.current?.insert(item.insert);
};

const confirm = () => {
const finalLatex = sanitizePlaceholders(latex);
if (finalLatex.trim().length === 0) return;
if (state.mode === "edit" && state.editTarget) {
handlers.onUpdate(state.editTarget, finalLatex);
} else {
handlers.onInsert(kind, finalLatex);
}
api.close();
};

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Confirm button can silently no-op on placeholder-only input.

isConfirmDisabled checks raw latex, but confirm() validates sanitizePlaceholders(latex). If a user inserts a palette template (e.g. \frac{#?}{#?}) and never fills the placeholders, the button stays enabled, but clicking it hits the early return at line 69 with no feedback — the modal doesn't close and nothing is inserted/updated.

🔧 Suggested fix: align the disabled check with the sanitized value
-  const isConfirmDisabled = latex.trim().length === 0;
+  const isConfirmDisabled = sanitizePlaceholders(latex).trim().length === 0;
📝 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
const isConfirmDisabled = latex.trim().length === 0;
const insertItem = (item: PaletteItem) => {
mfRef.current?.insert(item.insert);
};
const confirm = () => {
const finalLatex = sanitizePlaceholders(latex);
if (finalLatex.trim().length === 0) return;
if (state.mode === "edit" && state.editTarget) {
handlers.onUpdate(state.editTarget, finalLatex);
} else {
handlers.onInsert(kind, finalLatex);
}
api.close();
};
const isConfirmDisabled = sanitizePlaceholders(latex).trim().length === 0;
const insertItem = (item: PaletteItem) => {
mfRef.current?.insert(item.insert);
};
const confirm = () => {
const finalLatex = sanitizePlaceholders(latex);
if (finalLatex.trim().length === 0) return;
if (state.mode === "edit" && state.editTarget) {
handlers.onUpdate(state.editTarget, finalLatex);
} else {
handlers.onInsert(kind, finalLatex);
}
api.close();
};
🤖 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
`@examples/05-interoperability/04-converting-blocks-from-md/src/formula/FormulaEditorModal.tsx`
around lines 61 - 76, Update isConfirmDisabled in FormulaEditorModal to validate
the sanitized latex value, matching confirm’s sanitizePlaceholders(latex) check.
Keep the existing confirm behavior and ensure placeholder-only templates disable
the Confirm button before submission.

Comment on lines +80 to +122
return (
<div className="formula-modal-backdrop" onClick={api.close}>
<div
className="formula-modal"
role="dialog"
aria-label="Soạn thảo công thức"
onClick={(e) => e.stopPropagation()}
>
<div className="formula-modal-header">
<h2>Soạn thảo công thức</h2>
<div
className="formula-modal-mode"
role="tablist"
aria-label="Chế độ"
>
<button
type="button"
role="tab"
aria-selected={mode === "basic"}
data-test="formula-mode-basic"
onClick={() => setMode("basic")}
>
Cơ bản
</button>
<button
type="button"
role="tab"
aria-selected={mode === "advanced"}
data-test="formula-mode-advanced"
onClick={() => setMode("advanced")}
>
Nâng cao
</button>
</div>
<button
type="button"
className="formula-modal-close"
onClick={api.close}
aria-label="Đóng"
>
×
</button>
</div>

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Modal lacks aria-modal, focus trapping, and Escape-to-close.

The dialog has role="dialog" and aria-label, but no aria-modal="true", no initial-focus management, and no keyboard trap — keyboard/screen-reader users can Tab out into the underlying editor while the modal is "open," and there's no Escape shortcut to dismiss it.

🔧 Suggested minimal fix
       <div
         className="formula-modal"
         role="dialog"
+        aria-modal="true"
         aria-label="Soạn thảo công thức"
         onClick={(e) => e.stopPropagation()}
+        onKeyDown={(e) => e.key === "Escape" && api.close()}
       >

Combine with a focus trap (e.g. focus the first interactive element on open, restore focus to the trigger on close) for full keyboard support.

📝 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
return (
<div className="formula-modal-backdrop" onClick={api.close}>
<div
className="formula-modal"
role="dialog"
aria-label="Soạn thảo công thức"
onClick={(e) => e.stopPropagation()}
>
<div className="formula-modal-header">
<h2>Soạn thảo công thức</h2>
<div
className="formula-modal-mode"
role="tablist"
aria-label="Chế độ"
>
<button
type="button"
role="tab"
aria-selected={mode === "basic"}
data-test="formula-mode-basic"
onClick={() => setMode("basic")}
>
bản
</button>
<button
type="button"
role="tab"
aria-selected={mode === "advanced"}
data-test="formula-mode-advanced"
onClick={() => setMode("advanced")}
>
Nâng cao
</button>
</div>
<button
type="button"
className="formula-modal-close"
onClick={api.close}
aria-label="Đóng"
>
×
</button>
</div>
return (
<div className="formula-modal-backdrop" onClick={api.close}>
<div
className="formula-modal"
role="dialog"
aria-modal="true"
aria-label="Soạn thảo công thức"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.key === "Escape" && api.close()}
>
<div className="formula-modal-header">
<h2>Soạn thảo công thức</h2>
<div
className="formula-modal-mode"
role="tablist"
aria-label="Chế độ"
>
<button
type="button"
role="tab"
aria-selected={mode === "basic"}
data-test="formula-mode-basic"
onClick={() => setMode("basic")}
>
bản
</button>
<button
type="button"
role="tab"
aria-selected={mode === "advanced"}
data-test="formula-mode-advanced"
onClick={() => setMode("advanced")}
>
Nâng cao
</button>
</div>
<button
type="button"
className="formula-modal-close"
onClick={api.close}
aria-label="Đóng"
>
×
</button>
</div>
🤖 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
`@examples/05-interoperability/04-converting-blocks-from-md/src/formula/FormulaEditorModal.tsx`
around lines 80 - 122, Update the formula modal component around the dialog
container to add aria-modal="true", move initial focus into the modal when it
opens, trap Tab and Shift+Tab within its interactive elements, and close it on
Escape. Restore focus to the triggering element when api.close is invoked, while
preserving the existing backdrop and inner-click behavior.

Comment on lines +43 to +47
export function sanitizePlaceholders(latex: string): string {
// Remove unfilled MathLive placeholders like \placeholder{} or \placeholder{...}
// so downstream KaTeX rendering doesn't fail.
return latex.replace(/\\placeholder\{[^}]*\}/g, "");
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

sanitizePlaceholders regex doesn't handle nested braces.

/\\placeholder\{[^}]*\}/g can't match balanced nested groups, e.g. \placeholder{\frac{a}{b}} only strips up to the first }, leaving a truncated/invalid LaTeX fragment. renderLatex's catch-all .formula-error output prevents a hard crash, but the formula silently renders as an error instead of the intended stripped-placeholder content.

🔧 Suggested fix using a small balanced-brace scanner
-export function sanitizePlaceholders(latex: string): string {
-  // Remove unfilled MathLive placeholders like \placeholder{} or \placeholder{...}
-  // so downstream KaTeX rendering doesn't fail.
-  return latex.replace(/\\placeholder\{[^}]*\}/g, "");
-}
+export function sanitizePlaceholders(latex: string): string {
+  // Remove unfilled MathLive placeholders like \placeholder{} or \placeholder{...},
+  // including any nested braces, so downstream KaTeX rendering doesn't fail.
+  const marker = "\\placeholder{";
+  let result = "";
+  let i = 0;
+  while (i < latex.length) {
+    const start = latex.indexOf(marker, i);
+    if (start === -1) {
+      result += latex.slice(i);
+      break;
+    }
+    result += latex.slice(i, start);
+    let depth = 1;
+    let j = start + marker.length;
+    while (j < latex.length && depth > 0) {
+      if (latex[j] === "{") depth++;
+      else if (latex[j] === "}") depth--;
+      j++;
+    }
+    i = j;
+  }
+  return result;
+}
📝 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
export function sanitizePlaceholders(latex: string): string {
// Remove unfilled MathLive placeholders like \placeholder{} or \placeholder{...}
// so downstream KaTeX rendering doesn't fail.
return latex.replace(/\\placeholder\{[^}]*\}/g, "");
}
export function sanitizePlaceholders(latex: string): string {
// Remove unfilled MathLive placeholders like \placeholder{} or \placeholder{...},
// including any nested braces, so downstream KaTeX rendering doesn't fail.
const marker = "\\placeholder{";
let result = "";
let i = 0;
while (i < latex.length) {
const start = latex.indexOf(marker, i);
if (start === -1) {
result += latex.slice(i);
break;
}
result += latex.slice(i, start);
let depth = 1;
let j = start + marker.length;
while (j < latex.length && depth > 0) {
if (latex[j] === "{") depth++;
else if (latex[j] === "}") depth--;
j++;
}
i = j;
}
return result;
}
🤖 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
`@examples/05-interoperability/04-converting-blocks-from-md/src/formula/MathFieldWrapper.tsx`
around lines 43 - 47, Update sanitizePlaceholders to remove complete
\placeholder groups with nested balanced braces, replacing the current
non-nested regex with a small scanner that tracks brace depth and skips each
placeholder’s entire argument. Preserve all non-placeholder LaTeX content and
continue handling multiple placeholders in the same input.

Comment on lines +28 to +32
onOpenEditor={() =>
editor.openEdit({
kind: "inline",
latex: props.inlineContent.props.latex,
})

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C5 'onUpdate|target\.latex|formulaInline' \
  examples/05-interoperability/04-converting-blocks-from-md/src/App.tsx \
  examples/05-interoperability/04-converting-blocks-from-md/src/formula/formulaContext.tsx \
  examples/05-interoperability/04-converting-blocks-from-md/src/schema.tsx

Repository: TypeCellOS/BlockNote

Length of output: 7342


🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  examples/05-interoperability/04-converting-blocks-from-md/src/App.tsx
  examples/05-interoperability/04-converting-blocks-from-md/src/schema.tsx
  examples/05-interoperability/04-converting-blocks-from-md/src/formula/formulaContext.tsx
  examples/05-interoperability/04-converting-blocks-from-md/src/formula/FormulaInline.tsx
)

for f in "${files[@]}"; do
  echo "===== $f ====="
  sed -n '1,180p' "$f"
done

echo "===== package references ====="
fd -a 'package.json|pnpm-lock.yaml|yarn.lock|package-lock.json' examples/05-interoperability/04-converting-blocks-from-md | sed 's#^\./##' | while read -r f; do
  echo "--- $f"
  rg -n '`@blocknote/react`|`@blocknote/core`|`@blocknote/mantine`|`@blocknote/shadcn`' "$f" || true
done

Repository: TypeCellOS/BlockNote

Length of output: 8224


Use a stable inline-node identity when editing formulas.

formulaInline sends only latex, and onUpdate replaces every formulaInline in the current block whose props.latex matches. Duplicate formulas in the same block will be edited together; store a stable inline/range identity in FormulaTarget and target only that node.

🤖 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 `@examples/05-interoperability/04-converting-blocks-from-md/src/schema.tsx`
around lines 28 - 32, Update the formula editing flow around the inline editor
opener and FormulaTarget to retain a stable inline/range identity alongside the
LaTeX value. Pass that identity through formulaInline and use it in onUpdate to
replace only the targeted formula node, rather than matching every formulaInline
with identical props.latex.

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.

1 participant