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 | /** |
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 | } |