Files
wyucandClaude Opus 5.5 e07e629261 fix(sanitize): store inline math as LaTeX source and typeset it in the client (#1829)
* fix(sanitize): keep inline math through the scene-content sanitizer

The prose policy flattened the editor's inline formulas on every
persistence read and write: it dropped data-inline-math, the MathML
annotation, the layout SVG and KaTeX positioning, so saved formulas
rendered broken and their LaTeX source was lost.

Move the lift / sanitize / re-render approach from the standalone HTML
export into lib/sanitize/inline-math.ts and apply it in
sanitizeProseHtml, so the persistence boundary and the export share one
implementation. It parses with parse5, so it needs no DOM and runs on
the server and in the browser alike. Each formula is replaced by an
empty source span before sanitizing, and every surviving source span is
replaced afterwards by a fresh KaTeX render (trust: false); authored
markup never bypasses the policy. Prose without a formula skips the
parse and is sanitized byte-for-byte as before. Renders are cached by
source, and pathological markup falls back to the plain policy instead
of failing the read or write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sanitize): bound inline math rendering and detect encoded KaTeX classes

KaTeX markup is 100-150x the size of its source and is regenerated
synchronously on every persistence read and write, so one large formula
could block the event loop for seconds and pin hundreds of MB in the
render cache.

- Render a formula only if its source is at most 2000 characters, and
  within a per-call budget of 16000 source characters and 1000
  formulas. Anything else stays a source span showing its LaTeX as text,
  so nothing is lost and the editor still recovers it. Limits are counted
  in document order, never time, so repeated passes stay stable.
- Bound the render cache by markup size (1 MB) and never cache renders
  over 32 KB.
- Pre-check on `data-inline-math` or `annotation` instead of `katex`:
  element and attribute names cannot be written as character references,
  but a class value can, so an encoded `katex` class no longer skips the
  lift and loses its source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sanitize): store inline math as source and typeset it in the client

Rendering formulas on the server made every persistence read and write
run KaTeX on authored input, which user macros and long sources can turn
into seconds of CPU per document. Store each inline formula as its LaTeX
source instead and let the clients that display slides typeset it.

- The prose sanitizer reduces each inline formula (span[data-inline-math],
  else span.katex with a TeX annotation, entity-encoded class included)
  to <span data-inline-math="SRC">SRC</span> and allows only that
  attribute. The source doubles as text for consumers that do not render
  math. A source over 2000 characters is kept as plain text. The pre-pass
  uses htmlparser2, the parser sanitize-html uses, so it is linear and
  sees the same element boundaries; prose without a formula marker is
  sanitized exactly as before. The server never runs KaTeX, and the
  parse5 lift, render cache and budgets are gone.
- @openmaic/renderer typesets span[data-inline-math] in text, shape and
  table prose after injection (renderInlineMath / useInlineMath), with
  trust disabled, macro definitions blocked and bounded expansion. It
  also re-renders older content that still carries KaTeX markup. The
  app's slide renderer components use the same hook. Bump the renderer
  to 0.1.12.
- The standalone HTML export ships source-only formulas, which the
  player's renderer typesets, and ships the math fonts when a slide has
  an inline formula.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(slide-renderer): keep cell markup intact when formatting table text

formatText turned every space in a table cell into &nbsp;, including
the spaces inside tags, which broke any cell markup with attributes. An
inline formula in a cell was shown as its raw source instead of being
typeset. Only format the text between tags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(renderer): allow 1000 macro expansions when typesetting inline math

A cap of 50 expansions rejects realistic long formulas: built-in
commands such as \neq, \iff, \, and \dots each count, and a formula with
a few dozen of them needs over 100. Macro definitions are blocked, so
expansion stays linear in the source and 1000 is only a backstop. This
matches the shared safe KaTeX options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sanitize): replace self-closing inline formulas in SVG and MathML once

Inside SVG or MathML the parser honours `/>` and reports the close as
implied while still on the opening token, so the replacement ended
before the formula started and the original span stayed next to the
stored one; the next pass then stored it again. End a formula no earlier
than its opening tag. Adds SVG and MathML cases and fuzzes the pass for
idempotency and formula shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(renderer): keep inline math typeset across re-renders

React can rewrite `dangerouslySetInnerHTML` markup on a re-render even
when the HTML string is unchanged, which put the stored source spans
back after a move or resize while the effect, keyed on the HTML, did not
run again. Typeset after every commit instead: formulas already typeset
are skipped, and typeset formulas are cached by source and cloned, so a
re-render costs a query and, at most, cheap clones. Typesetting never
updates React state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(renderer): budget inline math typesetting per prose root

One slide packed with large formulas could block the page for hundreds of
milliseconds on every mount. Typeset at most 4000 characters of uncached
source or 100 uncached formulas per pass over a prose root before paint,
and the rest in idle time, a chunk of the same size per idle period. The
next commit, an unmount or new content cancels pending idle work. Past
50 000 characters or 1000 formulas per root, formulas are shown as their
LaTeX text. Slide snapshots typeset everything before capture. Covers
identical-content re-renders and A, B, A, A content sequences in both
renderers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(renderer): keep inline math progress across commits and complete snapshots per root

- Give injected prose a stable dangerouslySetInnerHTML value per markup
  string (text, shape label and each table cell, in both renderers), so
  React leaves unchanged markup alone. Typeset formulas and pending idle
  progress then survive re-renders; formulas too large to cache are no
  longer typeset again on every commit. The every-commit pass stays as a
  safety net.
- Mark each prose root and complete snapshots root by root, so each text
  box keeps its own hard cap as on the live slide instead of sharing one
  slide-wide count.
- Apply the hard cap from document order on every pass, whatever earlier
  passes typeset, and let a formula shown as text by an earlier cap be
  typeset once it falls within its root's cap.
- Note that the render cache bound counts serialized characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(renderer): bump to 0.1.13 after rebase

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(renderer): typeset inline math through the shared safeKatexOptions

Replace the local stand-in options (inert macro-definition commands,
trust: false, maxExpand 1000) with safeKatexOptions from @openmaic/dsl,
keeping the inline-specific settings (displayMode: false, output: html,
throwOnError: false, strict: ignore). The shared helper's expansion budget
is the same 1000, and it also keeps the built-in \tag/\notag state and
makes \long inert.

The math engine guard now lists the renderer's inline-math module as a
render-calls site and drops the standalone HTML rich-text module, which no
longer renders KaTeX itself.

Tests: ordinary formulas typeset byte-for-byte as with the previous
options, and formula-defined macros stay inert within one formula and
across formulas in the same prose root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sanitize): keep over-long inline formulas as formulas; bound typesetting only

The 2000-character bound dropped the data-inline-math marker at save time,
so an over-long formula became plain text for good. The bound now applies
only where formulas are typeset: the slide renderer already shows an
over-long source as text, and the editor's inline_math node now does the same
instead of typesetting it, using the renderer's MAX_INLINE_MATH_SOURCE.
@openmaic/editor 0.0.11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(sanitize): a 100 KB formula is kept as a formula in the node-environment suite

Follows the sanitizer change: the length bound applies to typesetting only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 11:58:01 +08:00
..