Files
Devin d39bc871b7 feat(ui): render markdown math (#878)
* feat(ui): render markdown math

* feat(ui): support more markdown math delimiters

* feat(ui): renders spaced dollar-delimited TeX support

* chore(deps): update katex

* Update bun.lock

chore(deps): update

chore(deps): update

chore(deps): update

chore(deps): update

chore(deps): update

* feat(highlight): highlight math

* feat(ui): support formule & text highlight together

* fix(ui): make rendered math drag-selectable for annotation

Drag-selecting an inline formula did nothing: the browser normalizes the
selection focus to offset 0 of the following text node, so the strict
anchor/focus check in selectionHasNonMathContent treated every inline
drag as "mixed prose+math" and bailed without claiming the event. Control
fell to web-highlighter, which excludes .katex/.math-annotatable, so no
annotation was created and the selection just collapsed.

Drop the anchor/focus check and rely on the existing clone-and-strip logic
(leftover text => genuinely mixed), plus a guard for selections wholly
inside one formula. Add a regression test that reproduces the real
spilled-endpoint drag, which the prior empty-selection tests missed.

Also paint a preview highlight the moment a formula's toolbar/popover
opens (stripped on cancel, committed on submit) and show cursor: pointer
on hover so formulas read as annotation targets.

* chore(ui): reconcile lockfile and math styles after rebase onto main

Rebase reconciliation, not new behavior:
- Re-add katex to bun.lock. The lockfile conflicted on every replayed
  commit and was resolved to main's copy throughout; regenerate it here so
  package.json (katex) and the lockfile agree again.
- Move the math annotation styles (.math-inline-annotation /
  .math-block-annotation) into packages/ui/theme.css. main relocated all
  annotation-highlight rules there to share them with the code-review
  description annotations; the PR had added the math variants to the old
  editor/index.css location.

* test(ui): lazy-load highlighter hook so DOM-less bun test skips cleanly

The math-annotation test statically imported useAnnotationHighlighter, which
pulls in @plannotator/web-highlighter — a UMD bundle that reads `window` at
module-eval time. Under the default `bun test` (no DOM), that threw a
ReferenceError on import, failing CI before the skipIf(!hasDom) guards ever
ran. Import the hook lazily, gated on hasDom, so the file loads and its tests
skip cleanly in CI while DOM_TESTS=1 still exercises them.

* fix(ui): stop display math from swallowing the document

The `$$`/`\[` block parser only recognized the closing delimiter when it was
the last non-space text on its line. A line like `$$E=mc^2$$.` (trailing
period) or `$$E=mc^2$$ where E is energy.` therefore looked like an
unterminated opener and consumed every following line until the next fence or
EOF — silently hiding the rest of the plan.

Detect the closing delimiter anywhere on the line and re-process any trailing
text as its own line, so the equation renders and the following content is
preserved instead of dropped. Same fix for the multi-line closing path and the
`\[ ... \]` branch. Adds parser regression tests (these run in CI; no DOM).

---------

Co-authored-by: ishowman <ishowman@users.noreply.github.com>
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
2026-07-01 10:48:53 -07:00

37 lines
938 B
TypeScript

import React, { useMemo } from 'react';
import katex from 'katex';
import type { Block } from '../../types';
type MathBlockProps = {
block: Block;
};
export const normalizeMathTex = (tex: string): string => tex.trim();
export const renderMathToHtml = (tex: string, displayMode: boolean): string => (
katex.renderToString(tex, {
displayMode,
throwOnError: false,
strict: 'warn',
trust: false,
output: 'html',
})
);
export const MathBlock: React.FC<MathBlockProps> = ({ block }) => {
const tex = normalizeMathTex(block.content);
const html = useMemo(() => renderMathToHtml(tex, true), [tex]);
return (
<div
className="math-block math-annotatable my-5 overflow-x-auto py-2 text-foreground"
data-block-id={block.id}
data-block-type="math"
data-math-tex={tex}
data-math-display="true"
aria-label={tex}
dangerouslySetInnerHTML={{ __html: html }}
/>
);
};