Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.45% covered (success)
98.45%
127 / 129
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
TexVC
98.45% covered (success)
98.45%
127 / 129
90.00% covered (success)
90.00%
9 / 10
50
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parse
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 check
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
8
 checkTreeIntents
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
11
 checkIntentArg
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 checkIntent
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 handleTexError
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
5
 getLocationInfo
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
2.09
 preProcessInput
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 postProcess
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
13
1<?php
2
3declare( strict_types = 1 );
4
5namespace MediaWiki\Extension\Math\WikiTexVC;
6
7use Exception;
8use LogicException;
9use MediaWiki\Extension\Math\WikiTexVC\Mhchem\MhchemParser;
10use MediaWiki\Extension\Math\WikiTexVC\MMLmappings\Util\MMLParsingUtil;
11use MediaWiki\Extension\Math\WikiTexVC\MMLmappings\Util\MMLutil;
12use MediaWiki\Extension\Math\WikiTexVC\Nodes\Fun2;
13use MediaWiki\Extension\Math\WikiTexVC\Nodes\TexArray;
14use MediaWiki\Extension\Math\WikiTexVC\Nodes\TexNode;
15use 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 */
25class 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