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