Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
22.42% covered (danger)
22.42%
50 / 223
25.00% covered (danger)
25.00%
4 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
ConverterRule
22.52% covered (danger)
22.52%
50 / 222
25.00% covered (danger)
25.00%
4 / 16
4845.27
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTextInBidtable
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 parseFlags
22.50% covered (danger)
22.50%
9 / 40
0.00% covered (danger)
0.00%
0 / 1
135.16
 parseRules
31.82% covered (danger)
31.82%
14 / 44
0.00% covered (danger)
0.00%
0 / 1
86.32
 getRulesDesc
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getRuleConvertedStr
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 getRuleConvertedTitle
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 generateConvTable
11.54% covered (danger)
11.54%
3 / 26
0.00% covered (danger)
0.00%
0 / 1
149.68
 parse
31.25% covered (danger)
31.25%
20 / 64
0.00% covered (danger)
0.00%
0 / 1
282.76
 hasRules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDisplay
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRulesAction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getConvTable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFlags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 * @author fdcn <fdcn64@gmail.com>, PhiLiP <philip.npc@gmail.com>
6 */
7
8namespace MediaWiki\Language;
9
10use MediaWiki\Logger\LoggerFactory;
11use Wikimedia\StringUtils\StringUtils;
12
13/**
14 * The rules used for language conversion, this processes the rules
15 * extracted by Parser from the `-{ }-` wikitext syntax.
16 *
17 * @ingroup Language
18 */
19class ConverterRule {
20    /**
21     * @var LanguageConverter
22     */
23    public $mConverter;
24    /** @var string|false */
25    public $mRuleDisplay = '';
26    /** @var string|false */
27    public $mRuleTitle = false;
28    /**
29     * @var string the text of the rules
30     */
31    public $mRules = '';
32    /** @var string */
33    public $mRulesAction = 'none';
34    /** @var array */
35    public $mFlags = [];
36    /** @var array */
37    public $mVariantFlags = [];
38    /** @var array */
39    public $mConvTable = [];
40    /**
41     * @var array of the translation in each variant
42     */
43    public $mBidtable = [];
44    /**
45     * @var array of the translation in each variant
46     */
47    public $mUnidtable = [];
48
49    /**
50     * @param LanguageConverter $converter
51     */
52    public function __construct( LanguageConverter $converter ) {
53        $this->mConverter = $converter;
54    }
55
56    /**
57     * Check if the variant array is in the convert array.
58     *
59     * @param array|string $variants Variant language code
60     * @return string|false Translated text
61     */
62    public function getTextInBidtable( $variants ) {
63        $variants = (array)$variants;
64        if ( !$variants ) {
65            return false;
66        }
67        foreach ( $variants as $variant ) {
68            if ( isset( $this->mBidtable[$variant] ) ) {
69                return $this->mBidtable[$variant];
70            }
71        }
72        return false;
73    }
74
75    /**
76     * Parse flags with syntax -{FLAG| ... }-
77     */
78    private function parseFlags( string $text ) {
79        $flags = [];
80        $variantFlags = [];
81
82        $sepPos = strpos( $text, '|' );
83        if ( $sepPos !== false ) {
84            $validFlags = $this->mConverter->getFlags();
85            $f = StringUtils::explode( ';', substr( $text, 0, $sepPos ) );
86            foreach ( $f as $ff ) {
87                $ff = trim( $ff );
88                if ( isset( $validFlags[$ff] ) ) {
89                    $flags[$validFlags[$ff]] = true;
90                }
91            }
92            $text = substr( $text, $sepPos + 1 );
93        }
94
95        if ( !$flags ) {
96            $flags['S'] = true;
97        } elseif ( isset( $flags['R'] ) ) {
98            // remove other flags
99            $flags = [ 'R' => true ];
100        } elseif ( isset( $flags['N'] ) ) {
101            // remove other flags
102            $flags = [ 'N' => true ];
103        } elseif ( isset( $flags['-'] ) ) {
104            // remove other flags
105            $flags = [ '-' => true ];
106        } elseif ( count( $flags ) === 1 && isset( $flags['T'] ) ) {
107            $flags['H'] = true;
108        } elseif ( isset( $flags['H'] ) ) {
109            // replace A flag, and remove other flags except T
110            $temp = [ '+' => true, 'H' => true ];
111            if ( isset( $flags['T'] ) ) {
112                $temp['T'] = true;
113            }
114            if ( isset( $flags['D'] ) ) {
115                $temp['D'] = true;
116            }
117            $flags = $temp;
118        } else {
119            if ( isset( $flags['A'] ) ) {
120                $flags['+'] = true;
121                $flags['S'] = true;
122            }
123            if ( isset( $flags['D'] ) ) {
124                unset( $flags['S'] );
125            }
126            // try to find flags like "zh-hans", "zh-hant"
127            // allow syntaxes like "-{zh-hans;zh-hant|XXXX}-"
128            $variantFlags = array_intersect( array_keys( $flags ), $this->mConverter->getVariants() );
129            if ( $variantFlags ) {
130                $variantFlags = array_fill_keys( $variantFlags, true );
131                $flags = [];
132            }
133        }
134        $this->mVariantFlags = $variantFlags;
135        $this->mRules = $text;
136        $this->mFlags = $flags;
137    }
138
139    /**
140     * Generate conversion table.
141     */
142    private function parseRules() {
143        $rules = $this->mRules;
144        $bidtable = [];
145        $unidtable = [];
146        $varsep_pattern = $this->mConverter->getVarSeparatorPattern();
147
148        // Split text according to $varsep_pattern, but ignore semicolons from HTML entities
149        $rules = preg_replace( '/(&[#a-zA-Z0-9]+);/', "$1\x01", $rules );
150        $choice = preg_split( $varsep_pattern, $rules );
151        if ( $choice === false ) {
152            $error = preg_last_error();
153            $errorText = preg_last_error_msg();
154            LoggerFactory::getInstance( 'parser' )->warning(
155                'ConverterRule preg_split error: {code} {errorText}',
156                [
157                    'code' => $error,
158                    'errorText' => $errorText
159                ]
160            );
161            $choice = [];
162        }
163        $choice = str_replace( "\x01", ';', $choice );
164
165        foreach ( $choice as $c ) {
166            $v = explode( ':', $c, 2 );
167            if ( count( $v ) !== 2 ) {
168                // syntax error, skip
169                continue;
170            }
171            $to = trim( $v[1] );
172            $v = trim( $v[0] );
173            $u = explode( '=>', $v, 2 );
174            $vv = $this->mConverter->validateVariant( $v );
175            // if $to is empty (which is also used as $from in bidtable),
176            // strtr() could return a wrong result.
177            if ( count( $u ) === 1 && $to !== '' && $vv ) {
178                $bidtable[$vv] = $to;
179            } elseif ( count( $u ) === 2 ) {
180                $from = trim( $u[0] );
181                $v = trim( $u[1] );
182                $vv = $this->mConverter->validateVariant( $v );
183                // if $from is empty, strtr() could return a wrong result.
184                if ( array_key_exists( $vv, $unidtable )
185                    && !is_array( $unidtable[$vv] )
186                    && $from !== ''
187                    && $vv ) {
188                    $unidtable[$vv] = [ $from => $to ];
189                } elseif ( $from !== '' && $vv ) {
190                    $unidtable[$vv][$from] = $to;
191                }
192            }
193            // syntax error, pass
194            if ( !isset( $this->mConverter->getVariantNames()[$vv] ) ) {
195                $bidtable = [];
196                $unidtable = [];
197                break;
198            }
199        }
200        $this->mBidtable = $bidtable;
201        $this->mUnidtable = $unidtable;
202    }
203
204    /**
205     * @return string
206     */
207    private function getRulesDesc() {
208        $codesep = $this->mConverter->getDescCodeSeparator();
209        $varsep = $this->mConverter->getDescVarSeparator();
210        $text = '';
211        foreach ( $this->mBidtable as $k => $v ) {
212            $text .= $this->mConverter->getVariantNames()[$k] . "$codesep$v$varsep";
213        }
214        foreach ( $this->mUnidtable as $k => $a ) {
215            foreach ( $a as $from => $to ) {
216                $text .= $from . '⇒' . $this->mConverter->getVariantNames()[$k] .
217                    "$codesep$to$varsep";
218            }
219        }
220        return $text;
221    }
222
223    /**
224     * Parse rules conversion.
225     *
226     * @param string $variant
227     *
228     * @return string
229     */
230    private function getRuleConvertedStr( $variant ) {
231        $bidtable = $this->mBidtable;
232        $unidtable = $this->mUnidtable;
233
234        if ( count( $bidtable ) + count( $unidtable ) === 0 ) {
235            return $this->mRules;
236        }
237
238        // display current variant in bidirectional array
239        $disp = $this->getTextInBidtable( $variant );
240        // or display current variant in fallbacks
241        if ( $disp === false ) {
242            $disp = $this->getTextInBidtable(
243                $this->mConverter->getVariantFallbacks( $variant ) );
244        }
245        // or display current variant in unidirectional array
246        if ( $disp === false && array_key_exists( $variant, $unidtable ) ) {
247            $disp = array_values( $unidtable[$variant] )[0];
248        }
249        // or display first text under disable manual convert
250        if ( $disp === false && $this->mConverter->getManualLevel()[$variant] === 'disable' ) {
251            if ( count( $bidtable ) > 0 ) {
252                $disp = array_values( $bidtable )[0];
253            } else {
254                $disp = array_values( array_values( $unidtable )[0] )[0];
255            }
256        }
257
258        return $disp;
259    }
260
261    /**
262     * Similar to getRuleConvertedStr(), but this prefers to use MediaWiki\Title\Title;
263     * use original page title if $variant === $this->mConverter->getMainCode(),
264     * and may return false in this case (so this title conversion rule
265     * will be ignored and the original title is shown).
266     *
267     * @since 1.22
268     * @param string $variant The variant code to display page title in
269     * @return string|false The converted title or false if just page name
270     */
271    private function getRuleConvertedTitle( $variant ) {
272        if ( $variant === $this->mConverter->getMainCode() ) {
273            // If a string targeting exactly this variant is set,
274            // use it. Otherwise, just return false, so the real
275            // page name can be shown (and because variant === main,
276            // there'll be no further automatic conversion).
277            $disp = $this->getTextInBidtable( $variant );
278            if ( $disp ) {
279                return $disp;
280            }
281            if ( array_key_exists( $variant, $this->mUnidtable ) ) {
282                $disp = array_values( $this->mUnidtable[$variant] )[0];
283            }
284            // Assigned above or still false.
285            return $disp;
286        }
287
288        return $this->getRuleConvertedStr( $variant );
289    }
290
291    /**
292     * Generate conversion table for all text.
293     */
294    private function generateConvTable() {
295        // Special case optimisation
296        if ( !$this->mBidtable && !$this->mUnidtable ) {
297            $this->mConvTable = [];
298            return;
299        }
300
301        $bidtable = $this->mBidtable;
302        $unidtable = $this->mUnidtable;
303        $manLevel = $this->mConverter->getManualLevel();
304
305        $vmarked = [];
306        foreach ( $this->mConverter->getVariants() as $v ) {
307            /* for bidirectional array
308                fill in the missing variants, if any,
309                with fallbacks */
310            if ( !isset( $bidtable[$v] ) ) {
311                $variantFallbacks =
312                    $this->mConverter->getVariantFallbacks( $v );
313                $vf = $this->getTextInBidtable( $variantFallbacks );
314                if ( $vf ) {
315                    $bidtable[$v] = $vf;
316                }
317            }
318
319            if ( isset( $bidtable[$v] ) ) {
320                foreach ( $vmarked as $vo ) {
321                    // use syntax: -{A|zh:WordZh;zh-tw:WordTw}-
322                    // or -{H|zh:WordZh;zh-tw:WordTw}-
323                    // or -{-|zh:WordZh;zh-tw:WordTw}-
324                    // to introduce a custom mapping between
325                    // words WordZh and WordTw in the whole text
326                    if ( $manLevel[$v] === 'bidirectional' ) {
327                        $this->mConvTable[$v][$bidtable[$vo]] = $bidtable[$v];
328                    }
329                    if ( $manLevel[$vo] === 'bidirectional' ) {
330                        $this->mConvTable[$vo][$bidtable[$v]] = $bidtable[$vo];
331                    }
332                }
333                $vmarked[] = $v;
334            }
335            /* for unidirectional array fill to convert tables */
336            if ( ( $manLevel[$v] === 'bidirectional' || $manLevel[$v] === 'unidirectional' )
337                && isset( $unidtable[$v] )
338            ) {
339                if ( isset( $this->mConvTable[$v] ) ) {
340                    $this->mConvTable[$v] = $unidtable[$v] + $this->mConvTable[$v];
341                } else {
342                    $this->mConvTable[$v] = $unidtable[$v];
343                }
344            }
345        }
346    }
347
348    /**
349     * Parse rules and flags.
350     * @param string $inner The contents of the rule between -{ and }-
351     * @param string|null $variant Variant language code
352     */
353    public function parse( string $inner, ?string $variant = null ): void {
354        if ( !$variant ) {
355            $variant = $this->mConverter->getPreferredVariant();
356        }
357
358        $this->parseFlags( $inner );
359        $flags = $this->mFlags;
360
361        // convert to specified variant
362        // syntax: -{zh-hans;zh-hant[;...]|<text to convert>}-
363        if ( $this->mVariantFlags ) {
364            // check if current variant in flags
365            if ( isset( $this->mVariantFlags[$variant] ) ) {
366                // then convert <text to convert> to current language
367                $this->mRules = $this->mConverter->autoConvert( $this->mRules,
368                    $variant );
369            } else {
370                // if the current variant is not in flags,
371                // then we check its fallback variants.
372                $variantFallbacks =
373                    $this->mConverter->getVariantFallbacks( $variant );
374                if ( is_array( $variantFallbacks ) ) {
375                    foreach ( $variantFallbacks as $variantFallback ) {
376                        // if current variant's fallback exist in flags
377                        if ( isset( $this->mVariantFlags[$variantFallback] ) ) {
378                            // then convert <text to convert> to fallback language
379                            $this->mRules =
380                                $this->mConverter->autoConvert( $this->mRules,
381                                    $variantFallback );
382                            break;
383                        }
384                    }
385                }
386            }
387            $this->mFlags = $flags = [ 'R' => true ];
388        }
389
390        if ( !isset( $flags['R'] ) && !isset( $flags['N'] ) ) {
391            // decode => HTML entities modified by Sanitizer::internalRemoveHtmlTags
392            $this->mRules = str_replace( '=&gt;', '=>', $this->mRules );
393            $this->parseRules();
394        }
395        $rules = $this->mRules;
396
397        if ( !$this->mBidtable && !$this->mUnidtable ) {
398            if ( isset( $flags['+'] ) || isset( $flags['-'] ) ) {
399                // fill all variants if the text in -{A/H/-|text}- is non-empty but without rules
400                if ( $rules !== '' ) {
401                    foreach ( $this->mConverter->getVariants() as $v ) {
402                        $this->mBidtable[$v] = $rules;
403                    }
404                }
405            } elseif ( !isset( $flags['N'] ) && !isset( $flags['T'] ) ) {
406                $this->mFlags = $flags = [ 'R' => true ];
407            }
408        }
409
410        $this->mRuleDisplay = false;
411        foreach ( $flags as $flag => $unused ) {
412            switch ( $flag ) {
413                case 'R':
414                    // if we don't do content convert, still strip the -{}- tags
415                    $this->mRuleDisplay = $rules;
416                    break;
417                case 'N':
418                    // process N flag: output current variant name
419                    $ruleVar = trim( $rules );
420                    $this->mRuleDisplay = $this->mConverter->getVariantNames()[$ruleVar] ?? '';
421                    break;
422                case 'D':
423                    // process D flag: output rules description
424                    $this->mRuleDisplay = $this->getRulesDesc();
425                    break;
426                case 'H':
427                    // process H,- flag or T only: output nothing
428                    $this->mRuleDisplay = '';
429                    break;
430                case '-':
431                    $this->mRulesAction = 'remove';
432                    $this->mRuleDisplay = '';
433                    break;
434                case '+':
435                    $this->mRulesAction = 'add';
436                    $this->mRuleDisplay = '';
437                    break;
438                case 'S':
439                    $this->mRuleDisplay = $this->getRuleConvertedStr( $variant );
440                    break;
441                case 'T':
442                    $this->mRuleTitle = $this->getRuleConvertedTitle( $variant );
443                    $this->mRuleDisplay = '';
444                    break;
445                default:
446                    // ignore unknown flags (but see error-case below)
447            }
448        }
449        if ( $this->mRuleDisplay === false ) {
450            $this->mRuleDisplay = '<span class="error">'
451                . wfMessage( 'converter-manual-rule-error' )->inContentLanguage()->escaped()
452                . '</span>';
453        }
454
455        $this->generateConvTable();
456    }
457
458    /**
459     * Checks if there are conversion rules.
460     * @return bool
461     */
462    public function hasRules() {
463        return $this->mRules !== '';
464    }
465
466    /**
467     * Get display text on markup -{...}-
468     * @return string
469     */
470    public function getDisplay() {
471        return $this->mRuleDisplay;
472    }
473
474    /**
475     * Get converted title.
476     * @return string|false
477     */
478    public function getTitle() {
479        return $this->mRuleTitle;
480    }
481
482    /**
483     * Return how to deal with conversion rules.
484     * @return string
485     */
486    public function getRulesAction() {
487        return $this->mRulesAction;
488    }
489
490    /**
491     * Get conversion table. (bidirectional and unidirectional
492     * conversion table)
493     * @return array
494     */
495    public function getConvTable() {
496        return $this->mConvTable;
497    }
498
499    /**
500     * Get conversion rules string.
501     * @return string
502     */
503    public function getRules() {
504        return $this->mRules;
505    }
506
507    /**
508     * Get conversion flags.
509     * @return array
510     */
511    public function getFlags() {
512        return $this->mFlags;
513    }
514}
515
516/** @deprecated class alias since 1.43 */
517class_alias( ConverterRule::class, 'ConverterRule' );