Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 302 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
MhchemTexify | |
0.00% |
0 / 302 |
|
0.00% |
0 / 8 |
22952 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
go | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
72 | |||
goInner | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
strReplaceFirst | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
go2 | |
0.00% |
0 / 207 |
|
0.00% |
0 / 1 |
8190 | |||
getArrow | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
156 | |||
getBond | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
506 | |||
getOperator | |
0.00% |
0 / 26 |
|
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 | |
9 | namespace MediaWiki\Extension\Math\WikiTexVC\Mhchem; |
10 | |
11 | use MediaWiki\Extension\Math\WikiTexVC\MHChem\MhchemUtil as MU; |
12 | use 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 | */ |
23 | class 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 | } |