import temml from 'temml'; import { mml2omml } from 'mathml2omml'; import { createLogger } from '@/lib/logger'; const log = createLogger('LatexToOmml'); /** * Strip MathML elements unsupported by mathml2omml (e.g. ``), * replacing them with their inner content. */ function stripUnsupportedMathML(mathml: string): string { const unsupported = ['mpadded']; let result = mathml; for (const tag of unsupported) { result = result.replace(new RegExp(`<${tag}[^>]*>`, 'g'), ''); result = result.replace(new RegExp(``, 'g'), ''); } return result; } /** * Build for math runs. PowerPoint requires Cambria Math font. * @param szHundredths - font size in hundredths of a point (e.g. 1200 = 12pt). Omit for no sz. */ function buildMathRPr(szHundredths?: number): string { const szAttr = szHundredths ? ` sz="${szHundredths}"` : ''; return ( `` + '' + '' + '' ); } /** * Post-process OMML for PPTX compatibility: * 1. Strip xmlns:w (wordprocessingml is DOCX-only, not valid in PPTX) * 2. Strip redundant xmlns:m (already declared at level) * 3. Inject with Cambria Math font (and optional sz) into and */ function postProcessOmml(omml: string, szHundredths?: number): string { let result = omml; const rpr = buildMathRPr(szHundredths); // Strip DOCX-only xmlns:w and redundant xmlns:m from result = result.replace(/ xmlns:w="[^"]*"/g, ''); result = result.replace(/ xmlns:m="[^"]*"/g, ''); // Insert before inside (only if not already present) result = result.replace(/(\s*)$1${rpr}$1 with result = result.replace(//g, `${rpr}`); // Fill empty with result = result.replace(/<\/m:ctrlPr>/g, `${rpr}`); return result; } /** * Convert a LaTeX string to OMML (Office Math Markup Language) XML. * * Pipeline: LaTeX → MathML (temml) → strip unsupported → OMML (mathml2omml) → inject font props * * @param latex - LaTeX math expression (without delimiters) * @param fontSize - Optional font size in points (e.g. 12). Applied as sz on every in the OMML. * @returns OMML XML string (an `` element), or `null` if conversion fails */ export function latexToOmml(latex: string, fontSize?: number): string | null { try { const mathml = temml.renderToString(latex); const cleaned = stripUnsupportedMathML(mathml); const omml = String(mml2omml(cleaned)); const szHundredths = fontSize ? Math.round(fontSize * 100) : undefined; return postProcessOmml(omml, szHundredths); } catch { log.warn(`Failed to convert: "${latex}"`); return null; } }