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