latex-to-omml.ts 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. import temml from 'temml';
  2. import { mml2omml } from 'mathml2omml';
  3. import { createLogger } from '@/lib/logger';
  4. const log = createLogger('LatexToOmml');
  5. /**
  6. * Strip MathML elements unsupported by mathml2omml (e.g. `<mpadded>`),
  7. * replacing them with their inner content.
  8. */
  9. function stripUnsupportedMathML(mathml: string): string {
  10. const unsupported = ['mpadded'];
  11. let result = mathml;
  12. for (const tag of unsupported) {
  13. result = result.replace(new RegExp(`<${tag}[^>]*>`, 'g'), '');
  14. result = result.replace(new RegExp(`</${tag}>`, 'g'), '');
  15. }
  16. return result;
  17. }
  18. /**
  19. * Build <a:rPr> for math runs. PowerPoint requires Cambria Math font.
  20. * @param szHundredths - font size in hundredths of a point (e.g. 1200 = 12pt). Omit for no sz.
  21. */
  22. function buildMathRPr(szHundredths?: number): string {
  23. const szAttr = szHundredths ? ` sz="${szHundredths}"` : '';
  24. return (
  25. `<a:rPr lang="en-US" i="1"${szAttr}>` +
  26. '<a:latin typeface="Cambria Math" panose="02040503050406030204" charset="0"/>' +
  27. '<a:cs typeface="Cambria Math" panose="02040503050406030204" charset="0"/>' +
  28. '</a:rPr>'
  29. );
  30. }
  31. /**
  32. * Post-process OMML for PPTX compatibility:
  33. * 1. Strip xmlns:w (wordprocessingml is DOCX-only, not valid in PPTX)
  34. * 2. Strip redundant xmlns:m (already declared at <p:sld> level)
  35. * 3. Inject <a:rPr> with Cambria Math font (and optional sz) into <m:r> and <m:ctrlPr>
  36. */
  37. function postProcessOmml(omml: string, szHundredths?: number): string {
  38. let result = omml;
  39. const rpr = buildMathRPr(szHundredths);
  40. // Strip DOCX-only xmlns:w and redundant xmlns:m from <m:oMath>
  41. result = result.replace(/ xmlns:w="[^"]*"/g, '');
  42. result = result.replace(/ xmlns:m="[^"]*"/g, '');
  43. // Insert <a:rPr> before <m:t> inside <m:r> (only if not already present)
  44. result = result.replace(/<m:r>(\s*)<m:t/g, `<m:r>$1${rpr}$1<m:t`);
  45. // Fill empty <m:ctrlPr/> with <a:rPr>
  46. result = result.replace(/<m:ctrlPr\/>/g, `<m:ctrlPr>${rpr}</m:ctrlPr>`);
  47. // Fill empty <m:ctrlPr></m:ctrlPr> with <a:rPr>
  48. result = result.replace(/<m:ctrlPr><\/m:ctrlPr>/g, `<m:ctrlPr>${rpr}</m:ctrlPr>`);
  49. return result;
  50. }
  51. /**
  52. * Convert a LaTeX string to OMML (Office Math Markup Language) XML.
  53. *
  54. * Pipeline: LaTeX → MathML (temml) → strip unsupported → OMML (mathml2omml) → inject font props
  55. *
  56. * @param latex - LaTeX math expression (without delimiters)
  57. * @param fontSize - Optional font size in points (e.g. 12). Applied as sz on every <a:rPr> in the OMML.
  58. * @returns OMML XML string (an `<m:oMath>` element), or `null` if conversion fails
  59. */
  60. export function latexToOmml(latex: string, fontSize?: number): string | null {
  61. try {
  62. const mathml = temml.renderToString(latex);
  63. const cleaned = stripUnsupportedMathML(mathml);
  64. const omml = String(mml2omml(cleaned));
  65. const szHundredths = fontSize ? Math.round(fontSize * 100) : undefined;
  66. return postProcessOmml(omml, szHundredths);
  67. } catch {
  68. log.warn(`Failed to convert: "${latex}"`);
  69. return null;
  70. }
  71. }