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:
Stegmujo 2022-08-17 15:26:25 +02:00 committed by Physikerwelt
parent 580571e067
commit 6be5724740
11 changed files with 5713 additions and 7 deletions

70
src/TexVC/Nodes/DQ.php Normal file
View 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
View 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';
}
}

View 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';
}
}

View file

@ -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
View 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

File diff suppressed because it is too large Load diff

View 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' );
}
}

View 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' );
}
}

View 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' );
}
}

View 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 );
}
}

File diff suppressed because it is too large Load diff