Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   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   * Moodle - Filter for converting TeX expressions to cached gif images
  19   *
  20   * This Moodle text filter converts TeX expressions delimited
  21   * by either $$...$$ or by <tex...>...</tex> tags to gif images using
  22   * mimetex.cgi obtained from http: *www.forkosh.com/mimetex.html authored by
  23   * John Forkosh john@forkosh.com.  Several binaries of this areincluded with
  24   * this distribution.
  25   * Note that there may be patent restrictions on the production of gif images
  26   * in Canada and some parts of Western Europe and Japan until July 2004.
  27   *
  28   * @package    filter
  29   * @subpackage tex
  30   * @copyright  2004 Zbigniew Fiedorowicz fiedorow@math.ohio-state.edu
  31   *             Originally based on code provided by Bruno Vernier bruno@vsbeducation.ca
  32   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  
  35  defined('MOODLE_INTERNAL') || die;
  36  
  37  require_once($CFG->libdir . '/classes/useragent.php');
  38  
  39  /**
  40   * Create TeX image link.
  41   *
  42   * @param string $imagefile name of file
  43   * @param string $tex TeX notation (html entities already decoded)
  44   * @param int $height O means automatic
  45   * @param int $width O means automatic
  46   * @param string $align
  47   * @param string $alt
  48   * @return string HTML markup
  49   */
  50  function filter_text_image($imagefile, $tex, $height, $width, $align, $alt) {
  51      global $CFG, $OUTPUT;
  52  
  53      if (!$imagefile) {
  54          throw new coding_exception('image file argument empty in filter_text_image()');
  55      }
  56  
  57      // Work out any necessary inline style.
  58      $rules = array();
  59      if ($align !== 'middle') {
  60          $rules[] = 'vertical-align:' . $align . ';';
  61      }
  62      if ($height) {
  63          $rules[] = 'height:' . $height . 'px;';
  64      }
  65      if ($width) {
  66          $rules[] = 'width:' . $width . 'px;';
  67      }
  68      if (!empty($rules)) {
  69          $style = ' style="' . implode('', $rules) . '" ';
  70      } else {
  71          $style = '';
  72      }
  73  
  74      // Prepare the title attribute.
  75      // Note that we retain the title tag as TeX format rather than using
  76      // the alt text, even if supplied. The alt text is intended for blind
  77      // users (to provide a text equivalent to the equation) while the title
  78      // is there as a convenience for sighted users who want to see the TeX
  79      // code.
  80      $title = 'title="'.s($tex).'"';
  81  
  82      if ($alt === '') {
  83          $alt = s($tex);
  84      } else {
  85          $alt = s(html_entity_decode($tex, ENT_QUOTES, 'UTF-8'));
  86      }
  87  
  88      // Build the output.
  89      $anchorcontents = "<img class=\"texrender\" $title alt=\"$alt\" src=\"";
  90      if ($CFG->slasharguments) {        // Use this method if possible for better caching
  91          $anchorcontents .= "$CFG->wwwroot/filter/tex/pix.php/$imagefile";
  92      } else {
  93          $anchorcontents .= "$CFG->wwwroot/filter/tex/pix.php?file=$imagefile";
  94      }
  95      $anchorcontents .= "\" $style/>";
  96  
  97      if (!file_exists("$CFG->dataroot/filter/tex/$imagefile") && has_capability('moodle/site:config', context_system::instance())) {
  98          $link = '/filter/tex/texdebug.php';
  99          $action = null;
 100      } else {
 101          $link = new moodle_url('/filter/tex/displaytex.php', array('texexp'=>$tex));
 102          $action = new popup_action('click', $link, 'popup', array('width'=>320,'height'=>240));
 103      }
 104      $output = $OUTPUT->action_link($link, $anchorcontents, $action, array('title'=>'TeX')); //TODO: the popups do not work when text caching is enabled!!
 105      $output = "<span class=\"MathJax_Preview\">$output</span><script type=\"math/tex\">$tex</script>";
 106  
 107      return $output;
 108  }
 109  
 110  
 111  /**
 112   * TeX filtering class.
 113   */
 114  class filter_tex extends moodle_text_filter {
 115      function filter($text, array $options = array()) {
 116  
 117          global $CFG, $DB;
 118  
 119          /// Do a quick check using stripos to avoid unnecessary work
 120          if ((!preg_match('/<tex/i', $text)) &&
 121                  (strpos($text,'$$') === false) &&
 122                  (strpos($text,'\\[') === false) &&
 123                  (strpos($text, '\\(') === false) &&
 124                  (!preg_match('/\[tex/i',$text))) {
 125              return $text;
 126          }
 127  
 128  #    //restrict filtering to forum 130 (Maths Tools on moodle.org)
 129  #    $scriptname = $_SERVER['SCRIPT_NAME'];
 130  #    if (!strstr($scriptname,'/forum/')) {
 131  #        return $text;
 132  #    }
 133  #    if (strstr($scriptname,'post.php')) {
 134  #        $parent = forum_get_post_full($_GET['reply']);
 135  #        $discussion = $DB->get_record("forum_discussions", array("id"=>$parent->discussion));
 136  #    } else if (strstr($scriptname,'discuss.php')) {
 137  #        $discussion = $DB->get_record("forum_discussions", array("id"=>$_GET['d']));
 138  #    } else {
 139  #        return $text;
 140  #    }
 141  #    if ($discussion->forum != 130) {
 142  #        return $text;
 143  #    }
 144          $text .= ' ';
 145          preg_match_all('/\$(\$\$+?)([^\$])/s',$text,$matches);
 146          for ($i=0; $i<count($matches[0]); $i++) {
 147              $replacement = str_replace('$','&#x00024;', $matches[1][$i]).$matches[2][$i];
 148              $text = str_replace($matches[0][$i], $replacement, $text);
 149          }
 150  
 151          // <tex> TeX expression </tex>
 152          // or <tex alt="My alternative text to be used instead of the TeX form"> TeX expression </tex>
 153          // or $$ TeX expression $$
 154          // or \[ TeX expression \]          // original tag of MathType and TeXaide (dlnsk)
 155          // or [tex] TeX expression [/tex]   // somtime it's more comfortable than <tex> (dlnsk)
 156          $rules = array(
 157              '<tex(?:\s+alt=["\'](.*?)["\'])?>(.+?)<\/tex>',
 158              '\$\$(.+?)\$\$',
 159              '\\\\\[(.+?)\\\\\]',
 160              '\\\\\((.+?)\\\\\)',
 161              '\\[tex\\](.+?)\\[\/tex\\]'
 162          );
 163          $megarule = '/' . implode('|', $rules) . '/is';
 164          preg_match_all($megarule, $text, $matches);
 165          for ($i=0; $i<count($matches[0]); $i++) {
 166              $texexp = '';
 167              for ($j = 0; $j < count($rules); $j++) {
 168                  $texexp .= $matches[$j + 2][$i];
 169              }
 170              $alt = $matches[1][$i];
 171              $texexp = str_replace('<nolink>','',$texexp);
 172              $texexp = str_replace('</nolink>','',$texexp);
 173              $texexp = str_replace('<span class="nolink">','',$texexp);
 174              $texexp = str_replace('</span>','',$texexp);
 175              $texexp = preg_replace("/<br[[:space:]]*\/?>/i", '', $texexp);  //dlnsk
 176              $align = "middle";
 177              if (preg_match('/^align=bottom /',$texexp)) {
 178                $align = "text-bottom";
 179                $texexp = preg_replace('/^align=bottom /','',$texexp);
 180              } else if (preg_match('/^align=top /',$texexp)) {
 181                $align = "text-top";
 182                $texexp = preg_replace('/^align=top /','',$texexp);
 183              }
 184  
 185              // decode entities encoded by editor, luckily there is very little chance of double decoding
 186              $texexp = html_entity_decode($texexp, ENT_QUOTES, 'UTF-8');
 187  
 188              if ($texexp === '') {
 189                  continue;
 190              }
 191  
 192              // Sanitize the decoded string, because filter_text_image() injects the final string between script tags.
 193              $texexp = clean_param($texexp, PARAM_TEXT);
 194  
 195              $md5 = md5($texexp);
 196              if (!$DB->record_exists("cache_filters", array("filter"=>"tex", "md5key"=>$md5))) {
 197                  $texcache = new stdClass();
 198                  $texcache->filter = 'tex';
 199                  $texcache->version = 1;
 200                  $texcache->md5key = $md5;
 201                  $texcache->rawtext = $texexp;
 202                  $texcache->timemodified = time();
 203                  $DB->insert_record("cache_filters", $texcache, false);
 204              }
 205              $convertformat = get_config('filter_tex', 'convertformat');
 206              if ($convertformat == 'svg' && !core_useragent::supports_svg()) {
 207                  $convertformat = 'png';
 208              }
 209              $filename = $md5.".{$convertformat}";
 210              $text = str_replace( $matches[0][$i], filter_text_image($filename, $texexp, 0, 0, $align, $alt), $text);
 211          }
 212          return $text;
 213      }
 214  }
 215  
 216