3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
23 * An expansion frame, used as a context to expand the result of preprocessToObj()
24 * @deprecated since 1.34, use PPFrame_Hash
27 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
28 class PPFrame_DOM
implements PPFrame
{
47 * Hashtable listing templates which are disallowed for expansion in this frame,
48 * having been encountered previously in parent frames.
50 public $loopCheckHash;
53 * Recursion depth of this frame, top = 0
54 * Note that this is NOT the same as expansion depth in expand()
58 private $volatile = false;
64 protected $childExpansionCache;
67 * Construct a new preprocessor frame.
68 * @param Preprocessor $preprocessor The parent preprocessor
70 public function __construct( $preprocessor ) {
71 $this->preprocessor
= $preprocessor;
72 $this->parser
= $preprocessor->parser
;
73 $this->title
= $this->parser
->getTitle();
74 $this->titleCache
= [ $this->title ?
$this->title
->getPrefixedDBkey() : false ];
75 $this->loopCheckHash
= [];
77 $this->childExpansionCache
= [];
81 * Create a new child frame
82 * $args is optionally a multi-root PPNode or array containing the template arguments
84 * @param bool|array $args
85 * @param Title|bool $title
86 * @param int $indexOffset
87 * @return PPTemplateFrame_DOM
89 public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
92 if ( $title === false ) {
93 $title = $this->title
;
95 if ( $args !== false ) {
97 if ( $args instanceof PPNode
) {
100 foreach ( $args as $arg ) {
101 if ( $arg instanceof PPNode
) {
104 if ( !$xpath ||
$xpath->document
!== $arg->ownerDocument
) {
105 $xpath = new DOMXPath( $arg->ownerDocument
);
108 $nameNodes = $xpath->query( 'name', $arg );
109 $value = $xpath->query( 'value', $arg );
110 if ( $nameNodes->item( 0 )->hasAttributes() ) {
111 // Numbered parameter
112 $index = $nameNodes->item( 0 )->attributes
->getNamedItem( 'index' )->textContent
;
113 $index = $index - $indexOffset;
114 if ( isset( $namedArgs[$index] ) ||
isset( $numberedArgs[$index] ) ) {
115 $this->parser
->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
116 wfEscapeWikiText( $this->title
),
117 wfEscapeWikiText( $title ),
118 wfEscapeWikiText( $index ) )->text() );
119 $this->parser
->addTrackingCategory( 'duplicate-args-category' );
121 $numberedArgs[$index] = $value->item( 0 );
122 unset( $namedArgs[$index] );
125 $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame
::STRIP_COMMENTS
) );
126 if ( isset( $namedArgs[$name] ) ||
isset( $numberedArgs[$name] ) ) {
127 $this->parser
->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
128 wfEscapeWikiText( $this->title
),
129 wfEscapeWikiText( $title ),
130 wfEscapeWikiText( $name ) )->text() );
131 $this->parser
->addTrackingCategory( 'duplicate-args-category' );
133 $namedArgs[$name] = $value->item( 0 );
134 unset( $numberedArgs[$name] );
138 return new PPTemplateFrame_DOM( $this->preprocessor
, $this, $numberedArgs, $namedArgs, $title );
142 * @throws MWException
143 * @param string|int $key
144 * @param string|PPNode_DOM|DOMNode|DOMNodeList $root
148 public function cachedExpand( $key, $root, $flags = 0 ) {
149 // we don't have a parent, so we don't have a cache
150 return $this->expand( $root, $flags );
154 * @throws MWException
155 * @param string|PPNode_DOM|DOMNode $root
159 public function expand( $root, $flags = 0 ) {
160 static $expansionDepth = 0;
161 if ( is_string( $root ) ) {
165 if ( ++
$this->parser
->mPPNodeCount
> $this->parser
->mOptions
->getMaxPPNodeCount() ) {
166 $this->parser
->limitationWarn( 'node-count-exceeded',
167 $this->parser
->mPPNodeCount
,
168 $this->parser
->mOptions
->getMaxPPNodeCount()
170 return '<span class="error">Node-count limit exceeded</span>';
173 if ( $expansionDepth > $this->parser
->mOptions
->getMaxPPExpandDepth() ) {
174 $this->parser
->limitationWarn( 'expansion-depth-exceeded',
176 $this->parser
->mOptions
->getMaxPPExpandDepth()
178 return '<span class="error">Expansion depth limit exceeded</span>';
181 if ( $expansionDepth > $this->parser
->mHighestExpansionDepth
) {
182 $this->parser
->mHighestExpansionDepth
= $expansionDepth;
185 if ( $root instanceof PPNode_DOM
) {
188 if ( $root instanceof DOMDocument
) {
189 $root = $root->documentElement
;
192 $outStack = [ '', '' ];
193 $iteratorStack = [ false, $root ];
194 $indexStack = [ 0, 0 ];
196 while ( count( $iteratorStack ) > 1 ) {
197 $level = count( $outStack ) - 1;
198 $iteratorNode =& $iteratorStack[$level];
199 $out =& $outStack[$level];
200 $index =& $indexStack[$level];
202 if ( $iteratorNode instanceof PPNode_DOM
) {
203 $iteratorNode = $iteratorNode->node
;
206 if ( is_array( $iteratorNode ) ) {
207 if ( $index >= count( $iteratorNode ) ) {
208 // All done with this iterator
209 $iteratorStack[$level] = false;
210 $contextNode = false;
212 $contextNode = $iteratorNode[$index];
215 } elseif ( $iteratorNode instanceof DOMNodeList
) {
216 if ( $index >= $iteratorNode->length
) {
217 // All done with this iterator
218 $iteratorStack[$level] = false;
219 $contextNode = false;
221 $contextNode = $iteratorNode->item( $index );
225 // Copy to $contextNode and then delete from iterator stack,
226 // because this is not an iterator but we do have to execute it once
227 $contextNode = $iteratorStack[$level];
228 $iteratorStack[$level] = false;
231 if ( $contextNode instanceof PPNode_DOM
) {
232 $contextNode = $contextNode->node
;
235 $newIterator = false;
237 if ( $contextNode === false ) {
239 } elseif ( is_string( $contextNode ) ) {
240 $out .= $contextNode;
241 } elseif ( is_array( $contextNode ) ||
$contextNode instanceof DOMNodeList
) {
242 $newIterator = $contextNode;
243 } elseif ( $contextNode instanceof DOMNode
) {
244 if ( $contextNode->nodeType
== XML_TEXT_NODE
) {
245 $out .= $contextNode->nodeValue
;
246 } elseif ( $contextNode->nodeName
== 'template' ) {
247 # Double-brace expansion
248 $xpath = new DOMXPath( $contextNode->ownerDocument
);
249 $titles = $xpath->query( 'title', $contextNode );
250 $title = $titles->item( 0 );
251 $parts = $xpath->query( 'part', $contextNode );
252 if ( $flags & PPFrame
::NO_TEMPLATES
) {
253 $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts );
255 $lineStart = $contextNode->getAttribute( 'lineStart' );
257 'title' => new PPNode_DOM( $title ),
258 'parts' => new PPNode_DOM( $parts ),
259 'lineStart' => $lineStart ];
260 $ret = $this->parser
->braceSubstitution( $params, $this );
261 if ( isset( $ret['object'] ) ) {
262 $newIterator = $ret['object'];
264 $out .= $ret['text'];
267 } elseif ( $contextNode->nodeName
== 'tplarg' ) {
268 # Triple-brace expansion
269 $xpath = new DOMXPath( $contextNode->ownerDocument
);
270 $titles = $xpath->query( 'title', $contextNode );
271 $title = $titles->item( 0 );
272 $parts = $xpath->query( 'part', $contextNode );
273 if ( $flags & PPFrame
::NO_ARGS
) {
274 $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts );
277 'title' => new PPNode_DOM( $title ),
278 'parts' => new PPNode_DOM( $parts ) ];
279 $ret = $this->parser
->argSubstitution( $params, $this );
280 if ( isset( $ret['object'] ) ) {
281 $newIterator = $ret['object'];
283 $out .= $ret['text'];
286 } elseif ( $contextNode->nodeName
== 'comment' ) {
288 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
289 # Not in RECOVER_COMMENTS mode (msgnw) though.
290 if ( ( $this->parser
->ot
['html']
291 ||
( $this->parser
->ot
['pre'] && $this->parser
->mOptions
->getRemoveComments() )
292 ||
( $flags & PPFrame
::STRIP_COMMENTS
)
293 ) && !( $flags & PPFrame
::RECOVER_COMMENTS
)
296 } elseif ( $this->parser
->ot
['wiki'] && !( $flags & PPFrame
::RECOVER_COMMENTS
) ) {
297 # Add a strip marker in PST mode so that pstPass2() can
298 # run some old-fashioned regexes on the result.
299 # Not in RECOVER_COMMENTS mode (extractSections) though.
300 $out .= $this->parser
->insertStripItem( $contextNode->textContent
);
302 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
303 $out .= $contextNode->textContent
;
305 } elseif ( $contextNode->nodeName
== 'ignore' ) {
306 # Output suppression used by <includeonly> etc.
307 # OT_WIKI will only respect <ignore> in substed templates.
308 # The other output types respect it unless NO_IGNORE is set.
309 # extractSections() sets NO_IGNORE and so never respects it.
310 if ( ( !isset( $this->parent
) && $this->parser
->ot
['wiki'] )
311 ||
( $flags & PPFrame
::NO_IGNORE
)
313 $out .= $contextNode->textContent
;
317 } elseif ( $contextNode->nodeName
== 'ext' ) {
319 $xpath = new DOMXPath( $contextNode->ownerDocument
);
320 $names = $xpath->query( 'name', $contextNode );
321 $attrs = $xpath->query( 'attr', $contextNode );
322 $inners = $xpath->query( 'inner', $contextNode );
323 $closes = $xpath->query( 'close', $contextNode );
324 if ( $flags & PPFrame
::NO_TAGS
) {
325 $s = '<' . $this->expand( $names->item( 0 ), $flags );
326 if ( $attrs->length
> 0 ) {
327 $s .= $this->expand( $attrs->item( 0 ), $flags );
329 if ( $inners->length
> 0 ) {
330 $s .= '>' . $this->expand( $inners->item( 0 ), $flags );
331 if ( $closes->length
> 0 ) {
332 $s .= $this->expand( $closes->item( 0 ), $flags );
340 'name' => new PPNode_DOM( $names->item( 0 ) ),
341 'attr' => $attrs->length
> 0 ?
new PPNode_DOM( $attrs->item( 0 ) ) : null,
342 'inner' => $inners->length
> 0 ?
new PPNode_DOM( $inners->item( 0 ) ) : null,
343 'close' => $closes->length
> 0 ?
new PPNode_DOM( $closes->item( 0 ) ) : null,
345 $out .= $this->parser
->extensionSubstitution( $params, $this );
347 } elseif ( $contextNode->nodeName
== 'h' ) {
349 $s = $this->expand( $contextNode->childNodes
, $flags );
351 # Insert a heading marker only for <h> children of <root>
352 # This is to stop extractSections from going over multiple tree levels
353 if ( $contextNode->parentNode
->nodeName
== 'root' && $this->parser
->ot
['html'] ) {
354 # Insert heading index marker
355 $headingIndex = $contextNode->getAttribute( 'i' );
356 $titleText = $this->title
->getPrefixedDBkey();
357 $this->parser
->mHeadings
[] = [ $titleText, $headingIndex ];
358 $serial = count( $this->parser
->mHeadings
) - 1;
359 $marker = Parser
::MARKER_PREFIX
. "-h-$serial-" . Parser
::MARKER_SUFFIX
;
360 $count = $contextNode->getAttribute( 'level' );
361 $s = substr( $s, 0, $count ) . $marker . substr( $s, $count );
362 $this->parser
->mStripState
->addGeneral( $marker, '' );
366 # Generic recursive expansion
367 $newIterator = $contextNode->childNodes
;
370 throw new MWException( __METHOD__
. ': Invalid parameter type' );
373 if ( $newIterator !== false ) {
374 if ( $newIterator instanceof PPNode_DOM
) {
375 $newIterator = $newIterator->node
;
378 $iteratorStack[] = $newIterator;
380 } elseif ( $iteratorStack[$level] === false ) {
381 // Return accumulated value to parent
382 // With tail recursion
383 while ( $iteratorStack[$level] === false && $level > 0 ) {
384 $outStack[$level - 1] .= $out;
385 array_pop( $outStack );
386 array_pop( $iteratorStack );
387 array_pop( $indexStack );
399 * @param string|PPNode_DOM|DOMNode ...$args
402 public function implodeWithFlags( $sep, $flags, ...$args ) {
405 foreach ( $args as $root ) {
406 if ( $root instanceof PPNode_DOM
) {
409 if ( !is_array( $root ) && !( $root instanceof DOMNodeList
) ) {
412 foreach ( $root as $node ) {
418 $s .= $this->expand( $node, $flags );
425 * Implode with no flags specified
426 * This previously called implodeWithFlags but has now been inlined to reduce stack depth
429 * @param string|PPNode_DOM|DOMNode ...$args
432 public function implode( $sep, ...$args ) {
435 foreach ( $args as $root ) {
436 if ( $root instanceof PPNode_DOM
) {
439 if ( !is_array( $root ) && !( $root instanceof DOMNodeList
) ) {
442 foreach ( $root as $node ) {
448 $s .= $this->expand( $node );
455 * Makes an object that, when expand()ed, will be the same as one obtained
459 * @param string|PPNode_DOM|DOMNode ...$args
461 * @suppress PhanParamSignatureMismatch
463 public function virtualImplode( $sep, ...$args ) {
467 foreach ( $args as $root ) {
468 if ( $root instanceof PPNode_DOM
) {
471 if ( !is_array( $root ) && !( $root instanceof DOMNodeList
) ) {
474 foreach ( $root as $node ) {
487 * Virtual implode with brackets
488 * @param string $start
491 * @param string|PPNode_DOM|DOMNode ...$args
493 * @suppress PhanParamSignatureMismatch
495 public function virtualBracketedImplode( $start, $sep, $end, ...$args ) {
499 foreach ( $args as $root ) {
500 if ( $root instanceof PPNode_DOM
) {
503 if ( !is_array( $root ) && !( $root instanceof DOMNodeList
) ) {
506 foreach ( $root as $node ) {
519 public function __toString() {
523 public function getPDBK( $level = false ) {
524 if ( $level === false ) {
525 return $this->title
->getPrefixedDBkey();
527 return $this->titleCache
[$level] ??
false;
534 public function getArguments() {
541 public function getNumberedArguments() {
548 public function getNamedArguments() {
553 * Returns true if there are no arguments in this frame
557 public function isEmpty() {
562 * @param int|string $name
563 * @return bool Always false in this implementation.
565 public function getArgument( $name ) {
570 * Returns true if the infinite loop check is OK, false if a loop is detected
572 * @param Title $title
575 public function loopCheck( $title ) {
576 return !isset( $this->loopCheckHash
[$title->getPrefixedDBkey()] );
580 * Return true if the frame is a template frame
584 public function isTemplate() {
589 * Get a title of frame
593 public function getTitle() {
598 * Set the volatile flag
602 public function setVolatile( $flag = true ) {
603 $this->volatile
= $flag;
607 * Get the volatile flag
611 public function isVolatile() {
612 return $this->volatile
;
620 public function setTTL( $ttl ) {
621 if ( $ttl !== null && ( $this->ttl
=== null ||
$ttl < $this->ttl
) ) {
631 public function getTTL() {