MDL-61981 filter_mathjaxloader: nested math environments

The MathJax filter used to break mathematics if inline math was
used inside display math. Nolink spans were added around
the inline math even though it was nested inside another math
environment. This fix is aware of the nesting so that the spans
may be inserted only at the outer math environment. No regular
expressions are used because they do not support detecting
arbitrary, unlimited nesting of parentheses in the text, which
is now needed.
This commit is contained in:
Markku Riekkinen
2018-09-21 09:58:27 +03:00
parent 6902f39141
commit 194ffa2344
+100 -21
View File
@@ -128,37 +128,116 @@ class filter_mathjaxloader extends moodle_text_filter {
$text = str_replace('\\]', '\\)', $text);
}
$hasinline = strpos($text, '\\(') !== false && strpos($text, '\\)') !== false;
$hasdisplay = (strpos($text, '$$') !== false) ||
(strpos($text, '\\[') !== false && strpos($text, '\\]') !== false);
$hasextra = false;
foreach ($extradelimiters as $extra) {
if ($extra && strpos($text, $extra) !== false) {
$hasextra = true;
break;
}
}
if ($hasinline || $hasdisplay || $hasextra) {
$hasdisplayorinline = false;
if ($hasextra) {
// If custom dilimeters are used, wrap whole text to prevent autolinking.
$text = '<span class="nolink">' . $text . '</span>';
} else {
// Wrap display and inline math environments in nolink spans.
// Do not wrap nested environments, i.e., if inline math is nested
// inside display math, only the outer display math is wrapped in
// a span. The span HTML inside a LaTex math environment would break
// MathJax. See MDL-61981.
list($text, $hasdisplayorinline) = $this->wrap_math_in_nolink($text);
}
if ($hasdisplayorinline || $hasextra) {
$PAGE->requires->yui_module('moodle-filter_mathjaxloader-loader', 'M.filter_mathjaxloader.typeset');
if ($hasextra) {
// If custom dilimeters are used, wrap whole text to prevent autolinking.
$text = '<span class="nolink">' . $text . '</span>';
} else {
if ($hasinline) {
// If the default inline TeX delimiters \( \) are present, wrap each pair in nolink.
$text = preg_replace('/\\\\\\([\S\s]*?\\\\\\)/u',
'<span class="nolink">\0</span>', $text);
}
if ($hasdisplay) {
// If default display TeX is used, wrap $$ $$ or \[ \] individually.
$text = preg_replace('/\$\$[\S\s]*?\$\$|\\\\\\[[\S\s]*?\\\\\\]/u',
'<span class="nolink">\0</span>', $text);
}
}
return '<span class="filter_mathjaxloader_equation">' . $text . '</span>';
}
return $text;
}
/**
* Find math environments in the $text and wrap them in no link spans
* (<span class="nolink"></span>). If math environments are nested, only
* the outer environment is wrapped in the span.
*
* The recognized math environments are \[ \] and $$ $$ for display
* mathematics and \( \) for inline mathematics.
*
* @param string $text The text to filter.
* @return array An array containing the potentially modified text and
* a boolean that is true if any changes were made to the text.
*/
protected function wrap_math_in_nolink($text) {
$i = 1;
$len = strlen($text);
$displaystart = -1;
$displaybracket = false;
$displaydollar = false;
$inlinestart = -1;
$changesdone = false;
// Loop over the $text once.
while ($i < $len) {
if ($displaystart === -1) {
// No display math has started yet.
if ($text[$i - 1] === '\\' && $text[$i] === '[') {
// Display mode \[ begins.
$displaystart = $i - 1;
$displaybracket = true;
} else if ($text[$i - 1] === '$' && $text[$i] === '$') {
// Display mode $$ begins.
$displaystart = $i - 1;
$displaydollar = true;
} else if ($text[$i - 1] === '\\' && $text[$i] === '(') {
// Inline math \( begins, not nested inside display math.
$inlinestart = $i - 1;
} else if ($text[$i - 1] === '\\' && $text[$i] === ')' && $inlinestart > -1) {
// Inline math ends, not nested inside display math.
// Wrap the span around it.
$text = $this->insert_span($text, $inlinestart, $i);
$inlinestart = -1; // Reset.
$i += 28; // The $text length changed due to the <span>.
$len += 28;
$changesdone = true;
}
} else {
// Display math open.
if (($text[$i - 1] === '\\' && $text[$i] === ']' && $displaybracket) ||
($text[$i - 1] === '$' && $text[$i] === '$' && $displaydollar)) {
// Display math ends, wrap the span around it.
$text = $this->insert_span($text, $displaystart, $i);
$displaystart = -1; // Reset.
$displaybracket = false;
$displaydollar = false;
$i += 28; // The $text length changed due to the <span>.
$len += 28;
$changesdone = true;
}
}
++$i;
}
return array($text, $changesdone);
}
/**
* Wrap a portion of the $text inside a no link span
* (<span class="nolink"></span>). The whole text is then returned.
*
* @param string $text The text to modify.
* @param int $start The start index of the substring in $text that should
* be wrapped in the span.
* @param int $end The end index of the substring in $text that should be
* wrapped in the span.
* @return string The whole $text with the span inserted around
* the defined substring.
*/
protected function insert_span($text, $start, $end) {
return substr_replace($text,
'<span class="nolink">'. substr($text, $start, $end - $start + 1) .'</span>',
$start,
$end - $start + 1);
}
}