Files
OpenMAIC/lib
wyucandClaude Opus 5.5 472b26d0d5 fix(math): disable formula-defined macros in KaTeX and Temml rendering (#1827)
* fix(math): disable formula-defined macros in every KaTeX and Temml render

Add safeKatexOptions to @openmaic/dsl, the one package every renderer,
importer, editor and the app already depend on. It merges hardened
settings into each caller's options: macro-defining commands (\def,
\gdef, \edef, \xdef, \global, \long, \let, \futurelet, \newcommand,
\renewcommand, \providecommand) become inert macros that consume the
definition, trust is forced off, maxExpand is pinned, and a fresh
macros object is built per call so nothing persists between renders.
Caller options and caller-supplied macros are kept.

Route every render through it: whiteboard and slide latex elements,
quiz math, standalone HTML inline math, PPTX export via Temml, the
editor's latex dialog and inline math node, generated latex elements,
and the importer's formula paths. Keep the importer's node-globals
rollup plugin off the dsl build, whose old parser cannot read `??`.

Tests cover each render path with nested-definition inputs, a corpus of
legitimate formulas that must render byte-identically, \gdef isolation,
and a guard that fails if a new KaTeX/Temml call skips the helper.

Bump @openmaic/dsl 0.11.3, editor 0.0.10, generation 0.3.16,
importer 0.3.1.

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

* fix(math): harden workbench chat math and newcommand parsing

Workbench chat Markdown rendered math through @streamdown/math, which
only forwards errorColor to rehype-katex. Add createSafeMathPlugin,
which keeps that plugin's remark side and rehype-katex instance but
replaces the rehype options with safeKatexOptions, and use it in the
chat text block.

Rework the inert \newcommand family: peek at unconsumed tokens so a
body written as {[} stays a body, accept the starred form, skip [n]
and [default] groups with brace-aware scanning, then consume exactly
one replacement body. Document that caller-supplied macros are trusted
code and must not be derived from content.

Replace the call-text coverage check with an import-aware guard built
on the TypeScript parser. It finds every runtime import of a math
engine (default, aliased, named, namespace, side-effect, import =
require, re-export, dynamic import, require) plus injected auto-render
script text, and requires each file to be on a reviewed allowlist.
Files that import KaTeX or Temml directly must use them only as render
calls with safeKatexOptions(...). The interactive post-processor is
allowlisted: its page already runs model-written scripts in a sandboxed
iframe. Fixtures cover each bypass shape.

Add KaTeX and Temml regression tests for the newcommand edge cases,
and the workbench chat path to the expansion-input suite.

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

* fix(math): fresh macros per chat formula and stricter render guard

KaTeX writes built-in state into the macros object while rendering (an
align environment sets \@eqnsw), and rehype-katex spreads one options
object into every render. The workbench chat plugin shared one macros
object, so that state leaked between formulas and between messages.
Make macros an enumerable getter that returns a fresh hardened object
on every read, so each render and each error retry gets its own. Add
cross-formula and cross-message regression tests.

Tighten the guard for files that import KaTeX or Temml directly: any
engine reach other than a static default or namespace import (require,
dynamic import, re-export, import = require, side-effect import) is
rejected; the options argument is checked at each method's own
position with nothing after it; and it must be a call to
safeKatexOptions imported from @openmaic/dsl, not a local look-alike.
Document the guard's scope: module imports only; global access, engine
code inside other HTML strings and require.resolve need review.

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

* fix(math): keep built-in equation numbering with macro definitions disabled

KaTeX and Temml implement \tag, \tag*, \nonumber and \notag as built-in
macros that expand to \gdef\df@tag{\text{...}} and \gdef\@eqnsw{0}.
Swallowing every \gdef therefore dropped equation tags and stopped
\nonumber / \notag from suppressing align/gather/equation numbers.

The inert \gdef now accepts exactly those two built-in shapes and stores
them the way the engines' own \gdef does; every other \gdef, and every
other definition command, stays inert. '@' is a letter in both lexers,
so formulas can name these macros directly; the allowed shapes add
nothing \tag* and \nonumber do not already provide:

- \@eqnsw may only be set to 0;
- \df@tag may only be one \text{...} group that never mentions \df@tag,
  so the tag is expanded exactly once (a self-reference would recurse
  until the call stack or the expansion budget runs out, as it does in
  the stock engines on \tag{\df@tag}).

Tests: tagged and \notag/\nonumber formulas render identically to stock
KaTeX (html, mathml, htmlAndMathml) and Temml; the slide element, quiz,
editor dialog and Workbench chat renderers show tags and suppress
numbers; definitions inside tags, self-referencing tags, nested tags and
non-built-in \df@tag/\@eqnsw shapes stay inert.

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

* fix(math): expand a tag in full only once per accepted definition

A formula can name \df@tag itself, so a long \tag*{...} (or an accepted
\gdef\df@tag{\text{...}}) followed by many \df@tag mentions re-expanded
the whole tag on every mention, up to the expansion budget: about 12 KB
of input produced several MB of output.

An accepted \df@tag is now stored as a macro that yields its body on the
first expansion and an empty \text{} afterwards. The engines expand
\df@tag once per formula, or once per align row before clearing it, so
tags render unchanged (stock-equality tests, including an align with a
tag on several rows, stay green in KaTeX and Temml). The empty \text{}
rather than nothing avoids a Temml TypeError on an empty tag.

Also document, and test through the PPTX export, that Temml throws a
TypeError on a script left bare by an inert definition (x^\notag); the
only Temml consumer catches it and returns null.

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

* chore(generation): bump to 0.3.17 after main took 0.3.16

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

---------

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