feat(math): emit data-mx-maths on send for cross-client LaTeX

Math rendering already shipped (both the $…$ shorthand and the spec
data-mx-maths form render incoming). But the composer emitted no math, so a
Lotus user's $E=mc^2$ went out as raw text — it rendered on Lotus (both sides
parse $…$) but showed as literal text on Element and other clients.

toMatrixCustomHTML now converts $…$/$$…$$ to
  <span|div data-mx-maths="LATEX"><code>LATEX</code></span|div>
(spec CS-API §11.5), reusing the existing splitMathSegments parser. Math is
extracted BEFORE markdown so LaTeX (_, *, \, {}) isn't mangled, and the emitted
span survives the block-markdown pass via the existing ignoreHTMLParseInlineMD
HTML-tag guard. A new allowMath opt threads through the top-level call sites;
code-line/code-block paths use empty opts so math is off inside code. The plain
body keeps literal $…$ as the fallback.

Scope: inline $…$ + single-line $$…$$. Multi-line block $$ (spans editor
paragraph nodes) deferred — still renders on Lotus via the plain-body path.

New output.test.ts (8 cases): span/div emission, escaping, markdown-bypass,
currency non-match, code-mark + code-block exclusion. 738 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 12:46:25 -04:00
co-authored by Claude Opus 4.8
parent 7ad948e26c
commit 33cb103abb
4 changed files with 99 additions and 0 deletions
+28
View File
@@ -12,14 +12,42 @@ import {
import { findAndReplace } from '../../utils/findAndReplace';
import { sanitizeForRegex } from '../../utils/regex';
import { isUserId } from '../../utils/matrix';
import { splitMathSegments } from '../../utils/mathParse';
export type OutputOptions = {
allowTextFormatting?: boolean;
allowInlineMarkdown?: boolean;
allowBlockMarkdown?: boolean;
allowMath?: boolean;
};
// Spec `data-mx-maths` markup (CS-API §11.5): the attribute holds the LaTeX; the
// <code> child is the fallback for clients that don't render math. sanitizeText
// escapes & < > " ' — correct for both the attribute value and the <code> text.
const mathToCustomHtml = (latex: string, block: boolean): string => {
const esc = sanitizeText(latex);
const tag = block ? 'div' : 'span';
return `<${tag} data-mx-maths="${esc}"><code>${esc}</code></${tag}>`;
};
const textToCustomHtml = (node: Text, opts: OutputOptions): string => {
// Convert `$…$`/`$$…$$` to `data-mx-maths` so math renders on other clients.
// Extracted BEFORE markdown so LaTeX (`_`, `*`, `\`, `{}`) isn't mangled; never
// applied inside inline code. Non-math text recurses with allowMath off so it
// still gets the normal marks + inline-markdown treatment.
if (opts.allowMath && !node.code) {
const segments = splitMathSegments(node.text);
if (segments.some((seg) => seg.type !== 'text')) {
return segments
.map((seg) =>
seg.type === 'text'
? textToCustomHtml({ ...node, text: seg.value }, { ...opts, allowMath: false })
: mathToCustomHtml(seg.value, seg.type === 'block')
)
.join('');
}
}
let string = sanitizeText(node.text);
if (opts.allowTextFormatting) {
if (node.bold) string = `<strong>${string}</strong>`;