Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
22.78% covered (danger)
22.78%
36 / 158
20.00% covered (danger)
20.00%
3 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
MMLParsingUtil
22.78% covered (danger)
22.78%
36 / 158
20.00% covered (danger)
20.00%
3 / 15
2010.07
0.00% covered (danger)
0.00%
0 / 1
 renderApplyFunction
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getFontArgs
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
272
 parseDefineColorExpression
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
4
 createNot
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 mapToDoubleStruckUnicode
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 mapToCaligraphicUnicode
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 mapToFrakturUnicode
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 addToChr
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 matchAlphanumeric
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getIntentContent
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getIntentParams
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getIntentArgs
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 forgeIntentToTopElement
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 addAttributesToMML
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
182
 forgeIntentToSpecificElement
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace MediaWiki\Extension\Math\WikiTexVC\MMLmappings\Util;
4
5use MediaWiki\Extension\Math\WikiTexVC\MMLmappings\TexConstants\Tag;
6use MediaWiki\Extension\Math\WikiTexVC\MMLmappings\TexConstants\TexClass;
7use MediaWiki\Extension\Math\WikiTexVC\MMLmappings\TexConstants\Variants;
8use MediaWiki\Extension\Math\WikiTexVC\MMLnodes\MMLmo;
9use MediaWiki\Extension\Math\WikiTexVC\MMLnodes\MMLmpadded;
10use MediaWiki\Extension\Math\WikiTexVC\MMLnodes\MMLmrow;
11use MediaWiki\Extension\Math\WikiTexVC\MMLnodes\MMLmtext;
12
13/**
14 * This class contains functionalities for MML-node
15 * parsing which can be extracted and are used
16 * for multiple functions.
17 */
18class MMLParsingUtil {
19    public static function renderApplyFunction() {
20        $mo = new MMLmo();
21        return $mo->encapsulateRaw( "&#x2061;" );
22    }
23
24    public static function getFontArgs( $name, $variant, $passedArgs ) {
25        $args = [];
26        switch ( $name ) {
27            case "cal":
28            case "mathcal":
29                $args = [ Tag::MJXVARIANT => "-tex-calligraphic", "mathvariant" => Variants::SCRIPT ];
30                break;
31            case "it":
32            case "mathit":
33                $args = [ Tag::MJXVARIANT => $variant, "mathvariant" => Variants::ITALIC ];
34                break;
35            case "bf":
36            case "mathbf":
37                $args = [ "mathvariant" => $variant ];
38                break;
39            // Sstatements from here come from other fct ok ? otherwise create second fct
40            case "textit":
41                $args = [ "mathvariant" => Variants::ITALIC ];
42                break;
43            case "textbf":
44                $args = [ "mathvariant" => Variants::BOLD ];
45                break;
46            case "textsf":
47                $args = [ "mathvariant" => Variants::SANSSERIF ];
48                break;
49            case "texttt":
50                $args = [ "mathvariant" => Variants::MONOSPACE ];
51                break;
52            case "textrm":
53                break;
54            case "emph":
55                // Toggle by passed args in emph
56                if ( isset( $passedArgs["mathvariant"] ) ) {
57                    if ( $passedArgs["mathvariant"] === Variants::ITALIC ) {
58                        $args = [ "mathvariant" => Variants::NORMAL ];
59                    }
60                } else {
61                    $args = [ "mathvariant" => Variants::ITALIC ];
62                }
63                break;
64            default:
65                $args = [ "mathvariant" => $variant ];
66
67        }
68        return $args;
69    }
70
71    /**
72     * Parses an expression that defines a color; this is usually an argument in Literal.
73     * Example expression is: "\definecolor {ultramarine}{rgb}{0,0.12549019607843,0.37647058823529}"
74     * @param string $input tex-string, which contains the expression
75     * @return array|null either an array which contains hex of parsed expression or null if not parsable
76     */
77    public static function parseDefineColorExpression( string $input ): ?array {
78        $returnObj = null;
79        $matches = [];
80        $matched = preg_match_all( '/\{(.*?)\}/', $input, $matches );
81        if ( !$matched ) {
82            return null;
83        }
84        $ctr = count( $matches[1] ?? [] );
85
86        if ( $ctr == 3 && $matches[1][1] === "rgb" ) {
87            $returnObj = [];
88            $rgbValues = explode( ",", $matches[1][2] );
89            $r  = round( floatval( $rgbValues[0] ) * 255 );
90            $g = round( floatval( $rgbValues[1] ) * 255 );
91            $b = round( floatval( $rgbValues[2] ) * 255 );
92            $color = sprintf( "#%02x%02x%02x", $r, $g, $b );
93            $returnObj["name"] = $matches[1][0];
94            $returnObj["type"] = "rgb";
95            $returnObj["hex"] = $color;
96        }
97
98        return $returnObj;
99    }
100
101    /**
102     * Creates a negation block in MathML, usually preceding the negated statement
103     * @return string negation block as MathML
104     */
105    public static function createNot() {
106        $mmlMrow = new MMLmrow( TexClass::REL );
107        $mpadded = new MMLmpadded( "", [ "width" => "0" ] );
108        $mtext = new MMLmtext();
109        return $mmlMrow->encapsulateRaw( $mpadded->encapsulateRaw( $mtext->encapsulateRaw( "&#x29F8;" ) ) );
110    }
111
112    public static function mapToDoubleStruckUnicode( $inputString ) {
113        $map = [
114            '0' => '&#x1D7D8;', '1' => '&#x1D7D9;', '2' => '&#x1D7DA;', '3' => '&#x1D7DB;', '4' => '&#x1D7DC;',
115            '5' => '&#x1D7DD;', '6' => '&#x1D7DE;', '7' => '&#x1D7DF;', '8' => '&#x1D7E0;', '9' => '&#x1D7E1;',
116            'A' => '&#x1D538;', 'B' => '&#x1D539;', 'C' => '&#x2102;', 'D' => '&#x1D53B;', 'E' => '&#x1D53C;',
117            'F' => '&#x1D53D;', 'G' => '&#x1D53E;', 'H' => '&#x210D;', 'I' => '&#x1D540;', 'J' => '&#x1D541;',
118            'K' => '&#x1D542;', 'L' => '&#x1D543;', 'M' => '&#x1D544;', 'N' => '&#x2115;', 'O' => '&#x1D546;',
119            'P' => '&#x2119;', 'Q' => '&#x211A;', 'R' => '&#x211D;', 'S' => '&#x1D54A;', 'T' => '&#x1D54B;',
120            'U' => '&#x1D54C;', 'V' => '&#x1D54D;', 'W' => '&#x1D54E;', 'X' => '&#x1D54F;', 'Y' => '&#x1D550;',
121            'Z' => '&#x2124;', 'a' => '&#x1D552;', 'b' => '&#x1D553;', 'c' => '&#x1D554;', 'd' => '&#x1D555;',
122            'e' => '&#x1D556;', 'f' => '&#x1D557;', 'g' => '&#x1D558;', 'h' => '&#x1D559;', 'i' => '&#x1D55A;',
123            'j' => '&#x1D55B;', 'k' => '&#x1D55C;', 'l' => '&#x1D55D;', 'm' => '&#x1D55E;', 'n' => '&#x1D55F;',
124            'o' => '&#x1D560;', 'p' => '&#x1D561;', 'q' => '&#x1D562;', 'r' => '&#x1D563;', 's' => '&#x1D564;',
125            't' => '&#x1D565;', 'u' => '&#x1D566;', 'v' => '&#x1D567;', 'w' => '&#x1D568;', 'x' => '&#x1D569;',
126            'y' => '&#x1D56A;', 'z' => '&#x1D56B;'
127        ];
128
129        return self::matchAlphanumeric( $inputString, $map );
130    }
131
132    public static function mapToCaligraphicUnicode( $inputString ) {
133        $map = [
134            '0' => '&#x1D7CE;', '1' => '&#x1D7CF;', '2' => '&#x1D7D0;', '3' => '&#x1D7D1;', '4' => '&#x1D7D2;',
135            '5' => '&#x1D7D3;', '6' => '&#x1D7D4;', '7' => '&#x1D7D5;', '8' => '&#x1D7D6;', '9' => '&#x1D7D7;',
136            'A' => '&#x1D49C;', 'B' => '&#x212C;', 'C' => '&#x1D49E;', 'D' => '&#x1D49F;', 'E' => '&#x2130;',
137            'F' => '&#x2131;', 'G' => '&#x1D4A2;', 'H' => '&#x210B;', 'I' => '&#x2110;', 'J' => '&#x1D4A5;',
138            'K' => '&#x1D4A6;', 'L' => '&#x2112;', 'M' => '&#x2133;', 'N' => '&#x1D4A9;', 'O' => '&#x1D4AA;',
139            'P' => '&#x1D4AB;', 'Q' => '&#x1D4AC;', 'R' => '&#x211B;', 'S' => '&#x1D4AE;', 'T' => '&#x1D4AF;',
140            'U' => '&#x1D4B0;', 'V' => '&#x1D4B1;', 'W' => '&#x1D4B2;', 'X' => '&#x1D4B3;', 'Y' => '&#x1D4B4;',
141            'Z' => '&#x1D4B5;', 'a' => '&#x1D4B6;', 'b' => '&#x1D4B7;', 'c' => '&#x1D4B8;', 'd' => '&#x1D4B9;',
142            'e' => '&#x212F;', 'f' => '&#x1D4BB;', 'g' => '&#x210A;', 'h' => '&#x1D4BD;', 'i' => '&#x1D4BE;',
143            'j' => '&#x1D4BF;', 'k' => '&#x1D4C0;', 'l' => '&#x1D4C1;', 'm' => '&#x1D4C2;', 'n' => '&#x1D4C3;',
144            'o' => '&#x2134;', 'p' => '&#x1D4C5;', 'q' => '&#x1D4C6;', 'r' => '&#x1D4C7;', 's' => '&#x1D4C8;',
145            't' => '&#x1D4C9;', 'u' => '&#x1D4CA;', 'v' => '&#x1D4CB;', 'w' => '&#x1D4CC;', 'x' => '&#x1D4CD;',
146            'y' => '&#x1D4CE;', 'z' => '&#x1D4CF;'
147        ];
148
149        return self::matchAlphanumeric( $inputString, $map );
150    }
151
152    public static function mapToFrakturUnicode( $inputString ): string {
153        $res = '';
154        $specialCases = [ 'C' => '&#x0212D;',
155            'H' => '&#x0210C;',
156            'I' => '&#x02111;',
157            'R' => '&#x0211C;',
158            'Z' => '&#x02124;' ];
159        foreach ( mb_str_split( $inputString ) as $chr ) {
160            // see https://www.w3.org/TR/mathml-core/#fraktur-mappings
161            if ( isset( $specialCases[$chr] ) ) {
162                $res .= $specialCases[$chr];
163                continue;
164            }
165            if ( $chr >= 'A' && $chr <= 'Z' ) {
166                $code = self::addToChr( $chr, '1D4C3' );
167                $res .= '&#x' . $code . ';';
168            } elseif ( $chr >= 'a' && $chr <= 'z' ) {
169                $code = self::addToChr( $chr, '1D4BD' );
170                $res .= '&#x' . $code . ';';
171            } else {
172                $res .= $chr;
173            }
174        }
175        return $res;
176    }
177
178    private static function addToChr( $chr, $base ): string {
179        return strtoupper( dechex( mb_ord( $chr ) + hexdec( $base ) ) );
180    }
181
182    public static function matchAlphanumeric( $inputString, $map ) {
183        // Replace each character in the input string with its caligraphic Unicode equivalent
184        return preg_replace_callback( '/[A-Za-z0-9]/u', static function ( $matches ) use ( $map ) {
185            return $map[$matches[0]] ?? $matches[0];
186        }, $inputString );
187    }
188
189    public static function getIntentContent( ?string $input ) {
190        if ( !$input ) {
191            return null;
192        }
193        $matchesInt = [];
194        $matchInt = preg_match( "/intent=[\'\"](.*)[\'\"]/", $input, $matchesInt );
195        if ( $matchInt && count( $matchesInt ) >= 2 ) {
196            return $matchesInt[1];
197        }
198        return null;
199    }
200
201    public static function getIntentParams( ?string $intentContent ) {
202        if ( !$intentContent ) {
203            return null;
204        }
205        $matchesParams = [];
206        // tbd eventually not only alphanumerical chars valid in intent params
207        $matchParams = preg_match_all( "/\\\$([a-zA-Z]+)/", $intentContent, $matchesParams );
208        if ( $matchParams && count( $matchesParams ) >= 2 ) {
209            return $matchesParams[1];
210        }
211        return null;
212    }
213
214    public static function getIntentArgs( ?string $input ) {
215        if ( !$input ) {
216            return null;
217        }
218        $matchesArgs = [];
219        $matchArg = preg_match( "/arg\s*=\s*[\'\"](.*?)[\'\"]/", $input, $matchesArgs );
220        if ( $matchArg && count( $matchesArgs ) >= 2 ) {
221            return $matchesArgs[1];
222        }
223        return null;
224    }
225
226    /**
227     * Converts a rendered MathML string to a XML tree and adds the attributes from input
228     * to the top-level element.Valid attributes for adding are "arg" and "intent.
229     * It overwrites pre-existing attributes in the top-level element.
230     * TBD: currently contains a hacky way to remove xml header in the output string
231     * example:" <msup intent="_($op,_of,$arg)">" intent attributes comes from input variables
232     * @param string $renderedMML defines input MathML string
233     * @param array $intentContentAtr defines attributes to add
234     * @return string MML with added attributes
235     */
236    public static function forgeIntentToTopElement( string $renderedMML, $intentContentAtr ) {
237        if ( !$intentContentAtr || !$renderedMML ) {
238            return $renderedMML;
239        }
240
241        return self::addAttributesToMML( $renderedMML, $intentContentAtr, "" );
242    }
243
244    /**
245     * Add parameters from aattributes to the MML string
246     * @param string $renderedMML defines input MathML string
247     * @param array $intentContentAtr defines attributes to add
248     * @param string $elementTag element tag when using foundNodes
249     * @param bool $useFoundNodes use found nodes
250     * @return string MML with added attributes
251     */
252    public static function addAttributesToMML( $renderedMML, $intentContentAtr, $elementTag, $useFoundNodes = false ) {
253        $xml = simplexml_load_string( $renderedMML );
254        if ( !$xml ) {
255            return "";
256        }
257        if ( $useFoundNodes ) {
258            $foundNodes = $xml->xpath( $elementTag );
259            if ( !( $foundNodes !== null && count( $foundNodes ) >= 1 ) ) {
260                return $renderedMML;
261            }
262        }
263
264        if ( isset( $intentContentAtr["intent"] ) ) {
265            if ( isset( $xml["intent"] ) ) {
266                $xml["intent"] = $intentContentAtr["intent"];
267            } elseif ( $intentContentAtr["intent"] != null && is_string( $intentContentAtr["intent"] ) ) {
268                $xml->addAttribute( "intent", $intentContentAtr["intent"] );
269            }
270        }
271        if ( isset( $intentContentAtr["arg"] ) ) {
272            if ( isset( $xml["arg"] ) ) {
273                $xml["arg"] = $intentContentAtr["arg"];
274            } elseif ( $intentContentAtr["arg"] != null && is_string( $intentContentAtr["arg"] ) ) {
275                $xml->addAttribute( "arg", $intentContentAtr["arg"] );
276            }
277        }
278
279        $hackyXML = str_replace( "<?xml version=\"1.0\"?>", "", $xml->asXML() );
280        return str_replace( "\n", "", $hackyXML );
281    }
282
283    public static function forgeIntentToSpecificElement( string $renderedMML, $intentContentAtr, string $elementTag ) {
284        if ( !$intentContentAtr || !$renderedMML || !$elementTag ) {
285            return $renderedMML;
286        }
287        return self::addAttributesToMML( $elementTag, $intentContentAtr, $elementTag, true );
288    }
289
290}