mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Math
synced 2024-11-23 23:25:02 +00:00
Add WAN Cache for native MathML rendering
* Cache results for checked tex and MathML string in one cache. * Remove access to parsetree * Introduce run method to speed up service wiring Note that the indirection table used in previous versions was abandoned here. texvc does only little unification's of the input string so that it is not expected that the overall savings in space and compute time warrant the additional table. Change-Id: Ib9ce3d2ab02bd9a2a0f9926db6b937435b7e5458
This commit is contained in:
parent
ceea8068d0
commit
16d1fdacf4
|
@ -88,6 +88,7 @@ class InputCheckFactory {
|
|||
*/
|
||||
public function newLocalChecker( string $input, string $type ): LocalChecker {
|
||||
return new LocalChecker(
|
||||
$this->cache,
|
||||
$input,
|
||||
$type
|
||||
);
|
||||
|
|
|
@ -3,32 +3,54 @@
|
|||
namespace MediaWiki\Extension\Math\InputCheck;
|
||||
|
||||
use Exception;
|
||||
use MediaWiki\Extension\Math\TexVC\Nodes\TexArray;
|
||||
use MediaWiki\Extension\Math\TexVC\TexVC;
|
||||
use Message;
|
||||
use WANObjectCache;
|
||||
|
||||
class LocalChecker extends BaseChecker {
|
||||
|
||||
public const VERSION = 1;
|
||||
private const VALID_TYPES = [ 'tex', 'inline-tex', 'chem' ];
|
||||
private ?Message $error = null;
|
||||
private ?TexArray $parseTree = null;
|
||||
|
||||
private ?string $mathMl = null;
|
||||
|
||||
/**
|
||||
* @param string $tex the TeX input string to be checked
|
||||
* @param string $type the input type
|
||||
*/
|
||||
public function __construct( $tex = '', string $type = 'tex' ) {
|
||||
if ( !in_array( $type, self::VALID_TYPES, true ) ) {
|
||||
$this->error = $this->errorObjectToMessage(
|
||||
(object)[ "error" => "Unsupported type passed to LocalChecker: " . $type ], "LocalCheck" );
|
||||
private string $type;
|
||||
private WANObjectCache $cache;
|
||||
|
||||
private bool $isChecked = false;
|
||||
|
||||
public function __construct( WANObjectCache $cache, $tex = '', string $type = 'tex' ) {
|
||||
$this->cache = $cache;
|
||||
parent::__construct( $tex );
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function isValid(): bool {
|
||||
$this->run();
|
||||
return parent::isValid();
|
||||
}
|
||||
|
||||
public function getValidTex(): ?string {
|
||||
$this->run();
|
||||
return parent::getValidTex();
|
||||
}
|
||||
|
||||
public function run() {
|
||||
if ( $this->isChecked ) {
|
||||
return;
|
||||
}
|
||||
if ( !in_array( $this->type, self::VALID_TYPES, true ) ) {
|
||||
$this->error = $this->errorObjectToMessage(
|
||||
(object)[ "error" => "Unsupported type passed to LocalChecker: " . $this->type ], "LocalCheck" );
|
||||
return;
|
||||
}
|
||||
parent::__construct( $tex );
|
||||
$options = $type === 'chem' ? [ "usemhchem" => true ] : null;
|
||||
try {
|
||||
$result = ( new TexVC() )->check( $tex, $options );
|
||||
$result = $this->cache->getWithSetCallback(
|
||||
$this->getInputCacheKey(),
|
||||
WANObjectCache::TTL_INDEFINITE,
|
||||
[ $this, 'runCheck' ],
|
||||
[ 'version' => self::VERSION ],
|
||||
);
|
||||
} catch ( Exception $e ) { // @codeCoverageIgnoreStart
|
||||
// This is impossible since errors are thrown only if the option debug would be set.
|
||||
$this->error = Message::newFromKey( 'math_failure' );
|
||||
|
@ -38,12 +60,13 @@ class LocalChecker extends BaseChecker {
|
|||
if ( $result['status'] === '+' ) {
|
||||
$this->isValid = true;
|
||||
$this->validTeX = $result['output'];
|
||||
$this->parseTree = $result['input'];
|
||||
$this->mathMl = $result['mathml'];
|
||||
} else {
|
||||
$this->error = $this->errorObjectToMessage(
|
||||
(object)[ "error" => (object)$result["error"] ],
|
||||
"LocalCheck" );
|
||||
}
|
||||
$this->isChecked = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,15 +74,44 @@ class LocalChecker extends BaseChecker {
|
|||
* @return ?Message
|
||||
*/
|
||||
public function getError(): ?Message {
|
||||
$this->run();
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function getParseTree(): ?TexArray {
|
||||
return $this->parseTree;
|
||||
}
|
||||
|
||||
public function getPresentationMathMLFragment(): string {
|
||||
$this->mathMl ??= $this->parseTree->renderMML();
|
||||
public function getPresentationMathMLFragment(): ?string {
|
||||
$this->run();
|
||||
return $this->mathMl;
|
||||
}
|
||||
|
||||
public function getInputCacheKey(): string {
|
||||
return $this->cache->makeGlobalKey(
|
||||
self::class,
|
||||
md5( $this->type . '-' . $this->inputTeX )
|
||||
);
|
||||
}
|
||||
|
||||
public function runCheck(): array {
|
||||
$options = $this->type === 'chem' ? [ "usemhchem" => true ] : null;
|
||||
try {
|
||||
$result = ( new TexVC() )->check( $this->inputTeX, $options );
|
||||
} catch ( Exception $e ) { // @codeCoverageIgnoreStart
|
||||
// This is impossible since errors are thrown only if the option debug would be set.
|
||||
$this->error = Message::newFromKey( 'math_failure' );
|
||||
|
||||
return [];
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
if ( $result['status'] === '+' ) {
|
||||
return [
|
||||
'status' => '+',
|
||||
'output' => $result['output'],
|
||||
'mathml' => $result['input']->renderMML()
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'status' => $result['status'],
|
||||
'error' => $result['error'],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ class MathNativeMML extends MathMathML {
|
|||
}
|
||||
$root = new MMLmath( "", $attributes );
|
||||
|
||||
$this->setMathml( $root->encapsulateRaw( $presentation ) );
|
||||
$this->setMathml( $root->encapsulateRaw( $presentation ?? '' ) );
|
||||
return StatusValue::newGood();
|
||||
}
|
||||
|
||||
|
|
|
@ -50,8 +50,17 @@ class InputCheckFactoryTest extends MediaWikiIntegrationTestCase {
|
|||
}
|
||||
|
||||
public function testInvalidLocalChecker() {
|
||||
$checker = $this->newServiceInstance( InputCheckFactory::class, [] )
|
||||
->newLocalChecker( 'FORMULA', 'INVALIDTYPE' );
|
||||
$myFactory = new InputCheckFactory(
|
||||
new ServiceOptions( InputCheckFactory::CONSTRUCTOR_OPTIONS, [
|
||||
'MathMathMLUrl' => 'something',
|
||||
'MathTexVCService' => 'local',
|
||||
'MathLaTeXMLTimeout' => 240
|
||||
] ),
|
||||
$this->fakeWAN,
|
||||
$this->fakeHTTP,
|
||||
LoggerFactory::getInstance( 'Math' )
|
||||
);
|
||||
$checker = $myFactory->newLocalChecker( 'FORMULA', 'INVALIDTYPE' );
|
||||
$this->assertInstanceOf( LocalChecker::class, $checker );
|
||||
$this->assertInstanceOf( Message::class, $checker->getError() );
|
||||
$this->assertFalse( $checker->isValid() );
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
namespace MediaWiki\Extension\Math\InputCheck;
|
||||
|
||||
use MediaWiki\Extension\Math\TexVC\Nodes\TexArray;
|
||||
use HashBagOStuff;
|
||||
use MediaWikiIntegrationTestCase;
|
||||
use Message;
|
||||
use WANObjectCache;
|
||||
|
||||
/**
|
||||
* @group Math
|
||||
|
@ -13,8 +14,12 @@ use Message;
|
|||
* @covers \MediaWiki\Extension\Math\InputCheck\LocalChecker
|
||||
*/
|
||||
class LocalCheckerTest extends MediaWikiIntegrationTestCase {
|
||||
|
||||
private const SAMPLE_KEY =
|
||||
'global:MediaWiki\Extension\Math\InputCheck\LocalChecker:d5f40adbd26ff8b19b2c33289d7334b6';
|
||||
|
||||
public function testValid() {
|
||||
$checker = new LocalChecker( '\sin x^2' );
|
||||
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\sin x^2' );
|
||||
$this->assertNull( $checker->getError() );
|
||||
$this->assertTrue( $checker->isValid() );
|
||||
$this->assertNull( $checker->getError() );
|
||||
|
@ -22,30 +27,30 @@ class LocalCheckerTest extends MediaWikiIntegrationTestCase {
|
|||
}
|
||||
|
||||
public function testValidTypeTex() {
|
||||
$checker = new LocalChecker( '\sin x^2', 'tex' );
|
||||
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\sin x^2', 'tex' );
|
||||
$this->assertTrue( $checker->isValid() );
|
||||
}
|
||||
|
||||
public function testValidTypeChem() {
|
||||
$checker = new LocalChecker( '{\\displaystyle {\\ce {\\cdot OHNO_{2}}}}', 'chem' );
|
||||
$checker = new LocalChecker( WANObjectCache::newEmpty(), '{\\displaystyle {\\ce {\\cdot OHNO_{2}}}}', 'chem' );
|
||||
$this->assertTrue( $checker->isValid() );
|
||||
}
|
||||
|
||||
public function testValidTypeInline() {
|
||||
$checker = new LocalChecker( '{\\textstyle \\log2 }', 'inline-tex' );
|
||||
$checker = new LocalChecker( WANObjectCache::newEmpty(), '{\\textstyle \\log2 }', 'inline-tex' );
|
||||
$this->assertTrue( $checker->isValid() );
|
||||
}
|
||||
|
||||
public function testInvalidType() {
|
||||
$checker = new LocalChecker( '\sin x^2', 'INVALIDTYPE' );
|
||||
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\sin x^2', 'INVALIDTYPE' );
|
||||
$this->assertInstanceOf( LocalChecker::class, $checker );
|
||||
$this->assertInstanceOf( Message::class, $checker->getError() );
|
||||
$this->assertFalse( $checker->isValid() );
|
||||
$this->assertNull( $checker->getParseTree() );
|
||||
$this->assertNull( $checker->getPresentationMathMLFragment() );
|
||||
}
|
||||
|
||||
public function testInvalid() {
|
||||
$checker = new LocalChecker( '\sin\newcommand' );
|
||||
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\sin\newcommand' );
|
||||
$this->assertFalse( $checker->isValid() );
|
||||
|
||||
$this->assertStringContainsString(
|
||||
|
@ -61,7 +66,7 @@ class LocalCheckerTest extends MediaWikiIntegrationTestCase {
|
|||
}
|
||||
|
||||
public function testErrorSyntax() {
|
||||
$checker = new LocalChecker( '\left(' );
|
||||
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\left(' );
|
||||
$this->assertFalse( $checker->isValid() );
|
||||
$this->assertStringContainsString(
|
||||
Message::newFromKey( 'math_syntax_error' )
|
||||
|
@ -73,37 +78,50 @@ class LocalCheckerTest extends MediaWikiIntegrationTestCase {
|
|||
);
|
||||
}
|
||||
|
||||
public function testGetParseTree() {
|
||||
$checker = new LocalChecker( 'e^{i \pi} + 1 = 0' );
|
||||
$this->assertTrue( $checker->isValid() );
|
||||
$parseTree = $checker->getParseTree();
|
||||
$this->assertInstanceOf( TexArray::class, $parseTree );
|
||||
$this->assertEquals( 5, $parseTree->getLength() );
|
||||
}
|
||||
|
||||
public function testGetParseTreeNull() {
|
||||
$checker = new LocalChecker( '\invalid' );
|
||||
$this->assertFalse( $checker->isValid() );
|
||||
$this->assertNull( $checker->getParseTree() );
|
||||
}
|
||||
|
||||
public function testGetParseTreeEmpty() {
|
||||
$checker = new LocalChecker( '' );
|
||||
$this->assertTrue( $checker->isValid() );
|
||||
$parseTree = $checker->getParseTree();
|
||||
$this->assertInstanceOf( TexArray::class, $parseTree );
|
||||
$this->assertSame( 0, $parseTree->getLength() );
|
||||
}
|
||||
|
||||
public function testGetMML() {
|
||||
$checker = new LocalChecker( 'e^{i \pi} + 1 = 0' );
|
||||
$checker = new LocalChecker( WANObjectCache::newEmpty(), 'e^{i \pi} + 1 = 0' );
|
||||
$mml = $checker->getPresentationMathMLFragment();
|
||||
$this->assertStringContainsString( '<mn>0</mn>', $mml );
|
||||
}
|
||||
|
||||
public function testGetMMLEmpty() {
|
||||
$checker = new LocalChecker( '' );
|
||||
$checker = new LocalChecker( WANObjectCache::newEmpty(), '' );
|
||||
$mml = $checker->getPresentationMathMLFragment();
|
||||
$this->assertSame( '', $mml );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Extension\Math\InputCheck\LocalChecker::getInputCacheKey
|
||||
*/
|
||||
public function testGetKey() {
|
||||
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\sin x^2', 'tex' );
|
||||
$this->assertSame( self::SAMPLE_KEY, $checker->getInputCacheKey() );
|
||||
}
|
||||
|
||||
public function testCache() {
|
||||
$fakeWAN = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
|
||||
$fakeContent = [ 'status' => '+', 'output' => 'out', 'mathml' => 'mml' ];
|
||||
$fakeWAN->set( self::SAMPLE_KEY,
|
||||
$fakeContent,
|
||||
WANObjectCache::TTL_INDEFINITE,
|
||||
[ 'version' => LocalChecker::VERSION ] );
|
||||
$checker = new LocalChecker( $fakeWAN, '\sin x^2', 'tex' );
|
||||
$this->assertSame( $fakeContent['output'], $checker->getValidTex() );
|
||||
$this->assertSame( $fakeContent['mathml'], $checker->getPresentationMathMLFragment() );
|
||||
$this->assertSame( true, $checker->isValid() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Extension\Math\InputCheck\LocalChecker::runCheck
|
||||
*/
|
||||
public function testRunChecks() {
|
||||
$fakeContent = [
|
||||
'status' => '+',
|
||||
'mathml' => '<mi>sin</mi><mo></mo><msup><mi>x</mi><mrow data-mjx-texclass="ORD"><mn>2</mn></mrow></msup>',
|
||||
'output' => '\\sin x^{2}'
|
||||
];
|
||||
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\sin x^2', 'tex' );
|
||||
$actual = $checker->runCheck();
|
||||
$this->assertArrayEquals( $fakeContent, $actual );
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue