Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
28.81% |
153 / 531 |
|
21.05% |
4 / 19 |
CRAP | |
0.00% |
0 / 1 |
TemplateHandler | |
28.81% |
153 / 531 |
|
21.05% |
4 / 19 |
7729.52 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
parserFunctionsWrapper | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
processToString | |
0.00% |
0 / 51 |
|
0.00% |
0 / 1 |
650 | |||
isSafeSubst | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
resolveTemplateTarget | |
37.00% |
37 / 100 |
|
0.00% |
0 / 1 |
239.29 | |||
flattenAndAppendToks | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
132 | |||
convertToString | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
6 | |||
enforceTemplateConstraints | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
expandTemplateNatively | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
90 | |||
processTemplateSource | |
80.00% |
12 / 15 |
|
0.00% |
0 / 1 |
2.03 | |||
encapTokens | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
processTemplateTokens | |
65.00% |
13 / 20 |
|
0.00% |
0 / 1 |
14.29 | |||
fetchTemplateAndTitle | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
hasTemplateToken | |
40.00% |
2 / 5 |
|
0.00% |
0 / 1 |
7.46 | |||
processSpecialMagicWord | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
expandTemplate | |
27.91% |
48 / 172 |
|
0.00% |
0 / 1 |
279.29 | |||
onTemplate | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
onTemplateArg | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
onTag | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Wt2Html\TT; |
5 | |
6 | use Wikimedia\Assert\Assert; |
7 | use Wikimedia\Assert\UnreachableException; |
8 | use Wikimedia\Parsoid\Ext\AsyncResult; |
9 | use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; |
10 | use Wikimedia\Parsoid\Fragments\DomPFragment; |
11 | use Wikimedia\Parsoid\Fragments\WikitextPFragment; |
12 | use Wikimedia\Parsoid\NodeData\TempData; |
13 | use Wikimedia\Parsoid\Tokens\CommentTk; |
14 | use Wikimedia\Parsoid\Tokens\EndTagTk; |
15 | use Wikimedia\Parsoid\Tokens\KV; |
16 | use Wikimedia\Parsoid\Tokens\NlTk; |
17 | use Wikimedia\Parsoid\Tokens\SelfclosingTagTk; |
18 | use Wikimedia\Parsoid\Tokens\SourceRange; |
19 | use Wikimedia\Parsoid\Tokens\TagTk; |
20 | use Wikimedia\Parsoid\Tokens\Token; |
21 | use Wikimedia\Parsoid\Utils\DOMCompat; |
22 | use Wikimedia\Parsoid\Utils\PHPUtils; |
23 | use Wikimedia\Parsoid\Utils\PipelineUtils; |
24 | use Wikimedia\Parsoid\Utils\Title; |
25 | use Wikimedia\Parsoid\Utils\TitleException; |
26 | use Wikimedia\Parsoid\Utils\TokenUtils; |
27 | use Wikimedia\Parsoid\Utils\WTUtils; |
28 | use Wikimedia\Parsoid\Wikitext\Wikitext; |
29 | use Wikimedia\Parsoid\Wt2Html\Frame; |
30 | use Wikimedia\Parsoid\Wt2Html\Params; |
31 | use Wikimedia\Parsoid\Wt2Html\TokenHandlerPipeline; |
32 | |
33 | /** |
34 | * Template and template argument handling. |
35 | */ |
36 | class TemplateHandler extends TokenHandler { |
37 | /** |
38 | * @var bool Should we wrap template tokens with template meta tags? |
39 | */ |
40 | private $wrapTemplates; |
41 | |
42 | /** |
43 | * @var AttributeExpander |
44 | * Local copy of the attribute expander to deal with template targets |
45 | * that are templated themselves |
46 | */ |
47 | private $ae; |
48 | |
49 | /** |
50 | * @var ParserFunctions |
51 | */ |
52 | private $parserFunctions; |
53 | |
54 | /** |
55 | * @var bool |
56 | */ |
57 | private $atMaxArticleSize; |
58 | |
59 | /** @var string|null */ |
60 | private $safeSubstRegex; |
61 | |
62 | /** |
63 | * @param TokenHandlerPipeline $manager |
64 | * @param array $options |
65 | * - ?bool inTemplate Is this being invoked while processing a template? |
66 | * - ?bool expandTemplates Should we expand templates encountered here? |
67 | * - ?string extTag The name of the extension tag, if any, which is being expanded. |
68 | */ |
69 | public function __construct( TokenHandlerPipeline $manager, array $options ) { |
70 | parent::__construct( $manager, $options ); |
71 | $this->parserFunctions = new ParserFunctions( $this->env ); |
72 | $this->ae = new AttributeExpander( $this->manager, [ |
73 | 'expandTemplates' => $this->options['expandTemplates'], |
74 | 'inTemplate' => $this->options['inTemplate'], |
75 | 'standalone' => true, |
76 | ] ); |
77 | $this->wrapTemplates = !$options['inTemplate']; |
78 | |
79 | // In the legacy parser, the call to replaceVariables from internalParse |
80 | // returns early if the text is already greater than the $wgMaxArticleSize |
81 | // We're going to compare and set a boolean here, then do the "early |
82 | // return" below. |
83 | $this->atMaxArticleSize = !$this->env->compareWt2HtmlLimit( |
84 | 'wikitextSize', |
85 | strlen( $this->env->topFrame->getSrcText() ) |
86 | ); |
87 | } |
88 | |
89 | /** |
90 | * Parser functions also need template wrapping. |
91 | * |
92 | * @param array $tokens |
93 | * @return array |
94 | */ |
95 | private function parserFunctionsWrapper( array $tokens ): array { |
96 | $chunkToks = []; |
97 | if ( $tokens ) { |
98 | // This is only for the Parsoid native expansion pipeline used in |
99 | // parser tests. The "" token sometimes changes foster parenting |
100 | // behavior and trips up some tests. |
101 | $tokens = array_values( array_filter( $tokens, static function ( $t ) { |
102 | return $t !== ''; |
103 | } ) ); |
104 | |
105 | // token chunk should be flattened |
106 | $flat = true; |
107 | foreach ( $tokens as $t ) { |
108 | if ( is_array( $t ) ) { |
109 | $flat = false; |
110 | break; |
111 | } |
112 | } |
113 | Assert::invariant( $flat, "Expected token chunk to be flattened" ); |
114 | |
115 | $chunkToks = $this->processTemplateTokens( $tokens ); |
116 | } |
117 | return $chunkToks; |
118 | } |
119 | |
120 | /** |
121 | * Take output of tokensToString and further postprocess it. |
122 | * - If it can be processed to a string which would be a valid template transclusion target, |
123 | * the return value will be [ $the_string_value, null ] |
124 | * - If not, the return value will be [ $partial_string, $unprocessed_token_array ] |
125 | * The caller can then decide if this would be a valid parser function call |
126 | * where the unprocessed token array would be part of the first arg to the parser function. |
127 | * Ex: With "{{uc:foo [[foo]] {{1x|foo}} bar}}", we return |
128 | * [ "uc:foo ", [ wikilink-token, " ", template-token, " bar" ] ] |
129 | * |
130 | * @param array $tokens |
131 | * @return array first element is always a string |
132 | */ |
133 | private function processToString( array $tokens ): array { |
134 | $maybeTarget = TokenUtils::tokensToString( $tokens, true, [ 'retainNLs' => true ] ); |
135 | if ( !is_array( $maybeTarget ) ) { |
136 | return [ $maybeTarget, null ]; |
137 | } |
138 | |
139 | $buf = $maybeTarget[0]; // Will always be a string |
140 | $tgtTokens = $maybeTarget[1]; |
141 | $preNlContent = null; |
142 | $i = 0; |
143 | $n = count( $tgtTokens ); |
144 | while ( $i < $n ) { |
145 | $ntt = $tgtTokens[$i]; |
146 | if ( is_string( $ntt ) ) { |
147 | $buf .= $ntt; |
148 | if ( $preNlContent !== null && !preg_match( '/^\s*$/D', $buf ) ) { |
149 | // intervening newline makes this an invalid template target |
150 | return [ $preNlContent, array_merge( [ $buf ], array_slice( $tgtTokens, $i ) ) ]; |
151 | } |
152 | } else { |
153 | switch ( get_class( $ntt ) ) { |
154 | case SelfclosingTagTk::class: |
155 | // Quotes are valid template targets |
156 | if ( $ntt->getName() === 'mw-quote' ) { |
157 | $buf .= $ntt->getAttributeV( 'value' ); |
158 | } elseif ( |
159 | !TokenUtils::isEmptyLineMetaToken( $ntt ) && |
160 | $ntt->getName() !== 'template' && |
161 | $ntt->getName() !== 'templatearg' && |
162 | // Ignore annotations in template targets |
163 | // NOTE(T295834): There's a large discussion about who's responsible |
164 | // for stripping these tags in I487baaafcf1ffd771cb6a9e7dd4fb76d6387e412 |
165 | !( |
166 | $ntt->getName() === 'meta' && |
167 | TokenUtils::matchTypeOf( $ntt, WTUtils::ANNOTATION_META_TYPE_REGEXP ) |
168 | ) && |
169 | // Note that OnlyInclude only converts to metas during TT |
170 | // in inTemplate context, but we shouldn't find ourselves |
171 | // here in that case. |
172 | !( |
173 | $ntt->getName() === 'meta' && |
174 | TokenUtils::matchTypeOf( $ntt, '#^mw:Includes/#' ) |
175 | ) |
176 | ) { |
177 | // We are okay with empty (comment-only) lines, |
178 | // {{..}} and {{{..}}} in template targets. |
179 | if ( $preNlContent !== null ) { |
180 | return [ $preNlContent, array_merge( [ $buf ], array_slice( $tgtTokens, $i ) ) ]; |
181 | } else { |
182 | return [ $buf, array_slice( $tgtTokens, $i ) ]; |
183 | } |
184 | } |
185 | break; |
186 | |
187 | case TagTk::class: |
188 | if ( TokenUtils::isEntitySpanToken( $ntt ) ) { |
189 | $buf .= $tgtTokens[$i + 1]; |
190 | $i += 2; |
191 | break; |
192 | } |
193 | // Fall-through |
194 | case EndTagTk::class: |
195 | if ( $preNlContent !== null ) { |
196 | return [ $preNlContent, array_merge( [ $buf ], array_slice( $tgtTokens, $i ) ) ]; |
197 | } else { |
198 | return [ $buf, array_slice( $tgtTokens, $i ) ]; |
199 | } |
200 | |
201 | case CommentTk::class: |
202 | // Ignore comments as well |
203 | break; |
204 | |
205 | case NlTk::class: |
206 | // Ignore only the leading or trailing newlines |
207 | // (modulo whitespace and comments) |
208 | // |
209 | // If we only have whitespace in $buf thus far, |
210 | // the newline can be ignored. But, if we have |
211 | // non-ws content in $buf, everything that follows |
212 | // can only be ws. |
213 | if ( preg_match( '/^\s*$/D', $buf ) ) { |
214 | $buf .= "\n"; |
215 | break; |
216 | } elseif ( $preNlContent === null ) { |
217 | // Buffer accumulated content |
218 | $preNlContent = $buf; |
219 | $buf = "\n"; |
220 | break; |
221 | } else { |
222 | return [ $preNlContent, array_merge( [ $buf ], array_slice( $tgtTokens, $i ) ) ]; |
223 | } |
224 | |
225 | default: |
226 | throw new UnreachableException( 'Unexpected token type: ' . get_class( $ntt ) ); |
227 | } |
228 | } |
229 | $i++; |
230 | } |
231 | |
232 | // All good! No newline / only whitespace/comments post newline. |
233 | // (Well, annotation metas and template(arg) tokens too) |
234 | return [ $preNlContent . $buf, null ]; |
235 | } |
236 | |
237 | /** |
238 | * Is the prefix "safesubst" |
239 | * @param string $prefix |
240 | * @return bool |
241 | */ |
242 | private function isSafeSubst( $prefix ): bool { |
243 | if ( $this->safeSubstRegex === null ) { |
244 | $this->safeSubstRegex = $this->env->getSiteConfig()->getMagicWordMatcher( 'safesubst' ); |
245 | } |
246 | return (bool)preg_match( $this->safeSubstRegex, $prefix . ':' ); |
247 | } |
248 | |
249 | /** |
250 | * @param TemplateEncapsulator $state |
251 | * @param string|Token|array $targetToks |
252 | * @param SourceRange $srcOffsets |
253 | * @return array|null |
254 | */ |
255 | private function resolveTemplateTarget( |
256 | TemplateEncapsulator $state, $targetToks, $srcOffsets |
257 | ): ?array { |
258 | $additionalToks = null; |
259 | if ( is_string( $targetToks ) ) { |
260 | $target = $targetToks; |
261 | } else { |
262 | $toks = !is_array( $targetToks ) ? [ $targetToks ] : $targetToks; |
263 | $toks = $this->processToString( $toks ); |
264 | [ $target, $additionalToks ] = $toks; |
265 | } |
266 | |
267 | $target = trim( $target ); |
268 | $pieces = explode( ':', $target ); |
269 | $untrimmedPrefix = $pieces[0]; |
270 | $prefix = trim( $pieces[0] ); |
271 | |
272 | // Parser function names usually (not always) start with a hash |
273 | $hasHash = substr( $target, 0, 1 ) === '#'; |
274 | // String found after the colon will be the parser function arg |
275 | $haveColon = count( $pieces ) > 1; |
276 | |
277 | // safesubst found in content should be treated as if no modifier were |
278 | // present. See https://en.wikipedia.org/wiki/Help:Substitution#The_safesubst:_modifier |
279 | if ( $haveColon && $this->isSafeSubst( $prefix ) ) { |
280 | $target = substr( $target, strlen( $untrimmedPrefix ) + 1 ); |
281 | array_shift( $pieces ); |
282 | $untrimmedPrefix = $pieces[0]; |
283 | $prefix = trim( $pieces[0] ); |
284 | $haveColon = count( $pieces ) > 1; |
285 | } |
286 | |
287 | $env = $this->env; |
288 | $siteConfig = $env->getSiteConfig(); |
289 | |
290 | // Additional tokens are only justifiable in parser functions scenario |
291 | if ( !$haveColon && $additionalToks ) { |
292 | return null; |
293 | } |
294 | |
295 | $pfArg = ''; |
296 | if ( $haveColon ) { |
297 | $pfArg = substr( $target, strlen( $untrimmedPrefix ) + 1 ); |
298 | if ( $additionalToks ) { |
299 | $pfArg = [ $pfArg ]; |
300 | PHPUtils::pushArray( $pfArg, $additionalToks ); |
301 | } |
302 | } |
303 | |
304 | // Check if we have a magic variable implemented by the legacy parser |
305 | $magicWordVar = $siteConfig->getMagicWordForVariable( $prefix ) ?? |
306 | $siteConfig->getMagicWordForVariable( mb_strtolower( $prefix ) ); |
307 | [ 'key' => $canonicalFunctionName, 'isNative' => $isNative ] = |
308 | $siteConfig->getMagicWordForParserFunction( $prefix ); |
309 | if ( $canonicalFunctionName !== null && !$isNative ) { |
310 | // Parsoid's PFragmentHandler handles both magic variables (T391063) |
311 | // and zero-argument parser functions, but in the legacy |
312 | // parser "nohash" parser functions without a colon must |
313 | // be magic variables; they won't be invoked as parser |
314 | // functions. |
315 | if ( ( !$hasHash ) && ( !$haveColon ) ) { |
316 | $canonicalFunctionName = null; |
317 | } |
318 | } |
319 | // Ensure that magic words registered by parsoid PFragment handlers |
320 | // aren't confused for magic variables implemented by the legacy parser |
321 | if ( $magicWordVar && $canonicalFunctionName === null ) { |
322 | $state->variableName = $magicWordVar; |
323 | return [ |
324 | 'isVariable' => true, |
325 | 'magicWordType' => $magicWordVar === '!' ? '!' : null, |
326 | 'name' => $magicWordVar, |
327 | // FIXME: Some made up synthetic title |
328 | 'title' => $env->makeTitleFromURLDecodedStr( "Special:Variable/$magicWordVar" ), |
329 | 'pfArg' => $pfArg, |
330 | 'srcOffsets' => new SourceRange( |
331 | $srcOffsets->start + strlen( $untrimmedPrefix ) + ( $haveColon ? 1 : 0 ), |
332 | $srcOffsets->end ), |
333 | ]; |
334 | } |
335 | |
336 | // FIXME: Checks for msgnw, msg, raw are missing at this point |
337 | |
338 | $broken = false; |
339 | if ( $canonicalFunctionName === null && $hasHash ) { |
340 | // If the target starts with a '#' it can't possibly be a template |
341 | // so this must be a "broken" parser function invocation |
342 | $canonicalFunctionName = substr( $prefix, 1 ); |
343 | $broken = true; |
344 | // @todo: Flag this as an author error somehow (T314524) |
345 | } |
346 | if ( $canonicalFunctionName !== null ) { |
347 | $state->parserFunctionName = $canonicalFunctionName; |
348 | // XXX this is made up. |
349 | $syntheticTitle = $env->makeTitleFromURLDecodedStr( |
350 | "Special:ParserFunction/$canonicalFunctionName", |
351 | $env->getSiteConfig()->canonicalNamespaceId( 'Special' ), |
352 | true // No exceptions |
353 | ); |
354 | // Note that parserFunctionName/$canonicalFunctionName is not |
355 | // necessarily a valid title! Parsing rules are pretty generous |
356 | // w/r/t valid parser function names. |
357 | if ( $syntheticTitle === null ) { |
358 | $syntheticTitle = $env->makeTitleFromText( |
359 | 'Special:ParserFunction/unknown' |
360 | ); |
361 | } |
362 | $ret = [ |
363 | 'isParserFunction' => true, |
364 | 'magicWordType' => null, |
365 | 'name' => $canonicalFunctionName, |
366 | 'localName' => $prefix, |
367 | 'title' => $syntheticTitle, // FIXME: Some made up synthetic title |
368 | 'pfArg' => $pfArg, |
369 | 'haveColon' => $haveColon, // FIXME: T391063 |
370 | 'srcOffsets' => new SourceRange( |
371 | $srcOffsets->start + strlen( $untrimmedPrefix ) + ( $haveColon ? 1 : 0 ), |
372 | $srcOffsets->end ), |
373 | ]; |
374 | |
375 | // Check if we have a Parsoid PFragment handler for this parser func |
376 | // ($canonicalFunctionName is invalid/not localized if this is |
377 | // $broken) |
378 | $pFragmentHandler = ( $broken || !$isNative ) ? null : |
379 | $siteConfig->getPFragmentHandlerImpl( $canonicalFunctionName ); |
380 | if ( $pFragmentHandler ) { |
381 | $ret['handler'] = $pFragmentHandler; |
382 | $ret['handlerOptions'] = $siteConfig->getPFragmentHandlerConfig( |
383 | $canonicalFunctionName |
384 | )['options'] ?? []; |
385 | $state->isV3ParserFunction = true; |
386 | } |
387 | return $ret; |
388 | } |
389 | |
390 | // We've exhausted the parser-function scenarios, and we still have additional tokens. |
391 | if ( $additionalToks ) { |
392 | return null; |
393 | } |
394 | |
395 | // `resolveTitle()` adds the namespace prefix when it resolves fragments |
396 | // and relative titles, and a leading colon should resolve to a template |
397 | // from the main namespace, hence we omit a default when making a title |
398 | $namespaceId = strspn( $target, ':#/.' ) ? |
399 | null : $siteConfig->canonicalNamespaceId( 'template' ); |
400 | |
401 | // Resolve a possibly relative link and |
402 | // normalize the target before template processing. |
403 | $title = null; |
404 | try { |
405 | $title = $env->resolveTitle( $target ); |
406 | } catch ( TitleException $e ) { |
407 | // Invalid template target! |
408 | return null; |
409 | } |
410 | |
411 | // Entities in transclusions aren't decoded in the PHP parser |
412 | // So, treat the title as a url-decoded string! |
413 | $title = $env->makeTitleFromURLDecodedStr( $title, $namespaceId, true ); |
414 | if ( !$title ) { |
415 | // Invalid template target! |
416 | return null; |
417 | } |
418 | |
419 | // data-mw.target.href should be a url |
420 | $state->resolvedTemplateTarget = $env->makeLink( $title ); |
421 | |
422 | return [ |
423 | 'magicWordType' => null, |
424 | 'name' => $title->getPrefixedDBKey(), |
425 | 'title' => $title, |
426 | ]; |
427 | } |
428 | |
429 | /** |
430 | * Flatten |
431 | * @param (Token|string)[] $tokens |
432 | * @param ?string $prefix |
433 | * @param Token|string|(Token|string)[] $t |
434 | * @return array |
435 | */ |
436 | private function flattenAndAppendToks( |
437 | array $tokens, ?string $prefix, $t |
438 | ): array { |
439 | if ( is_array( $t ) ) { |
440 | $len = count( $t ); |
441 | if ( $len > 0 ) { |
442 | if ( $prefix !== null && $prefix !== '' ) { |
443 | $tokens[] = $prefix; |
444 | } |
445 | PHPUtils::pushArray( $tokens, $t ); |
446 | } |
447 | } elseif ( is_string( $t ) ) { |
448 | $len = strlen( $t ); |
449 | if ( $len > 0 ) { |
450 | if ( $prefix !== null && $prefix !== '' ) { |
451 | $tokens[] = $prefix; |
452 | } |
453 | $tokens[] = $t; |
454 | } |
455 | } else { |
456 | if ( $prefix !== null && $prefix !== '' ) { |
457 | $tokens[] = $prefix; |
458 | } |
459 | $tokens[] = $t; |
460 | } |
461 | |
462 | return $tokens; |
463 | } |
464 | |
465 | /** |
466 | * By default, don't attempt to expand any templates in the wikitext that will be reprocessed. |
467 | * |
468 | * @param Token $token |
469 | * @param bool $expandTemplates |
470 | * @return TemplateExpansionResult |
471 | */ |
472 | private function convertToString( Token $token, bool $expandTemplates = false ): TemplateExpansionResult { |
473 | $frame = $this->manager->getFrame(); |
474 | $tsr = $token->dataParsoid->tsr; |
475 | $src = substr( $token->dataParsoid->src, 1, -1 ); |
476 | $startOffset = $tsr->start + 1; |
477 | $srcOffsets = new SourceRange( $startOffset, $startOffset + strlen( $src ) ); |
478 | |
479 | $toks = PipelineUtils::processContentInPipeline( |
480 | $this->env, $frame, $src, [ |
481 | 'pipelineType' => 'wikitext-to-expanded-tokens', |
482 | 'pipelineOpts' => [ |
483 | 'inTemplate' => $this->options['inTemplate'], |
484 | 'expandTemplates' => $expandTemplates && $this->options['expandTemplates'], |
485 | ], |
486 | 'sol' => false, |
487 | // FIXME: Set toplevel when bailing |
488 | // 'toplevel' => $this->atTopLevel, |
489 | 'srcOffsets' => $srcOffsets, |
490 | ] |
491 | ); |
492 | TokenUtils::stripEOFTkfromTokens( $toks ); |
493 | return new TemplateExpansionResult( array_merge( [ '{' ], $toks, [ '}' ] ), true ); |
494 | } |
495 | |
496 | /** |
497 | * Enforce template loops / loop depth limit constraints and emit |
498 | * error message if constraints are violated. |
499 | * |
500 | * @param mixed $target |
501 | * @param Title $title |
502 | * @param bool $ignoreLoop |
503 | * @return ?array |
504 | */ |
505 | private function enforceTemplateConstraints( $target, Title $title, bool $ignoreLoop ): ?array { |
506 | $error = $this->manager->getFrame()->loopAndDepthCheck( |
507 | $title, $this->env->getSiteConfig()->getMaxTemplateDepth(), |
508 | $ignoreLoop |
509 | ); |
510 | |
511 | return $error ? [ // Loop detected or depth limit exceeded, abort! |
512 | new TagTk( 'span', [ new KV( 'class', 'error' ) ] ), |
513 | $error, |
514 | new SelfclosingTagTk( 'wikilink', [ new KV( 'href', $target, null, '', '' ) ] ), |
515 | new EndTagTk( 'span' ), |
516 | ] : null; |
517 | } |
518 | |
519 | /** |
520 | * Fetch, tokenize and token-transform a template after all arguments and |
521 | * the target were expanded. |
522 | * |
523 | * @param TemplateEncapsulator $state |
524 | * @param array $resolvedTgt |
525 | * @param array $attribs |
526 | * @return TemplateExpansionResult |
527 | */ |
528 | private function expandTemplateNatively( |
529 | TemplateEncapsulator $state, array $resolvedTgt, array $attribs |
530 | ): TemplateExpansionResult { |
531 | $env = $this->env; |
532 | $encap = $this->options['expandTemplates'] && $this->wrapTemplates; |
533 | |
534 | // XXX: wrap attribs in object with .dict() and .named() methods, |
535 | // and each member (key/value) into object with .tokens(), .dom() and |
536 | // .wikitext() methods (subclass of Array) |
537 | |
538 | $target = $resolvedTgt['name']; |
539 | if ( isset( $resolvedTgt['isParserFunction'] ) || isset( $resolvedTgt['isVariable'] ) ) { |
540 | // FIXME: HARDCODED to core parser function implementations! |
541 | // These should go through function hook registrations in the |
542 | // ParserTests mock setup ideally. But, it is complicated because the |
543 | // Parsoid core parser function versions have "token" versions |
544 | // which are incompatible with implementation in FunctionHookHandler |
545 | // and FunctionArgs. So, we continue down this hacky path for now. |
546 | if ( $target === '=' ) { |
547 | $target = 'equal'; // '=' is not a valid character in function names |
548 | } |
549 | $target = 'pf_' . $target; |
550 | // FIXME: Parsoid may not have implemented the parser function natively |
551 | // Emit an error message, but encapsulate it so it roundtrips back. |
552 | if ( !is_callable( [ $this->parserFunctions, $target ] ) ) { |
553 | // FIXME: Consolidate error response format with enforceTemplateConstraints |
554 | $err = 'Parser function implementation for ' . $target . ' missing in Parsoid.'; |
555 | return new TemplateExpansionResult( [ $err ], false, $encap ); |
556 | } |
557 | |
558 | $pfAttribs = new Params( $attribs ); |
559 | $pfAttribs->args[0] = new KV( |
560 | // FIXME: This is bogus, but preserves borked b/c |
561 | TokenUtils::tokensToString( $resolvedTgt['pfArg'] ), [], |
562 | $resolvedTgt['srcOffsets']->expandTsrK() |
563 | ); |
564 | $env->log( 'debug', 'entering prefix', $target, $state->token ); |
565 | $res = $this->parserFunctions->$target( |
566 | $state->token, $this->manager->getFrame(), $pfAttribs ); |
567 | if ( $this->wrapTemplates ) { |
568 | $res = $this->parserFunctionsWrapper( $res ); |
569 | } |
570 | return new TemplateExpansionResult( $res, false, $encap ); |
571 | } |
572 | |
573 | // Loop detection needs to be enabled since we're doing our own template expansion |
574 | $error = $this->enforceTemplateConstraints( $target, $resolvedTgt['title'], false ); |
575 | if ( $error ) { |
576 | // FIXME: Should we be encapsulating here? |
577 | // Inconsistent with the other place constrainsts are enforced. |
578 | return new TemplateExpansionResult( $error, false, $encap ); |
579 | } |
580 | |
581 | // XXX: notes from brion's mediawiki.parser.environment |
582 | // resolve template name |
583 | // load template w/ canonical name |
584 | // load template w/ variant names (language variants) |
585 | |
586 | // Fetch template source and expand it |
587 | $src = $this->fetchTemplateAndTitle( $target, $attribs ); |
588 | if ( $src !== null ) { |
589 | $toks = $this->processTemplateSource( |
590 | $this->manager->getFrame(), |
591 | $state->token, |
592 | [ |
593 | 'name' => $target, |
594 | 'title' => $resolvedTgt['title'], |
595 | 'attribs' => array_slice( $attribs, 1 ), // strip template target |
596 | ], |
597 | $src, |
598 | $this->options |
599 | ); |
600 | return new TemplateExpansionResult( $toks, true, $encap ); |
601 | } else { |
602 | // Convert to a wikilink (which will become a redlink after the redlinks pass). |
603 | $toks = [ new SelfclosingTagTk( 'wikilink' ) ]; |
604 | $hrefSrc = ':' . strtr( $resolvedTgt['name'], '_', ' ' ); |
605 | $toks[0]->attribs[] = new KV( 'href', $hrefSrc, null, null, $hrefSrc ); |
606 | return new TemplateExpansionResult( $toks, false, $encap ); |
607 | } |
608 | } |
609 | |
610 | /** |
611 | * Process a fetched template source to a token stream. |
612 | */ |
613 | private function processTemplateSource( |
614 | Frame $frame, Token $token, array $tplArgs, string $src, |
615 | array $options = [] |
616 | ): array { |
617 | if ( $this->env->hasDumpFlag( 'tplsrc' ) ) { |
618 | PipelineUtils::dumpTplSrc( |
619 | $this->env, $token, $tplArgs['name'], $src, false |
620 | ); |
621 | } |
622 | $this->env->log( 'debug', 'TemplateHandler.processTemplateSource', |
623 | $tplArgs['name'], $tplArgs['attribs'] ); |
624 | $toks = PipelineUtils::processTemplateSource( |
625 | $this->env, |
626 | $frame, |
627 | $token, |
628 | $tplArgs, |
629 | $src, |
630 | $options |
631 | ); |
632 | return $this->processTemplateTokens( $toks ); |
633 | } |
634 | |
635 | /** |
636 | * Process the main template element, including the arguments. |
637 | * |
638 | * @param TemplateEncapsulator $state |
639 | * @param array $tokens |
640 | * @return array |
641 | */ |
642 | private function encapTokens( TemplateEncapsulator $state, array $tokens ): array { |
643 | // Template encapsulation normally wouldn't happen in nested context, |
644 | // since they should have already been expanded, and indeed we set |
645 | // expandTemplates === false in processTemplateSource. However, |
646 | // extension tags from templates can have content that requires wikitext |
647 | // parsing and, due to precedence, contain unexpanded templates. |
648 | // |
649 | // For example, {{1x|hi<ref>{{1x|ho}}</ref>}} |
650 | // |
651 | // Since extensions can require template expansion unconditionally, we can |
652 | // end up here inTemplate, in which case the substrings of env.page.src |
653 | // used in getArgInfo are no longer accurate, and so tplarginfo should be |
654 | // omitted. Presumably, template wrapping in the dom post processor won't |
655 | // be happening anyways, so this is unnecessary work as it is. |
656 | Assert::invariant( |
657 | $this->wrapTemplates, 'Encapsulating tokens when not wrapping!' |
658 | ); |
659 | return $state->encapTokens( $tokens ); |
660 | } |
661 | |
662 | /** |
663 | * Handle chunk emitted from the input pipeline after feeding it a template. |
664 | * |
665 | * @param array $chunk |
666 | * @return array |
667 | */ |
668 | private function processTemplateTokens( array $chunk ): array { |
669 | TokenUtils::stripEOFTkfromTokens( $chunk ); |
670 | |
671 | foreach ( $chunk as $i => $t ) { |
672 | if ( !$t ) { |
673 | continue; |
674 | } |
675 | |
676 | if ( isset( $t->dataParsoid->tsr ) ) { |
677 | unset( $t->dataParsoid->tsr ); |
678 | } |
679 | Assert::invariant( !isset( $t->dataParsoid->tmp->endTSR ), |
680 | "Expected endTSR to not be set on templated content." ); |
681 | if ( $t instanceof SelfclosingTagTk && |
682 | strtolower( $t->getName() ) === 'meta' && |
683 | TokenUtils::hasTypeOf( $t, 'mw:Placeholder' ) |
684 | ) { |
685 | // replace with empty string to avoid metas being foster-parented out |
686 | $chunk[$i] = ''; |
687 | } |
688 | } |
689 | |
690 | // FIXME: What is this stuff here? Why do we care about stripping out comments |
691 | // so much that we create a new token array for every expanded template? |
692 | // Unlikely to help perf very much. |
693 | if ( !$this->options['expandTemplates'] ) { |
694 | // Ignore comments in template transclusion mode |
695 | $newChunk = []; |
696 | for ( $i = 0, $n = count( $chunk ); $i < $n; $i++ ) { |
697 | if ( !( $chunk[$i] instanceof CommentTk ) ) { |
698 | $newChunk[] = $chunk[$i]; |
699 | } |
700 | } |
701 | $chunk = $newChunk; |
702 | } |
703 | |
704 | $this->env->log( 'debug', 'TemplateHandler.processTemplateTokens', $chunk ); |
705 | return $chunk; |
706 | } |
707 | |
708 | /** |
709 | * Fetch a template. |
710 | * |
711 | * @param string $templateName |
712 | * @param array $attribs |
713 | * @return ?string |
714 | */ |
715 | private function fetchTemplateAndTitle( string $templateName, array $attribs ): ?string { |
716 | $env = $this->env; |
717 | if ( isset( $env->pageCache[$templateName] ) ) { |
718 | return $env->pageCache[$templateName]; |
719 | } |
720 | |
721 | $start = microtime( true ); |
722 | $pageContent = $env->getDataAccess()->fetchTemplateSource( |
723 | $env->getPageConfig(), |
724 | Title::newFromText( $templateName, $env->getSiteConfig() ) |
725 | ); |
726 | if ( $env->profiling() ) { |
727 | $profile = $env->getCurrentProfile(); |
728 | $profile->bumpMWTime( "TemplateFetch", 1000 * ( microtime( true ) - $start ), "api" ); |
729 | $profile->bumpCount( "TemplateFetch" ); |
730 | } |
731 | |
732 | // FIXME: |
733 | // 1. Hard-coded 'main' role |
734 | return $pageContent ? $pageContent->getContent( 'main' ) : null; |
735 | } |
736 | |
737 | /** |
738 | * @param mixed $tokens |
739 | * @return bool |
740 | */ |
741 | private static function hasTemplateToken( $tokens ): bool { |
742 | if ( is_array( $tokens ) ) { |
743 | foreach ( $tokens as $t ) { |
744 | if ( TokenUtils::isTemplateToken( $t ) ) { |
745 | return true; |
746 | } |
747 | } |
748 | } |
749 | return false; |
750 | } |
751 | |
752 | /** |
753 | * Process the special magic word as specified by $resolvedTgt['magicWordType']. |
754 | * ``` |
755 | * magicWordType === '!' => {{!}} is the magic word |
756 | * ``` |
757 | * @param TemplateEncapsulator $state |
758 | * @param array $resolvedTgt |
759 | * @return TemplateExpansionResult |
760 | */ |
761 | private function processSpecialMagicWord( |
762 | TemplateEncapsulator $state, array $resolvedTgt |
763 | ): TemplateExpansionResult { |
764 | $env = $this->env; |
765 | $tplToken = $state->token; |
766 | |
767 | // Special case for {{!}} magic word. |
768 | // |
769 | // If we tokenized as a magic word, we meant for it to expand to a |
770 | // string. The tokenizer has handling for this syntax in table |
771 | // positions. However, proceeding to go through template expansion |
772 | // will reparse it as a table cell token. Hence this special case |
773 | // handling to avoid that path. |
774 | if ( $resolvedTgt['magicWordType'] === '!' ) { |
775 | // If we're not at the top level, return a table cell. This will always |
776 | // be the case. Either {{!}} was tokenized as a td, or it was tokenized |
777 | // as template but the recursive call to fetch its content returns a |
778 | // single | in an ambiguous context which will again be tokenized as td. |
779 | // In any case, this should only be relevant for parserTests. |
780 | if ( $this->options['inTemplate'] ) { |
781 | $td = new TagTk( 'td' ); |
782 | $td->dataParsoid->getTemp()->attrSrc = ''; |
783 | $td->dataParsoid->setTempFlag( TempData::AT_SRC_START ); |
784 | $toks = [ $td ]; |
785 | } else { |
786 | $toks = [ '|' ]; |
787 | } |
788 | return new TemplateExpansionResult( $toks, false, (bool)$this->wrapTemplates ); |
789 | } |
790 | |
791 | throw new UnreachableException( |
792 | 'Unsupported magic word type: ' . ( $resolvedTgt['magicWordType'] ?? 'null' ) |
793 | ); |
794 | } |
795 | |
796 | private function expandTemplate( TemplateEncapsulator $state ): TemplateExpansionResult { |
797 | $env = $this->env; |
798 | $token = $state->token; |
799 | $expandTemplates = $this->options['expandTemplates']; |
800 | |
801 | // Since AttributeExpander runs later in the pipeline than TemplateHandler, |
802 | // if the template name is templated, use our copy of AttributeExpander |
803 | // to process the first attribute to tokens, and force reprocessing of this |
804 | // template token since we will then know the actual template target. |
805 | if ( $expandTemplates && self::hasTemplateToken( $token->attribs[0]->k ) ) { |
806 | $ret = $this->ae->expandFirstAttribute( $token ); |
807 | $toks = $ret->tokens ?? null; |
808 | Assert::invariant( $toks && count( $toks ) === 1 && $toks[0] === $token, |
809 | "Expected only the input token as the return value." ); |
810 | } |
811 | |
812 | if ( $this->atMaxArticleSize ) { |
813 | // As described above, if we were already greater than $wgMaxArticleSize |
814 | // we're going to return the tokens without expanding them. |
815 | // (This case is where the original article as fetched from the DB |
816 | // or passed to the API exceeded max article size.) |
817 | return $this->convertToString( $token ); |
818 | } |
819 | |
820 | // There's no point in proceeding if we've already hit the maximum inclusion size |
821 | // XXX should this be combined with the previous test? |
822 | if ( !$env->bumpWt2HtmlResourceUse( 'wikitextSize', 0 ) ) { |
823 | // FIXME: The legacy parser would try to make this a link and |
824 | // elsewhere we'd return the $e->getMessage() |
825 | // (This case is where the template post-expansion accumulation is |
826 | // over the maximum wikitext size.) |
827 | // XXX: It could be combined with the previous test, but we might |
828 | // want to use different error messages in the future. |
829 | return $this->convertToString( $token ); |
830 | } |
831 | |
832 | $toks = null; |
833 | $text = $token->dataParsoid->src ?? ''; |
834 | |
835 | $tgt = $this->resolveTemplateTarget( |
836 | $state, $token->attribs[0]->k, $token->attribs[0]->srcOffsets->key |
837 | ); |
838 | |
839 | if ( $expandTemplates && $tgt === null ) { |
840 | // Target contains tags, convert template braces and pipes back into text |
841 | // Re-join attribute tokens with '=' and '|' |
842 | return $this->convertToString( $token, true ); |
843 | } |
844 | |
845 | if ( isset( $tgt['magicWordType'] ) ) { |
846 | return $this->processSpecialMagicWord( $state, $tgt ); |
847 | } |
848 | |
849 | $frame = $this->manager->getFrame(); |
850 | if ( isset( $tgt['handler'] ) ) { |
851 | $handler = $tgt['handler']; |
852 | $extApi = new ParsoidExtensionAPI( $env, [ |
853 | 'wt2html' => [ |
854 | 'frame' => $frame, |
855 | 'parseOpts' => $this->options, |
856 | ], |
857 | ] ); |
858 | $args = []; |
859 | // Don't pass '' as the "1st argument" if the parser function |
860 | // didn't have a colon delimiter. |
861 | if ( count( $token->attribs ) > 1 || $tgt['haveColon'] ) { |
862 | // Trim before colon to make first argument |
863 | $args[] = new KV( '', $tgt['pfArg'], $tgt['srcOffsets']->expandTsrV() ); |
864 | } |
865 | for ( $i = 1; $i < count( $token->attribs ); $i++ ) { |
866 | $args[] = $token->attribs[$i]; |
867 | } |
868 | // FIXME: this will be refactored to use the tokenizer (T390344) |
869 | $arguments = new TemplateHandlerArguments( $env, $frame, $args ); |
870 | $hasAsyncContent = $tgt['handlerOptions']['hasAsyncContent'] ?? false; |
871 | if ( $hasAsyncContent ) { |
872 | // The HAS_ASYNC_CONTENT flag needs to be set by the fragment |
873 | // handler if this handler can *ever* return async content, |
874 | // regardless of whether this particular fragment was ready. |
875 | $env->getMetadata()->setOutputFlag( 'has-async-content' ); |
876 | } |
877 | $fragment = $handler->sourceToFragment( |
878 | $extApi, |
879 | $arguments, |
880 | false /* this is using {{ ... }} syntax */ |
881 | ); |
882 | if ( $fragment instanceof AsyncResult ) { |
883 | Assert::invariant( |
884 | $hasAsyncContent, |
885 | "returning async result without declaration" |
886 | ); |
887 | $env->getMetadata()->setOutputFlag( 'async-not-ready' ); |
888 | $fragment = $fragment->fallbackContent( $extApi ); |
889 | if ( $fragment === null ) { |
890 | // Create localized fallback message |
891 | $doc = $env->getTopLevelDoc(); |
892 | $msg = $doc->createDocumentFragment(); |
893 | $span = $doc->createElement( 'span' ); |
894 | $span->setAttribute( 'class', 'mw-async-not-ready' ); |
895 | DOMCompat::append( |
896 | $span, |
897 | WTUtils::createPageContentI18nFragment( |
898 | $doc, |
899 | $env->getSiteConfig()->getAsyncFallbackMessageKey(), |
900 | null |
901 | ) |
902 | ); |
903 | $msg->appendChild( $span ); |
904 | $fragment = DomPFragment::newFromDocumentFragment( $msg, null ); |
905 | } |
906 | } |
907 | // Map fragment to parsoid wikitext + embedded markers |
908 | [ |
909 | 'wikitext' => $wikitext, |
910 | ] = PipelineUtils::preparePFragment( |
911 | $env, |
912 | $this->manager->getFrame(), |
913 | $fragment, |
914 | [ |
915 | // options |
916 | ] |
917 | ); |
918 | $tplToks = $this->processTemplateSource( |
919 | $this->manager->getFrame(), |
920 | $token, |
921 | [ |
922 | 'name' => $tgt['name'], |
923 | 'title' => $tgt['title'], |
924 | 'attribs' => [], |
925 | ], |
926 | $wikitext, |
927 | [ |
928 | // We need to expand embedded {{#parsoid-fragment}} |
929 | // markers still (T385806) |
930 | 'expandTemplates' => true, |
931 | ] + $this->options |
932 | ); |
933 | return new TemplateExpansionResult( |
934 | $tplToks, true, $this->wrapTemplates |
935 | ); |
936 | } |
937 | |
938 | if ( $env->nativeTemplateExpansionEnabled() ) { |
939 | // Expand argument keys |
940 | $newAttribs = AttributeTransformManager::process( |
941 | $frame, |
942 | [ 'expandTemplates' => false, 'inTemplate' => true ], |
943 | $token->attribs |
944 | ) ?? $token->attribs; |
945 | $target = $newAttribs[0]->k; |
946 | if ( !$target ) { |
947 | $env->log( 'debug', 'No template target! ', $newAttribs ); |
948 | } |
949 | // Resolve the template target again now that the template token's |
950 | // attributes have been expanded by the AttributeTransformManager |
951 | $resolvedTgt = $this->resolveTemplateTarget( $state, $target, $newAttribs[0]->srcOffsets->key ); |
952 | if ( $resolvedTgt === null ) { |
953 | // Target contains tags, convert template braces and pipes back into text |
954 | // Re-join attribute tokens with '=' and '|' |
955 | return $this->convertToString( $token, true ); |
956 | } else { |
957 | return $this->expandTemplateNatively( $state, $resolvedTgt, $newAttribs ); |
958 | } |
959 | } elseif ( $expandTemplates ) { |
960 | // Use MediaWiki's preprocessor |
961 | // |
962 | // The tokenizer needs to use `text` as the cache key for caching |
963 | // expanded tokens from the expanded transclusion text that we get |
964 | // from the preprocessor, since parameter substitution will already |
965 | // have taken place. |
966 | // |
967 | // It's sufficient to pass `[]` in place of attribs since they |
968 | // won't be used. In `usePHPPreProcessor`, there is no parameter |
969 | // substitution coming from the frame. |
970 | |
971 | /* If $tgt is not null, target will be present. */ |
972 | $templateName = $tgt['name']; |
973 | $templateTitle = $tgt['title']; |
974 | // FIXME: This is a source of a lot of issues since templateargs |
975 | // get looked up from the Frame and yield these tokens which then enter |
976 | // the token stream. See T301948 and others from wmf.22 |
977 | // $attribs = array_slice( $token->attribs, 1 ); // Strip template name |
978 | $attribs = []; |
979 | |
980 | // We still need to check for limit violations because of the |
981 | // higher precedence of extension tags, which can result in nested |
982 | // templates even while using the php preprocessor for expansion. |
983 | $error = $this->enforceTemplateConstraints( $templateName, $templateTitle, true ); |
984 | if ( $error ) { |
985 | // FIXME: Should we be encapsulating here? |
986 | // Inconsistent with the other place constrainsts are enforced. |
987 | return new TemplateExpansionResult( $error ); |
988 | } |
989 | |
990 | if ( str_starts_with( $text, PipelineUtils::PARSOID_FRAGMENT_PREFIX ) ) { |
991 | // See PipelineUtils::pFragmentToParsoidFragmentMarkers() |
992 | // This is an atomic DOM subtree/forest, and so we're going |
993 | // to process it all the way to DOM. Contrast with our |
994 | // handling of a PFragment return value from a parser |
995 | // function below, which process to tokens only. |
996 | $pFragment = $env->getPFragment( $text ); |
997 | $domFragment = $pFragment->asDom( |
998 | new ParsoidExtensionAPI( |
999 | $env, [ |
1000 | 'wt2html' => [ |
1001 | 'frame' => $this->manager->getFrame(), |
1002 | 'parseOpts' => [ |
1003 | // This fragment comes from a template and it is important to set |
1004 | // the 'inTemplate' parse option for it. |
1005 | 'inTemplate' => true, |
1006 | // There might be translcusions within this fragment and we want |
1007 | // to expand them. Ex: {{1x|<ref>{{my-tpl}}foo</ref>}} |
1008 | 'expandTemplates' => true |
1009 | ] + $this->options |
1010 | ] |
1011 | ] |
1012 | ) |
1013 | ); |
1014 | $toks = PipelineUtils::tunnelDOMThroughTokens( $env, $token, $domFragment, [] ); |
1015 | $toks = $this->processTemplateTokens( $toks ); |
1016 | // This is an internal strip marker, it should be wrapped at a |
1017 | // higher level and we don't need to wrap it again. |
1018 | $wrapTemplates = false; |
1019 | return new TemplateExpansionResult( $toks, true, $wrapTemplates ); |
1020 | } else { |
1021 | // Fetch and process the template expansion |
1022 | $error = false; |
1023 | $fragment = Wikitext::preprocessFragment( |
1024 | $env, WikitextPFragment::newFromWt( $text, null ), $error |
1025 | ); |
1026 | if ( $error ) { |
1027 | return new TemplateExpansionResult( |
1028 | [ $fragment->killMarkers() ], false, $this->wrapTemplates |
1029 | ); |
1030 | } else { |
1031 | if ( |
1032 | $fragment instanceof WikitextPFragment && |
1033 | !$fragment->containsMarker() |
1034 | ) { |
1035 | // Optimize simple case |
1036 | $wikitext = $fragment->killMarkers(); |
1037 | $expandTemplates = false; |
1038 | } else { |
1039 | // This is a mixed expansion which contains wikitext and |
1040 | // atomic PFragments. Process this to tokens. |
1041 | // (Contrast with the processing of {{#parsoid-fragment}} |
1042 | // above, which represents an atomic PFragment.) |
1043 | [ |
1044 | 'wikitext' => $wikitext, |
1045 | ] = PipelineUtils::preparePFragment( |
1046 | $env, |
1047 | $this->manager->getFrame(), |
1048 | $fragment, |
1049 | [ |
1050 | // options |
1051 | ] |
1052 | ); |
1053 | // We need to expand embedded {{#parsoid-fragment}} |
1054 | // markers still (T385806) |
1055 | $expandTemplates = true; |
1056 | } |
1057 | $tplToks = $this->processTemplateSource( |
1058 | $this->manager->getFrame(), |
1059 | $token, |
1060 | [ |
1061 | 'name' => $templateName, |
1062 | 'title' => $templateTitle, |
1063 | 'attribs' => $attribs |
1064 | ], |
1065 | $wikitext, |
1066 | [ |
1067 | 'expandTemplates' => $expandTemplates, |
1068 | ] + $this->options |
1069 | ); |
1070 | return new TemplateExpansionResult( |
1071 | $tplToks, true, $this->wrapTemplates |
1072 | ); |
1073 | } |
1074 | } |
1075 | } else { |
1076 | // We don't perform recursive template expansion- something |
1077 | // template-like that the PHP parser did not expand. This is |
1078 | // encapsulated already, so just return the plain text. |
1079 | Assert::invariant( TokenUtils::isTemplateToken( $token ), "Expected template token." ); |
1080 | return $this->convertToString( $token ); |
1081 | } |
1082 | } |
1083 | |
1084 | /** |
1085 | * Main template token handler. |
1086 | * |
1087 | * Expands target and arguments (both keys and values) and either directly |
1088 | * calls or sets up the callback to expandTemplate, which then fetches and |
1089 | * processes the template. |
1090 | * |
1091 | * @param Token $token |
1092 | * @return TokenHandlerResult |
1093 | */ |
1094 | private function onTemplate( Token $token ): TokenHandlerResult { |
1095 | $state = new TemplateEncapsulator( |
1096 | $this->env, $this->manager->getFrame(), $token, 'mw:Transclusion' |
1097 | ); |
1098 | $res = $this->expandTemplate( $state ); |
1099 | $toks = $res->tokens; |
1100 | if ( $res->encap ) { |
1101 | $toks = $this->encapTokens( $state, $toks ); |
1102 | } |
1103 | if ( $res->shuttle ) { |
1104 | // Shuttle tokens to the end of the stage since they've gone through the |
1105 | // rest of the handlers in the current pipeline in the pipeline above. |
1106 | $toks = $this->manager->shuttleTokensToEndOfStage( $toks ); |
1107 | } |
1108 | return new TokenHandlerResult( $toks ); |
1109 | } |
1110 | |
1111 | /** |
1112 | * Expand template arguments with tokens from the containing frame. |
1113 | * @param Token $token |
1114 | * @return TokenHandlerResult |
1115 | */ |
1116 | private function onTemplateArg( Token $token ): TokenHandlerResult { |
1117 | $toks = $this->manager->getFrame()->expandTemplateArg( $token ); |
1118 | |
1119 | if ( $this->wrapTemplates && $this->options['expandTemplates'] ) { |
1120 | // This is a bare use of template arg syntax at the top level |
1121 | // outside any template use context. Wrap this use with RDF attrs. |
1122 | // so that this chunk can be RT-ed en-masse. |
1123 | $state = new TemplateEncapsulator( |
1124 | $this->env, $this->manager->getFrame(), $token, 'mw:Param' |
1125 | ); |
1126 | $toks = $this->encapTokens( $state, $toks ); |
1127 | } |
1128 | |
1129 | // Shuttle tokens to the end of the stage since they've gone through the |
1130 | // rest of the handlers in the current pipeline in the pipeline above. |
1131 | $toks = $this->manager->shuttleTokensToEndOfStage( $toks ); |
1132 | |
1133 | return new TokenHandlerResult( $toks ); |
1134 | } |
1135 | |
1136 | public function onTag( Token $token ): ?TokenHandlerResult { |
1137 | switch ( $token->getName() ) { |
1138 | case "template": |
1139 | return $this->onTemplate( $token ); |
1140 | case "templatearg": |
1141 | return $this->onTemplateArg( $token ); |
1142 | default: |
1143 | return null; |
1144 | } |
1145 | } |
1146 | } |