MDL-65394 filter: only do mathjax parsing on strings that need it
[moodle.git] / filter / mathjaxloader / filter.php
CommitLineData
7f675315
DW
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * This filter provides automatic support for MathJax
19 *
20 * @package filter_mathjaxloader
21 * @copyright 2013 Damyon Wiese (damyon@moodle.com)
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25defined('MOODLE_INTERNAL') || die();
26
27/**
28 * Mathjax filtering
29 */
30class filter_mathjaxloader extends moodle_text_filter {
31
32 /*
33 * Perform a mapping of the moodle language code to the equivalent for MathJax.
34 *
35 * @param string $moodlelangcode - The moodle language code - e.g. en_pirate
36 * @return string The MathJax language code.
37 */
38 public function map_language_code($moodlelangcode) {
47cf9455
DM
39
40 // List of language codes found in the MathJax/localization/ directory.
41 $mathjaxlangcodes = [
42 'ar', 'ast', 'bcc', 'bg', 'br', 'ca', 'cdo', 'ce', 'cs', 'cy', 'da', 'de', 'diq', 'en', 'eo', 'es', 'fa',
43 'fi', 'fr', 'gl', 'he', 'ia', 'it', 'ja', 'kn', 'ko', 'lb', 'lki', 'lt', 'mk', 'nl', 'oc', 'pl', 'pt',
44 'pt-br', 'qqq', 'ru', 'scn', 'sco', 'sk', 'sl', 'sv', 'th', 'tr', 'uk', 'vi', 'zh-hans', 'zh-hant'
45 ];
46
47 // List of explicit mappings and known exceptions (moodle => mathjax).
48 $explicit = [
49 'cz' => 'cs',
50 'pt_br' => 'pt-br',
51 'zh_tw' => 'zh-hant',
52 'zh_cn' => 'zh-hans',
53 ];
54
55 // If defined, explicit mapping takes the highest precedence.
56 if (isset($explicit[$moodlelangcode])) {
57 return $explicit[$moodlelangcode];
7f675315
DW
58 }
59
47cf9455 60 // If there is exact match, it will be probably right.
7f675315
DW
61 if (in_array($moodlelangcode, $mathjaxlangcodes)) {
62 return $moodlelangcode;
63 }
64
47cf9455
DM
65 // Finally try to find the best matching mathjax pack.
66 $parts = explode('_', $moodlelangcode, 2);
67 if (in_array($parts[0], $mathjaxlangcodes)) {
68 return $parts[0];
7f675315 69 }
47cf9455
DM
70
71 // No more guessing, use English.
7f675315
DW
72 return 'en';
73 }
74
75 /*
76 * Add the javascript to enable mathjax processing on this page.
77 *
78 * @param moodle_page $page The current page.
79 * @param context $context The current context.
80 */
aaf6f1e8 81 public function setup($page, $context) {
7f675315 82
20482f9d 83 if ($page->requires->should_create_one_time_item_now('filter_mathjaxloader-scripts')) {
8ce58c99 84 $url = get_config('filter_mathjaxloader', 'httpsurl');
7f675315
DW
85 $lang = $this->map_language_code(current_language());
86 $url = new moodle_url($url, array('delayStartupUntil' => 'configured'));
87
88 $moduleconfig = array(
89 'name' => 'mathjax',
90 'fullpath' => $url
91 );
92
aaf6f1e8 93 $page->requires->js_module($moduleconfig);
7f675315
DW
94
95 $config = get_config('filter_mathjaxloader', 'mathjaxconfig');
3db9004f
DW
96 $wwwroot = new moodle_url('/');
97
98 $config = str_replace('{wwwroot}', $wwwroot->out(true), $config);
7f675315
DW
99
100 $params = array('mathjaxconfig' => $config, 'lang' => $lang);
101
aaf6f1e8 102 $page->requires->yui_module('moodle-filter_mathjaxloader-loader', 'M.filter_mathjaxloader.configure', array($params));
7f675315
DW
103 }
104 }
105
106 /*
107 * This function wraps the filtered text in a span, that mathjaxloader is configured to process.
108 *
109 * @param string $text The text to filter.
110 * @param array $options The filter options.
111 */
112 public function filter($text, array $options = array()) {
aaf6f1e8
DW
113 global $PAGE;
114
cc90cedc 115 $legacy = get_config('filter_mathjaxloader', 'texfiltercompatibility');
894e2039 116 $extradelimiters = explode(',', get_config('filter_mathjaxloader', 'additionaldelimiters'));
cc90cedc
DW
117 if ($legacy) {
118 // This replaces any of the tex filter maths delimiters with the default for inline maths in MathJAX "\( blah \)".
441f94b2 119 // E.g. "<tex.*> blah </tex>".
cc90cedc 120 $text = preg_replace('|<(/?) *tex( [^>]*)?>|u', '[\1tex]', $text);
441f94b2 121 // E.g. "[tex.*] blah [/tex]".
cc90cedc
DW
122 $text = str_replace('[tex]', '\\(', $text);
123 $text = str_replace('[/tex]', '\\)', $text);
441f94b2 124 // E.g. "$$ blah $$".
6aec17bd 125 $text = preg_replace('|\$\$([\S\s]*?)\$\$|u', '\\(\1\\)', $text);
441f94b2 126 // E.g. "\[ blah \]".
cc90cedc
DW
127 $text = str_replace('\\[', '\\(', $text);
128 $text = str_replace('\\]', '\\)', $text);
129 }
130
894e2039 131 $hasextra = false;
894e2039
DW
132 foreach ($extradelimiters as $extra) {
133 if ($extra && strpos($text, $extra) !== false) {
134 $hasextra = true;
135 break;
136 }
137 }
194ffa23
MR
138
139 $hasdisplayorinline = false;
140 if ($hasextra) {
141 // If custom dilimeters are used, wrap whole text to prevent autolinking.
142 $text = '<span class="nolink">' . $text . '</span>';
ebe3110d
RW
143 } else if (preg_match('/\\\\[[(]/', $text) || preg_match('/\$\$/', $text)) {
144 // Only parse the text if there are mathjax symbols in it. The recognized
145 // math environments are \[ \] and $$ $$ for display mathematics and \( \)
146 // for inline mathematics.
147 // Note: 2 separate regexes seems to perform better here than using a single
148 // regex with groupings.
149
194ffa23
MR
150 // Wrap display and inline math environments in nolink spans.
151 // Do not wrap nested environments, i.e., if inline math is nested
152 // inside display math, only the outer display math is wrapped in
153 // a span. The span HTML inside a LaTex math environment would break
154 // MathJax. See MDL-61981.
155 list($text, $hasdisplayorinline) = $this->wrap_math_in_nolink($text);
156 }
157
158 if ($hasdisplayorinline || $hasextra) {
aaf6f1e8 159 $PAGE->requires->yui_module('moodle-filter_mathjaxloader-loader', 'M.filter_mathjaxloader.typeset');
194ffa23
MR
160 return '<span class="filter_mathjaxloader_equation">' . $text . '</span>';
161 }
162 return $text;
163 }
164
165 /**
166 * Find math environments in the $text and wrap them in no link spans
167 * (<span class="nolink"></span>). If math environments are nested, only
168 * the outer environment is wrapped in the span.
169 *
170 * The recognized math environments are \[ \] and $$ $$ for display
171 * mathematics and \( \) for inline mathematics.
172 *
173 * @param string $text The text to filter.
174 * @return array An array containing the potentially modified text and
175 * a boolean that is true if any changes were made to the text.
176 */
177 protected function wrap_math_in_nolink($text) {
178 $i = 1;
179 $len = strlen($text);
180 $displaystart = -1;
181 $displaybracket = false;
182 $displaydollar = false;
183 $inlinestart = -1;
184 $changesdone = false;
185 // Loop over the $text once.
186 while ($i < $len) {
187 if ($displaystart === -1) {
188 // No display math has started yet.
189 if ($text[$i - 1] === '\\' && $text[$i] === '[') {
190 // Display mode \[ begins.
191 $displaystart = $i - 1;
192 $displaybracket = true;
193 } else if ($text[$i - 1] === '$' && $text[$i] === '$') {
194 // Display mode $$ begins.
195 $displaystart = $i - 1;
196 $displaydollar = true;
197 } else if ($text[$i - 1] === '\\' && $text[$i] === '(') {
198 // Inline math \( begins, not nested inside display math.
199 $inlinestart = $i - 1;
200 } else if ($text[$i - 1] === '\\' && $text[$i] === ')' && $inlinestart > -1) {
201 // Inline math ends, not nested inside display math.
202 // Wrap the span around it.
203 $text = $this->insert_span($text, $inlinestart, $i);
204
205 $inlinestart = -1; // Reset.
206 $i += 28; // The $text length changed due to the <span>.
207 $len += 28;
208 $changesdone = true;
638b4738 209 }
194ffa23
MR
210 } else {
211 // Display math open.
212 if (($text[$i - 1] === '\\' && $text[$i] === ']' && $displaybracket) ||
213 ($text[$i - 1] === '$' && $text[$i] === '$' && $displaydollar)) {
214 // Display math ends, wrap the span around it.
215 $text = $this->insert_span($text, $displaystart, $i);
216
217 $displaystart = -1; // Reset.
218 $displaybracket = false;
219 $displaydollar = false;
220 $i += 28; // The $text length changed due to the <span>.
221 $len += 28;
222 $changesdone = true;
638b4738
DT
223 }
224 }
194ffa23
MR
225
226 ++$i;
7f675315 227 }
194ffa23
MR
228 return array($text, $changesdone);
229 }
230
231 /**
232 * Wrap a portion of the $text inside a no link span
233 * (<span class="nolink"></span>). The whole text is then returned.
234 *
235 * @param string $text The text to modify.
236 * @param int $start The start index of the substring in $text that should
237 * be wrapped in the span.
238 * @param int $end The end index of the substring in $text that should be
239 * wrapped in the span.
240 * @return string The whole $text with the span inserted around
241 * the defined substring.
242 */
243 protected function insert_span($text, $start, $end) {
244 return substr_replace($text,
245 '<span class="nolink">'. substr($text, $start, $end - $start + 1) .'</span>',
246 $start,
247 $end - $start + 1);
7f675315
DW
248 }
249}