Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 163 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
LanguageVariantHandler | |
0.00% |
0 / 163 |
|
0.00% |
0 / 5 |
3192 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
convertOne | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
2 | |||
compressSpArray | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
onLanguageVariant | |
0.00% |
0 / 126 |
|
0.00% |
0 / 1 |
2162 | |||
onTag | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Wt2Html\TT; |
5 | |
6 | use Wikimedia\Parsoid\NodeData\DataParsoid; |
7 | use Wikimedia\Parsoid\Tokens\EndTagTk; |
8 | use Wikimedia\Parsoid\Tokens\EOFTk; |
9 | use Wikimedia\Parsoid\Tokens\KV; |
10 | use Wikimedia\Parsoid\Tokens\SourceRange; |
11 | use Wikimedia\Parsoid\Tokens\TagTk; |
12 | use Wikimedia\Parsoid\Tokens\Token; |
13 | use Wikimedia\Parsoid\Utils\ContentUtils; |
14 | use Wikimedia\Parsoid\Utils\DOMUtils; |
15 | use Wikimedia\Parsoid\Utils\PHPUtils; |
16 | use Wikimedia\Parsoid\Utils\PipelineUtils; |
17 | use Wikimedia\Parsoid\Wikitext\Consts; |
18 | use Wikimedia\Parsoid\Wt2Html\TokenHandlerPipeline; |
19 | |
20 | /** |
21 | * Handler for language conversion markup, which looks like `-{ ... }-`. |
22 | */ |
23 | class LanguageVariantHandler extends TokenHandler { |
24 | /** @inheritDoc */ |
25 | public function __construct( TokenHandlerPipeline $manager, array $options ) { |
26 | parent::__construct( $manager, $options ); |
27 | } |
28 | |
29 | /** |
30 | * convert one variant text to dom. |
31 | * @param TokenHandlerPipeline $manager |
32 | * @param array $options |
33 | * @param string $t |
34 | * @param array $attribs |
35 | * @return array |
36 | */ |
37 | private function convertOne( TokenHandlerPipeline $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' => 'expanded-tokens-to-fragment', |
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, 'fragment' => 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 TokenHandlerPipeline#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 | } |