Enable MathML rendering mode

This change allows to registered users
to test the new MathML rendering mode.
Invalid settings for math rendering mode
will default to MathMathML.

Change-Id: I75f24cb762609d6728247e3758fcc18f2ebfc6e6
This commit is contained in:
physikerwelt (Moritz Schubotz) 2014-06-10 18:49:20 +02:00 committed by Frédéric Wang
parent 04ce4a02c7
commit cbbf5453d1
5 changed files with 283 additions and 32 deletions

View file

@ -36,7 +36,7 @@ define( 'MW_MATH_SIMPLE', 1 ); /// @deprecated
define( 'MW_MATH_HTML', 2 ); /// @deprecated
define( 'MW_MATH_SOURCE', 3 );
define( 'MW_MATH_MODERN', 4 ); /// @deprecated
define( 'MW_MATH_MATHML', 5 ); /// @deprecated
define( 'MW_MATH_MATHML', 5 );
define( 'MW_MATH_MATHJAX', 6 ); /// @deprecated
define( 'MW_MATH_LATEXML', 7 ); /// new in 1.22
/**@}*/
@ -52,7 +52,7 @@ define( 'MW_MATHSTYLE_INLINE', 2 ); // small operators inline
/**@}*/
/**@var array defines the mode allowed on the server */
$wgMathValidModes = array( MW_MATH_PNG, MW_MATH_SOURCE );
$wgMathValidModes = array( MW_MATH_PNG, MW_MATH_SOURCE, MW_MATH_MATHML );
/*
* The default rendering mode for anonymous users.
@ -126,7 +126,7 @@ $wgUseMathJax = false;
* see http://www.formulasearchengine.com/mathoid
* TODO: Move documentation to WMF
*/
$wgMathMathMLUrl = 'http://gw124.iu.xsede.org:10042'; // Sponsored by https://www.xsede.org/
$wgMathMathMLUrl = 'http://mathoid.testme.wmflabs.org';
/**
* The timeout for the HTTP-Request sent to the MathML to render an equation,

View file

@ -15,6 +15,38 @@ class MathMathML extends MathRenderer {
protected $hosts;
/** @var boolean if false MathML output is not validated */
private $XMLValidation = true;
protected $inputType = 'tex';
/**
* @param string $inputType
*/
public function setInputType($inputType)
{
$this->inputType = $inputType;
}
/**
* @return string
*/
public function getInputType()
{
return $this->inputType;
}
public function __construct( $tex = '', $params = array() ) {
global $wgMathMathMLUrl;
parent::__construct( $tex, $params );
$this->setMode( MW_MATH_MATHML );
$this->hosts = $wgMathMathMLUrl;
if ( isset( $params['type'] ) ) {
if ( $params['type'] == 'pmml' ) {
$this->inputType = 'pmml';
$this->setMathml( '<math>' . $tex . '</math>' );
} elseif ( $params['type'] == 'ascii' ) {
$this->inputType = 'ascii';
}
}
}
/**
* Gets the allowed root elements the rendered math tag might have.
@ -75,8 +107,14 @@ class MathMathML extends MathRenderer {
$dbres = $this->readFromDatabase();
if ( $dbres ) {
if ( $this->isValidMathML( $this->getMathml() ) ) {
wfDebugLog( "Math", "Valid entry found in database." );
wfDebugLog( "Math", "Valid MathML entry found in database." );
if ( $this->getSvg() ) {
wfDebugLog( "Math", "SVG-fallback found in database." );
return false;
} else {
wfDebugLog( "Math", "SVG-fallback missing." );
return true;
}
} else {
wfDebugLog( "Math", "Malformatted entry found in database" );
return true;
@ -109,9 +147,16 @@ class MathMathML extends MathRenderer {
wfProfileIn( __METHOD__ );
$error = '';
$res = null;
if ( !$host ) {
$host = self::pickHost();
}
if ( !$post ) {
$this->getPostData();
}
$options = array( 'method' => 'POST', 'postData' => $post, 'timeout' => $wgMathLaTeXMLTimeout );
/** @var $req (CurlHttpRequest|PhpHttpRequest) the request object */
$req = $httpRequestClass::factory( $host, $options );
/** @var Status the request status */
$status = $req->execute();
if ( $status->isGood() ) {
$res = $req->getContent();
@ -137,6 +182,107 @@ class MathMathML extends MathRenderer {
}
}
/**
* Picks a MathML daemon.
* If more than one demon are available one is chosen from the
* $wgMathMathMLUrl array.
* @return string
*/
protected function pickHost() {
if ( is_array( $this->hosts ) ) {
$host = array_rand( $this->hosts );
} else {
$host = $this->hosts;
}
wfDebugLog( "Math", "picking host " . $host );
return $host;
}
/**
* Calculates the HTTP POST Data for the request. Depends on the settings
* and the input string only.
* @return string HTTP POST data
*/
public function getPostData() {
$input = $this->getTex();
if ( $this->inputType == 'pmml' || $this->getMode() == MW_MATH_LATEXML && $this->getMathml() ) {
$out = 'type=mml&q=' . rawurlencode( $this->getMathml() );
} elseif ( $this->inputType == 'ascii' ) {
$out = 'type=asciimath&q=' . rawurlencode( $input );
} else {
if ( $this->getMathStyle() == MW_MATHSTYLE_INLINE_DISPLAYSTYLE ) {
// default preserve the (broken) layout as it was
$input = '{\\displaystyle ' . $input . '}';
}
$out = 'type=tex&q=' . rawurlencode( $input );
}
wfDebugLog( "Math", 'Get post data: ' . $out );
return $out;
}
/**
* Does the actual web request to convert TeX to MathML.
* @return boolean
*/
protected function doRender() {
global $wgMathDebug;
if ( $this->getTex() === '' ) {
wfDebugLog( 'Math', 'Rendering was requested, but no TeX string is specified.' );
$this->lastError = $this->getError( 'math_empty_tex' );
return false;
}
$res = '';
$host = self::pickHost();
$post = $this->getPostData();
$this->lastError = '';
$requestResult = $this->makeRequest( $host, $post, $res, $this->lastError );
if ( $requestResult ) {
$jsonResult = json_decode( $res );
if ( $jsonResult && json_last_error() === JSON_ERROR_NONE ) {
if ( $jsonResult->success ) {
if ( $this->getMode() == MW_MATH_LATEXML ||
$this->inputType == 'pmml' ||
$this->isValidMathML( $jsonResult->mml ) ) {
$xmlObject = new XmlTypeCheck( $jsonResult->svg, null, false );
if ( ! $xmlObject->wellFormed ) {
$this->lastError = $this->getError( 'math_invalidxml', $host );
return false;
} else {
$this->setSvg( $jsonResult->svg );
}
if ( $wgMathDebug ) {
$this->setLog( $jsonResult->log );
}
if ( $this->getMode() != MW_MATH_LATEXML && $this->inputType != 'pmml') {
$this->setMathml( $jsonResult->mml );
}
return true;
} else {
$this->lastError = $this->getError( 'math_unknown_error', $host );
return false;
}
} else {
// Do not print bad mathml. It's probably too verbose and might
// mess up the browser output.
$this->lastError = $this->getError( 'math_invalidxml', $host );
wfDebugLog( 'Math', "\nMathML InvalidMathML:"
. var_export( array( 'post' => $post, 'host' => $host
, 'result' => $res ), true ) . "\n\n" );
return false;
}
} else {
$this->lastError = $this->getError( 'math_invalidjson', $host );
wfDebugLog( 'Math', "\nMathML InvalidJSON:"
. var_export( array( 'post' => $post, 'host' => $host
, 'res' => $res ), true ) . "\n\n" );
return false;
}
} else {
// Error message has already been set.
return false;
}
}
/**
* Checks if the input is valid MathML,
* and if the root element has the name math
@ -161,7 +307,7 @@ class MathMathML extends MathRenderer {
} else {
$name = $xmlObject->getRootElement();
$elementSplit = explode( ':', $name );
if ( is_array($elementSplit) ){
if ( is_array($elementSplit) ) {
$localName = end( $elementSplit );
} else {
$localName = $name;
@ -175,29 +321,65 @@ class MathMathML extends MathRenderer {
return $out;
}
/* (non-PHPdoc)
* @see MathRenderer::writeCache()
/**
* @param int $mode
* @param boolean $noRender
* @return type
*/
public function writeCache() {
if ( $this->isChanged() ) {
$this->writeToDatabase();
private function getFallbackImageUrl( $mode = MW_MATH_MATHML, $noRender = false ) {
return SpecialPage::getTitleFor( 'MathShowImage' )->getLocalURL( array(
'hash' => $this->getMd5(),
'mode' => $mode,
'noRender' => $noRender )
);
}
/**
* Helper function to correct the style information for a
* linked SVG image.
* @param string $svg SVG-image data
* @param string $style current style information to be updated
*/
public function correctSvgStyle( $svg, &$style ) {
if ( preg_match( '/style="([^"]*)"/', $svg, $styles ) ) {
$style = $styles[1];
if ( $this->getMathStyle() === MW_MATHSTYLE_DISPLAY ) {
// TODO: Improve style cleaning
$style = preg_replace( '/margin\-(left|right)\:\s*\d+(\%|in|cm|mm|em|ex|pt|pc|px)\;/', '', $style );
$style .= 'display: block; margin-left: auto; margin-right: auto;';
}
}
}
/**
* Picks a daemon.
* If more than one demon are available one is chosen from the
* hosts array.
* @return string
* Gets img tag for math image
* @param int $mode if MW_MATH_MATHML a png is used instead of an svg image
* @param boolean $noRender if true no rendering will be performed if the image is not stored in the database
* @param boolean|string $classOverride if classOverride is false the class name will be calculated by getClassName
* @return string XML the image html tag
*/
protected function pickHost() {
if ( is_array( $this->hosts ) ) {
$host = array_rand( $this->hosts );
public function getFallbackImage( $mode = MW_MATH_MATHML, $noRender = false, $classOverride = false ) {
$url = $this->getFallbackImageUrl( $mode , $noRender );
if ( $mode == MW_MATH_PNG ) {
$png = true;
} else {
$host = $this->hosts;
$png = false;
}
wfDebugLog( "Math", "picking host " . $host );
return $host;
$attribs = array();
if ( $classOverride === false ) { // $class = '' suppresses class attribute
$class = $this->getClassName( true, $png );
$style = $png ? 'display: none;' : '';
} else {
$class = $classOverride;
$style = '';
}
if ( !$png ) {
$this->correctSvgStyle( $this->getSvg(), $style );
}
if ( $class ) { $attribs['class'] = $class; }
if ( $style ) { $attribs['style'] = $style; }
// an alternative for svg might be an object with type="image/svg+xml"
return Xml::element( 'img', $this->getAttributes( 'img', $attribs , array( 'src' => $url ) ) );
}
@ -239,6 +421,9 @@ class MathMathML extends MathRenderer {
$element = 'span';
}
$attribs = array();
if ( $this->getID() !== '' ) {
$attribs['id'] = $this->getID();
}
$output = HTML::openElement( $element, $attribs );
// MathML has to be wrapped into a div or span in order to be able to hide it.
if ( $this->getMathStyle() == MW_MATHSTYLE_DISPLAY ) {
@ -250,7 +435,27 @@ class MathMathML extends MathRenderer {
$mml = $this->getMathml();
}
$output .= Xml::tags( $element, array( 'class' => $this->getClassName(), 'style' => 'display: none;' ), $mml );
$output .= $this->getFallbackImage( $this->getMode() ) . "\n";
$output .= $this->getFallbackImage( MW_MATH_PNG ) . "\n";
$output .= HTML::closeElement( $element );
return $output;
}
protected function dbOutArray() {
$out = parent::dbOutArray();
if ($this->getMathTableName() == 'mathoid' ) {
$out['math_input'] = $out['math_inputtex'];
unset($out['math_inputtex']);
}
return $out;
}
protected function dbInArray() {
$out = parent::dbInArray();
if ($this->getMathTableName() == 'mathoid' ) {
$out = array_diff( $out, array( 'math_inputtex' ) );
$out[] = 'math_input';
}
return $out;
}
}

View file

@ -161,12 +161,15 @@ abstract class MathRenderer {
case MW_MATH_SOURCE:
$renderer = new MathSource( $tex, $params );
break;
case MW_MATH_PNG:
$renderer = new MathTexvc( $tex, $params );
break;
case MW_MATH_LATEXML:
$renderer = new MathLaTeXML( $tex, $params );
break;
case MW_MATH_PNG:
case MW_MATH_MATHML:
default:
$renderer = new MathTexvc( $tex, $params );
$renderer = new MathMathML( $tex, $params );
}
wfDebugLog ( "Math", 'start rendering $' . $renderer->tex . '$ in mode ' . $mode );
$renderer->setMathStyle( $mathStyle );
@ -431,8 +434,8 @@ abstract class MathRenderer {
* @return bool
*/
public function setMode( $newMode ) {
global$wgMathValidModes;
if ( in_array($newMode, $wgMathValidModes ) ) {
global $wgMathValidModes;
if ( in_array( $newMode, $wgMathValidModes ) ) {
$this->mode = $newMode;
return true;
} else {

View file

@ -43,9 +43,9 @@ class MathLaTeXMLTest extends MediaWikiTestCase {
$this->setMwGlobals( 'wgMathValidModes', array( MW_MATH_LATEXML ) );
$renderer = MathRenderer::getRenderer( "a+b", array(), MW_MATH_LATEXML );
$this->assertTrue( $renderer->render( true ) );
$expected = '<span><span class="mwe-math-mathml-inline" style="display: none;"><math xmlns="http://www.w3.org/1998/Math/MathML" id="p1.1.m1" class="ltx_Math" alttext="{\displaystyle a+b}" display="inline" xml:id="p1.1.m1.1" xref="p1.1.m1.1.cmml"><semantics xml:id="p1.1.m1.1a" xref="p1.1.m1.1.cmml"><mrow xml:id="p1.1.m1.1.4" xref="p1.1.m1.1.4.cmml"><mi xml:id="p1.1.m1.1.1" xref="p1.1.m1.1.1.cmml">a</mi><mo xml:id="p1.1.m1.1.2" xref="p1.1.m1.1.2.cmml">+</mo><mi xml:id="p1.1.m1.1.3" xref="p1.1.m1.1.3.cmml">b</mi></mrow><annotation-xml encoding="MathML-Content" xml:id="p1.1.m1.1.cmml" xref="p1.1.m1.1"><apply xml:id="p1.1.m1.1.4.cmml" xref="p1.1.m1.1.4"><plus xml:id="p1.1.m1.1.2.cmml" xref="p1.1.m1.1.2"/><ci xml:id="p1.1.m1.1.1.cmml" xref="p1.1.m1.1.1">a</ci><ci xml:id="p1.1.m1.1.3.cmml" xref="p1.1.m1.1.3">b</ci></apply></annotation-xml><annotation encoding="application/x-tex" xml:id="p1.1.m1.1b" xref="p1.1.m1.1.cmml">{\displaystyle a+b}</annotation></semantics></math></span></span>';
$expected = '<math xmlns="http://www.w3.org/1998/Math/MathML" id="p1.1.m1" class="ltx_Math" alttext="{\displaystyle a+b}" display="inline" xml:id="p1.1.m1.1" xref="p1.1.m1.1.cmml"><semantics xml:id="p1.1.m1.1a" xref="p1.1.m1.1.cmml"><mrow xml:id="p1.1.m1.1.4" xref="p1.1.m1.1.4.cmml"><mi xml:id="p1.1.m1.1.1" xref="p1.1.m1.1.1.cmml">a</mi><mo xml:id="p1.1.m1.1.2" xref="p1.1.m1.1.2.cmml">+</mo><mi xml:id="p1.1.m1.1.3" xref="p1.1.m1.1.3.cmml">b</mi></mrow><annotation-xml encoding="MathML-Content" xml:id="p1.1.m1.1.cmml" xref="p1.1.m1.1"><apply xml:id="p1.1.m1.1.4.cmml" xref="p1.1.m1.1.4"><plus xml:id="p1.1.m1.1.2.cmml" xref="p1.1.m1.1.2"/><ci xml:id="p1.1.m1.1.1.cmml" xref="p1.1.m1.1.1">a</ci><ci xml:id="p1.1.m1.1.3.cmml" xref="p1.1.m1.1.3">b</ci></apply></annotation-xml><annotation encoding="application/x-tex" xml:id="p1.1.m1.1b" xref="p1.1.m1.1.cmml">{\displaystyle a+b}</annotation></semantics></math>';
$real = preg_replace( "/\n\s*/", '', $renderer->getHtmlOutput() );
$this->assertEquals( $expected, $real
$this->assertContains( $expected, $real
, "Rendering of a+b in plain Text mode." .
$renderer->getLastError() );
}

View file

@ -1,4 +1,4 @@
<?php
<?php
/**
* Test the MathML output format.
*
@ -25,6 +25,17 @@ class MathMathMLTest extends MediaWikiTestCase {
self::$timeout = $timeout;
}
/**
* Test rendering the string '0' see
* https://trac.mathweb.org/LaTeXML/ticket/1752
*/
public function testSpecialCaseText() {
$renderer = MathRenderer::getRenderer( 'x^2+\text{a sample Text}', array(), MW_MATH_MATHML );
$expected = 'a sample Text</mtext>';
$this->assertTrue( $renderer->render() );
$this->assertContains( $expected, $renderer->getHtmlOutput(), 'Rendering the String "\text{CR}"' );
}
/**
* Tests behavior of makeRequest() that communicates with the host.
* Testcase: Invalid request.
@ -104,6 +115,38 @@ class MathMathMLTest extends MediaWikiTestCase {
$this->assertFalse( $renderer->isValidMathML( $invalidSample ), 'test if math expression is invalid mathml sample' );
}
/**
* Checks the basic functionality
* i.e. if the span element is generated right.
*/
public function testIntegration() {
global $wgMathMathMLTimeout;
$svgRef = '<svg xmlns:xlink="http://www.w3.org/1999/xlink" style="width: 5.111ex; height: 2ex; vertical-align: -0.333ex; margin-top: 1px; margin-right: 0px; margin-bottom: 1px; margin-left: 0px; position: static; " viewBox="0 -724.9033013280564 2195.4444444444443 837.8066026561128" xmlns="http://www.w3.org/2000/svg"><defs id="MathJax_SVG_glyphs"><path id="MJMATHI-61" stroke-width="10" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"></path><path id="MJMAIN-2B" stroke-width="10" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path><path id="MJMATHI-62" stroke-width="10" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"></path></defs><g stroke="black" fill="black" stroke-width="0" transform="matrix(1 0 0 -1 0 0)"><use href="#MJMATHI-61" xlink:href="#MJMATHI-61"></use><use href="#MJMAIN-2B" x="756" y="0" xlink:href="#MJMAIN-2B"></use><use href="#MJMATHI-62" x="1761" y="0" xlink:href="#MJMATHI-62"></use></g></svg>';
$wgMathMathMLTimeout = 20;
$renderer = MathRenderer::getRenderer( "a+b", array(), MW_MATH_MATHML );
$this->assertTrue( $renderer->render( true ) );
$real = str_replace( "\n", '', $renderer->getHtmlOutput() );
$expected = '<mo>+</mo>';
$this->assertContains( $expected, $real, "Rendering of a+b in plain MathML mode" );
$this->assertEquals( $svgRef, $renderer->getSvg() );
}
/**
* Checks the experimental option to 'render' MathML input
*/
public function testPmmlInput() {
// sample from 'Navajo Coal Combustion and Respiratory Health Near Shiprock, New Mexico' in ''Journal of Environmental and Public Health'' , vol. 2010p.
// authors Joseph E. Bunnell; Linda V. Garcia; Jill M. Furst; Harry Lerch; Ricardo A. Olea; Stephen E. Suitt; Allan Kolker
$inputSample = '<msub> <mrow> <mi> P</mi> </mrow> <mrow> <mi> i</mi> <mi> j</mi> </mrow> </msub> <mo> =</mo> <mfrac> <mrow> <mn> 100</mn> <msub> <mrow> <mi> d</mi> </mrow> <mrow> <mi> i</mi> <mi> j</mi> </mrow> </msub> </mrow> <mrow> <mn> 6.75</mn> <msub> <mrow> <mi> r</mi> </mrow> <mrow> <mi> j</mi> </mrow> </msub> </mrow> </mfrac> <mo> ,</mo> </math>';
$attribs = array( 'type' => 'pmml' );
$renderer = new MathMathML( $inputSample, $attribs );
$this->assertEquals( 'pmml', $renderer->getInputType(), 'Input type was not set correctly' );
$this->assertTrue( $renderer->render(), 'Failed to render with error:' . $renderer->getLastError() );
$real = MathRenderer::renderMath( $inputSample, $attribs, MW_MATH_MATHML );
$expected = 'hash=5628b8248b79267ecac656102334d5e3&amp;mode=5';
$this->assertContains( $expected, $real, 'Link to SVG image missing' );
}
}
/**