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