UNPKG

mathpix-markdown-it

Version:

Mathpix-markdown-it is an open source implementation of the mathpix-markdown spec written in Typescript. It relies on the following open source libraries: MathJax v3 (to render math with SVGs), markdown-it (for standard Markdown parsing)

319 lines 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.renderMathInElement = void 0; var tslib_1 = require("tslib"); var mathjax_1 = require("../mathjax"); var sre_browser_1 = require("../sre/sre-browser"); var utils_1 = require("./utils"); var RE_TEX_DISPLAY_DOLLARS = /^\$\$[\s\S]*\$\$$/; var RE_TEX_DISPLAY_BRACKETS = /^\\\[[\s\S]*\\\]$/; var RE_TEX_INLINE_PARENS = /^\\\([\s\S]*\\\)$/; var RE_TEX_INLINE_DOLLAR = /^\$[\s\S]*\$$/; var RE_TEX_ENV_WHOLE = /^\\begin\{[^}]+\}[\s\S]*\\end\{[^}]+\}$/; var defaultConfig = { accessibility: { assistive_mml: true, include_speech: false }, outMath: { output_format: "svg", include_svg: true }, width: 1200 }; /** * Returns true if the element appears to be already typeset by Mathpix/MathJax. * Used to prevent double-rendering during autorender passes. * * Heuristics: * - Prefer an explicit marker attribute set by our renderer. * - Otherwise detect common MathJax output containers/markers. */ var isAlreadyRenderedMath = function (el) { // Fast & explicit: our own marker. if (el.hasAttribute('data-mathpix-typeset')) return true; // Single DOM query for common MathJax output. var alreadyRenderedSelector = [ 'mjx-container', 'mjx-container[jax]', 'mjx-assistive-mml', '[data-mjx-container]', '.mjx-container', '.MathJax', // common class used by MathJax output ].join(','); return el.querySelector(alreadyRenderedSelector) !== null; }; /** * Returns true if the element contains exactly one meaningful child node and it is a MathML <math> element. * Example accepted structure: * <span class="math-block"><math xmlns="http://www.w3.org/1998/Math/MathML">...</math></span> */ var isOnlyMathMLElement = function (el) { var children = Array.from(el.childNodes).filter(function (n) { var _a; return !(n.nodeType === Node.TEXT_NODE && !((_a = n.textContent) !== null && _a !== void 0 ? _a : '').trim()); }); return children.length === 1 && children[0].nodeType === Node.ELEMENT_NODE && children[0].tagName.toLowerCase() === 'math'; }; /** * Extracts the MathML markup from the given element and determines * whether it should be treated as display (block) math. * * Display mode is inferred from: * - the MathML `display="block"` attribute, or * - a wrapper element having the `math-block` CSS class. */ var getMathMLString = function (el) { var mathEl = el.querySelector('math'); var displayAttr = mathEl === null || mathEl === void 0 ? void 0 : mathEl.getAttribute('display'); var display = displayAttr === 'block' || el.classList.contains('math-block'); return { mathml: mathEl.outerHTML, display: display }; }; /** * If the input string is entirely wrapped in a single pair of outer TeX math delimiters, * strips those outer delimiters and returns the inner TeX plus an inferred display mode. * Returns null if the input is not fully wrapped. */ var stripOuterMathDelimitersIfWhole = function (raw) { var text = (raw !== null && raw !== void 0 ? raw : '').trim(); if (!text) return null; // $$...$$ if (RE_TEX_DISPLAY_DOLLARS.test(text)) { return { tex: text.slice(2, -2).trim(), display: true }; } // \[...\] if (RE_TEX_DISPLAY_BRACKETS.test(text)) { return { tex: text.slice(2, -2).trim(), display: true }; } // \(...\) if (RE_TEX_INLINE_PARENS.test(text)) { return { tex: text.slice(2, -2).trim(), display: false }; } // $...$ (but not $$...$$) if (RE_TEX_INLINE_DOLLAR.test(text) && !(text.startsWith('$$') && text.endsWith('$$'))) { return { tex: text.slice(1, -1).trim(), display: false }; } // \begin{...}...\end{...} (whole string) if (RE_TEX_ENV_WHOLE.test(text)) { return { tex: text, display: true }; } return null; }; /** * Decides whether a given element should be typeset as math. * - If the element contains a single MathML node (<math>...</math>) -> returns a MathML target. * - If the element already contains rendered math (SVG/MathJax containers) -> returns null. * - If the element contains non-trivial child elements (except <br>) -> returns null. * - Otherwise, if its text is fully wrapped in TeX math delimiters -> returns a TeX target. */ var shouldTypesetNode = function (el) { var _a; // 0) Pure MathML input (render it) if (isOnlyMathMLElement(el)) { var _b = getMathMLString(el), mathml = _b.mathml, display = _b.display; return { kind: 'mathml', mathml: mathml, display: display }; } // 1) Already rendered (svg/mjx-container/etc.) -> skip if (isAlreadyRenderedMath(el)) { return null; } // 2) Contains child elements other than <br> -> skip var hasNonTrivialChildElements = Array.from(el.childNodes).some(function (node) { if (node.nodeType !== Node.ELEMENT_NODE) return false; return node.tagName.toLowerCase() !== 'br'; }); if (hasNonTrivialChildElements) { return null; } // 3) Otherwise, check for fully-wrapped TeX delimiters var text = (_a = el.textContent) !== null && _a !== void 0 ? _a : ''; var match = stripOuterMathDelimitersIfWhole(text); if (!match) { return null; } return { kind: 'tex', tex: match.tex, display: match.display }; }; /** * Resolves accessibility feature flags from user config. * Default behavior: if config is missing or null, both flags are disabled * (accessibility must be explicitly opted-in via config). * Note: in normal usage this is always called with the merged config object * from renderMathInElement, so the null path is a safety fallback. */ var resolveA11yFlags = function (a11y) { var cfg = (a11y && typeof a11y === 'object') ? a11y : null; if (!cfg) { return { assistiveMml: false, includeSpeech: false }; } return { assistiveMml: cfg.assistive_mml === true, includeSpeech: cfg.include_speech === true, }; }; /** * Builds MathJax accessibility options based on user config. * Returns `undefined` if all accessibility features are disabled. */ var buildAccessibilityOptions = function (a11y) { return tslib_1.__awaiter(void 0, void 0, void 0, function () { var _a, assistiveMml, includeSpeech, opts, sre; return tslib_1.__generator(this, function (_b) { switch (_b.label) { case 0: _a = resolveA11yFlags(a11y), assistiveMml = _a.assistiveMml, includeSpeech = _a.includeSpeech; // If all features are disabled, return undefined to avoid enabling a11y pipeline. if (!assistiveMml && !includeSpeech) { return [2 /*return*/, undefined]; } opts = {}; if (assistiveMml) { opts.assistiveMml = true; } if (!includeSpeech) return [3 /*break*/, 2]; return [4 /*yield*/, (0, sre_browser_1.loadSreAsync)()]; case 1: sre = _b.sent(); if (sre) { opts.sre = sre; } _b.label = 2; case 2: return [2 /*return*/, opts]; } }); }); }; /** * Replace element contents with rendered math HTML. * Note: this will remove existing child nodes inside `el` (but keeps the element itself). */ var setInnerHTML = function (el, html) { el.innerHTML = html; }; /** * Typeset MathJax math inside a container element. * Searches for `.math-inline` and `.math-block`, detects whether each node contains pure MathML or TeX, * and replaces its inner HTML with MathJax output. */ var renderMathInElement = function (container, config) { return tslib_1.__awaiter(void 0, void 0, void 0, function () { var cfg, outMath, cwidth, mathNodes, accessibility, mathNodes_1, mathNodes_1_1, mathEl, target, metric, result, isInline, isBlock, width, widthEx; var e_1, _a; return tslib_1.__generator(this, function (_b) { switch (_b.label) { case 0: cfg = tslib_1.__assign(tslib_1.__assign(tslib_1.__assign({}, defaultConfig), config), { accessibility: tslib_1.__assign(tslib_1.__assign({}, defaultConfig.accessibility), config === null || config === void 0 ? void 0 : config.accessibility), outMath: tslib_1.__assign(tslib_1.__assign({}, defaultConfig.outMath), config === null || config === void 0 ? void 0 : config.outMath) }); outMath = cfg.outMath; cwidth = cfg.width && cfg.width > 0 ? cfg.width : 1200; mathNodes = Array.from(container.querySelectorAll('.math-inline, .math-block')); return [4 /*yield*/, buildAccessibilityOptions(cfg.accessibility)]; case 1: accessibility = _b.sent(); // Start a new "render session" so a11y ids (aria-labelledby) can be generated consistently. mathjax_1.MathJax.beginRender(cfg === null || cfg === void 0 ? void 0 : cfg.previewUuid); mathjax_1.MathJax.Reset(); try { for (mathNodes_1 = tslib_1.__values(mathNodes), mathNodes_1_1 = mathNodes_1.next(); !mathNodes_1_1.done; mathNodes_1_1 = mathNodes_1.next()) { mathEl = mathNodes_1_1.value; target = shouldTypesetNode(mathEl); if (!target) continue; try { metric = { cwidth: cwidth }; result = target.kind === 'mathml' ? mathjax_1.MathJax.TypesetMathML(target.mathml, { display: target.display, metric: metric, outMath: outMath, accessibility: accessibility, }) : mathjax_1.MathJax.Typeset(target.tex, { display: target.display, metric: metric, outMath: outMath, accessibility: accessibility, }); // Replace content with MathJax output. setInnerHTML(mathEl, result.html); // Apply width-related attributes (matching server-side rendering behavior). if (result === null || result === void 0 ? void 0 : result.data) { isInline = mathEl.classList.contains('math-inline'); isBlock = mathEl.classList.contains('math-block'); width = result.data.width; widthEx = result.data.widthEx; // For block math: data-width="full" when the equation is full-width if (isBlock && width === 'full') { mathEl.setAttribute('data-width', 'full'); } // For inline math: data-overflow="visible" when the equation is very narrow if (isInline && typeof widthEx === 'number' && widthEx < 2) { mathEl.setAttribute('data-overflow', 'visible'); } } // Mark as already typeset to avoid re-processing on future passes. mathEl.setAttribute('data-mathpix-typeset', 'true'); } catch (err) { // Do not fail the whole render if one formula is broken. // Consider logging or attaching an error marker if you want debugging visibility. console.error('[renderMathInElement] Failed to typeset node:', err, mathEl); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (mathNodes_1_1 && !mathNodes_1_1.done && (_a = mathNodes_1.return)) _a.call(mathNodes_1); } finally { if (e_1) throw e_1.error; } } return [2 /*return*/]; } }); }); }; exports.renderMathInElement = renderMathInElement; /** * Read config from the global `window.MathpixRenderConfig` if provided, * otherwise fall back to `defaultConfig`. * * Note: we intentionally read config at execution time (not module init time) * so apps can set `window.MathpixRenderConfig` before DOMContentLoaded. */ var getGlobalConfig = function () { return window.MathpixRenderConfig || defaultConfig; }; /** * Auto-render math inside a root element (defaults to document.body). * This function is meant to be called once on page load. */ var autoRender = function () { // Prefer rendering inside a narrower scope if the integrator provides it. var config = getGlobalConfig(); (0, exports.renderMathInElement)(document.body, config).catch(function (err) { console.error('[MathpixRender] autoRender failed:', err); }); }; // Auto-render on DOMContentLoaded (browser only). if ((0, utils_1.isBrowser)()) { // If DOM isn't ready, wait once; otherwise run immediately. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', autoRender, { once: true }); } else { autoRender(); } /** * Global API exposed for integrators (optional usage). * - `renderMathInElement`: render MathML/LaTeX content to SVG */ window.MathpixRender = { renderMathInElement: exports.renderMathInElement }; } //# sourceMappingURL=auto-render.js.map