Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
20.64% covered (danger)
20.64%
45 / 218
13.64% covered (danger)
13.64%
3 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
TexArray
20.64% covered (danger)
20.64%
45 / 218
13.64% covered (danger)
13.64%
3 / 22
6381.10
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 checkForStyleArgs
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 checkForColor
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 checkForColorDefinition
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 checkForSideset
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 checkForLimits
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
182
 checkForNot
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 checkForDerivatives
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 checkForNamedFctArgs
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
 renderMML
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
306
 createMMLwithContext
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
 addDerivativesContext
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 inCurlies
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 extractSubscripts
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 extractIdentifiers
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
10
 getModIdent
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 push
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pop
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 first
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 second
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 unshift
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkInput
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
1<?php
2
3declare( strict_types = 1 );
4
5namespace MediaWiki\Extension\Math\WikiTexVC\Nodes;
6
7use InvalidArgumentException;
8use MediaWiki\Extension\Math\WikiTexVC\MMLmappings\BaseMappings;
9use MediaWiki\Extension\Math\WikiTexVC\MMLmappings\Util\MMLParsingUtil;
10use MediaWiki\Extension\Math\WikiTexVC\MMLmappings\Util\MMLutil;
11use MediaWiki\Extension\Math\WikiTexVC\MMLnodes\MMLmo;
12use MediaWiki\Extension\Math\WikiTexVC\MMLnodes\MMLmstyle;
13use MediaWiki\Extension\Math\WikiTexVC\MMLnodes\MMLmsup;
14use MediaWiki\Extension\Math\WikiTexVC\TexUtil;
15
16class TexArray extends TexNode {
17
18    public function __construct( ...$args ) {
19        $nargs = [];
20
21        foreach ( $args as &$arg ) {
22            if ( $arg !== null ) {
23                array_push( $nargs, $arg );
24            }
25        }
26
27        self::checkInput( $nargs );
28        parent::__construct( ...$nargs );
29    }
30
31    public function checkForStyleArgs( $node ) {
32        if ( $node instanceof Literal ) {
33            $name = trim( $node->getArg() );
34            switch ( $name ) {
35                case "\\displaystyle":
36                    return [ "displaystyle" => "true", "scriptlevel" => "0" ];
37                case "\\scriptstyle":
38                    return [ "displaystyle" => "false", "scriptlevel" => "1" ];
39                case "\\scriptscriptstyle":
40                    return [ "displaystyle" => "false", "scriptlevel" => "2" ];
41                case "\\textstyle":
42                    return [ "displaystyle" => "false", "scriptlevel" => "0" ];
43            }
44        }
45        return null;
46    }
47
48    /**
49     * Checks if an TexNode of Literal contains color information (color, pagecolor)
50     * and returns info how to continue with the parsing.
51     * @param TexNode $node node to check if it contains color info
52     * @return array index 0: (bool) was color element found, index 1: (string) specified color
53     */
54    public function checkForColor( TexNode $node ) {
55        if ( $node instanceof Literal ) {
56            $name = trim( $node->getArg() );
57            if ( str_contains( $name, "\\color" ) ) {
58                $foundOperatorContent = MMLutil::initalParseLiteralExpression( $node->getArg() );
59                if ( !$foundOperatorContent ) {
60                    // discarding color elements which not specify color
61                    return [ true, null ];
62                } else {
63                    return [ true, $foundOperatorContent[2][0] ];
64                }
65            } elseif ( str_contains( $name, "\\pagecolor" ) ) {
66                return [ true, null ];
67            }
68        }
69        return [ false, null ];
70    }
71
72    public function checkForColorDefinition( TexNode $node ): ?array {
73        if ( $node instanceof Literal ) {
74            $name = trim( $node->getArg() );
75            if ( str_contains( $name, "\\definecolor" ) ) {
76                return MMLParsingUtil::parseDefineColorExpression( $node->getArg() );
77            }
78        }
79        return null;
80    }
81
82    /**
83     * Checks two sequential nodes in TexArray if they contain information on sideset expressions.
84     * @param TexNode $currentNode first node in array to check (for sideset expression)
85     * @param TexNode|null $nextNode second node in array to check (for succeeding operator)
86     * @return TexNode|null the succeeding operator for further Parsing or null if sideset not found or invalid
87     */
88    public function checkForSideset( TexNode $currentNode, ?TexNode $nextNode ): ?TexNode {
89        if ( !( $currentNode instanceof Fun2nb && $currentNode->getFname() == "\\sideset" ) ) {
90            return null;
91        }
92        if ( $nextNode instanceof Literal ) {
93            return $nextNode;
94        }
95        if ( $nextNode instanceof FQ ) {
96            return $nextNode;
97        }
98        return null;
99    }
100
101    public function checkForLimits( $currentNode, $nextNode ) {
102        // Preceding 'lim' in example: "\\lim_{x \\to 2}"
103        if ( ( $currentNode instanceof DQ || $currentNode instanceof FQ )
104            && $currentNode->containsFunc( "\\lim" ) ) {
105
106            if ( $currentNode->getBase() instanceof TexArray ) {
107                return [ $currentNode->getBase()->getArgs()[0], false ];
108            } else {
109                return [ $currentNode->getBase(), false ];
110            }
111        }
112
113        /** Find cases which have preceding Literals with nullary_macro-type operators i.e.:
114         * "\iint\limits_D \, dx\,dy"
115         */
116        $tu = TexUtil::getInstance();
117
118        // Check whether the current node is a possible preceding literal
119        if ( !( $currentNode instanceof Literal
120            && ( $tu->nullary_macro( trim( $currentNode->getArg() ) )
121            || trim( $currentNode->getArg() ) == "\\lim" ) ) ) {
122            return [ null, false ];
123        }
124
125        // Check whether the next node is a possible limits construct
126        if ( !( ( $nextNode instanceof DQ || $nextNode instanceof FQ )
127            && $nextNode->getBase() instanceof Literal
128            && ( $nextNode->containsFunc( "\\limits" ) || $nextNode->containsFunc( "\\nolimits" ) )
129            ) ) {
130            return [ null, false ];
131
132        }
133        return [ $currentNode, true ];
134    }
135
136    public function checkForNot( $currentNode ): bool {
137        if ( $currentNode instanceof Literal && trim( $currentNode->getArg() ) == "\\not" ) {
138            return true;
139        }
140        return false;
141    }
142
143    public function checkForDerivatives( $iStart, $args ): int {
144        $ctr = 0;
145        for ( $i = $iStart, $count = count( $this->args ); $i < $count; $i++ ) {
146            $followUp = $args[$i];
147            if ( $followUp instanceof Literal && $followUp->getArg() === "'" ) {
148                $ctr++;
149            } else {
150                break;
151            }
152        }
153
154        return $ctr;
155    }
156
157    public function checkForNamedFctArgs( $currentNode, $nextNode ) {
158        // Check if current node is named function
159        $hasNamedFct = false;
160        if ( $currentNode instanceof TexArray && count( $currentNode->args ) == 2 ) {
161            $tu = TexUtil::getInstance();
162            $currentNodeContent = $currentNode->getArgs()[0];
163            if ( $currentNodeContent instanceof Literal &&
164                $tu->latex_function_names( $currentNodeContent->getArg() ) ) {
165                $hasNamedFct = true;
166            }
167        }
168
169        // Check if there is a valid argument as next parameter
170        $hasValidParameters = false;
171        if ( !$hasNamedFct ) {
172            return [ $hasNamedFct, $hasValidParameters ];
173        }
174
175        if ( $nextNode ) {
176            $hasValidParameters = true;
177        }
178
179        return [ $hasNamedFct, $hasValidParameters ];
180    }
181
182    public function renderMML( $arguments = [], $state = [] ) {
183        // Everything here is for parsing displaystyle, probably refactored to WikiTexVC grammar later
184        $fullRenderedArray = "";
185        $mmlStyles = [];
186        $currentColor = null;
187
188        for ( $i = 0, $count = count( $this->args ); $i < $count; $i++ ) {
189            $current = $this->args[$i];
190            if ( isset( $this->args[$i + 1] ) ) {
191                $next = $this->args[$i + 1];
192            } else {
193                $next = null;
194            }
195            // Check for sideset
196            $foundSideset = $this->checkForSideset( $current, $next );
197            if ( $foundSideset ) {
198                $state["sideset"] = $foundSideset;
199                // Skipping the succeeding Literal
200                $i++;
201            }
202
203            // Check for limits
204            $foundLimits = $this->checkForLimits( $current, $next );
205            if ( $foundLimits[0] ) {
206                $state["limits"] = $foundLimits[0];
207                if ( $foundLimits[1] ) {
208                    continue;
209                }
210            }
211
212            // Check for Not
213            $foundNot = $this->checkForNot( $current );
214            if ( $foundNot ) {
215                $state["not"] = true;
216                continue;
217            }
218
219            // Check for derivatives
220            $foundDeriv = $this->checkForDerivatives( $i + 1, $this->args );
221            if ( $foundDeriv > 0 ) {
222                // skip the next indices which are derivative characters
223                $i += $foundDeriv;
224                $state["deriv"] = $foundDeriv;
225            }
226
227            // Check if there is a new color definition and add it to state
228            $foundColorDef = $this->checkForColorDefinition( $current );
229            if ( $foundColorDef ) {
230                $state["colorDefinitions"][$foundColorDef["name"]] = $foundColorDef;
231                continue;
232            }
233            // Pass preceding color info to state
234            $foundColor = $this->checkForColor( $current );
235            if ( $foundColor[0] ) {
236                $currentColor = $foundColor[1];
237                // Skipping the color element itself for rendering
238                continue;
239            }
240            $styleArguments = $this->checkForStyleArgs( $current );
241
242            $foundNamedFct = $this->checkForNamedFctArgs( $current, $next );
243            if ( $foundNamedFct[0] ) {
244                $state["foundNamedFct"] = $foundNamedFct;
245            }
246
247            if ( $styleArguments ) {
248                $state["styleargs"] = $styleArguments;
249                if ( $next instanceof Curly ) {
250                    // Wrap with style-tags when the next element is a Curly which determines start and end tag.
251                    $mmlStyle = new MMLmstyle( "", $styleArguments );
252                    $fullRenderedArray .= $mmlStyle->getStart();
253                    $fullRenderedArray .= $this->createMMLwithContext( $currentColor, $next, $state, $arguments );
254                    $fullRenderedArray .= $mmlStyle->getEnd();
255                    $mmlStyle = null;
256                    unset( $state["styleargs"] );
257                    $i++;
258                } else {
259                    // Start the style indicator in cases like \textstyle abc
260                    $mmlStyle = new MMLmstyle( "", $styleArguments );
261                    $fullRenderedArray .= $mmlStyle->getStart();
262                    $mmlStyles[] = $mmlStyle->getEnd();
263
264                }
265            } else {
266                $fullRenderedArray .= $this->createMMLwithContext( $currentColor, $current, $state, $arguments );
267            }
268
269            if ( array_key_exists( "not", $state ) ) {
270                unset( $state["not"] );
271            }
272            if ( array_key_exists( "limits", $state ) ) {
273                unset( $state["limits"] );
274            }
275            if ( array_key_exists( "deriv", $state ) ) {
276                unset( $state["deriv"] );
277            }
278        }
279
280        foreach ( array_reverse( $mmlStyles ) as $mmlStyleEnd ) {
281            $fullRenderedArray .= $mmlStyleEnd;
282        }
283
284        return $fullRenderedArray;
285    }
286
287    private function createMMLwithContext( $currentColor, $currentNode, $state, $arguments ) {
288        if ( $currentColor ) {
289            if ( array_key_exists( "colorDefinitions", $state )
290                && is_array( $state["colorDefinitions"] )
291                && array_key_exists( $currentColor, $state["colorDefinitions"] ?? [] )
292                && is_array( $state["colorDefinitions"][$currentColor] )
293                && array_key_exists( "hex", $state["colorDefinitions"][$currentColor] )
294               ) {
295                $displayedColor = $state["colorDefinitions"][$currentColor]["hex"];
296
297            } else {
298                $resColor = BaseMappings::getColorByKey( $currentColor );
299                $displayedColor = $resColor ? $resColor[0] : $currentColor;
300            }
301            $mmlStyleColor = new MMLmstyle( "", [ "mathcolor" => $displayedColor ] );
302            $ret = $mmlStyleColor->encapsulateRaw( $currentNode->renderMML( $arguments, $state ) );
303        } else {
304            $ret = $currentNode->renderMML( $arguments, $state );
305        }
306
307        return $this->addDerivativesContext( $state, $ret );
308    }
309
310    /**
311     * If derivative was recognized, add the corresponding derivative math operator
312     * to the mml and wrap with msup element.
313     * @param array $state state indicator which indicates derivative
314     * @param string $mml mathml input
315     * @return string mml with additional mml-elements for derivatives
316     */
317    public function addDerivativesContext( $state, string $mml ): string {
318        if ( array_key_exists( "deriv", $state ) && $state["deriv"] > 0 ) {
319            $msup = new MMLmsup();
320            $moDeriv = new MMLmo();
321
322            if ( $state["deriv"] == 1 ) {
323                $derInfo = "&#x2032;";
324            } elseif ( $state["deriv"] == 2 ) {
325                $derInfo = "&#x2033;";
326            } elseif ( $state["deriv"] == 3 ) {
327                $derInfo = "&#x2034;";
328            } elseif ( $state["deriv"] == 4 ) {
329                $derInfo = "&#x2057;";
330            } else {
331                $derInfo = str_repeat( "&#x2032;", $state["deriv"] );
332            }
333
334            $mml = $msup->encapsulateRaw( $mml . $moDeriv->encapsulateRaw( $derInfo ) );
335        }
336        return $mml;
337    }
338
339    public function inCurlies() {
340        if ( isset( $this->args[0] ) && count( $this->args ) == 1 ) {
341            return $this->args[0]->inCurlies();
342        } else {
343            return '{' . $this->render() . '}';
344        }
345    }
346
347    public function extractSubscripts() {
348        $y = [];
349
350        foreach ( $this->args as $x ) {
351            $y = array_merge( $y, $x->extractSubscripts() );
352        }
353        if ( isset( $this->args[0] ) && ( count( $this->args ) == count( $y ) ) ) {
354            return implode( '', $y );
355        }
356        return [];
357    }
358
359    public function extractIdentifiers( $args = null ) {
360        if ( $args == null ) {
361            $args = $this->args;
362        }
363        $list = parent::extractIdentifiers( $args );
364        $outpos = 0;
365        $offset = 0;
366        $int = 0;
367
368        for ( $inpos = 0; $inpos < count( $list ); $inpos++ ) {
369            $outpos = $inpos - $offset;
370            switch ( $list[$inpos] ) {
371                case '\'':
372                    $list[$outpos - 1] .= '\'';
373                    $offset++;
374                    break;
375                case '\\int':
376                    $int++;
377                    $offset++;
378                    break;
379                case '\\mathrm{d}':
380                case 'd':
381                    if ( $int ) {
382                        $int--;
383                        $offset++;
384                        break;
385                    }
386                // no break
387                default:
388                    if ( isset( $list[0] ) ) {
389                        $list[$outpos] = $list[$inpos];
390                    }
391            }
392        }
393        return array_slice( $list, 0, count( $list ) - $offset );
394    }
395
396    public function getModIdent() {
397        $y = [];
398
399        foreach ( $this->args as $x ) {
400            $y = array_merge( $y, $x->getModIdent() );
401        }
402
403        if ( isset( $this->args[0] ) && ( count( $this->args ) == count( $y ) ) ) {
404            return implode( "", $y );
405        }
406        return [];
407    }
408
409    public function push( ...$elements ) {
410        self::checkInput( $elements );
411
412        array_push( $this->args, ...$elements );
413    }
414
415    public function pop() {
416        array_splice( $this->args, 0, 1 );
417    }
418
419    /**
420     * @return TexNode|string|null first value
421     */
422    public function first() {
423        if ( isset( $this->args[0] ) ) {
424            return $this->args[0];
425        } else {
426            return null;
427        }
428    }
429
430    /**
431     * @return TexNode|string|null second value
432     */
433    public function second() {
434        if ( isset( $this->args[1] ) ) {
435            return $this->args[1];
436        } else {
437            return null;
438        }
439    }
440
441    public function unshift( ...$elements ) {
442        array_unshift( $this->args, ...$elements );
443    }
444
445    /**
446     * @throws InvalidArgumentException if args not of correct type
447     * @param TexNode[] $args input args
448     * @return void
449     */
450    private static function checkInput( $args ): void {
451        foreach ( $args as $arg ) {
452            if ( !( $arg instanceof TexNode ) ) {
453                throw new InvalidArgumentException( 'Wrong input type specified in input elements.' );
454            }
455        }
456    }
457
458}