MediaWiki master
PPFrame_Hash.php
Go to the documentation of this file.
1<?php
8namespace MediaWiki\Parser;
9
10use InvalidArgumentException;
16use RuntimeException;
17use Stringable;
18use Wikimedia\Parsoid\Fragments\HeadingPFragment;
19
24// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
25class PPFrame_Hash implements Stringable, PPFrame {
26
30 public $parser;
31
36
40 public $title;
41
46
53
59 public $depth;
60
62 private $volatile = false;
64 private $ttl = null;
65
73 private $maxPPNodeCount;
77 private $maxPPExpandDepth;
78
79 private bool $useHeadingPFragments;
80
84 public function __construct( $preprocessor ) {
85 $this->preprocessor = $preprocessor;
86 $this->parser = $preprocessor->parser;
87 $this->title = $this->parser->getTitle();
88 $this->maxPPNodeCount = $this->parser->getOptions()->getMaxPPNodeCount();
89 $this->maxPPExpandDepth = $this->parser->getOptions()->getMaxPPExpandDepth();
90 $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
91 $this->loopCheckHash = [];
92 $this->depth = 0;
93 $this->childExpansionCache = [];
94
95 $config = MediaWikiServices::getInstance()->getMainConfig();
96 $this->useHeadingPFragments = in_array(
97 'HeadingPFragment',
99 true
100 );
101 }
102
112 public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
113 $namedArgs = [];
114 $numberedArgs = [];
115 if ( $title === false ) {
117 }
118 if ( $args !== false ) {
119 if ( $args instanceof PPNode_Hash_Array ) {
120 $args = $args->value;
121 } elseif ( !is_array( $args ) ) {
122 throw new InvalidArgumentException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
123 }
124 foreach ( $args as $arg ) {
125 $bits = $arg->splitArg();
126 if ( $bits['index'] !== '' ) {
127 // Numbered parameter
128 $index = $bits['index'] - $indexOffset;
129 if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
130 $this->parser->getOutput()->addWarningMsg(
131 'duplicate-args-warning',
132 wfEscapeWikiText( (string)$this->title ),
133 wfEscapeWikiText( (string)$title ),
134 Message::numParam( $index )
135 );
136 $this->parser->addTrackingCategory( 'duplicate-args-category' );
137 }
138 $numberedArgs[$index] = $bits['value'];
139 unset( $namedArgs[$index] );
140 } else {
141 // Named parameter
142 $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
143 if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
144 $this->parser->getOutput()->addWarningMsg(
145 'duplicate-args-warning',
146 wfEscapeWikiText( (string)$this->title ),
147 wfEscapeWikiText( (string)$title ),
148 // @phan-suppress-next-line SecurityCheck-DoubleEscaped
149 wfEscapeWikiText( $name )
150 );
151 $this->parser->addTrackingCategory( 'duplicate-args-category' );
152 }
153 $namedArgs[$name] = $bits['value'];
154 unset( $numberedArgs[$name] );
155 }
156 }
157 }
158 return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
159 }
160
167 public function cachedExpand( $key, $root, $flags = 0 ) {
168 // we don't have a parent, so we don't have a cache
169 return $this->expand( $root, $flags );
170 }
171
177 public function expand( $root, $flags = 0 ) {
178 static $expansionDepth = 0;
179 if ( is_string( $root ) ) {
180 return $root;
181 }
182
183 if ( ++$this->parser->mPPNodeCount > $this->maxPPNodeCount ) {
184 $this->parser->limitationWarn( 'node-count-exceeded',
185 $this->parser->mPPNodeCount,
186 $this->maxPPNodeCount
187 );
188 return '<span class="error">Node-count limit exceeded</span>';
189 }
190 if ( $expansionDepth > $this->maxPPExpandDepth ) {
191 $this->parser->limitationWarn( 'expansion-depth-exceeded',
192 $expansionDepth,
193 $this->maxPPExpandDepth
194 );
195 return '<span class="error">Expansion depth limit exceeded</span>';
196 }
197 ++$expansionDepth;
198 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
199 $this->parser->mHighestExpansionDepth = $expansionDepth;
200 }
201
202 $outStack = [ '', '' ];
203 $iteratorStack = [ false, $root ];
204 $indexStack = [ 0, 0 ];
205
206 while ( count( $iteratorStack ) > 1 ) {
207 $level = count( $outStack ) - 1;
208 $iteratorNode =& $iteratorStack[$level];
209 $out =& $outStack[$level];
210 $index =& $indexStack[$level];
211
212 if ( is_array( $iteratorNode ) ) {
213 if ( $index >= count( $iteratorNode ) ) {
214 // All done with this iterator
215 $iteratorStack[$level] = false;
216 $contextNode = false;
217 } else {
218 $contextNode = $iteratorNode[$index];
219 $index++;
220 }
221 } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
222 if ( $index >= $iteratorNode->getLength() ) {
223 // All done with this iterator
224 $iteratorStack[$level] = false;
225 $contextNode = false;
226 } else {
227 $contextNode = $iteratorNode->item( $index );
228 $index++;
229 }
230 } else {
231 // Copy to $contextNode and then delete from iterator stack,
232 // because this is not an iterator but we do have to execute it once
233 $contextNode = $iteratorStack[$level];
234 $iteratorStack[$level] = false;
235 }
236
237 $newIterator = false;
238 $contextName = false;
239 $contextChildren = false;
240
241 if ( $contextNode === false ) {
242 // nothing to do
243 } elseif ( is_string( $contextNode ) ) {
244 $out .= $contextNode;
245 } elseif ( $contextNode instanceof PPNode_Hash_Array ) {
246 $newIterator = $contextNode;
247 } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
248 // No output
249 } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
250 $out .= $contextNode->value;
251 } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
252 $contextName = $contextNode->name;
253 $contextChildren = $contextNode->getRawChildren();
254 } elseif ( is_array( $contextNode ) ) {
255 // Node descriptor array
256 if ( count( $contextNode ) !== 2 ) {
257 throw new RuntimeException( __METHOD__ .
258 ': found an array where a node descriptor should be' );
259 }
260 [ $contextName, $contextChildren ] = $contextNode;
261 } else {
262 throw new RuntimeException( __METHOD__ . ': Invalid parameter type' );
263 }
264
265 // Handle node descriptor array or tree object
266 if ( $contextName === false ) {
267 // Not a node, already handled above
268 } elseif ( $contextName[0] === '@' ) {
269 // Attribute: no output
270 } elseif ( $contextName === 'template' ) {
271 # Double-brace expansion
272 $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
273 if ( $flags & PPFrame::NO_TEMPLATES ) {
274 $newIterator = $this->virtualBracketedImplode(
275 '{{', '|', '}}',
276 $bits['title'],
277 $bits['parts']
278 );
279 } else {
280 $ret = $this->parser->braceSubstitution( $bits, $this );
281 if ( isset( $ret['object'] ) ) {
282 $newIterator = $ret['object'];
283 } else {
284 $out .= $ret['text'];
285 }
286 }
287 } elseif ( $contextName === 'tplarg' ) {
288 # Triple-brace expansion
289 $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
290 if ( $flags & PPFrame::NO_ARGS ) {
291 $newIterator = $this->virtualBracketedImplode(
292 '{{{', '|', '}}}',
293 $bits['title'],
294 $bits['parts']
295 );
296 } else {
297 $ret = $this->parser->argSubstitution( $bits, $this );
298 if ( isset( $ret['object'] ) ) {
299 $newIterator = $ret['object'];
300 } else {
301 $out .= $ret['text'];
302 }
303 }
304 } elseif ( $contextName === 'comment' ) {
305 # HTML-style comment
306 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
307 # Not in RECOVER_COMMENTS mode (msgnw) though.
308 if ( ( $this->parser->getOutputType() === Parser::OT_HTML
309 || ( $this->parser->getOutputType() === Parser::OT_PREPROCESS &&
310 $this->parser->getOptions()->getRemoveComments() )
311 || ( $flags & PPFrame::STRIP_COMMENTS )
312 ) && !( $flags & PPFrame::RECOVER_COMMENTS )
313 ) {
314 $out .= '';
315 } elseif (
316 $this->parser->getOutputType() === Parser::OT_WIKI &&
317 !( $flags & PPFrame::RECOVER_COMMENTS )
318 ) {
319 # Add a strip marker in PST mode so that pstPass2() can
320 # run some old-fashioned regexes on the result.
321 # Not in RECOVER_COMMENTS mode (extractSections) though.
322 $out .= $this->parser->insertStripItem( $contextChildren[0] );
323 } else {
324 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
325 $out .= $contextChildren[0];
326 }
327 } elseif ( $contextName === 'ignore' ) {
328 # Output suppression used by <includeonly> etc.
329 # OT_WIKI will only respect <ignore> in substed templates.
330 # The other output types respect it unless NO_IGNORE is set.
331 # extractSections() sets NO_IGNORE and so never respects it.
332 if ( ( !isset( $this->parent ) && $this->parser->getOutputType() === Parser::OT_WIKI )
333 || ( $flags & PPFrame::NO_IGNORE )
334 ) {
335 $out .= $contextChildren[0];
336 } else {
337 // $out .= '';
338 }
339 } elseif ( $contextName === 'ext' ) {
340 # Extension tag
341 $bits = PPNode_Hash_Tree::splitRawExt( $contextChildren ) +
342 [ 'attr' => null, 'inner' => null, 'close' => null ];
343 if ( $flags & PPFrame::NO_TAGS ) {
344 $s = '<' . $bits['name']->getFirstChild()->value;
345 if ( $bits['attr'] ) {
346 $s .= $bits['attr']->getFirstChild()->value;
347 }
348 if ( $bits['inner'] ) {
349 $s .= '>' . $bits['inner']->getFirstChild()->value;
350 if ( $bits['close'] ) {
351 $s .= $bits['close']->getFirstChild()->value;
352 }
353 } else {
354 $s .= '/>';
355 }
356 $out .= $s;
357 } else {
358 $out .= $this->parser->extensionSubstitution( $bits, $this );
359 }
360 } elseif ( $contextName === 'h' ) {
361 # Heading
362 if ( $this->parser->getOutputType() === Parser::OT_HTML ) {
363 # Expand immediately and insert heading index marker
364 $s = $this->expand( $contextChildren, $flags );
365 $bits = PPNode_Hash_Tree::splitRawHeading( $contextChildren );
366 $titleText = $this->title->getPrefixedDBkey();
367 $this->parser->mHeadings[] = [ $titleText, $bits['i'] ];
368 $serial = count( $this->parser->mHeadings ) - 1;
369 $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
370 $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
371 $this->parser->getStripState()->addGeneral( $marker, '' );
372 $out .= $s;
373 } elseif (
374 $this->useHeadingPFragments &&
375 $this->parser->useParsoidFragments() &&
376 $this->parser->getOutputType() === Parser::OT_PREPROCESS
377 ) {
378 $s = $this->expand( $contextChildren, $flags );
379 $fragment = DataAccess::unstripForParsoid( $s, $this->parser );
380 $bits = PPNode_Hash_Tree::splitRawHeading( $contextChildren );
381 $pFragment = new HeadingPFragment( $fragment, $this->title, $bits['i'] );
382
383 // Just generate a unique marker
384 $this->parser->mHeadings[] = [];
385 $serial = count( $this->parser->mHeadings ) - 1;
386 $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
387
388 $this->parser->getStripState()->addParsoidOpaque( $marker, $pFragment );
389 $out .= $marker;
390 } else {
391 # Expand in virtual stack
392 $newIterator = $contextChildren;
393 }
394 } else {
395 # Generic recursive expansion
396 $newIterator = $contextChildren;
397 }
398
399 if ( $newIterator !== false ) {
400 $outStack[] = '';
401 $iteratorStack[] = $newIterator;
402 $indexStack[] = 0;
403 } elseif ( $iteratorStack[$level] === false ) {
404 // Return accumulated value to parent
405 // With tail recursion
406 while ( $iteratorStack[$level] === false && $level > 0 ) {
407 $outStack[$level - 1] .= $out;
408 array_pop( $outStack );
409 array_pop( $iteratorStack );
410 array_pop( $indexStack );
411 $level--;
412 }
413 }
414 }
415 --$expansionDepth;
416 return $outStack[0];
417 }
418
425 public function implodeWithFlags( $sep, $flags, ...$args ) {
426 $first = true;
427 $s = '';
428 foreach ( $args as $root ) {
429 if ( $root instanceof PPNode_Hash_Array ) {
430 $root = $root->value;
431 }
432 if ( !is_array( $root ) ) {
433 $root = [ $root ];
434 }
435 foreach ( $root as $node ) {
436 if ( $first ) {
437 $first = false;
438 } else {
439 $s .= $sep;
440 }
441 $s .= $this->expand( $node, $flags );
442 }
443 }
444 return $s;
445 }
446
454 public function implode( $sep, ...$args ) {
455 $first = true;
456 $s = '';
457 foreach ( $args as $root ) {
458 if ( $root instanceof PPNode_Hash_Array ) {
459 $root = $root->value;
460 }
461 if ( !is_array( $root ) ) {
462 $root = [ $root ];
463 }
464 foreach ( $root as $node ) {
465 if ( $first ) {
466 $first = false;
467 } else {
468 $s .= $sep;
469 }
470 $s .= $this->expand( $node );
471 }
472 }
473 return $s;
474 }
475
484 public function virtualImplode( $sep, ...$args ) {
485 $out = [];
486 $first = true;
487
488 foreach ( $args as $root ) {
489 if ( $root instanceof PPNode_Hash_Array ) {
490 $root = $root->value;
491 }
492 if ( !is_array( $root ) ) {
493 $root = [ $root ];
494 }
495 foreach ( $root as $node ) {
496 if ( $first ) {
497 $first = false;
498 } else {
499 $out[] = $sep;
500 }
501 $out[] = $node;
502 }
503 }
504 return new PPNode_Hash_Array( $out );
505 }
506
516 public function virtualBracketedImplode( $start, $sep, $end, ...$args ) {
517 $out = [ $start ];
518 $first = true;
519
520 foreach ( $args as $root ) {
521 if ( $root instanceof PPNode_Hash_Array ) {
522 $root = $root->value;
523 }
524 if ( !is_array( $root ) ) {
525 $root = [ $root ];
526 }
527 foreach ( $root as $node ) {
528 if ( $first ) {
529 $first = false;
530 } else {
531 $out[] = $sep;
532 }
533 $out[] = $node;
534 }
535 }
536 $out[] = $end;
537 return new PPNode_Hash_Array( $out );
538 }
539
540 public function __toString() {
541 return 'frame{}';
542 }
543
548 public function getPDBK( $level = false ) {
549 if ( $level === false ) {
550 return $this->title->getPrefixedDBkey();
551 } else {
552 return $this->titleCache[$level] ?? false;
553 }
554 }
555
559 public function getArguments() {
560 return [];
561 }
562
566 public function getNumberedArguments() {
567 return [];
568 }
569
573 public function getNamedArguments() {
574 return [];
575 }
576
582 public function isEmpty() {
583 return true;
584 }
585
590 public function getArgument( $name ) {
591 return false;
592 }
593
601 public function loopCheck( $title ) {
602 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
603 }
604
610 public function isTemplate() {
611 return false;
612 }
613
619 public function getTitle() {
620 return $this->title;
621 }
622
628 public function setVolatile( $flag = true ) {
629 $this->volatile = $flag;
630 }
631
637 public function isVolatile() {
638 return $this->volatile;
639 }
640
645 public function setTTL( $ttl ) {
646 wfDeprecated( __METHOD__, '1.44' );
647 if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
648 $this->ttl = $ttl;
649 }
650 }
651
656 public function getTTL() {
657 wfDeprecated( __METHOD__, '1.46' );
658 return $this->ttl;
659 }
660}
661
663class_alias( PPFrame_Hash::class, 'PPFrame_Hash' );
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
A class containing constants representing the names of configuration variables.
const ReturnExperimentalPFragmentTypes
Name constant for the ReturnExperimentalPFragmentTypes setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
An expansion frame, used as a context to expand the result of preprocessToObj()
implodeWithFlags( $sep, $flags,... $args)
setVolatile( $flag=true)
Set the volatile flag.
loopCheck( $title)
Returns true if the infinite loop check is OK, false if a loop is detected.
int $depth
Recursion depth of this frame, top = 0 Note that this is NOT the same as expansion depth in expand()
true[] $loopCheckHash
Hashtable listing templates which are disallowed for expansion in this frame, having been encountered...
isEmpty()
Returns true if there are no arguments in this frame.
cachedExpand( $key, $root, $flags=0)
virtualImplode( $sep,... $args)
Makes an object that, when expand()ed, will be the same as one obtained with implode()
implode( $sep,... $args)
Implode with no flags specified This previously called implodeWithFlags but has now been inlined to r...
getTitle()
Get a title of frame.
isTemplate()
Return true if the frame is a template frame.
isVolatile()
Get the volatile flag.
virtualBracketedImplode( $start, $sep, $end,... $args)
Virtual implode with brackets.
newChild( $args=false, $title=false, $indexOffset=0)
Create a new child frame $args is optionally a multi-root PPNode or array containing the template arg...
static splitRawHeading(array $children)
Like splitHeading() but for a raw child array.
static splitRawExt(array $children)
Like splitExt() but for a raw child array.
static splitRawTemplate(array $children)
Like splitTemplate() but for a raw child array.
Expansion frame with template arguments.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:139
const OT_PREPROCESS
Output type: like Parser::preprocess()
Definition Parser.php:181
const OT_WIKI
Output type: like Parser::preSaveTransform()
Definition Parser.php:179
const OT_HTML
Output type: like Parser::parse()
Definition Parser.php:177
Implement Parsoid's abstract class for data access.
static unstripForParsoid(string $wikitext, Parser $parser)
Where the result has strip state markers, tunnel this content through Parsoid as a PFragment type.
Represents a title within MediaWiki.
Definition Title.php:69
getPrefixedDBkey()
Get the prefixed database key form.
Definition Title.php:1845