mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Math
synced 2024-11-28 01:10:09 +00:00
Add some basic nodes and texUtil
Texutil related functionalities and tests will come in other changeset.
Related code:
fb56991251/lib/nodes/
Bug: T312528
Change-Id: Iead338a31403348603442b43ae802270f6b1d675
This commit is contained in:
parent
580571e067
commit
6be5724740
70
src/TexVC/Nodes/DQ.php
Normal file
70
src/TexVC/Nodes/DQ.php
Normal file
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace MediaWiki\Extension\Math\TexVC\Nodes;
|
||||
|
||||
class DQ extends TexNode {
|
||||
|
||||
/** @var TexNode */
|
||||
private $base;
|
||||
/** @var TexNode */
|
||||
private $down;
|
||||
|
||||
public function __construct( TexNode $base, TexNode $down ) {
|
||||
parent::__construct( $base, $down );
|
||||
$this->base = $base;
|
||||
$this->down = $down;
|
||||
}
|
||||
|
||||
public function render() {
|
||||
return $this->base->render() . '_' . $this->down->inCurlies();
|
||||
}
|
||||
|
||||
public function extractIdentifiers( $args = null ) {
|
||||
$d = $this->down->extractSubscripts();
|
||||
$b = $this->base->extractIdentifiers();
|
||||
if ( is_array( $b ) && count( $b ) > 1 ) {
|
||||
return parent::extractIdentifiers();
|
||||
}
|
||||
|
||||
if ( isset( $b[0] ) && $b[0] === '\'' ) {
|
||||
return array_merge( $b, $d );
|
||||
}
|
||||
|
||||
if ( isset( $d[0] ) && isset( $b[0] ) ) {
|
||||
if ( $b[0] === '\\int' ) {
|
||||
return array_merge( $b, $d );
|
||||
}
|
||||
return [ $b[0] . '_{' . $d[0] . '}' ];
|
||||
}
|
||||
|
||||
return parent::extractIdentifiers();
|
||||
}
|
||||
|
||||
public function extractSubscripts() {
|
||||
$d = array_merge( [], $this->down->extractSubscripts() );
|
||||
$b = $this->base->extractSubscripts();
|
||||
if ( isset( $b[0] ) && isset( $d[0] ) ) {
|
||||
return [ $b[0] . '_{' . implode( '', $d ) . '}' ];
|
||||
}
|
||||
return parent::extractSubscripts();
|
||||
}
|
||||
|
||||
public function getModIdent() {
|
||||
$d = $this->down->extractSubscripts();
|
||||
$b = $this->base->getModIdent();
|
||||
if ( isset( $b[0] ) && $b[0] === '\'' ) {
|
||||
return [];
|
||||
}
|
||||
if ( isset( $d[0] ) && isset( $b[0] ) ) {
|
||||
return [ $b[0] . '_{' . $d[0] . '}' ];
|
||||
}
|
||||
|
||||
return parent::getModIdent();
|
||||
}
|
||||
|
||||
public function name() {
|
||||
return 'DQ';
|
||||
}
|
||||
}
|
30
src/TexVC/Nodes/FQ.php
Normal file
30
src/TexVC/Nodes/FQ.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace MediaWiki\Extension\Math\TexVC\Nodes;
|
||||
|
||||
class FQ extends TexNode {
|
||||
|
||||
/** @var TexNode */
|
||||
private $base;
|
||||
/** @var TexNode */
|
||||
private $up;
|
||||
/** @var TexNode */
|
||||
private $down;
|
||||
|
||||
public function __construct( TexNode $base, TexNode $down, TexNode $up ) {
|
||||
parent::__construct( $base, $down, $up );
|
||||
$this->base = $base;
|
||||
$this->up = $up;
|
||||
$this->down = $down;
|
||||
}
|
||||
|
||||
public function render() {
|
||||
return $this->base->render() . '_' . $this->down->inCurlies() . '^' . $this->up->inCurlies();
|
||||
}
|
||||
|
||||
public function name() {
|
||||
return 'FQ';
|
||||
}
|
||||
}
|
55
src/TexVC/Nodes/Literal.php
Normal file
55
src/TexVC/Nodes/Literal.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace MediaWiki\Extension\Math\TexVC\Nodes;
|
||||
|
||||
use MediaWiki\Extension\Math\TexVC\TexUtil;
|
||||
|
||||
class Literal extends TexNode {
|
||||
|
||||
/** @var string */
|
||||
private $arg;
|
||||
private $literals;
|
||||
private $extendedLiterals;
|
||||
|
||||
public function __construct( string $arg ) {
|
||||
parent::__construct( $arg );
|
||||
$this->arg = $arg;
|
||||
$tu = new TexUtil();
|
||||
$this->literals = array_keys( $tu->getBaseElements()['is_literal'] );
|
||||
$this->extendedLiterals = $this->literals;
|
||||
array_push( $this->extendedLiterals, [ '\\infty', '\\emptyset' ] );
|
||||
}
|
||||
|
||||
public function extractIdentifiers( $args = null ) {
|
||||
return $this->getLiteral( $this->literals, '/^([a-zA-Z\']|\\\int)$/' );
|
||||
}
|
||||
|
||||
public function extractSubscripts() {
|
||||
return $this->getLiteral( $this->extendedLiterals, '/^([0-9a-zA-Z+\',-])$/' );
|
||||
}
|
||||
|
||||
public function getModIdent() {
|
||||
if ( $this->arg === '\\ ' ) {
|
||||
return [ '\\ ' ];
|
||||
}
|
||||
return $this->getLiteral( $this->literals, '/^([0-9a-zA-Z\'])$/' );
|
||||
}
|
||||
|
||||
private function getLiteral( $lit, $regexp ) {
|
||||
$s = trim( $this->arg );
|
||||
|
||||
if ( preg_match( $regexp, $s ) == 1 ) {
|
||||
return [ $s ];
|
||||
} elseif ( in_array( $s, $lit ) ) {
|
||||
return [ $s ];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function name() {
|
||||
return 'LITERAL';
|
||||
}
|
||||
}
|
|
@ -8,14 +8,8 @@ use InvalidArgumentException;
|
|||
|
||||
class TexNode {
|
||||
|
||||
/** @var list<TexNode, string> */
|
||||
private $args;
|
||||
|
||||
/**
|
||||
* Creates a TexNode
|
||||
* @param list<TexNode, string> ...$args arguments for this node
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct( ...$args ) {
|
||||
foreach ( $args as &$arg ) {
|
||||
if ( !( $arg instanceof TexNode || is_string( $arg ) ) ) {
|
||||
|
@ -41,7 +35,6 @@ class TexNode {
|
|||
|
||||
/**
|
||||
* Wraps the rendered result in curly brackets.
|
||||
* @throws InvalidArgumentException
|
||||
* @return string rendered result in curlies.
|
||||
*/
|
||||
public function inCurlies() {
|
||||
|
|
95
src/TexVC/TexUtil.php
Normal file
95
src/TexVC/TexUtil.php
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace MediaWiki\Extension\Math\TexVC;
|
||||
|
||||
use MWException;
|
||||
|
||||
class TexUtil {
|
||||
private $allFunctions;
|
||||
private $baseElements;
|
||||
|
||||
/**
|
||||
* Loads the file texutil.json
|
||||
* allFunctions holds the root-level function keys
|
||||
* other objects are second level elements and hold all functions which are assigned to this second level elements
|
||||
*/
|
||||
public function __construct() {
|
||||
$jsonContent = $this->getJSON();
|
||||
// dynamically create functions from the content
|
||||
$this->allFunctions = [];
|
||||
$this->baseElements = [];
|
||||
|
||||
foreach ( $jsonContent as $key => $value ) {
|
||||
// Adding all basic elements as functions
|
||||
foreach ( $value as $elementKey => $element ) {
|
||||
if ( !array_key_exists( $elementKey, $this->baseElements ) ) {
|
||||
$this->baseElements[$elementKey] = [];
|
||||
$this->baseElements[$elementKey][$key] = $element;
|
||||
|
||||
} else {
|
||||
if ( !array_key_exists( $key, $this->baseElements[$elementKey] ) ) {
|
||||
$this->baseElements[$elementKey][$key] = $element;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Adding function to all functions
|
||||
$this->allFunctions[$key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returning the base elements array.
|
||||
* This is only used for testing in TexUtilTest.php.
|
||||
* @return array
|
||||
*/
|
||||
public function getBaseElements() {
|
||||
return $this->baseElements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getting an element by key in allFunctions.
|
||||
* If the key is defined, return true if not false.
|
||||
* @param string $key string to check in allFunctions
|
||||
* @return bool
|
||||
*/
|
||||
public function getAllFunctionsAt( string $key ) {
|
||||
if ( array_key_exists( $key, $this->allFunctions ) ) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to directly call functions defined in from the json-file.
|
||||
* @param mixed $func
|
||||
* @param mixed $params
|
||||
* @return false|mixed
|
||||
* @throws MWException
|
||||
*/
|
||||
public function __call( $func, $params ) {
|
||||
if ( array_key_exists( $func, $this->baseElements ) ) {
|
||||
$currentFunction = $this->baseElements[$func];
|
||||
if ( array_key_exists( $params[0], $currentFunction ) ) {
|
||||
return $currentFunction[$params[0]];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
throw new MWException( "Function not defined in json " . $func );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the json file to an object
|
||||
* @return array
|
||||
*/
|
||||
private function getJSON() {
|
||||
$file = file_get_contents( __DIR__ . '/texutil.json' );
|
||||
$json = json_decode( $file, true );
|
||||
return $json;
|
||||
}
|
||||
}
|
2593
src/TexVC/texutil.json
Normal file
2593
src/TexVC/texutil.json
Normal file
File diff suppressed because it is too large
Load diff
45
tests/phpunit/unit/TexVC/Nodes/DQTest.php
Normal file
45
tests/phpunit/unit/TexVC/Nodes/DQTest.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\Math\Tests\TexVC\Nodes;
|
||||
|
||||
use ArgumentCountError;
|
||||
use MediaWiki\Extension\Math\TexVC\Nodes\DQ;
|
||||
use MediaWiki\Extension\Math\TexVC\Nodes\Literal;
|
||||
use MediaWiki\Extension\Math\TexVC\Nodes\TexNode;
|
||||
use MediaWikiUnitTestCase;
|
||||
use RuntimeException;
|
||||
use TypeError;
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Extension\Math\TexVC\Nodes\DQ
|
||||
*/
|
||||
class DQTest extends MediaWikiUnitTestCase {
|
||||
|
||||
public function testEmptyDQ() {
|
||||
$this->expectException( ArgumentCountError::class );
|
||||
new DQ();
|
||||
throw new ArgumentCountError( 'Should not create an empty dq' );
|
||||
}
|
||||
|
||||
public function testOneArgumentDQ() {
|
||||
$this->expectException( ArgumentCountError::class );
|
||||
new DQ( new Literal( 'a' ) );
|
||||
throw new ArgumentCountError( 'Should not create a dq with one argument' );
|
||||
}
|
||||
|
||||
public function testIncorrectTypeDQ() {
|
||||
$this->expectException( TypeError::class );
|
||||
new DQ( 'a', 'b' );
|
||||
throw new RuntimeException( 'Should not create a dq with incorrect type' );
|
||||
}
|
||||
|
||||
public function testBasicDQ() {
|
||||
$dq = new DQ( new Literal( 'a' ), new Literal( 'b' ) );
|
||||
$this->assertEquals( 'a_{b}', $dq->render(), 'Should create a basic dq' );
|
||||
}
|
||||
|
||||
public function testEmptyBaseDQ() {
|
||||
$dq = new DQ( new TexNode(), new Literal( 'b' ) );
|
||||
$this->assertEquals( '_{b}', $dq->render(), 'Should create an empty base dq' );
|
||||
}
|
||||
}
|
39
tests/phpunit/unit/TexVC/Nodes/FQTest.php
Normal file
39
tests/phpunit/unit/TexVC/Nodes/FQTest.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\Math\Tests\TexVC\Nodes;
|
||||
|
||||
use ArgumentCountError;
|
||||
use MediaWiki\Extension\Math\TexVC\Nodes\FQ;
|
||||
use MediaWiki\Extension\Math\TexVC\Nodes\Literal;
|
||||
use MediaWikiUnitTestCase;
|
||||
use TypeError;
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Extension\Math\TexVC\Nodes\FQ
|
||||
*/
|
||||
class FQTest extends MediaWikiUnitTestCase {
|
||||
|
||||
public function testEmptyFQ() {
|
||||
$this->expectException( ArgumentCountError::class );
|
||||
new FQ();
|
||||
throw new ArgumentCountError( 'Should not create an empty fq' );
|
||||
}
|
||||
|
||||
public function testOneArgumentFQ() {
|
||||
$this->expectException( ArgumentCountError::class );
|
||||
new FQ( new Literal( 'a' ) );
|
||||
throw new ArgumentCountError( 'Should not create a fq with one argument' );
|
||||
}
|
||||
|
||||
public function testIncorrectTypeFQ() {
|
||||
$this->expectException( TypeError::class );
|
||||
new FQ( 'a', 'b', 'c' );
|
||||
throw new TypeError( 'Should not create a fq with incorrect type' );
|
||||
}
|
||||
|
||||
public function testBasicFQ() {
|
||||
$fq = new FQ( new Literal( 'a' ), new Literal( 'b' ), new Literal( 'c' ) );
|
||||
$this->assertEquals( 'a_{b}^{c}', $fq->render(), 'Should create a basic fq' );
|
||||
}
|
||||
|
||||
}
|
72
tests/phpunit/unit/TexVC/Nodes/LiteralTest.php
Normal file
72
tests/phpunit/unit/TexVC/Nodes/LiteralTest.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\Math\Tests\TexVC\Nodes;
|
||||
|
||||
use ArgumentCountError;
|
||||
use MediaWiki\Extension\Math\TexVC\Nodes\Literal;
|
||||
use MediaWiki\Extension\Math\TexVC\Nodes\TexNode;
|
||||
use MediaWikiUnitTestCase;
|
||||
use TypeError;
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Extension\Math\TexVC\Nodes\Literal
|
||||
*/
|
||||
class LiteralTest extends MediaWikiUnitTestCase {
|
||||
|
||||
public function testBaseclass() {
|
||||
$this->expectException( ArgumentCountError::class );
|
||||
new Literal();
|
||||
throw new ArgumentCountError( 'Should not create an empty literal' );
|
||||
}
|
||||
|
||||
public function testOneArgument() {
|
||||
$this->expectException( ArgumentCountError::class );
|
||||
new Literal( 'a', 'b' );
|
||||
throw new ArgumentCountError( 'Should not create a literal with more than one argument' );
|
||||
}
|
||||
|
||||
public function testArgumentType() {
|
||||
$this->expectException( TypeError::class );
|
||||
new Literal( new Texnode() );
|
||||
throw new TypeError( 'Should not create a literal with incorrect type' );
|
||||
}
|
||||
|
||||
public function testOnlyOneArgument() {
|
||||
$lit = new Literal( 'hello world' );
|
||||
$this->assertEquals( 'hello world', $lit->render(),
|
||||
'Should create an literal with only one argument' );
|
||||
}
|
||||
|
||||
public function testRenderNodeBase() {
|
||||
$lit = new Literal( 'hello world' );
|
||||
$node = new TexNode( $lit );
|
||||
$this->assertEquals( 'hello world', $node->render(),
|
||||
'Should render within node base class' );
|
||||
}
|
||||
|
||||
public function testNode() {
|
||||
$lit = new Literal( 'hello world' );
|
||||
$node = new TexNode( $lit );
|
||||
$this->assertEquals( 'hello world', $node->render(),
|
||||
'Should render within node base class' );
|
||||
}
|
||||
|
||||
public function testExtractIdentifierModifications() {
|
||||
$n = new Literal( 'a' );
|
||||
$this->assertEquals( [ 'a' ], $n->getModIdent(),
|
||||
'Should extract identifier modifications' );
|
||||
}
|
||||
|
||||
public function testExtraSpace() {
|
||||
$n = new Literal( '\\ ' );
|
||||
$this->assertEquals( [ '\\ ' ], $n->getModIdent(),
|
||||
'Identifier modifications should report extra space' );
|
||||
}
|
||||
|
||||
public function testExtractSubscripts() {
|
||||
$n = new Literal( '\\beta' );
|
||||
$this->assertEquals( [ '\\beta' ], $n->extractSubscripts(),
|
||||
'Should extract subscripts' );
|
||||
}
|
||||
|
||||
}
|
121
tests/phpunit/unit/TexVC/TexUtilTest.php
Normal file
121
tests/phpunit/unit/TexVC/TexUtilTest.php
Normal file
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\Math\Tests\TexVC;
|
||||
|
||||
use MediaWiki\Extension\Math\TexVC\TexUtil;
|
||||
use MediaWikiUnitTestCase;
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Extension\Math\TexVC\TexUtil
|
||||
*/
|
||||
class TexUtilTest extends MediaWikiUnitTestCase {
|
||||
|
||||
/**
|
||||
* Basic test for tex util.
|
||||
*/
|
||||
public function testTexUtil() {
|
||||
$tu = new TexUtil();
|
||||
// Testing all functions
|
||||
$this->assertTrue( $tu->getAllFunctionsAt( "\\AA" ) );
|
||||
$this->assertFalse( $tu->getAllFunctionsAt( "\\notlisted" ) );
|
||||
// Testing other functions
|
||||
$this->assertTrue( $tu->mhchem_macro_2pc( "\\color" ) );
|
||||
$this->assertFalse( $tu->mhchem_macro_2pc( "not listed" ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing a checksum for the parsed object against a checksum of the json file contents.
|
||||
* @return void
|
||||
*/
|
||||
public function testChecksum() {
|
||||
$tu = new TexUtil();
|
||||
|
||||
$out = []; // { }
|
||||
$sets = [
|
||||
'ams_required',
|
||||
'big_literals',
|
||||
'box_functions',
|
||||
'cancel_required',
|
||||
'color_function',
|
||||
'color_required',
|
||||
'declh_function',
|
||||
'definecolor_function',
|
||||
'euro_required',
|
||||
'fun_ar1',
|
||||
'fun_ar1nb',
|
||||
'fun_ar1opt',
|
||||
'fun_ar2',
|
||||
'fun_ar2nb',
|
||||
'fun_infix',
|
||||
'fun_mhchem',
|
||||
'hline_function',
|
||||
'ignore_identifier',
|
||||
'latex_function_names',
|
||||
'left_function',
|
||||
'mathoid_required',
|
||||
'mediawiki_function_names',
|
||||
'mhchem_bond',
|
||||
'mhchem_macro_1p',
|
||||
'mhchem_macro_2p',
|
||||
'mhchem_macro_2pc',
|
||||
'mhchem_macro_2pu',
|
||||
'mhchem_required',
|
||||
'mhchem_single_macro',
|
||||
'nullary_macro',
|
||||
'nullary_macro_in_mbox',
|
||||
'other_delimiters1',
|
||||
'other_delimiters2',
|
||||
'right_function',
|
||||
'teubner_required',
|
||||
];
|
||||
|
||||
// Reading data from TexUtil.
|
||||
foreach ( $sets as $set ) {
|
||||
$baseElements = $tu->getBaseElements();
|
||||
foreach ( $baseElements[$set] as $key => $value ) {
|
||||
if ( !array_key_exists( $key, $out ) ) {
|
||||
$out[$key] = [];
|
||||
}
|
||||
$out[$key][$set] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$maps = [
|
||||
'deprecated_nullary_macro_aliase',
|
||||
'nullary_macro_aliase',
|
||||
'other_delimiters2',
|
||||
'other_fun_ar1',
|
||||
'is_literal',
|
||||
'is_letter_mod'
|
||||
];
|
||||
|
||||
foreach ( $maps as $map ) {
|
||||
$baseElements = $tu->getBaseElements();
|
||||
foreach ( $baseElements[$map] as $key => $value ) {
|
||||
if ( !array_key_exists( $key, $out ) ) {
|
||||
$out[$key] = [];
|
||||
}
|
||||
$out[$key][$map] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting output alphabetically encP - out not sorted correctly
|
||||
ksort( $out );
|
||||
foreach ( $out as &$op ) {
|
||||
ksort( $op );
|
||||
}
|
||||
|
||||
// Loading local json file
|
||||
$file = file_get_contents( __DIR__ . '/texutil.json' );
|
||||
$fileP = str_replace( [ "\n", "\t", " " ], "", $file );
|
||||
|
||||
$encP = json_encode( $out );
|
||||
$hashOutput = $this->getHash( $encP );
|
||||
$hashFile = $this->getHash( $fileP );
|
||||
$this->assertEquals( $hashFile, $hashOutput );
|
||||
}
|
||||
|
||||
private function getHash( $input ) {
|
||||
return hash( 'sha256', $input );
|
||||
}
|
||||
}
|
2593
tests/phpunit/unit/TexVC/texutil.json
Normal file
2593
tests/phpunit/unit/TexVC/texutil.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue