Merge branch 'MDL-61981-master_nestedmath' of git://github.com/Mankro/moodle

This commit is contained in:
Eloy Lafuente (stronk7) 2018-09-25 23:36:17 +02:00
commit d813c9e745
2 changed files with 202 additions and 21 deletions

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) {
$PAGE->requires->yui_module('moodle-filter_mathjaxloader-loader', 'M.filter_mathjaxloader.typeset');
$hasdisplayorinline = false;
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);
}
// 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');
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);
}
}

View File

@ -0,0 +1,102 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Provides the {@link filter_mathjaxloader_filtermath_testcase} class.
*
* @package filter_mathjaxloader
* @category test
* @copyright 2018 Markku Riekkinen
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot.'/filter/mathjaxloader/filter.php');
/**
* Unit tests for the MathJax loader filter.
*
* @copyright 2018 Markku Riekkinen
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class filter_mathjaxloader_filtermath_testcase extends advanced_testcase {
/**
* Test the functionality of {@link filter_mathjaxloader::filter()}.
*
* @param string $inputtext The text given by the user.
* @param string $expected The expected output after filtering.
*
* @dataProvider test_math_filtering_inputs
*/
public function test_math_filtering($inputtext, $expected) {
$filter = new filter_mathjaxloader(context_system::instance(), []);
$this->assertEquals($expected, $filter->filter($inputtext));
}
/**
* Data provider for {@link self::test_math_filtering()}.
*
* @return array of [inputtext, expectedoutput] tuples.
*/
public function test_math_filtering_inputs() {
return [
// One inline formula.
['Some inline math \\( y = x^2 \\).',
'<span class="filter_mathjaxloader_equation">Some inline math <span class="nolink">\\( y = x^2 \\)</span>.</span>'],
// One inline and one display.
['Some inline math \\( y = x^2 \\) and display formula \\[ S = \\sum_{n=1}^{\\infty} 2^n \\]',
'<span class="filter_mathjaxloader_equation">Some inline math <span class="nolink">\\( y = x^2 \\)</span> and '
. 'display formula <span class="nolink">\\[ S = \\sum_{n=1}^{\\infty} 2^n \\]</span></span>'],
// One display and one inline.
['Display formula \\[ S = \\sum_{n=1}^{\\infty} 2^n \\] and some inline math \\( y = x^2 \\).',
'<span class="filter_mathjaxloader_equation">Display formula <span class="nolink">\\[ S = \\sum_{n=1}^{\\infty} 2^n \\]</span> and '
. 'some inline math <span class="nolink">\\( y = x^2 \\)</span>.</span>'],
// One inline and one display (with dollars).
['Some inline math \\( y = x^2 \\) and display formula $$ S = \\sum_{n=1}^{\\infty} 2^n $$',
'<span class="filter_mathjaxloader_equation">Some inline math <span class="nolink">\\( y = x^2 \\)</span> and '
. 'display formula <span class="nolink">$$ S = \\sum_{n=1}^{\\infty} 2^n $$</span></span>'],
// One display (with dollars) and one inline.
['Display formula $$ S = \\sum_{n=1}^{\\infty} 2^n $$ and some inline math \\( y = x^2 \\).',
'<span class="filter_mathjaxloader_equation">Display formula <span class="nolink">$$ S = \\sum_{n=1}^{\\infty} 2^n $$</span> and '
. 'some inline math <span class="nolink">\\( y = x^2 \\)</span>.</span>'],
// Inline math environment nested inside display environment (using a custom LaTex macro).
['\\[ \\newcommand{\\False}{\\mathsf{F}} \\newcommand{\\NullF}{\\fbox{\\(\\False\\)}} \\] '
. 'Text with inline formula using the custom LaTex macro \\( a = \\NullF \\).',
'<span class="filter_mathjaxloader_equation"><span class="nolink">'
. '\\[ \\newcommand{\\False}{\\mathsf{F}} \\newcommand{\\NullF}{\\fbox{\\(\\False\\)}} \\]</span> '
. 'Text with inline formula using the custom LaTex macro <span class="nolink">\\( a = \\NullF \\)</span>.</span>'],
// Nested environments and some more content.
['\\[ \\newcommand{\\False}{\\mathsf{F}} \\newcommand{\\NullF}{\\fbox{\\(\\False\\)}} \\] '
. 'Text with inline formula using the custom LaTex macro \\( a = \\NullF \\). Finally, a display formula '
. '$$ b = \\NullF $$',
'<span class="filter_mathjaxloader_equation"><span class="nolink">'
. '\\[ \\newcommand{\\False}{\\mathsf{F}} \\newcommand{\\NullF}{\\fbox{\\(\\False\\)}} \\]</span> '
. 'Text with inline formula using the custom LaTex macro <span class="nolink">\\( a = \\NullF \\)</span>. '
. 'Finally, a display formula <span class="nolink">$$ b = \\NullF $$</span></span>'],
// Broken math: the delimiters ($$) are not closed.
['Writing text and starting display math. $$ k = i^3 \\newcommand{\\False}{\\mathsf{F}} \\newcommand{\\NullF}{\\fbox{\\(\\False\\)}} '
. 'More text and inline math \\( x = \\NullF \\).',
'Writing text and starting display math. $$ k = i^3 \\newcommand{\\False}{\\mathsf{F}} \\newcommand{\\NullF}{\\fbox{\\(\\False\\)}} '
. 'More text and inline math \\( x = \\NullF \\).'],
];
}
}