Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 302
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
MhchemTexify
0.00% covered (danger)
0.00%
0 / 302
0.00% covered (danger)
0.00%
0 / 8
22952
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 go
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
 goInner
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 strReplaceFirst
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 go2
0.00% covered (danger)
0.00%
0 / 207
0.00% covered (danger)
0.00%
0 / 1
8190
 getArrow
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
156
 getBond
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
506
 getOperator
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
272
1<?php
2/**
3 * Copyright (c) 2023 Johannes Stegmüller
4 *
5 * This file is a port of mhchemParser originally authored by Martin Hensel in javascript/typescript.
6 * The original license for this software can be found in the accompanying LICENSE.mhchemParser-ts.txt file.
7 */
8
9namespace MediaWiki\Extension\Math\WikiTexVC\Mhchem;
10
11use MediaWiki\Extension\Math\WikiTexVC\MHChem\MhchemUtil as MU;
12use RuntimeException;
13
14/**
15 * Takes MhchemParser output and convert it to TeX
16 *
17 * Functionality is the same as mhchemTexify class at ~line 1505 in mhchemParser.js
18 * in mhchemParser by Martin Hensel.
19 *
20 * @author Johannes Stegmüller
21 * @license GPL-2.0-or-later
22 */
23class MhchemTexify {
24
25    /** @var bool optimize the output TeX for WikiTexVC */
26    private bool $optimizeForTexVC;
27
28    /**
29     * Takes MhchemParser output and convert it to TeX
30     * @param bool $optimizeForTexVC optimizes the output for WikiTexVC grammar by
31     * wrapping dimensions for some TeX commands in curly brackets.
32     */
33    public function __construct( bool $optimizeForTexVC = false ) {
34        $this->optimizeForTexVC = $optimizeForTexVC;
35    }
36
37    /**
38     * @param array|mixed $input
39     * @param bool $addOuterBraces
40     */
41    public function go( $input, bool $addOuterBraces ): string {
42        if ( !MhchemUtil::issetJS( $input ) ) {
43            return "";
44        }
45        $res = "";
46        $cee = false;
47        for ( $i = 0; $i < count( $input ); $i++ ) {
48            $inputI = $input[$i];
49
50            if ( is_string( $inputI ) ) {
51                $res .= $inputI;
52            } else {
53                $res .= self::go2( $inputI );
54                if ( $inputI["type_"] === '1st-level escape' ) {
55                    $cee = true;
56                }
57            }
58        }
59        if ( $addOuterBraces && !$cee && $res ) {
60            $res = "{" . $res . "}";
61        }
62        return $res;
63    }
64
65    private function goInner( array $input ): string {
66        return self::go( $input, false );
67    }
68
69    private function strReplaceFirst( string $search, string $replace, string $subject ): string {
70        return implode( $replace, explode( $search, $subject, 2 ) );
71    }
72
73    private function go2( array $buf ): string {
74        switch ( $buf["type_"] ) {
75            case 'chemfive':
76                $res = "";
77                $b5 = [
78                    "a" => self::goInner( $buf["a"] ),
79                    "b" => self::goInner( $buf["b"] ),
80                    "p" => self::goInner( $buf["p"] ),
81                    "o" => self::goInner( $buf["o"] ),
82                    "q" => self::goInner( $buf["q"] ),
83                    "d" => self::goInner( $buf["d"] )
84                ];
85                if ( MU::issetJS( $b5["a"] ) ) {
86                    if ( preg_match( "/^[+\-]/", $b5["a"] ) ) {
87                        $b5["a"] = "{" . $b5["a"] . "}";
88                    }
89                    $res .= $b5["a"] . "\\,";
90                }
91                if ( MU::issetJS( $b5["b"] ) || MU::issetJS( $b5["p"] ) ) {
92                    $res .= "{\\vphantom{A}}";
93                    $res .= "^{\\hphantom{" . ( $b5["b"] ) . "}}_{\\hphantom{" . ( $b5["p"] ) . "}}";
94                    $res .= !$this->optimizeForTexVC ? "\\mkern-1.5mu" : "\\mkern{-1.5mu}";
95                    $res .= "{\\vphantom{A}}";
96                    $res .= "^{\\smash[t]{\\vphantom{2}}\\llap{" . ( $b5["b"] ) . "}}";
97                    $res .= "_{\\vphantom{2}\\llap{\\smash[t]{" . ( $b5["p"] ) . "}}}";
98                }
99
100                if ( MU::issetJS( $b5["o"] ) ) {
101                    if ( preg_match( "/^[+\-]/", $b5["o"] ) ) {
102                        $b5["o"] = "{" . $b5["o"] . "}";
103                    }
104                    $res .= $b5["o"];
105                }
106                if ( isset( $buf["dType"] ) && $buf["dType"] === 'kv' ) {
107                    if ( MU::issetJS( $b5["d"] ) || MU::issetJS( $b5["q"] ) ) {
108                        $res .= "{\\vphantom{A}}";
109                    }
110                    if ( MU::issetJS( $b5["d"] ) ) {
111                        $res .= "^{" . $b5["d"] . "}";
112                    }
113                    if ( MU::issetJS( $b5["q"] ) ) {
114                        $res .= "_{\\smash[t]{" . $b5["q"] . "}}";
115                    }
116                } elseif ( MU::issetJS( $buf["dType"] ?? null ) && $buf["dType"] === 'oxidation' ) {
117                    if ( MU::issetJS( $b5["d"] ) ) {
118                        $res .= "{\\vphantom{A}}";
119                        $res .= "^{" . $b5["d"] . "}";
120                    }
121                    if ( MU::issetJS( $b5["q"] ) ) {
122                        $res .= "{\\vphantom{A}}";
123                        $res .= "_{\\smash[t]{" . $b5["q"] . "}}";
124                    }
125                } else {
126                    if ( MU::issetJS( $b5["q"] ) ) {
127                        $res .= "{\\vphantom{A}}";
128                        $res .= "_{\\smash[t]{" . $b5["q"] . "}}";
129                    }
130                    if ( MU::issetJS( $b5["d"] ) ) {
131                        $res .= "{\\vphantom{A}}";
132                        $res .= "^{" . $b5["d"] . "}";
133                    }
134                }
135                break;
136            case 'roman numeral':
137            case 'rm':
138                $res = "\\mathrm{" . $buf["p1"] . "}";
139                break;
140            case 'text':
141                if ( preg_match( "/[\^_]/", $buf["p1"] ) ) {
142                    $buf["p1"] = self::strReplaceFirst( "-", "\\text{-}",
143                        self::strReplaceFirst( " ", "~", $buf["p1"] ) );
144                    $res = "\\mathrm{" . $buf["p1"] . "}";
145                } else {
146                    $res = "\\text{" . $buf["p1"] . "}";
147                }
148                break;
149            case 'state of aggregation':
150                $res = ( !$this->optimizeForTexVC ? "\\mskip2mu " : "\\mskip{2mu} " ) . self::goInner( $buf["p1"] );
151                break;
152            case 'state of aggregation subscript':
153                $res = ( !$this->optimizeForTexVC ? "\\mskip1mu " : "\\mskip{1mu} " ) . self::goInner( $buf["p1"] );
154                break;
155            case 'bond':
156                $res = self::getBond( $buf["kind_"] );
157                if ( !$res ) {
158                    throw new RuntimeException( "MhchemErrorBond: mhchem Error. Unknown bond type ("
159                        . $buf["kind_"] . ")" );
160                }
161                break;
162            case 'frac':
163                $c = "\\frac{" . $buf["p1"] . "}{" . $buf["p2"] . "}";
164                $res = "\\mathchoice{\\textstyle" . $c . "}{" . $c . "}{" . $c . "}{" . $c . "}";
165                break;
166            case 'pu-frac':
167                $d = "\\frac{" . self::goInner( $buf["p1"] ) . "}{" . self::goInner( $buf["p2"] ) . "}";
168                $res = "\\mathchoice{\\textstyle" . $d . "}{" . $d . "}{" . $d . "}{" . $d . "}";
169                break;
170            case '1st-level escape':
171            case 'tex-math':
172                $res = $buf["p1"] . " ";
173                break;
174            case 'frac-ce':
175                $res = "\\frac{" . self::goInner( $buf["p1"] ) . "}{" . self::goInner( $buf["p2"] ) . "}";
176                break;
177            case 'overset':
178                $res = "\\overset{" . self::goInner( $buf["p1"] ) . "}{" . self::goInner( $buf["p2"] ) . "}";
179                break;
180            case 'underset':
181                $res = "\\underset{" . self::goInner( $buf["p1"] ) . "}{" . self::goInner( $buf["p2"] ) . "}";
182                break;
183            case 'underbrace':
184                $res = "\\underbrace{" . self::goInner( $buf["p1"] ) . "}_{" . self::goInner( $buf["p2"] ) . "}";
185                break;
186            case 'color':
187                $res = "{\\color{" . $buf["color1"] . "}{" . self::goInner( $buf["color2"] ) . "}}";
188                break;
189            case 'color0':
190                $res = "\\color{" . $buf["color"] . "}";
191                break;
192            case 'arrow':
193                $b6 = [
194                    "rd" => self::goInner( $buf["rd"] ),
195                    "rq" => self::goInner( $buf["rq"] )
196                ];
197                $arrow = self::getArrow( $buf["r"] );
198                if ( MU::issetJS( $b6["rd"] ) || MU::issetJS( $b6["rq"] ) ) {
199                    if ( $buf["r"] === "<=>" || $buf["r"] === "<=>>" || $buf["r"] === "<<=>" || $buf["r"] === "<-->" ) {
200                        $arrow = "\\long" . $arrow;
201                        if ( MU::issetJS( $b6["rd"] ) ) {
202                            $arrow = "\\overset{" . $b6["rd"] . "}{" . $arrow . "}";
203                        }
204                        if ( MU::issetJS( $b6["rq"] ) ) {
205                            if ( $buf["r"] === "<-->" ) {
206                                $arrow = !$this->optimizeForTexVC ?
207                                    "\\underset{\\lower2mu{" . $b6["rq"] . "}}{" . $arrow . "}"
208                                    : "\\underset{\\lower{2mu}{" . $b6["rq"] . "}}{" . $arrow . "}";
209                            } else {
210                                $arrow = !$this->optimizeForTexVC ?
211                                    "\\underset{\\lower6mu{" . $b6["rq"] . "}}{" . $arrow . "}"
212                                    : "\\underset{\\lower{6mu}{" . $b6["rq"] . "}}{" . $arrow . "}";
213                            }
214                        }
215                        $arrow = " {}\\mathrel{" . $arrow . "}{} ";
216                    } else {
217                        if ( MU::issetJS( $b6["rq"] ) ) {
218                            $arrow .= "[{" . $b6["rq"] . "}]";
219                        }
220                        $arrow .= "{" . $b6["rd"] . "}";
221                        $arrow = " {}\\mathrel{\\x" . $arrow . "}{} ";
222                    }
223                } else {
224                    $arrow = " {}\\mathrel{\\long" . $arrow . "}{} ";
225                }
226                $res = $arrow;
227                break;
228            case 'operator':
229                $res = self::getOperator( $buf["kind_"] );
230                break;
231            default:
232                $res = null;
233        }
234        if ( $res !== null ) {
235            return $res;
236        }
237
238        switch ( $buf["type_"] ) {
239            case 'space':
240                $res = " ";
241                break;
242            case 'tinySkip':
243                $res = !$this->optimizeForTexVC ? '\\mkern2mu' : '\\mkern{2mu}';
244                break;
245            case 'pu-space-1':
246            case 'entitySkip':
247                $res = "~";
248                break;
249            case 'pu-space-2':
250                $res = !$this->optimizeForTexVC ? "\\mkern3mu " : "\\mkern{3mu} ";
251                break;
252            case '1000 separator':
253                $res = !$this->optimizeForTexVC ? "\\mkern2mu " : "\\mkern{2mu} ";
254                break;
255            case 'commaDecimal':
256                $res = "{,}";
257                break;
258            case 'comma enumeration L':
259                $res = "{" . $buf["p1"] . "}" . ( !$this->optimizeForTexVC ? "\\mkern6mu " : "\\mkern{6mu} " );
260                break;
261            case 'comma enumeration M':
262                $res = "{" . $buf["p1"] . "}" . ( !$this->optimizeForTexVC ? "\\mkern3mu " : "\\mkern{3mu} " );
263                break;
264            case 'comma enumeration S':
265                $res = "{" . $buf["p1"] . "}" . ( !$this->optimizeForTexVC ? "\\mkern1mu " : "\\mkern{1mu} " );
266                break;
267            case 'hyphen':
268                $res = "\\text{-}";
269                break;
270            case 'addition compound':
271                $res = "\\,{\\cdot}\\,";
272                break;
273            case 'electron dot':
274                $res = !$this->optimizeForTexVC ?
275                    "\\mkern1mu \\bullet\\mkern1mu " : "\\mkern{1mu} \\bullet\\mkern{1mu} ";
276                break;
277            case 'KV x':
278                $res = "{\\times}";
279                break;
280            case 'prime':
281                $res = "\\prime ";
282                break;
283            case 'cdot':
284                $res = "\\cdot ";
285                break;
286            case 'tight cdot':
287                $res = !$this->optimizeForTexVC ? "\\mkern1mu{\\cdot}\\mkern1mu " : "\\mkern{1mu}{\\cdot}\\mkern{1mu} ";
288                break;
289            case 'times':
290                $res = "\\times ";
291                break;
292            case 'circa':
293                $res = "{\\sim}";
294                break;
295            case '^':
296                $res = "uparrow";
297                break;
298            case 'v':
299                $res = "downarrow";
300                break;
301            case 'ellipsis':
302                $res = "\\ldots ";
303                break;
304            case '/':
305                $res = "/";
306                break;
307            case ' / ':
308                $res = "\\,/\\,";
309                break;
310            default:
311                throw new RuntimeException( "MhchemBugT: mhchem bug T. Please report." );
312        }
313        return $res;
314    }
315
316    private function getArrow( string $a ): string {
317        switch ( $a ) {
318            case "\u2192":
319            case "\u27F6":
320            case "->":
321                return "rightarrow";
322            case "<-":
323                return "leftarrow";
324            case "<->":
325                return "leftrightarrow";
326            case "<-->":
327                return "leftrightarrows";
328            case "\u21CC":
329            case "<=>":
330                return "rightleftharpoons";
331            case "<=>>":
332                return "Rightleftharpoons";
333            case "<<=>":
334                return "Leftrightharpoons";
335            default:
336                throw new RuntimeException( "MhchemBugT: mhchem bug T. Please report." );
337        }
338    }
339
340    private function getBond( string $a ): string {
341        switch ( $a ) {
342            case "1":
343            case "-":
344                return "{-}";
345            case "2":
346            case "=":
347                return "{=}";
348            case "3":
349            case "#":
350                return "{\\equiv}";
351            case "~":
352                return "{\\tripledash}";
353            case "~-":
354                return !$this->optimizeForTexVC ? "{\\rlap{\\lower.1em{-}}\\raise.1em{\\tripledash}}"
355                    : "{\\rlap{\\lower{.1em}{-}}\\raise{.1em}{\\tripledash}}";
356            case "~--":
357            case "~=":
358                return !$this->optimizeForTexVC ? "{\\rlap{\\lower.2em{-}}\\rlap{\\raise.2em{\\tripledash}}-}"
359                    : "{\\rlap{\\lower{.2em}{-}}\\rlap{\\raise{.2em}{\\tripledash}}-}";
360            case "-~-":
361                return !$this->optimizeForTexVC ? "{\\rlap{\\lower.2em{-}}\\rlap{\\raise.2em{-}}\\tripledash}"
362                    : "{\\rlap{\\lower{.2em}{-}}\\rlap{\\raise{.2em}{-}}\\tripledash}";
363            case "...":
364                return "{{\\cdot}{\\cdot}{\\cdot}}";
365            case "....":
366                return "{{\\cdot}{\\cdot}{\\cdot}{\\cdot}}";
367            case "->":
368                return "{\\rightarrow}";
369            case "<-":
370                return "{\\leftarrow}";
371            case "<":
372                return "{<}";
373            case ">":
374                return "{>}";
375            default:
376                throw new RuntimeException( "MhchemBugT: mhchem bug T. Please report." );
377        }
378    }
379
380    private function getOperator( string $a ): string {
381        switch ( $a ) {
382            case "+":
383                return " {}+{} ";
384            case "-":
385                return " {}-{} ";
386            case "=":
387                return " {}={} ";
388            case "<":
389                return " {}<{} ";
390            case ">":
391                return " {}>{} ";
392            case "<<":
393                return " {}\\ll{} ";
394            case ">>":
395                return " {}\\gg{} ";
396            case "\\pm":
397                return " {}\\pm{} ";
398            case "$\\approx$":
399            case "\\approx":
400                return " {}\\approx{} ";
401            case "(v)":
402            case "v":
403                return " \\downarrow{} ";
404            case "(^)":
405            case "^":
406                return " \\uparrow{} ";
407            default:
408                throw new RuntimeException( "MhchemBugT: mhchem bug T. Please report." );
409        }
410    }
411
412}