Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
98.45% |
127 / 129 |
|
90.00% |
9 / 10 |
CRAP | |
0.00% |
0 / 1 |
| TexVC | |
98.45% |
127 / 129 |
|
90.00% |
9 / 10 |
50 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| parse | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| check | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
8 | |||
| checkTreeIntents | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
11 | |||
| checkIntentArg | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| checkIntent | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| handleTexError | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
5 | |||
| getLocationInfo | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
2.09 | |||
| preProcessInput | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
| postProcess | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
13 | |||
| 1 | <?php |
| 2 | |
| 3 | declare( strict_types = 1 ); |
| 4 | |
| 5 | namespace MediaWiki\Extension\Math\WikiTexVC; |
| 6 | |
| 7 | use Exception; |
| 8 | use LogicException; |
| 9 | use MediaWiki\Extension\Math\WikiTexVC\Mhchem\MhchemParser; |
| 10 | use MediaWiki\Extension\Math\WikiTexVC\MMLmappings\Util\MMLParsingUtil; |
| 11 | use MediaWiki\Extension\Math\WikiTexVC\MMLmappings\Util\MMLutil; |
| 12 | use MediaWiki\Extension\Math\WikiTexVC\Nodes\Fun2; |
| 13 | use MediaWiki\Extension\Math\WikiTexVC\Nodes\TexArray; |
| 14 | use MediaWiki\Extension\Math\WikiTexVC\Nodes\TexNode; |
| 15 | use stdClass; |
| 16 | |
| 17 | /** |
| 18 | * A TeX/LaTeX validator and MathML converter. |
| 19 | * WikiTexVC takes user input and validates it while replacing |
| 20 | * MediaWiki-specific functions. The validator component is a PHP port of the JavaScript port of texvc, |
| 21 | * which was originally written in Ocaml for the Math extension. |
| 22 | * |
| 23 | * @author Johannes Stegmüller |
| 24 | */ |
| 25 | class TexVC { |
| 26 | private Parser $parser; |
| 27 | private TexUtil $tu; |
| 28 | |
| 29 | private const PKGS = [ |
| 30 | 'ams', |
| 31 | 'cancel', |
| 32 | 'color', |
| 33 | 'euro', |
| 34 | 'teubner', |
| 35 | 'mhchem', |
| 36 | 'mathoid', |
| 37 | 'mhchemtexified', |
| 38 | 'intent' |
| 39 | ]; |
| 40 | |
| 41 | public function __construct() { |
| 42 | $this->parser = new Parser(); |
| 43 | $this->tu = TexUtil::getInstance(); |
| 44 | } |
| 45 | |
| 46 | /** |
| 47 | * Usually this step is done implicitly within the check-method. |
| 48 | * @param string $input tex-string as input for the grammar |
| 49 | * @param null|array $options array options for the grammar. |
| 50 | * @return mixed output of the grammar. |
| 51 | * @throws SyntaxError when SyntaxError in the input |
| 52 | */ |
| 53 | public function parse( $input, $options = [] ) { |
| 54 | return $this->parser->parse( $input, $options ); |
| 55 | } |
| 56 | |
| 57 | /** status is one character: |
| 58 | * + : success! result is in 'output' |
| 59 | * E : Lexer exception raised |
| 60 | * F : TeX function not recognized |
| 61 | * S : Parsing error |
| 62 | * C : Content requires disabled package |
| 63 | * I : Intent syntax error |
| 64 | * - : Generic/Default failure code. Might be an invalid argument, |
| 65 | * output file already exist, a problem with an external |
| 66 | * command ... |
| 67 | * @param string|TexArray|stdClass $input tex to be checked as string, |
| 68 | * can also be the output of former parser call |
| 69 | * @param array $options array options for settings of the check |
| 70 | * @param array &$warnings reference on warnings occurring during the check |
| 71 | * @param bool $texifyMhchem create TeX for mhchem in input before checking further |
| 72 | * @return array|string[] output with information status (see above) |
| 73 | * @throws Exception in case of a major problem with the check and activated debug option. |
| 74 | */ |
| 75 | public function check( $input, $options = [], &$warnings = [], bool $texifyMhchem = false ) { |
| 76 | $options = ParserUtil::createOptions( $options ); |
| 77 | try { |
| 78 | $input = $this->preProcessInput( $texifyMhchem, $options, $input ); |
| 79 | } catch ( Exception $ex ) { |
| 80 | if ( |
| 81 | $ex instanceof SyntaxError && |
| 82 | !$options['oldtexvc'] && |
| 83 | str_starts_with( $ex->getMessage(), 'Deprecation' ) |
| 84 | ) { |
| 85 | $warnings[] = [ |
| 86 | 'type' => 'texvc-deprecation', |
| 87 | 'details' => $this->handleTexError( $ex, $options ) |
| 88 | ]; |
| 89 | $options['oldtexvc'] = true; |
| 90 | return $this->check( $input, $options, $warnings ); |
| 91 | } |
| 92 | |
| 93 | if ( $ex instanceof SyntaxError && $options['usemhchem'] && !$options['oldmhchem'] ) { |
| 94 | $warnings[] = [ |
| 95 | 'type' => 'mhchem-deprecation', |
| 96 | 'details' => $this->handleTexError( $ex, $options ) |
| 97 | ]; |
| 98 | $options['oldmhchem'] = true; |
| 99 | return $this->check( $input, $options, $warnings ); |
| 100 | } |
| 101 | return $this->handleTexError( $ex, $options ); |
| 102 | } |
| 103 | $output = $input->render(); |
| 104 | |
| 105 | $result = [ |
| 106 | 'inputN' => $input, |
| 107 | 'status' => '+', |
| 108 | 'output' => $output, |
| 109 | 'warnings' => $warnings, |
| 110 | 'input' => $input, |
| 111 | 'success' => true, |
| 112 | ]; |
| 113 | |
| 114 | return $this->postProcess( $options, $input, $result ); |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * @param string|TexNode|null $inputTree |
| 119 | * @return array|true |
| 120 | */ |
| 121 | private function checkTreeIntents( $inputTree ) { |
| 122 | if ( is_string( $inputTree ) || !$inputTree ) { |
| 123 | return true; |
| 124 | } |
| 125 | foreach ( $inputTree->getArgs() as $value ) { |
| 126 | if ( $value instanceof Fun2 && $value->getFname() === "\\intent" ) { |
| 127 | $intentStr = MMLutil::squashLitsToUnitIntent( $value->getArg2() ); |
| 128 | $intentContent = MMLParsingUtil::getIntentContent( $intentStr ); |
| 129 | $intentArg = MMLParsingUtil::getIntentArgs( $intentStr ); |
| 130 | $argch = self::checkIntentArg( $intentArg ); |
| 131 | if ( !$argch ) { |
| 132 | return [ |
| 133 | 'success' => false, |
| 134 | 'info' => 'malformatted intent argument', |
| 135 | ]; |
| 136 | } |
| 137 | // do check on arg1 |
| 138 | $ret = !$intentContent ? true : $this->checkIntent( $intentContent ); |
| 139 | if ( !$ret || ( isset( $ret['success'] ) && $ret['success'] == false ) ) { |
| 140 | return $ret; |
| 141 | } |
| 142 | return $this->checkTreeIntents( $value->getArg1() ); |
| 143 | } |
| 144 | |
| 145 | return self::checkTreeIntents( $value ); |
| 146 | } |
| 147 | return true; |
| 148 | } |
| 149 | |
| 150 | public static function checkIntentArg( ?string $input ): bool { |
| 151 | // arg has roughly the same specs like the NCName in parserintent.pegjs |
| 152 | return !$input || preg_match( '/^[a-zA-Z0-9._-]+$/', $input ); |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * @return true|array |
| 157 | */ |
| 158 | public function checkIntent( string $input ) { |
| 159 | // Very early intent syntax checker |
| 160 | try { |
| 161 | $parserIntent = new ParserIntent(); |
| 162 | $parserIntent->parse( $input ); |
| 163 | return true; |
| 164 | } catch ( Exception $exception ) { |
| 165 | return $this->handleTexError( $exception, null ); |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | public function handleTexError( Exception $e, ?array $options = null ): array { |
| 170 | if ( $options && $options['debug'] ) { |
| 171 | // @phan-suppress-next-line PhanThrowTypeAbsent |
| 172 | throw $e; |
| 173 | } |
| 174 | $report = [ 'success' => false, 'warnings' => [] ]; |
| 175 | if ( $e instanceof SyntaxError ) { |
| 176 | if ( $e->getMessage() === 'Illegal TeX function' ) { |
| 177 | $report['status'] = 'F'; |
| 178 | $report['details'] = $e->found; |
| 179 | } else { |
| 180 | $report['status'] = 'S'; |
| 181 | $report['details'] = $e->getMessage(); |
| 182 | } |
| 183 | |
| 184 | $report += $this->getLocationInfo( $e ); |
| 185 | |
| 186 | $report['error'] = [ |
| 187 | 'message' => $e->getMessage(), |
| 188 | 'expected' => $e->expected, |
| 189 | 'found' => $e->found, |
| 190 | 'location' => [ |
| 191 | // This currently only has the start location. The end is not noted in SyntaxError in PHP |
| 192 | // this issue is tracked in: https://phabricator.wikimedia.org/T321060 |