Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.26% covered (warning)
72.26%
284 / 393
30.43% covered (danger)
30.43%
7 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
ConverterRule
72.45% covered (warning)
72.45%
284 / 392
30.43% covered (danger)
30.43%
7 / 23
695.37
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getTextInBidtable
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getFragmentInBidtable
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getValueInBidtable
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 parseFlags
80.49% covered (warning)
80.49%
33 / 41
0.00% covered (danger)
0.00%
0 / 1
17.90
 parseRules
68.89% covered (warning)
68.89%
31 / 45
0.00% covered (danger)
0.00%
0 / 1
23.71
 getRulesDesc
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
6.10
 getRulesDescFragment
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
4
 getRuleConvertedValue
53.33% covered (warning)
53.33%
8 / 15
0.00% covered (danger)
0.00%
0 / 1
14.50
 getRuleConvertedValueForTitle
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
6.00
 generateConvTable
94.44% covered (success)
94.44%
34 / 36
0.00% covered (danger)
0.00%
0 / 1
20.07
 parse
65.62% covered (warning)
65.62%
42 / 64
0.00% covered (danger)
0.00%
0 / 1
59.85
 parseElement
67.53% covered (warning)
67.53%
52 / 77
0.00% covered (danger)
0.00%
0 / 1
57.78
 hasRules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDisplay
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
3.47
 getDisplayFragment
41.18% covered (danger)
41.18%
7 / 17
0.00% covered (danger)
0.00%
0 / 1
4.83
 getTitle
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getTitleFragment
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getTitleValue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 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\Debug\DeprecationHelper;
11use MediaWiki\Logger\LoggerFactory;
12use Wikimedia\Assert\Assert;
13use Wikimedia\Parsoid\DOM\Document;
14use Wikimedia\Parsoid\DOM\DocumentFragment;
15use Wikimedia\Parsoid\DOM\Element;
16use Wikimedia\Parsoid\Utils\DOMCompat;
17use Wikimedia\Parsoid\Utils\DOMDataUtils;
18use Wikimedia\Parsoid\Utils\DOMUtils;
19use Wikimedia\Parsoid\Utils\WTUtils;
20use Wikimedia\StringUtils\StringUtils;
21
22/**
23 * The rules used for language conversion, this processes the rules
24 * extracted by Parser from the `-{ }-` wikitext syntax.
25 *
26 * @ingroup Language
27 */
28class ConverterRule {
29    use DeprecationHelper;
30
31    private LanguageConverter $mConverter;
32
33    private DocumentFragment|string|false $mRuleDisplay = '';
34
35    private DocumentFragment|string|false $mRuleTitle = false;
36    /**
37     * @var string the text of the rules
38     */
39    private $mRules = '';
40    /** @var string */
41    private $mRulesAction = 'none';
42    /** @var array */
43    private $mFlags = [];
44    /** @var array */
45    private $mVariantFlags = [];
46    /** @var array */
47    private $mConvTable = [];
48    /**
49     * @var array<string,DocumentFragment|string> the translation in each variant
50     */
51    private $mBidtable = [];
52    /**
53     * @var array<string,array<string,DocumentFragment|string>> the translation in each variant
54     */
55    private $mUnidtable = [];
56
57    /**
58     * @param LanguageConverter $converter
59     */
60    public function __construct( LanguageConverter $converter ) {
61        $this->mConverter = $converter;
62        $this->deprecatePublicProperty( 'mConverter', '1.46', __CLASS__ );
63        $this->deprecatePublicProperty( 'mRuleDisplay', '1.46', __CLASS__ );
64        $this->deprecatePublicProperty( 'mRuleTitle', '1.46', __CLASS__ );
65        $this->deprecatePublicProperty( 'mRules', '1.46', __CLASS__ );
66        $this->deprecatePublicProperty( 'mRulesAction', '1.46', __CLASS__ );
67        $this->deprecatePublicProperty( 'mFlags', '1.46', __CLASS__ );
68        $this->deprecatePublicProperty( 'mVariantFlags', '1.46', __CLASS__ );
69        $this->deprecatePublicProperty( 'mConvTable', '1.46', __CLASS__ );
70        $this->deprecatePublicProperty( 'mBidtable', '1.46', __CLASS__ );
71        $this->deprecatePublicProperty( 'mUnidtable', '1.46', __CLASS__ );
72    }
73
74    /**
75     * Check if the variant array is in the convert array.
76     *
77     * @param array|string $variants Variant language code
78     * @return string|false Translated text
79     */
80    public function getTextInBidtable( $variants ): string|false {
81        $val = $this->getValueInBidtable( $variants );
82        if ( $val instanceof DocumentFragment ) {
83            return $val->textContent;
84        } else {
85            return $val ?? false;
86        }
87    }
88
89    /**
90     * Check if the variant array is in the convert array.
91     *
92     * @param Document $ownerDocument An owner document for the returned fragment
93     * @param array|string $variants Variant language code
94     * @return ?DocumentFragment Translated text
95     */
96    public function getFragmentInBidtable( Document $ownerDocument, $variants ): ?DocumentFragment {
97        $val = $this->getValueInBidtable( $variants );
98        if ( $val instanceof DocumentFragment || $val === null ) {
99            return $val;
100        }
101        $df = $ownerDocument->createDocumentFragment();
102        DOMCompat::replaceChildren( $df, $val );
103        return $df;
104    }
105
106    /**
107     * Check if the variant array is in the convert array.
108     *
109     * @param array|string $variants Variant language code
110     * @return DocumentFragment|string|null Translated contents
111     */
112    private function getValueInBidtable( $variants ) {
113        $variants = (array)$variants;
114        if ( !$variants ) {
115            return null;
116        }
117        foreach ( $variants as $variant ) {
118            if ( isset( $this->mBidtable[$variant] ) ) {
119                return $this->mBidtable[$variant];
120            }
121        }
122        return null;
123    }
124
125    /**
126     * Parse flags with syntax -{FLAG| ... }-
127     */
128    private function parseFlags( string $text ) {
129        $flags = [];
130        $variantFlags = [];
131
132        $sepPos = strpos( $text, '|' );
133        if ( $sepPos !== false ) {
134            $validFlags = $this->mConverter->getFlags();
135            $f = StringUtils::explode( ';', substr( $text, 0, $sepPos ) );
136            foreach ( $f as $ff ) {
137                $ff = trim( $ff );
138                // Allow variants to be specified as BCP-47
139                $ff = $this->mConverter->validateVariant( $ff ) ?? $ff;
140                if ( isset( $validFlags[$ff] ) ) {
141                    $flags[$validFlags[$ff]] = true;
142                }
143            }
144            $text = substr( $text, $sepPos + 1 );
145        }
146
147        if ( !$flags ) {
148            $flags['S'] = true;
149        } elseif ( isset( $flags['R'] ) ) {
150            // remove other flags
151            $flags = [ 'R' => true ];
152        } elseif ( isset( $flags['N'] ) ) {
153            // remove other flags
154            $flags = [ 'N' => true ];
155        } elseif ( isset( $flags['-'] ) ) {
156            // remove other flags
157            $flags = [ '-' => true ];
158        } elseif ( count( $flags ) === 1 && isset( $flags['T'] ) ) {
159            $flags['H'] = true;
160        } elseif ( isset( $flags['H'] ) ) {
161            // replace A flag, and remove other flags except T
162            $temp = [ '+' => true, 'H' => true ];
163            if ( isset( $flags['T'] ) ) {
164                $temp['T'] = true;
165            }
166            if ( isset( $flags['D'] ) ) {
167                $temp['D'] = true;
168            }
169            $flags = $temp;
170        } else {
171            if ( isset( $flags['A'] ) ) {
172                $flags['+'] = true;
173                $flags['S'] = true;
174            }
175            if ( isset( $flags['D'] ) ) {
176                unset( $flags['S'] );
177            }
178            // try to find flags like "zh-hans", "zh-hant"
179            // allow syntaxes like "-{zh-hans;zh-hant|XXXX}-"
180            $variantFlags = array_intersect( array_keys( $flags ), $this->mConverter->getVariants() );
181            if ( $variantFlags ) {
182                $variantFlags = array_fill_keys( $variantFlags, true );
183                $flags = [];
184            }
185        }
186        $this->mVariantFlags = $variantFlags;
187        $this->mRules = $text;
188        $this->mFlags = $flags;
189    }
190
191    /**
192     * Generate conversion table.
193     */
194    private function parseRules() {
195        $rules = $this->mRules;
196        $bidtable = [];
197        $unidtable = [];
198        $varsep_pattern = $this->mConverter->getVarSeparatorPattern();
199
200        // Split text according to $varsep_pattern, but ignore semicolons from HTML entities
201        $rules = preg_replace( '/(&[#a-zA-Z0-9]+);/', "$1\x01", $rules );
202        $choice = preg_split( $varsep_pattern, $rules );
203        if ( $choice === false ) {
204            $error = preg_last_error();
205            $errorText = preg_last_error_msg();
206            LoggerFactory::getInstance( 'parser' )->warning(
207                'ConverterRule preg_split error: {code} {errorText}',
208                [
209                    'code' => $error,
210                    'errorText' => $errorText
211                ]
212            );
213            $choice = [];
214        }
215        $choice = str_replace( "\x01", ';', $choice );
216
217        foreach ( $choice as $c ) {
218            $v = explode( ':', $c, 2 );
219            if ( count( $v ) !== 2 ) {
220                // syntax error, skip
221                continue;
222            }
223            $to = trim( $v[1] );
224            $v = trim( $v[0] );
225            $u = explode( '=>', $v, 2 );
226            $vv = $this->mConverter->validateVariant( $v );
227            // if $to is empty (which is also used as $from in bidtable),
228            // strtr() could return a wrong result.
229            if ( count( $u ) === 1 && $to !== '' && $vv ) {
230                $bidtable[$vv] = $to;
231            } elseif ( count( $u ) === 2 ) {
232                $from = trim( $u[0] );
233                $v = trim( $u[1] );
234                $vv = $this->mConverter->validateVariant( $v );
235                // if $from is empty, strtr() could return a wrong result.
236                if ( $vv && array_key_exists( $vv, $unidtable )
237                    && !is_array( $unidtable[$vv] )
238                    && $from !== ''
239                ) {
240                    $unidtable[$vv] = [ $from => $to ];
241                } elseif ( $from !== '' && $vv ) {
242                    $unidtable[$vv][$from] = $to;
243                }
244            }
245            // syntax error, pass
246            if (
247                $vv === null ||
248                !isset( $this->mConverter->getVariantNames()[$vv] )
249            ) {
250                $bidtable = [];
251                $unidtable = [];
252                break;
253            }
254        }
255        $this->mBidtable = $bidtable;
256        $this->mUnidtable = $unidtable;
257    }
258
259    private function getRulesDesc(): string {
260        $codesep = $this->mConverter->getDescCodeSeparator();
261        $varsep = $this->mConverter->getDescVarSeparator();
262        $text = '';
263        foreach ( $this->mBidtable as $k => $v ) {
264            if ( $v instanceof DocumentFragment ) {
265                $v = $v->textContent;
266            }
267            $text .= $this->mConverter->getVariantNames()[$k] . "$codesep$v$varsep";
268        }
269        foreach ( $this->mUnidtable as $k => $a ) {
270            foreach ( $a as $from => $to ) {
271                if ( $to instanceof DocumentFragment ) {
272                    $to = $to->textContent;
273                }
274                $text .= $from . '⇒' . $this->mConverter->getVariantNames()[$k] .
275                    "$codesep$to$varsep";
276            }
277        }
278        return $text;
279    }
280
281    private function getRulesDescFragment( Document $ownerDocument ): DocumentFragment {
282        $codesep = $this->mConverter->getDescCodeSeparator();
283        $varsep = $this->mConverter->getDescVarSeparator();
284        $df = $ownerDocument->createDocumentFragment();
285        foreach ( $this->mBidtable as $k => $v ) {
286            DOMCompat::append(
287                $df,
288                $this->mConverter->getVariantNames()[$k],
289                $codesep,
290                DOMDataUtils::cloneDocumentFragment( $v ),
291                $varsep
292            );
293        }
294        foreach ( $this->mUnidtable as $k => $a ) {
295            foreach ( $a as $from => $to ) {
296                DOMCompat::append(
297                    $df,
298                    $from,
299                    '⇒',
300                    $this->mConverter->getVariantNames()[$k],
301                    $codesep,
302                    DOMDataUtils::cloneDocumentFragment( $to ),
303                    $varsep
304                );
305            }
306        }
307        return $df;
308    }
309
310    /**
311     * Parse rules conversion.
312     *
313     * @param string $variant
314     *
315     * @return DocumentFragment|string|null
316     */
317    private function getRuleConvertedValue( string $variant ): DocumentFragment|string|null {
318        $bidtable = $this->mBidtable;
319        $unidtable = $this->mUnidtable;
320
321        if ( count( $bidtable ) + count( $unidtable ) === 0 ) {
322            return $this->mRules;
323        }
324
325        // display current variant in bidirectional array
326        $disp = $this->getValueInBidtable( $variant );
327        // or display current variant in fallbacks
328        if ( $disp === null ) {
329            $disp = $this->getValueInBidtable(
330                $this->mConverter->getVariantFallbacks( $variant ) );
331        }
332        // or display current variant in unidirectional array
333        if ( $disp === null && array_key_exists( $variant, $unidtable ) ) {
334            $disp = array_values( $unidtable[$variant] )[0];
335        }
336        // or display first text under disable manual convert
337        if ( $disp === null && $this->mConverter->getManualLevel()[$variant] === 'disable' ) {
338            if ( count( $bidtable ) > 0 ) {
339                $disp = array_values( $bidtable )[0];
340            } else {
341                $disp = array_values( array_values( $unidtable )[0] )[0];
342            }
343        }
344
345        return $disp;
346    }
347
348    /**
349     * Similar to getRuleConvertedValue(), but this prefers to use the
350     * original page title if $variant === $this->mConverter->getMainCode(),
351     * and may return false in this case (so this title conversion rule
352     * will be ignored and the original title is shown).
353     *
354     * @since 1.22
355     * @param string $variant The variant code to display page title in
356     * @return DocumentFragment|string|null The converted title or null if just page name
357     */
358    private function getRuleConvertedValueForTitle( string $variant ): DocumentFragment|string|null {
359        if ( $variant === $this->mConverter->getMainCode() ) {
360            // If a string targeting exactly this variant is set,
361            // use it. Otherwise, just return false, so the real
362            // page name can be shown (and because variant === main,
363            // there'll be no further automatic conversion).
364            $disp = $this->getValueInBidtable( $variant );
365            if ( $disp ) {
366                return $disp;
367            }
368            if ( array_key_exists( $variant, $this->mUnidtable ) ) {
369                $disp = array_values( $this->mUnidtable[$variant] )[0];
370            }
371            // Assigned above or still null.
372            return $disp;
373        }
374
375        return $this->getRuleConvertedValue( $variant );
376    }
377
378    /**
379     * Generate conversion table for all text.
380     */
381    private function generateConvTable() {
382        // Special case optimisation
383        if ( !$this->mBidtable && !$this->mUnidtable ) {
384            $this->mConvTable = [];
385            return;
386        }
387
388        $bidtable = $this->mBidtable;
389        $unidtable = $this->mUnidtable;
390        $manLevel = $this->mConverter->getManualLevel();
391
392        // Flatten rules which are DocumentFragments
393        foreach ( $bidtable as $k => $val ) {
394            if ( $val instanceof DocumentFragment ) {
395                $val = trim( $val->textContent );
396                if ( $val ) {
397                    $bidtable[$k] = $val;
398                } else {
399                    # if $from is empty, the strtr() hidden inside
400                    # ReplacementArray::replace() could return a wrong result
401                    unset( $bidtable[$k] );
402                }
403            }
404        }
405        foreach ( $unidtable as $k => &$pairs ) {
406            foreach ( $pairs as $from => &$to ) {
407                if ( $to instanceof DocumentFragment ) {
408                    // Note that $to and $pairs are references
409                    $to = trim( $to->textContent );
410                }
411            }
412        }
413
414        $vmarked = [];
415        foreach ( $this->mConverter->getVariants() as $v ) {
416            /* for bidirectional array
417                fill in the missing variants, if any,
418                with fallbacks */
419            if ( !isset( $bidtable[$v] ) ) {
420                $variantFallbacks =
421                    $this->mConverter->getVariantFallbacks( $v );
422                $vf = $this->getTextInBidtable( $variantFallbacks );
423                if ( $vf ) {
424                    $bidtable[$v] = $vf;
425                }
426            }
427
428            if ( isset( $bidtable[$v] ) ) {
429                foreach ( $vmarked as $vo ) {
430                    // use syntax: -{A|zh:WordZh;zh-tw:WordTw}-
431                    // or -{H|zh:WordZh;zh-tw:WordTw}-
432                    // or -{-|zh:WordZh;zh-tw:WordTw}-
433                    // to introduce a custom mapping between
434                    // words WordZh and WordTw in the whole text
435                    if ( $manLevel[$v] === 'bidirectional' ) {
436                        $this->mConvTable[$v][$bidtable[$vo]] = $bidtable[$v];
437                    }
438                    if ( $manLevel[$vo] === 'bidirectional' ) {
439                        $this->mConvTable[$vo][$bidtable[$v]] = $bidtable[$vo];
440                    }
441                }
442                $vmarked[] = $v;
443            }
444            /* for unidirectional array fill to convert tables */
445            if ( ( $manLevel[$v] === 'bidirectional' || $manLevel[$v] === 'unidirectional' )
446                && isset( $unidtable[$v] )
447            ) {
448                if ( isset( $this->mConvTable[$v] ) ) {
449                    $this->mConvTable[$v] = $unidtable[$v] + $this->mConvTable[$v];
450                } else {
451                    $this->mConvTable[$v] = $unidtable[$v];
452                }
453            }
454        }
455    }
456
457    /**
458     * Parse rules and flags.
459     * @param string $inner The contents of the rule between -{ and }-
460     * @param string|null $variant Variant language code
461     */
462    public function parse( string $inner, ?string $variant = null ): void {
463        if ( !$variant ) {
464            $variant = $this->mConverter->getPreferredVariant();
465        }
466
467        $this->parseFlags( $inner );
468        $flags = $this->mFlags;
469
470        // convert to specified variant
471        // syntax: -{zh-hans;zh-hant[;...]|<text to convert>}-
472        if ( $this->mVariantFlags ) {
473            // check if current variant in flags
474            if ( isset( $this->mVariantFlags[$variant] ) ) {
475                // then convert <text to convert> to current language
476                $this->mRules = $this->mConverter->autoConvert( $this->mRules,
477                    $variant );
478            } else {
479                // if the current variant is not in flags,
480                // then we check its fallback variants.
481                $variantFallbacks =
482                    $this->mConverter->getVariantFallbacks( $variant );
483                if ( is_array( $variantFallbacks ) ) {
484                    foreach ( $variantFallbacks as $variantFallback ) {
485                        // if current variant's fallback exist in flags
486                        if ( isset( $this->mVariantFlags[$variantFallback] ) ) {
487                            // then convert <text to convert> to fallback language
488                            $this->mRules =
489                                $this->mConverter->autoConvert( $this->mRules,
490                                    $variantFallback );
491                            break;
492                        }
493                    }
494                }
495            }
496            // If no variant matched, fallback to displaying the filter rule
497            // contents as literal text instead.
498            // T424275: should probably display an error message instead
499            $this->mFlags = $flags = [ 'R' => true ];
500        }
501
502        if ( !isset( $flags['R'] ) && !isset( $flags['N'] ) ) {
503            // decode => HTML entities modified by Sanitizer::internalRemoveHtmlTags
504            $this->mRules = str_replace( '=&gt;', '=>', $this->mRules );
505            $this->parseRules();
506        }
507        $rules = $this->mRules;
508
509        if ( !$this->mBidtable && !$this->mUnidtable ) {
510            if ( isset( $flags['+'] ) || isset( $flags['-'] ) ) {
511                // fill all variants if the text in -{A/H/-|text}- is non-empty but without rules
512                if ( $rules !== '' ) {
513                    foreach ( $this->mConverter->getVariants() as $v ) {
514                        $this->mBidtable[$v] = $rules;
515                    }
516                }
517            } elseif ( !isset( $flags['N'] ) && !isset( $flags['T'] ) ) {
518                $this->mFlags = $flags = [ 'R' => true ];
519            }
520        }
521
522        $this->mRuleDisplay = false;
523        foreach ( $flags as $flag => $unused ) {
524            switch ( $flag ) {
525                case 'R':
526                    // if we don't do content convert, still strip the -{}- tags
527                    $this->mRuleDisplay = $rules;
528                    break;
529                case 'N':
530                    // process N flag: output current variant name
531                    $vv = LanguageCode::bcp47ToInternal( trim( $rules ) );
532                    $this->mRuleDisplay = $this->mConverter->getVariantNames()[$vv] ?? '';
533                    break;
534                case 'D':
535                    // process D flag: output rules description
536                    $this->mRuleDisplay = $this->getRulesDesc();
537                    break;
538                case 'H':
539                    // process H,- flag or T only: output nothing
540                    $this->mRuleDisplay = '';
541                    break;
542                case '-':
543                    $this->mRulesAction = 'remove';
544                    $this->mRuleDisplay = '';
545                    break;
546                case '+':
547                    $this->mRulesAction = 'add';
548                    $this->mRuleDisplay = '';
549                    break;
550                case 'S':
551                    $this->mRuleDisplay = $this->getRuleConvertedValue( $variant ) ?? false;
552                    break;
553                case 'T':
554                    $this->mRuleTitle = $this->getRuleConvertedValueForTitle( $variant ) ?? false;
555                    $this->mRuleDisplay = '';
556                    break;
557                default:
558                    // ignore unknown flags (but see error-case below)
559            }
560        }
561        if ( $this->mRuleDisplay === false ) {
562            $this->mRuleDisplay = '<span class="error">'
563                . wfMessage( 'converter-manual-rule-error' )->inContentLanguage()->escaped()
564                . '</span>';
565        }
566
567        $this->generateConvTable();
568    }
569
570    /**
571     * Parse rules and flags from MediaWiki DOM Spec.
572     * @param Element $el Language converter rule (an Element with a valid
573     *   `data-mw-variant` attribute).
574     * @param ?string $variant Variant language code
575     * @return ?string If non-null, the rule display should be converted to
576     *   the returned variant.
577     */
578    public function parseElement( Element $el, ?string $variant = null ): ?string {
579        $hidden = ( DOMUtils::nodeName( $el ) === 'meta' );
580        if ( !$variant ) {
581            $variant = $this->mConverter->getPreferredVariant();
582        }
583        $dmv = DOMDataUtils::getDataMwVariant( $el );
584        Assert::invariant( $dmv !== null, "Element must have data-mw-variant" );
585        if ( $dmv->filter ) {
586            // Check if the current variant is in the filter
587            $langs = array_map( fn ( $code )=>$this->mConverter->validateVariant( $code ), $dmv->filter->langs );
588            if ( in_array( $variant, $langs, true ) ) {
589                $this->mRuleDisplay = DOMDataUtils::cloneDocumentFragment(
590                    $dmv->filter->text
591                );
592                $this->mFlags['R'] = true;
593                return $variant;
594            }
595            // If the current variant is not in the filter, then
596            // check the fallback variants.
597            $variantFallbacks =
598                $this->mConverter->getVariantFallbacks( $variant );
599            if ( is_array( $variantFallbacks ) ) {
600                foreach ( $variantFallbacks as $variantFallback ) {
601                    if ( in_array( $variantFallback, $langs, true ) ) {
602                        // convert to fallback language
603                        $this->mRuleDisplay = DOMDataUtils::cloneDocumentFragment(
604                            $dmv->filter->text
605                        );
606                        $this->mFlags['R'] = true;
607                        return $variantFallback;
608                    }
609                }
610            }
611            // No matching variant; fallback to displaying the empty string.
612            // T424275: should probably display an error message instead
613            $this->mRuleDisplay = '';
614            $this->mFlags['R'] = true;
615        }
616        if ( $dmv->disabled ) {
617            $this->mFlags['R'] = true;
618            $df = $dmv->disabled;
619            '@phan-var DocumentFragment $df';
620            $this->mRuleDisplay = DOMDataUtils::cloneDocumentFragment(
621                $df
622            );
623        }
624        if ( $dmv->twoway ) {
625            $bidtable = [];
626            foreach ( $dmv->twoway as $case ) {
627                $vv = trim( $case->lang );
628                if ( $vv === '*' ) {
629                    $vs = $this->mConverter->getVariants();
630                } else {
631                    $vv = $this->mConverter->validateVariant( $vv );
632                    $vs = $vv ? [ $vv ] : [];
633                }
634                foreach ( $vs as $v ) {
635                    $bidtable[$v] = DOMDataUtils::cloneDocumentFragment(
636                        $case->text
637                    );
638                }
639            }
640            $this->mBidtable = $bidtable;
641        }
642        if ( $dmv->oneway ) {
643            $unidtable = [];
644            foreach ( $dmv->oneway as $case ) {
645                $vv = $this->mConverter->validateVariant( trim( $case->lang ) );
646                if ( !$vv ) {
647                    continue;
648                }
649                $from = trim( $case->from->textContent );
650                if ( !$from ) {
651                    continue;
652                }
653                $unidtable[$vv] ??= [];
654                $unidtable[$vv][$from] = $case->to;
655            }
656            $this->mUnidtable = $unidtable;
657        }
658        if ( $dmv->add ) {
659            $this->mRulesAction = 'add';
660        }
661        if ( $dmv->remove ) {
662            $this->mRulesAction = 'remove';
663        }
664        if ( $dmv->title ) {
665            // Special case: T with no variants is used verbatim *unless*
666            // we're in the main code (that is, no automatic conversion)
667            if ( $dmv->disabled && $variant !== $this->mConverter->getMainCode() ) {
668                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
669                $this->mRuleTitle = DOMDataUtils::cloneDocumentFragment( $dmv->disabled );
670            } else {
671                $this->mRuleTitle = $this->getRuleConvertedValueForTitle( $variant ) ?? false;
672            }
673        }
674        if ( !$hidden ) {
675            if ( $dmv->error ) {
676                $this->mRuleDisplay = false;
677            } elseif ( $dmv->name ) {
678                $vv = LanguageCode::bcp47ToInternal( trim( $dmv->name->textContent ) );
679                $this->mRuleDisplay = $this->mConverter->getVariantNames()[$vv] ?? '';
680            } elseif ( $dmv->oneway || $dmv->twoway ) {
681                $this->mRuleDisplay = $dmv->describe
682                    // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
683                    ? $this->getRulesDescFragment( $el->ownerDocument )
684                    : ( $this->getRuleConvertedValue( $variant ) ?? false );
685            }
686        }
687        $this->generateConvTable();
688        // Recurse into the result if disabled; otherwise we're done
689        // with this rule.
690        return $dmv->disabled ? 'x-raw' : null;
691    }
692
693    /**
694     * Checks if there are conversion rules.
695     * @return bool
696     */
697    public function hasRules() {
698        return $this->mRules !== '';
699    }
700
701    /**
702     * Get display text on markup -{...}-
703     * @return string
704     */
705    public function getDisplay() {
706        $display = $this->mRuleDisplay;
707        if ( $display instanceof DocumentFragment ) {
708            return $display->textContent;
709        } elseif ( $display === false ) {
710            return '<span class="error">'
711                . wfMessage( 'converter-manual-rule-error' )->inContentLanguage()->escaped()
712                . '</span>';
713        }
714        return $display;
715    }
716
717    /**
718     * Get display text on markup -{...}- as a DocumentFragment
719     * @param Document $ownerDocument An owner document for the returned fragment
720     * @return DocumentFragment
721     */
722    public function getDisplayFragment( Document $ownerDocument ): DocumentFragment {
723        $display = $this->mRuleDisplay;
724        if ( $display instanceof DocumentFragment ) {
725            return $display;
726        } elseif ( $display === false ) {
727            $span = $ownerDocument->createElement( 'span' );
728            $span->setAttribute( 'class', 'error' );
729            $span->appendChild(
730                WTUtils::createPageContentI18nFragment(
731                    $ownerDocument, 'converter-manual-rule-error'
732                )
733            );
734            $df = $ownerDocument->createDocumentFragment();
735            $df->appendChild( $span );
736            return $df;
737        }
738        $df = $ownerDocument->createDocumentFragment();
739        DOMCompat::replaceChildren( $df, $display );
740        return $df;
741    }
742
743    /**
744     * Get converted title.
745     * @return string|false
746     */
747    public function getTitle(): string|false {
748        $title = $this->mRuleTitle;
749        if ( $title instanceof DocumentFragment ) {
750            return $title->textContent;
751        }
752        return $title;
753    }
754
755    /**
756     * Get converted title as a DocumentFragment.
757     * @param Document $ownerDocument An owner document for the returned fragment
758     * @return ?DocumentFragment
759     */
760    public function getTitleFragment( Document $ownerDocument ): ?DocumentFragment {
761        $title = $this->mRuleTitle;
762        if ( $title instanceof DocumentFragment || $title === false ) {
763            return $title ?: null;
764        }
765        $df = $ownerDocument->createDocumentFragment();
766        DOMCompat::replaceChildren( $df, $title );
767        return $df;
768    }
769
770    /** @internal */
771    public function getTitleValue(): string|DocumentFragment|null {
772        return $this->mRuleTitle ?: null;
773    }
774
775    /**
776     * Return how to deal with conversion rules.
777     * @return string
778     */
779    public function getRulesAction() {
780        return $this->mRulesAction;
781    }
782
783    /**
784     * Get conversion table. (bidirectional and unidirectional
785     * conversion table)
786     * @return array
787     */
788    public function getConvTable() {
789        return $this->mConvTable;
790    }
791
792    /**
793     * Get conversion rules string.
794     * @return string
795     */
796    public function getRules() {
797        return $this->mRules;
798    }
799
800    /**
801     * Get conversion flags.
802     * @return array
803     */
804    public function getFlags() {
805        return $this->mFlags;
806    }
807}
808
809/** @deprecated class alias since 1.43 */
810class_alias( ConverterRule::class, 'ConverterRule' );