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