Math.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. <?php
  2. /**
  3. * Contain everything related to <math> </math> parsing
  4. * @file
  5. * @ingroup Parser
  6. */
  7. /**
  8. * Takes LaTeX fragments, sends them to a helper program (texvc) for rendering
  9. * to rasterized PNG and HTML and MathML approximations. An appropriate
  10. * rendering form is picked and returned.
  11. *
  12. * @author Tomasz Wegrzanowski, with additions by Brion Vibber (2003, 2004)
  13. * @ingroup Parser
  14. */
  15. class MathRenderer {
  16. var $mode = MW_MATH_MODERN;
  17. var $tex = '';
  18. var $inputhash = '';
  19. var $hash = '';
  20. var $html = '';
  21. var $mathml = '';
  22. var $conservativeness = 0;
  23. function __construct( $tex, $params=array() ) {
  24. $this->tex = $tex;
  25. $this->params = $params;
  26. }
  27. function setOutputMode( $mode ) {
  28. $this->mode = $mode;
  29. }
  30. function render() {
  31. global $wgTmpDirectory, $wgInputEncoding;
  32. global $wgTexvc;
  33. $fname = 'MathRenderer::render';
  34. if( $this->mode == MW_MATH_SOURCE ) {
  35. # No need to render or parse anything more!
  36. return ('$ '.htmlspecialchars( $this->tex ).' $');
  37. }
  38. if( $this->tex == '' ) {
  39. return; # bug 8372
  40. }
  41. if( !$this->_recall() ) {
  42. # Ensure that the temp and output directories are available before continuing...
  43. if( !file_exists( $wgTmpDirectory ) ) {
  44. if( !wfMkdirParents( $wgTmpDirectory ) ) {
  45. return $this->_error( 'math_bad_tmpdir' );
  46. }
  47. } elseif( !is_dir( $wgTmpDirectory ) || !is_writable( $wgTmpDirectory ) ) {
  48. return $this->_error( 'math_bad_tmpdir' );
  49. }
  50. if( function_exists( 'is_executable' ) && !is_executable( $wgTexvc ) ) {
  51. return $this->_error( 'math_notexvc' );
  52. }
  53. $cmd = $wgTexvc . ' ' .
  54. escapeshellarg( $wgTmpDirectory ).' '.
  55. escapeshellarg( $wgTmpDirectory ).' '.
  56. escapeshellarg( $this->tex ).' '.
  57. escapeshellarg( $wgInputEncoding );
  58. if ( wfIsWindows() ) {
  59. # Invoke it within cygwin sh, because texvc expects sh features in its default shell
  60. $cmd = 'sh -c ' . wfEscapeShellArg( $cmd );
  61. }
  62. wfDebug( "TeX: $cmd\n" );
  63. $contents = `$cmd 2>&1`;
  64. wfDebug( "TeX output:\n $contents\n---\n" );
  65. if (strlen($contents) == 0) {
  66. return $this->_error( 'math_unknown_error' );
  67. }
  68. $retval = substr ($contents, 0, 1);
  69. $errmsg = '';
  70. if (($retval == 'C') || ($retval == 'M') || ($retval == 'L')) {
  71. if ($retval == 'C') {
  72. $this->conservativeness = 2;
  73. } else if ($retval == 'M') {
  74. $this->conservativeness = 1;
  75. } else {
  76. $this->conservativeness = 0;
  77. }
  78. $outdata = substr ($contents, 33);
  79. $i = strpos($outdata, "\000");
  80. $this->html = substr($outdata, 0, $i);
  81. $this->mathml = substr($outdata, $i+1);
  82. } else if (($retval == 'c') || ($retval == 'm') || ($retval == 'l')) {
  83. $this->html = substr ($contents, 33);
  84. if ($retval == 'c') {
  85. $this->conservativeness = 2;
  86. } else if ($retval == 'm') {
  87. $this->conservativeness = 1;
  88. } else {
  89. $this->conservativeness = 0;
  90. }
  91. $this->mathml = NULL;
  92. } else if ($retval == 'X') {
  93. $this->html = NULL;
  94. $this->mathml = substr ($contents, 33);
  95. $this->conservativeness = 0;
  96. } else if ($retval == '+') {
  97. $this->html = NULL;
  98. $this->mathml = NULL;
  99. $this->conservativeness = 0;
  100. } else {
  101. $errbit = htmlspecialchars( substr($contents, 1) );
  102. switch( $retval ) {
  103. case 'E':
  104. $errmsg = $this->_error( 'math_lexing_error', $errbit );
  105. break;
  106. case 'S':
  107. $errmsg = $this->_error( 'math_syntax_error', $errbit );
  108. break;
  109. case 'F':
  110. $errmsg = $this->_error( 'math_unknown_function', $errbit );
  111. break;
  112. default:
  113. $errmsg = $this->_error( 'math_unknown_error', $errbit );
  114. }
  115. }
  116. if ( !$errmsg ) {
  117. $this->hash = substr ($contents, 1, 32);
  118. }
  119. wfRunHooks( 'MathAfterTexvc', array( &$this, &$errmsg ) );
  120. if ( $errmsg ) {
  121. return $errmsg;
  122. }
  123. if (!preg_match("/^[a-f0-9]{32}$/", $this->hash)) {
  124. return $this->_error( 'math_unknown_error' );
  125. }
  126. if( !file_exists( "$wgTmpDirectory/{$this->hash}.png" ) ) {
  127. return $this->_error( 'math_image_error' );
  128. }
  129. if( filesize( "$wgTmpDirectory/{$this->hash}.png" ) == 0 ) {
  130. return $this->_error( 'math_image_error' );
  131. }
  132. $hashpath = $this->_getHashPath();
  133. if( !file_exists( $hashpath ) ) {
  134. if( !@wfMkdirParents( $hashpath, 0755 ) ) {
  135. return $this->_error( 'math_bad_output' );
  136. }
  137. } elseif( !is_dir( $hashpath ) || !is_writable( $hashpath ) ) {
  138. return $this->_error( 'math_bad_output' );
  139. }
  140. if( !rename( "$wgTmpDirectory/{$this->hash}.png", "$hashpath/{$this->hash}.png" ) ) {
  141. return $this->_error( 'math_output_error' );
  142. }
  143. # Now save it back to the DB:
  144. if ( !wfReadOnly() ) {
  145. $outmd5_sql = pack('H32', $this->hash);
  146. $md5_sql = pack('H32', $this->md5); # Binary packed, not hex
  147. $dbw = wfGetDB( DB_MASTER );
  148. $dbw->replace( 'math', array( 'math_inputhash' ),
  149. array(
  150. 'math_inputhash' => $dbw->encodeBlob($md5_sql),
  151. 'math_outputhash' => $dbw->encodeBlob($outmd5_sql),
  152. 'math_html_conservativeness' => $this->conservativeness,
  153. 'math_html' => $this->html,
  154. 'math_mathml' => $this->mathml,
  155. ), $fname
  156. );
  157. }
  158. // If we're replacing an older version of the image, make sure it's current.
  159. global $wgUseSquid;
  160. if ( $wgUseSquid ) {
  161. $urls = array( $this->_mathImageUrl() );
  162. $u = new SquidUpdate( $urls );
  163. $u->doUpdate();
  164. }
  165. }
  166. return $this->_doRender();
  167. }
  168. function _error( $msg, $append = '' ) {
  169. $mf = htmlspecialchars( wfMsg( 'math_failure' ) );
  170. $errmsg = htmlspecialchars( wfMsg( $msg ) );
  171. $source = htmlspecialchars( str_replace( "\n", ' ', $this->tex ) );
  172. return "<strong class='error'>$mf ($errmsg$append): $source</strong>\n";
  173. }
  174. function _recall() {
  175. global $wgMathDirectory;
  176. $fname = 'MathRenderer::_recall';
  177. $this->md5 = md5( $this->tex );
  178. $dbr = wfGetDB( DB_SLAVE );
  179. $rpage = $dbr->selectRow( 'math',
  180. array( 'math_outputhash','math_html_conservativeness','math_html','math_mathml' ),
  181. array( 'math_inputhash' => $dbr->encodeBlob(pack("H32", $this->md5))), # Binary packed, not hex
  182. $fname
  183. );
  184. if( $rpage !== false && is_object( $rpage ) ) {
  185. # Tailing 0x20s can get dropped by the database, add it back on if necessary:
  186. $xhash = unpack( 'H32md5', $dbr->decodeBlob($rpage->math_outputhash) . " " );
  187. $this->hash = $xhash ['md5'];
  188. $this->conservativeness = $rpage->math_html_conservativeness;
  189. $this->html = $rpage->math_html;
  190. $this->mathml = $rpage->math_mathml;
  191. $filename = $this->_getHashPath() . "/{$this->hash}.png";
  192. if( file_exists( $filename ) ) {
  193. if( filesize( $filename ) == 0 ) {
  194. // Some horrible error corrupted stuff :(
  195. @unlink( $filename );
  196. } else {
  197. return true;
  198. }
  199. }
  200. if( file_exists( $wgMathDirectory . "/{$this->hash}.png" ) ) {
  201. $hashpath = $this->_getHashPath();
  202. if( !file_exists( $hashpath ) ) {
  203. if( !@wfMkdirParents( $hashpath, 0755 ) ) {
  204. return false;
  205. }
  206. } elseif( !is_dir( $hashpath ) || !is_writable( $hashpath ) ) {
  207. return false;
  208. }
  209. if ( function_exists( "link" ) ) {
  210. return link ( $wgMathDirectory . "/{$this->hash}.png",
  211. $hashpath . "/{$this->hash}.png" );
  212. } else {
  213. return rename ( $wgMathDirectory . "/{$this->hash}.png",
  214. $hashpath . "/{$this->hash}.png" );
  215. }
  216. }
  217. }
  218. # Missing from the database and/or the render cache
  219. return false;
  220. }
  221. /**
  222. * Select among PNG, HTML, or MathML output depending on
  223. */
  224. function _doRender() {
  225. if( $this->mode == MW_MATH_MATHML && $this->mathml != '' ) {
  226. return Xml::tags( 'math',
  227. $this->_attribs( 'math',
  228. array( 'xmlns' => 'http://www.w3.org/1998/Math/MathML' ) ),
  229. $this->mathml );
  230. }
  231. if (($this->mode == MW_MATH_PNG) || ($this->html == '') ||
  232. (($this->mode == MW_MATH_SIMPLE) && ($this->conservativeness != 2)) ||
  233. (($this->mode == MW_MATH_MODERN || $this->mode == MW_MATH_MATHML) && ($this->conservativeness == 0))) {
  234. return $this->_linkToMathImage();
  235. } else {
  236. return Xml::tags( 'span',
  237. $this->_attribs( 'span',
  238. array( 'class' => 'texhtml' ) ),
  239. $this->html );
  240. }
  241. }
  242. function _attribs( $tag, $defaults=array(), $overrides=array() ) {
  243. $attribs = Sanitizer::validateTagAttributes( $this->params, $tag );
  244. $attribs = Sanitizer::mergeAttributes( $defaults, $attribs );
  245. $attribs = Sanitizer::mergeAttributes( $attribs, $overrides );
  246. return $attribs;
  247. }
  248. function _linkToMathImage() {
  249. $url = $this->_mathImageUrl();
  250. return Xml::element( 'img',
  251. $this->_attribs(
  252. 'img',
  253. array(
  254. 'class' => 'tex',
  255. 'alt' => $this->tex ),
  256. array(
  257. 'src' => $url ) ) );
  258. }
  259. function _mathImageUrl() {
  260. global $wgMathPath;
  261. $dir = $this->_getHashSubPath();
  262. return "$wgMathPath/$dir/{$this->hash}.png";
  263. }
  264. function _getHashPath() {
  265. global $wgMathDirectory;
  266. $path = $wgMathDirectory .'/' . $this->_getHashSubPath();
  267. wfDebug( "TeX: getHashPath, hash is: $this->hash, path is: $path\n" );
  268. return $path;
  269. }
  270. function _getHashSubPath() {
  271. return substr($this->hash, 0, 1)
  272. .'/'. substr($this->hash, 1, 1)
  273. .'/'. substr($this->hash, 2, 1);
  274. }
  275. public static function renderMath( $tex, $params=array() ) {
  276. global $wgUser;
  277. # SEAN: We need this since XML code like &amp; &lt; cannot
  278. # be processed as is.
  279. $tex = html_entity_decode($tex);
  280. $math = new MathRenderer( $tex, $params );
  281. $math->setOutputMode( $wgUser->getOption('math'));
  282. return $math->render();
  283. }
  284. }