Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 163
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
LanguageVariantHandler
0.00% covered (danger)
0.00%
0 / 163
0.00% covered (danger)
0.00%
0 / 5
3192
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
 convertOne
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 compressSpArray
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 onLanguageVariant
0.00% covered (danger)
0.00%
0 / 126
0.00% covered (danger)
0.00%
0 / 1
2162
 onTag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Wt2Html\TT;
5
6use Wikimedia\Parsoid\NodeData\DataParsoid;
7use Wikimedia\Parsoid\Tokens\EndTagTk;
8use Wikimedia\Parsoid\Tokens\EOFTk;
9use Wikimedia\Parsoid\Tokens\KV;
10use Wikimedia\Parsoid\Tokens\SourceRange;
11use Wikimedia\Parsoid\Tokens\TagTk;
12use Wikimedia\Parsoid\Tokens\Token;
13use Wikimedia\Parsoid\Utils\ContentUtils;
14use Wikimedia\Parsoid\Utils\DOMUtils;
15use Wikimedia\Parsoid\Utils\PHPUtils;
16use Wikimedia\Parsoid\Utils\PipelineUtils;
17use Wikimedia\Parsoid\Wikitext\Consts;
18use Wikimedia\Parsoid\Wt2Html\TokenTransformManager;
19
20/**
21 * Handler for language conversion markup, which looks like `-{ ... }-`.
22 */
23class LanguageVariantHandler extends TokenHandler {
24    /** @inheritDoc */
25    public function __construct( TokenTransformManager $manager, array $options ) {
26        parent::__construct( $manager, $options );
27    }
28
29    /**
30     * convert one variant text to dom.
31     * @param TokenTransformManager $manager
32     * @param array $options
33     * @param string $t
34     * @param array $attribs
35     * @return array
36     */
37    private function convertOne( TokenTransformManager $manager, array $options, string $t,
38        array $attribs ): array {
39        // we're going to fetch the actual token list from attribs
40        // (this ensures that it has gone through the earlier stages
41        // of the pipeline already to be expanded)
42        $t = PHPUtils::stripPrefix( $t, 'mw:lv' );
43        $srcOffsets = $attribs[$t]->srcOffsets;
44        $domFragment = PipelineUtils::processContentInPipeline(
45            $this->env, $manager->getFrame(), array_merge( $attribs[$t]->v, [ new EOFTk() ] ),
46            [
47                'pipelineType' => 'tokens/x-mediawiki/expanded',
48                'pipelineOpts' => [
49                    'inlineContext' => true,
50                    'expandTemplates' => $options['expandTemplates'],
51                    'inTemplate' => $options['inTemplate']
52                ],
53                'srcOffsets' => $srcOffsets->value ?? null,
54                'sol' => true
55            ]
56        );
57        return [
58            'xmlstr' => ContentUtils::ppToXML(
59                $domFragment, [ 'innerXML' => true ]
60            ),
61            'isBlock' => DOMUtils::hasBlockElementDescendant( $domFragment ),
62        ];
63    }
64
65    /**
66     * compress a whitespace sequence
67     * @param ?array $a
68     * @return ?array
69     */
70    private function compressSpArray( ?array $a ): ?array {
71        $result = [];
72        $ctr = 0;
73        if ( $a === null ) {
74            return $a;
75        }
76        foreach ( $a as $sp ) {
77            if ( $sp === '' ) {
78                $ctr++;
79            } else {
80                if ( $ctr > 0 ) {
81                    $result[] = $ctr;
82                    $ctr = 0;
83                }
84                $result[] = $sp;
85            }
86        }
87        if ( $ctr > 0 ) {
88            $result[] = $ctr;
89        }
90        return $result;
91    }
92
93    /**
94     * Main handler.
95     * See {@link TokenTransformManager#addTransform}'s transformation parameter
96     * @param Token $token
97     * @return TokenHandlerResult|null
98     */
99    private function onLanguageVariant( Token $token ): ?TokenHandlerResult {
100        $manager = $this->manager;
101        $options = $this->options;
102        $attribs = $token->attribs;
103        $dataParsoid = $token->dataParsoid;
104        $tsr = $dataParsoid->tsr;
105        $flags = $dataParsoid->flags;
106        $flagSp = $dataParsoid->flagSp;
107        $isMeta = false;
108        $sawFlagA = false;
109
110        // remove trailing semicolon marker, if present
111        $trailingSemi = false;
112        if ( count( $dataParsoid->texts ) &&
113            ( $dataParsoid->texts[count( $dataParsoid->texts ) - 1]['semi'] ?? null )
114        ) {
115            $trailingSemi = array_pop( $dataParsoid->texts )['sp'] ?? null;
116        }
117        // convert all variant texts to DOM
118        $isBlock = false;
119        $texts = array_map( function ( array $t ) use ( $manager, $options, $attribs, &$isBlock ) {
120            $text = null;
121            $from = null;
122            $to = null;
123            if ( isset( $t['twoway'] ) ) {
124                $text = $this->convertOne( $manager, $options, $t['text'], $attribs );
125                $isBlock = $isBlock || !empty( $text['isBlock'] );
126                return [ 'lang' => $t['lang'], 'text' => $text['xmlstr'], 'twoway' => true, 'sp' => $t['sp'] ];
127            } elseif ( isset( $t['lang'] ) ) {
128                $from = $this->convertOne( $manager, $options, $t['from'], $attribs );
129                $to = $this->convertOne( $manager, $options, $t['to'], $attribs );
130                $isBlock = $isBlock || !empty( $from['isBlock'] ) || !empty( $to['isBlock'] );
131                return [ 'lang' => $t['lang'], 'from' => $from['xmlstr'], 'to' => $to['xmlstr'],
132                    'sp' => $t['sp'] ];
133            } else {
134                $text = $this->convertOne( $manager, $options, $t['text'], $attribs );
135                $isBlock = $isBlock || !empty( $text['isBlock'] );
136                return [ 'text' => $text['xmlstr'], 'sp' => [] ];
137            }
138        }, $dataParsoid->texts );
139        // collect two-way/one-way conversion rules
140        $oneway = [];
141        $twoway = [];
142        $sawTwoway = false;
143        $sawOneway = false;
144        $textSp = null;
145        $twowaySp = [];
146        $onewaySp = [];
147        foreach ( $texts as $t ) {
148            if ( isset( $t['twoway'] ) ) {
149                $twoway[] = [ 'l' => $t['lang'], 't' => $t['text'] ];
150                array_push( $twowaySp, $t['sp'][0], $t['sp'][1], $t['sp'][2] );
151                $sawTwoway = true;
152            } elseif ( isset( $t['lang'] ) ) {
153                $oneway[] = [ 'l' => $t['lang'], 'f' => $t['from'], 't' => $t['to'] ];
154                array_push( $onewaySp, $t['sp'][0], $t['sp'][1], $t['sp'][2], $t['sp'][3] );
155                $sawOneway = true;
156            }
157        }
158
159        // To avoid too much data-mw bloat, only the top level keys in
160        // data-mw-variant are "human readable".  Nested keys are single-letter:
161        // `l` for `language`, `t` for `text` or `to`, `f` for `from`.
162        $dataMWV = null;
163        if ( count( $flags ) === 0 && count( $dataParsoid->variants ) > 0 ) {
164            // "Restrict possible variants to a limited set"
165            $dataMWV = [
166                'filter' => [ 'l' => $dataParsoid->variants, 't' => $texts[0]['text'] ],
167                'show' => true
168            ];
169        } else {
170            $dataMWV = [];
171            foreach ( $flags as $f ) {
172                if ( array_key_exists( $f, Consts::$LCFlagMap ) ) {
173                    if ( Consts::$LCFlagMap[$f] ) {
174                        $dataMWV[Consts::$LCFlagMap[$f]] = true;
175                        if ( $f === 'A' ) {
176                            $sawFlagA = true;
177                        }
178                    }
179                } else {
180                    $dataMWV['error'] = true;
181                }
182            }
183            // (this test is done at the top of ConverterRule::getRuleConvertedStr)
184            // (also partially in ConverterRule::parse)
185            if ( count( $texts ) === 1 &&
186                !isset( $texts[0]['lang'] ) && !isset( $dataMWV['name'] )
187            ) {
188                if ( isset( $dataMWV['add'] ) || isset( $dataMWV['remove'] ) ) {
189                    $variants = [ '*' ];
190                    $twoway = array_map( static function ( string $code ) use ( $texts, &$sawTwoway ) {
191                        return [ 'l' => $code, 't' => $texts[0]['text'] ];
192                    }, $variants );
193                    $sawTwoway = true;
194                } else {
195                    $dataMWV['disabled'] = true;
196                    unset( $dataMWV['describe'] );
197                }
198            }
199            if ( isset( $dataMWV['describe'] ) ) {
200                if ( !$sawFlagA ) {
201                    $dataMWV['show'] = true;
202                }
203            }
204            if ( isset( $dataMWV['disabled'] ) || isset( $dataMWV['name'] ) ) {
205                if ( isset( $dataMWV['disabled'] ) ) {
206                    $dataMWV['disabled'] = [ 't' => $texts[0]['text'] ?? '' ];
207                } else {
208                    $dataMWV['name'] = [ 't' => $texts[0]['text'] ?? '' ];
209                }
210                if ( isset( $dataMWV['title'] ) || isset( $dataMWV['add'] ) ) {
211                    unset( $dataMWV['show'] );
212                } else {
213                    $dataMWV['show'] = true;
214                }
215            } elseif ( $sawTwoway ) {
216                $dataMWV['twoway'] = $twoway;
217                $textSp = $twowaySp;
218                if ( $sawOneway ) {
219                    $dataMWV['error'] = true;
220                }
221            } else {
222                $dataMWV['oneway'] = $oneway;
223                $textSp = $onewaySp;
224                if ( !$sawOneway ) {
225                    $dataMWV['error'] = true;
226                }
227            }
228        }
229        // Use meta/not meta instead of explicit 'show' flag.
230        $isMeta = !isset( $dataMWV['show'] );
231        unset( $dataMWV['show'] );
232        // Trim some data from data-parsoid if it matches the defaults
233        if ( count( $flagSp ) === 2 * count( $dataParsoid->original ) ) {
234            $result = true;
235            foreach ( $flagSp as $s ) {
236                if ( $s !== '' ) {
237                    $result = false;
238                    break;
239                }
240            }
241            if ( $result ) {
242                $flagSp = null;
243            }
244        }
245        if ( $trailingSemi !== false && $textSp ) {
246            $textSp[] = $trailingSemi;
247        }
248
249        // Our markup is always the same, except for the contents of
250        // the data-mw-variant attribute and whether it's a span, div, or a
251        // meta, depending on (respectively) whether conversion output
252        // contains only inline content, could contain block content,
253        // or never contains any content.
254
255        $das = new DataParsoid;
256        $das->fl = $dataParsoid->original; // original "fl"ags
257        $flSp = $this->compressSpArray( $flagSp ); // spaces around flags
258        if ( $flSp !== null ) {
259            $das->flSp = $flSp;
260        }
261        $das->src = $dataParsoid->src;
262        $tSp = $this->compressSpArray( $textSp ); // spaces around texts
263        if ( $tSp !== null ) {
264            $das->tSp = $tSp;
265        }
266        $das->tsr = new SourceRange( $tsr->start, $isMeta ? $tsr->end : ( $tsr->end - 2 ) );
267
268        PHPUtils::sortArray( $dataMWV );
269        $tokens = [
270            new TagTk( $isMeta ? 'meta' : ( $isBlock ? 'div' : 'span' ), [
271                    new KV( 'typeof', 'mw:LanguageVariant' ),
272                    new KV( 'data-mw-variant', PHPUtils::jsonEncode( $dataMWV ) )
273                ], $das
274            )
275        ];
276        if ( !$isMeta ) {
277            $metaDP = new DataParsoid;
278            $metaDP->tsr = new SourceRange( $tsr->end - 2, $tsr->end );
279            $tokens[] = new EndTagTk( $isBlock ? 'div' : 'span', [], $metaDP );
280        }
281
282        return new TokenHandlerResult( $tokens );
283    }
284
285    /**
286     * @inheritDoc
287     */
288    public function onTag( Token $token ): ?TokenHandlerResult {
289        return $token->getName() === 'language-variant' ? $this->onLanguageVariant( $token ) : null;
290    }
291}