Completed
Branch master (939199)
by
unknown
39:35
created

includes/parser/Preprocessor_Hash.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Preprocessor using PHP arrays
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Parser
22
 */
23
24
/**
25
 * Differences from DOM schema:
26
 *   * attribute nodes are children
27
 *   * "<h>" nodes that aren't at the top are replaced with <possible-h>
28
 *
29
 * Nodes are stored in a recursive array data structure. A node store is an
30
 * array where each element may be either a scalar (representing a text node)
31
 * or a "descriptor", which is a two-element array where the first element is
32
 * the node name and the second element is the node store for the children.
33
 *
34
 * Attributes are represented as children that have a node name starting with
35
 * "@", and a single text node child.
36
 *
37
 * @todo: Consider replacing descriptor arrays with objects of a new class.
38
 * Benchmark and measure resulting memory impact.
39
 *
40
 * @ingroup Parser
41
 */
42
// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
43
class Preprocessor_Hash extends Preprocessor {
44
	// @codingStandardsIgnoreEnd
45
46
	/**
47
	 * @var Parser
48
	 */
49
	public $parser;
50
51
	const CACHE_PREFIX = 'preprocess-hash';
52
	const CACHE_VERSION = 2;
53
54
	public function __construct( $parser ) {
55
		$this->parser = $parser;
56
	}
57
58
	/**
59
	 * @return PPFrame_Hash
60
	 */
61
	public function newFrame() {
62
		return new PPFrame_Hash( $this );
63
	}
64
65
	/**
66
	 * @param array $args
67
	 * @return PPCustomFrame_Hash
68
	 */
69
	public function newCustomFrame( $args ) {
70
		return new PPCustomFrame_Hash( $this, $args );
71
	}
72
73
	/**
74
	 * @param array $values
75
	 * @return PPNode_Hash_Array
76
	 */
77
	public function newPartNodeArray( $values ) {
78
		$list = [];
79
80
		foreach ( $values as $k => $val ) {
81
			if ( is_int( $k ) ) {
82
				$store = [ [ 'part', [
83
					[ 'name', [ [ '@index', [ $k ] ] ] ],
84
					[ 'value', [ strval( $val ) ] ],
85
				] ] ];
86
			} else {
87
				$store = [ [ 'part', [
88
					[ 'name', [ strval( $k ) ] ],
89
					'=',
90
					[ 'value', [ strval( $val ) ] ],
91
				] ] ];
92
			}
93
94
			$list[] = new PPNode_Hash_Tree( $store, 0 );
95
		}
96
97
		$node = new PPNode_Hash_Array( $list );
98
		return $node;
99
	}
100
101
	/**
102
	 * Preprocess some wikitext and return the document tree.
103
	 *
104
	 * @param string $text The text to parse
105
	 * @param int $flags Bitwise combination of:
106
	 *    Parser::PTD_FOR_INCLUSION    Handle "<noinclude>" and "<includeonly>" as if the text is being
107
	 *                                 included. Default is to assume a direct page view.
108
	 *
109
	 * The generated DOM tree must depend only on the input text and the flags.
110
	 * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
111
	 *
112
	 * Any flag added to the $flags parameter here, or any other parameter liable to cause a
113
	 * change in the DOM tree for a given text, must be passed through the section identifier
114
	 * in the section edit link and thus back to extractSections().
115
	 *
116
	 * @throws MWException
117
	 * @return PPNode_Hash_Tree
118
	 */
119
	public function preprocessToObj( $text, $flags = 0 ) {
120
		$tree = $this->cacheGetTree( $text, $flags );
121
		if ( $tree !== false ) {
122
			$store = json_decode( $tree );
123
			if ( is_array( $store ) ) {
124
				return new PPNode_Hash_Tree( $store, 0 );
125
			}
126
		}
127
128
		$forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
129
130
		$xmlishElements = $this->parser->getStripList();
131
		$xmlishAllowMissingEndTag = [ 'includeonly', 'noinclude', 'onlyinclude' ];
132
		$enableOnlyinclude = false;
133 View Code Duplication
		if ( $forInclusion ) {
134
			$ignoredTags = [ 'includeonly', '/includeonly' ];
135
			$ignoredElements = [ 'noinclude' ];
136
			$xmlishElements[] = 'noinclude';
137
			if ( strpos( $text, '<onlyinclude>' ) !== false
138
				&& strpos( $text, '</onlyinclude>' ) !== false
139
			) {
140
				$enableOnlyinclude = true;
141
			}
142
		} else {
143
			$ignoredTags = [ 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ];
144
			$ignoredElements = [ 'includeonly' ];
145
			$xmlishElements[] = 'includeonly';
146
		}
147
		$xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
148
149
		// Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
150
		$elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
151
152
		$stack = new PPDStack_Hash;
153
154
		$searchBase = "[{<\n";
155
		// For fast reverse searches
156
		$revText = strrev( $text );
157
		$lengthText = strlen( $text );
158
159
		// Input pointer, starts out pointing to a pseudo-newline before the start
160
		$i = 0;
161
		// Current accumulator. See the doc comment for Preprocessor_Hash for the format.
162
		$accum =& $stack->getAccum();
163
		// True to find equals signs in arguments
164
		$findEquals = false;
165
		// True to take notice of pipe characters
166
		$findPipe = false;
167
		$headingIndex = 1;
168
		// True if $i is inside a possible heading
169
		$inHeading = false;
170
		// True if there are no more greater-than (>) signs right of $i
171
		$noMoreGT = false;
172
		// Map of tag name => true if there are no more closing tags of given type right of $i
173
		$noMoreClosingTag = [];
174
		// True to ignore all input up to the next <onlyinclude>
175
		$findOnlyinclude = $enableOnlyinclude;
176
		// Do a line-start run without outputting an LF character
177
		$fakeLineStart = true;
178
179
		while ( true ) {
180
			// $this->memCheck();
181
182
			if ( $findOnlyinclude ) {
183
				// Ignore all input up to the next <onlyinclude>
184
				$startPos = strpos( $text, '<onlyinclude>', $i );
185
				if ( $startPos === false ) {
186
					// Ignored section runs to the end
187
					$accum[] = [ 'ignore', [ substr( $text, $i ) ] ];
188
					break;
189
				}
190
				$tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
191
				$accum[] = [ 'ignore', [ substr( $text, $i, $tagEndPos - $i ) ] ];
192
				$i = $tagEndPos;
193
				$findOnlyinclude = false;
194
			}
195
196
			if ( $fakeLineStart ) {
197
				$found = 'line-start';
198
				$curChar = '';
199 View Code Duplication
			} else {
200
				# Find next opening brace, closing brace or pipe
201
				$search = $searchBase;
202
				if ( $stack->top === false ) {
203
					$currentClosing = '';
204
				} else {
205
					$currentClosing = $stack->top->close;
206
					$search .= $currentClosing;
207
				}
208
				if ( $findPipe ) {
209
					$search .= '|';
210
				}
211
				if ( $findEquals ) {
212
					// First equals will be for the template
213
					$search .= '=';
214
				}
215
				$rule = null;
216
				# Output literal section, advance input counter
217
				$literalLength = strcspn( $text, $search, $i );
218
				if ( $literalLength > 0 ) {
219
					self::addLiteral( $accum, substr( $text, $i, $literalLength ) );
220
					$i += $literalLength;
221
				}
222
				if ( $i >= $lengthText ) {
223
					if ( $currentClosing == "\n" ) {
224
						// Do a past-the-end run to finish off the heading
225
						$curChar = '';
226
						$found = 'line-end';
227
					} else {
228
						# All done
229
						break;
230
					}
231
				} else {
232
					$curChar = $text[$i];
233
					if ( $curChar == '|' ) {
234
						$found = 'pipe';
235
					} elseif ( $curChar == '=' ) {
236
						$found = 'equals';
237
					} elseif ( $curChar == '<' ) {
238
						$found = 'angle';
239
					} elseif ( $curChar == "\n" ) {
240
						if ( $inHeading ) {
241
							$found = 'line-end';
242
						} else {
243
							$found = 'line-start';
244
						}
245
					} elseif ( $curChar == $currentClosing ) {
246
						$found = 'close';
247
					} elseif ( isset( $this->rules[$curChar] ) ) {
248
						$found = 'open';
249
						$rule = $this->rules[$curChar];
250
					} else {
251
						# Some versions of PHP have a strcspn which stops on null characters
252
						# Ignore and continue
253
						++$i;
254
						continue;
255
					}
256
				}
257
			}
258
259
			if ( $found == 'angle' ) {
260
				$matches = false;
261
				// Handle </onlyinclude>
262 View Code Duplication
				if ( $enableOnlyinclude
263
					&& substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>'
264
				) {
265
					$findOnlyinclude = true;
266
					continue;
267
				}
268
269
				// Determine element name
270 View Code Duplication
				if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
271
					// Element name missing or not listed
272
					self::addLiteral( $accum, '<' );
273
					++$i;
274
					continue;
275
				}
276
				// Handle comments
277
				if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
278
279
					// To avoid leaving blank lines, when a sequence of
280
					// space-separated comments is both preceded and followed by
281
					// a newline (ignoring spaces), then
282
					// trim leading and trailing spaces and the trailing newline.
283
284
					// Find the end
285
					$endPos = strpos( $text, '-->', $i + 4 );
286
					if ( $endPos === false ) {
287
						// Unclosed comment in input, runs to end
288
						$inner = substr( $text, $i );
289
						$accum[] = [ 'comment', [ $inner ] ];
290
						$i = $lengthText;
291
					} else {
292
						// Search backwards for leading whitespace
293
						$wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0;
294
295
						// Search forwards for trailing whitespace
296
						// $wsEnd will be the position of the last space (or the '>' if there's none)
297
						$wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 );
298
299
						// Keep looking forward as long as we're finding more
300
						// comments.
301
						$comments = [ [ $wsStart, $wsEnd ] ];
302 View Code Duplication
						while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) {
303
							$c = strpos( $text, '-->', $wsEnd + 4 );
304
							if ( $c === false ) {
305
								break;
306
							}
307
							$c = $c + 2 + strspn( $text, " \t", $c + 3 );
308
							$comments[] = [ $wsEnd + 1, $c ];
309
							$wsEnd = $c;
310
						}
311
312
						// Eat the line if possible
313
						// TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
314
						// the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
315
						// it's a possible beneficial b/c break.
316
						if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
317
							&& substr( $text, $wsEnd + 1, 1 ) == "\n"
318
						) {
319
							// Remove leading whitespace from the end of the accumulator
320
							$wsLength = $i - $wsStart;
321
							$endIndex = count( $accum ) - 1;
322
323
							// Sanity check
324
							if ( $wsLength > 0
325
								&& $endIndex >= 0
326
								&& is_string( $accum[$endIndex] )
327
								&& strspn( $accum[$endIndex], " \t", -$wsLength ) === $wsLength
328
							) {
329
								$accum[$endIndex] = substr( $accum[$endIndex], 0, -$wsLength );
330
							}
331
332
							// Dump all but the last comment to the accumulator
333
							foreach ( $comments as $j => $com ) {
334
								$startPos = $com[0];
335
								$endPos = $com[1] + 1;
336
								if ( $j == ( count( $comments ) - 1 ) ) {
337
									break;
338
								}
339
								$inner = substr( $text, $startPos, $endPos - $startPos );
340
								$accum[] = [ 'comment', [ $inner ] ];
341
							}
342
343
							// Do a line-start run next time to look for headings after the comment
344
							$fakeLineStart = true;
345
						} else {
346
							// No line to eat, just take the comment itself
347
							$startPos = $i;
348
							$endPos += 2;
349
						}
350
351 View Code Duplication
						if ( $stack->top ) {
352
							$part = $stack->top->getCurrentPart();
353
							if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) {
354
								$part->visualEnd = $wsStart;
355
							}
356
							// Else comments abutting, no change in visual end
357
							$part->commentEnd = $endPos;
358
						}
359
						$i = $endPos + 1;
360
						$inner = substr( $text, $startPos, $endPos - $startPos + 1 );
361
						$accum[] = [ 'comment', [ $inner ] ];
362
					}
363
					continue;
364
				}
365
				$name = $matches[1];
366
				$lowerName = strtolower( $name );
367
				$attrStart = $i + strlen( $name ) + 1;
368
369
				// Find end of tag
370
				$tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
371
				if ( $tagEndPos === false ) {
372
					// Infinite backtrack
373
					// Disable tag search to prevent worst-case O(N^2) performance
374
					$noMoreGT = true;
375
					self::addLiteral( $accum, '<' );
376
					++$i;
377
					continue;
378
				}
379
380
				// Handle ignored tags
381
				if ( in_array( $lowerName, $ignoredTags ) ) {
382
					$accum[] = [ 'ignore', [ substr( $text, $i, $tagEndPos - $i + 1 ) ] ];
383
					$i = $tagEndPos + 1;
384
					continue;
385
				}
386
387
				$tagStartPos = $i;
388
				if ( $text[$tagEndPos - 1] == '/' ) {
389
					// Short end tag
390
					$attrEnd = $tagEndPos - 1;
391
					$inner = null;
392
					$i = $tagEndPos + 1;
393
					$close = null;
394
				} else {
395
					$attrEnd = $tagEndPos;
396
					// Find closing tag
397
					if (
398
						!isset( $noMoreClosingTag[$name] ) &&
399
						preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
400
							$text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 )
401
					) {
402
						$inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
403
						$i = $matches[0][1] + strlen( $matches[0][0] );
404
						$close = $matches[0][0];
405 View Code Duplication
					} else {
406
						// No end tag
407
						if ( in_array( $name, $xmlishAllowMissingEndTag ) ) {
408
							// Let it run out to the end of the text.
409
							$inner = substr( $text, $tagEndPos + 1 );
410
							$i = $lengthText;
411
							$close = null;
412
						} else {
413
							// Don't match the tag, treat opening tag as literal and resume parsing.
414
							$i = $tagEndPos + 1;
415
							self::addLiteral( $accum,
416
								substr( $text, $tagStartPos, $tagEndPos + 1 - $tagStartPos ) );
417
							// Cache results, otherwise we have O(N^2) performance for input like <foo><foo><foo>...
418
							$noMoreClosingTag[$name] = true;
419
							continue;
420
						}
421
					}
422
				}
423
				// <includeonly> and <noinclude> just become <ignore> tags
424
				if ( in_array( $lowerName, $ignoredElements ) ) {
425
					$accum[] = [ 'ignore', [ substr( $text, $tagStartPos, $i - $tagStartPos ) ] ];
426
					continue;
427
				}
428
429 View Code Duplication
				if ( $attrEnd <= $attrStart ) {
430
					$attr = '';
431
				} else {
432
					// Note that the attr element contains the whitespace between name and attribute,
433
					// this is necessary for precise reconstruction during pre-save transform.
434
					$attr = substr( $text, $attrStart, $attrEnd - $attrStart );
435
				}
436
437
				$children = [
438
					[ 'name', [ $name ] ],
439
					[ 'attr', [ $attr ] ] ];
440
				if ( $inner !== null ) {
441
					$children[] = [ 'inner', [ $inner ] ];
442
				}
443
				if ( $close !== null ) {
444
					$children[] = [ 'close', [ $close ] ];
445
				}
446
				$accum[] = [ 'ext', $children ];
447
			} elseif ( $found == 'line-start' ) {
448
				// Is this the start of a heading?
449
				// Line break belongs before the heading element in any case
450
				if ( $fakeLineStart ) {
451
					$fakeLineStart = false;
452
				} else {
453
					self::addLiteral( $accum, $curChar );
454
					$i++;
455
				}
456
457
				$count = strspn( $text, '=', $i, 6 );
458
				if ( $count == 1 && $findEquals ) {
459
					// DWIM: This looks kind of like a name/value separator.
460
					// Let's let the equals handler have it and break the potential
461
					// heading. This is heuristic, but AFAICT the methods for
462
					// completely correct disambiguation are very complex.
463
				} elseif ( $count > 0 ) {
464
					$piece = [
465
						'open' => "\n",
466
						'close' => "\n",
467
						'parts' => [ new PPDPart_Hash( str_repeat( '=', $count ) ) ],
468
						'startPos' => $i,
469
						'count' => $count ];
470
					$stack->push( $piece );
471
					$accum =& $stack->getAccum();
472
					extract( $stack->getFlags() );
473
					$i += $count;
474
				}
475
			} elseif ( $found == 'line-end' ) {
476
				$piece = $stack->top;
477
				// A heading must be open, otherwise \n wouldn't have been in the search list
478
				assert( $piece->open === "\n" );
479
				$part = $piece->getCurrentPart();
480
				// Search back through the input to see if it has a proper close.
481
				// Do this using the reversed string since the other solutions
482
				// (end anchor, etc.) are inefficient.
483
				$wsLength = strspn( $revText, " \t", $lengthText - $i );
484
				$searchStart = $i - $wsLength;
485 View Code Duplication
				if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
486
					// Comment found at line end
487
					// Search for equals signs before the comment
488
					$searchStart = $part->visualEnd;
489
					$searchStart -= strspn( $revText, " \t", $lengthText - $searchStart );
490
				}
491
				$count = $piece->count;
492
				$equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
493
				if ( $equalsLength > 0 ) {
494 View Code Duplication
					if ( $searchStart - $equalsLength == $piece->startPos ) {
0 ignored issues
show
The property startPos does not seem to exist in PPDStack.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
495
						// This is just a single string of equals signs on its own line
496
						// Replicate the doHeadings behavior /={count}(.+)={count}/
497
						// First find out how many equals signs there really are (don't stop at 6)
498
						$count = $equalsLength;
499
						if ( $count < 3 ) {
500
							$count = 0;
501
						} else {
502
							$count = min( 6, intval( ( $count - 1 ) / 2 ) );
503
						}
504
					} else {
505
						$count = min( $equalsLength, $count );
506
					}
507
					if ( $count > 0 ) {
508
						// Normal match, output <h>
509
						$element = [ [ 'possible-h',
510
							array_merge(
511
								[
512
									[ '@level', [ $count ] ],
513
									[ '@i', [ $headingIndex++ ] ]
514
								],
515
								$accum
516
							)
517
						] ];
518
					} else {
519
						// Single equals sign on its own line, count=0
520
						$element = $accum;
521
					}
522
				} else {
523
					// No match, no <h>, just pass down the inner text
524
					$element = $accum;
525
				}
526
				// Unwind the stack
527
				$stack->pop();
528
				$accum =& $stack->getAccum();
529
				extract( $stack->getFlags() );
530
531
				// Append the result to the enclosing accumulator
532
				array_splice( $accum, count( $accum ), 0, $element );
533
534
				// Note that we do NOT increment the input pointer.
535
				// This is because the closing linebreak could be the opening linebreak of
536
				// another heading. Infinite loops are avoided because the next iteration MUST
537
				// hit the heading open case above, which unconditionally increments the
538
				// input pointer.
539 View Code Duplication
			} elseif ( $found == 'open' ) {
540
				# count opening brace characters
541
				$count = strspn( $text, $curChar, $i );
542
543
				# we need to add to stack only if opening brace count is enough for one of the rules
544
				if ( $count >= $rule['min'] ) {
545
					# Add it to the stack
546
					$piece = [
547
						'open' => $curChar,
548
						'close' => $rule['end'],
549
						'count' => $count,
550
						'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ),
551
					];
552
553
					$stack->push( $piece );
554
					$accum =& $stack->getAccum();
555
					extract( $stack->getFlags() );
556
				} else {
557
					# Add literal brace(s)
558
					self::addLiteral( $accum, str_repeat( $curChar, $count ) );
559
				}
560
				$i += $count;
561
			} elseif ( $found == 'close' ) {
562
				$piece = $stack->top;
563
				# lets check if there are enough characters for closing brace
564
				$maxCount = $piece->count;
565
				$count = strspn( $text, $curChar, $i, $maxCount );
566
567
				# check for maximum matching characters (if there are 5 closing
568
				# characters, we will probably need only 3 - depending on the rules)
569
				$rule = $this->rules[$piece->open];
570 View Code Duplication
				if ( $count > $rule['max'] ) {
571
					# The specified maximum exists in the callback array, unless the caller
572
					# has made an error
573
					$matchingCount = $rule['max'];
574
				} else {
575
					# Count is less than the maximum
576
					# Skip any gaps in the callback array to find the true largest match
577
					# Need to use array_key_exists not isset because the callback can be null
578
					$matchingCount = $count;
579
					while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
580
						--$matchingCount;
581
					}
582
				}
583
584
				if ( $matchingCount <= 0 ) {
585
					# No matching element found in callback array
586
					# Output a literal closing brace and continue
587
					self::addLiteral( $accum, str_repeat( $curChar, $count ) );
588
					$i += $count;
589
					continue;
590
				}
591
				$name = $rule['names'][$matchingCount];
592
				if ( $name === null ) {
593
					// No element, just literal text
594
					$element = $piece->breakSyntax( $matchingCount );
595
					self::addLiteral( $element, str_repeat( $rule['end'], $matchingCount ) );
596
				} else {
597
					# Create XML element
598
					$parts = $piece->parts;
599
					$titleAccum = $parts[0]->out;
600
					unset( $parts[0] );
601
602
					$children = [];
603
604
					# The invocation is at the start of the line if lineStart is set in
605
					# the stack, and all opening brackets are used up.
606
					if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
607
						$children[] = [ '@lineStart', [ 1 ] ];
608
					}
609
					$titleNode = [ 'title', $titleAccum ];
610
					$children[] = $titleNode;
611
					$argIndex = 1;
612
					foreach ( $parts as $part ) {
613
						if ( isset( $part->eqpos ) ) {
614
							$equalsNode = $part->out[$part->eqpos];
615
							$nameNode = [ 'name', array_slice( $part->out, 0, $part->eqpos ) ];
616
							$valueNode = [ 'value', array_slice( $part->out, $part->eqpos + 1 ) ];
617
							$partNode = [ 'part', [ $nameNode, $equalsNode, $valueNode ] ];
618
							$children[] = $partNode;
619
						} else {
620
							$nameNode = [ 'name', [ [ '@index', [ $argIndex++ ] ] ] ];
621
							$valueNode = [ 'value', $part->out ];
622
							$partNode = [ 'part', [ $nameNode, $valueNode ] ];
623
							$children[] = $partNode;
624
						}
625
					}
626
					$element = [ [ $name, $children ] ];
627
				}
628
629
				# Advance input pointer
630
				$i += $matchingCount;
631
632
				# Unwind the stack
633
				$stack->pop();
634
				$accum =& $stack->getAccum();
635
636
				# Re-add the old stack element if it still has unmatched opening characters remaining
637 View Code Duplication
				if ( $matchingCount < $piece->count ) {
638
					$piece->parts = [ new PPDPart_Hash ];
639
					$piece->count -= $matchingCount;
640
					# do we still qualify for any callback with remaining count?
641
					$min = $this->rules[$piece->open]['min'];
642
					if ( $piece->count >= $min ) {
643
						$stack->push( $piece );
644
						$accum =& $stack->getAccum();
645
					} else {
646
						self::addLiteral( $accum, str_repeat( $piece->open, $piece->count ) );
647
					}
648
				}
649
650
				extract( $stack->getFlags() );
651
652
				# Add XML element to the enclosing accumulator
653
				array_splice( $accum, count( $accum ), 0, $element );
654 View Code Duplication
			} elseif ( $found == 'pipe' ) {
655
				$findEquals = true; // shortcut for getFlags()
656
				$stack->addPart();
657
				$accum =& $stack->getAccum();
658
				++$i;
659
			} elseif ( $found == 'equals' ) {
660
				$findEquals = false; // shortcut for getFlags()
661
				$accum[] = [ 'equals', [ '=' ] ];
662
				$stack->getCurrentPart()->eqpos = count( $accum ) - 1;
663
				++$i;
664
			}
665
		}
666
667
		# Output any remaining unclosed brackets
668
		foreach ( $stack->stack as $piece ) {
669
			array_splice( $stack->rootAccum, count( $stack->rootAccum ), 0, $piece->breakSyntax() );
670
		}
671
672
		# Enable top-level headings
673
		foreach ( $stack->rootAccum as &$node ) {
674
			if ( is_array( $node ) && $node[PPNode_Hash_Tree::NAME] === 'possible-h' ) {
675
				$node[PPNode_Hash_Tree::NAME] = 'h';
676
			}
677
		}
678
679
		$rootStore = [ [ 'root', $stack->rootAccum ] ];
680
		$rootNode = new PPNode_Hash_Tree( $rootStore, 0 );
681
682
		// Cache
683
		$tree = json_encode( $rootStore, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
684
		if ( $tree !== false ) {
685
			$this->cacheSetTree( $text, $flags, $tree );
686
		}
687
688
		return $rootNode;
689
	}
690
691
	private static function addLiteral( array &$accum, $text ) {
692
		$n = count( $accum );
693
		if ( $n && is_string( $accum[$n - 1] ) ) {
694
			$accum[$n - 1] .= $text;
695
		} else {
696
			$accum[] = $text;
697
		}
698
	}
699
}
700
701
/**
702
 * Stack class to help Preprocessor::preprocessToObj()
703
 * @ingroup Parser
704
 */
705
// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
706
class PPDStack_Hash extends PPDStack {
707
	// @codingStandardsIgnoreEnd
708
709
	public function __construct() {
710
		$this->elementClass = 'PPDStackElement_Hash';
711
		parent::__construct();
712
		$this->rootAccum = [];
713
	}
714
}
715
716
/**
717
 * @ingroup Parser
718
 */
719
// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
720
class PPDStackElement_Hash extends PPDStackElement {
721
	// @codingStandardsIgnoreEnd
722
723
	public function __construct( $data = [] ) {
724
		$this->partClass = 'PPDPart_Hash';
725
		parent::__construct( $data );
726
	}
727
728
	/**
729
	 * Get the accumulator that would result if the close is not found.
730
	 *
731
	 * @param int|bool $openingCount
732
	 * @return array
733
	 */
734
	public function breakSyntax( $openingCount = false ) {
735
		if ( $this->open == "\n" ) {
736
			$accum = $this->parts[0]->out;
737
		} else {
738
			if ( $openingCount === false ) {
739
				$openingCount = $this->count;
740
			}
741
			$accum = [ str_repeat( $this->open, $openingCount ) ];
742
			$lastIndex = 0;
743
			$first = true;
744
			foreach ( $this->parts as $part ) {
745
				if ( $first ) {
746
					$first = false;
747
				} elseif ( is_string( $accum[$lastIndex] ) ) {
748
					$accum[$lastIndex] .= '|';
749
				} else {
750
					$accum[++$lastIndex] = '|';
751
				}
752
				foreach ( $part->out as $node ) {
753
					if ( is_string( $node ) && is_string( $accum[$lastIndex] ) ) {
754
						$accum[$lastIndex] .= $node;
755
					} else {
756
						$accum[++$lastIndex] = $node;
757
					}
758
				}
759
			}
760
		}
761
		return $accum;
762
	}
763
}
764
765
/**
766
 * @ingroup Parser
767
 */
768
// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
769
class PPDPart_Hash extends PPDPart {
770
	// @codingStandardsIgnoreEnd
771
772
	public function __construct( $out = '' ) {
773
		if ( $out !== '' ) {
774
			$accum = [ $out ];
775
		} else {
776
			$accum = [];
777
		}
778
		parent::__construct( $accum );
779
	}
780
}
781
782
/**
783
 * An expansion frame, used as a context to expand the result of preprocessToObj()
784
 * @ingroup Parser
785
 */
786
// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
787
class PPFrame_Hash implements PPFrame {
788
	// @codingStandardsIgnoreEnd
789
790
	/**
791
	 * @var Parser
792
	 */
793
	public $parser;
794
795
	/**
796
	 * @var Preprocessor
797
	 */
798
	public $preprocessor;
799
800
	/**
801
	 * @var Title
802
	 */
803
	public $title;
804
	public $titleCache;
805
806
	/**
807
	 * Hashtable listing templates which are disallowed for expansion in this frame,
808
	 * having been encountered previously in parent frames.
809
	 */
810
	public $loopCheckHash;
811
812
	/**
813
	 * Recursion depth of this frame, top = 0
814
	 * Note that this is NOT the same as expansion depth in expand()
815
	 */
816
	public $depth;
817
818
	private $volatile = false;
819
	private $ttl = null;
820
821
	/**
822
	 * @var array
823
	 */
824
	protected $childExpansionCache;
825
826
	/**
827
	 * Construct a new preprocessor frame.
828
	 * @param Preprocessor $preprocessor The parent preprocessor
829
	 */
830 View Code Duplication
	public function __construct( $preprocessor ) {
831
		$this->preprocessor = $preprocessor;
832
		$this->parser = $preprocessor->parser;
833
		$this->title = $this->parser->mTitle;
834
		$this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
835
		$this->loopCheckHash = [];
836
		$this->depth = 0;
837
		$this->childExpansionCache = [];
838
	}
839
840
	/**
841
	 * Create a new child frame
842
	 * $args is optionally a multi-root PPNode or array containing the template arguments
843
	 *
844
	 * @param array|bool|PPNode_Hash_Array $args
845
	 * @param Title|bool $title
846
	 * @param int $indexOffset
847
	 * @throws MWException
848
	 * @return PPTemplateFrame_Hash
849
	 */
850
	public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
851
		$namedArgs = [];
852
		$numberedArgs = [];
853
		if ( $title === false ) {
854
			$title = $this->title;
855
		}
856
		if ( $args !== false ) {
857
			if ( $args instanceof PPNode_Hash_Array ) {
858
				$args = $args->value;
859
			} elseif ( !is_array( $args ) ) {
860
				throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
861
			}
862
			foreach ( $args as $arg ) {
863
				$bits = $arg->splitArg();
864
				if ( $bits['index'] !== '' ) {
865
					// Numbered parameter
866
					$index = $bits['index'] - $indexOffset;
867
					if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
868
						$this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
869
							wfEscapeWikiText( $this->title ),
870
							wfEscapeWikiText( $title ),
871
							wfEscapeWikiText( $index ) )->text() );
872
						$this->parser->addTrackingCategory( 'duplicate-args-category' );
873
					}
874
					$numberedArgs[$index] = $bits['value'];
875
					unset( $namedArgs[$index] );
876 View Code Duplication
				} else {
877
					// Named parameter
878
					$name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
879
					if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
880
						$this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
881
							wfEscapeWikiText( $this->title ),
882
							wfEscapeWikiText( $title ),
883
							wfEscapeWikiText( $name ) )->text() );
884
						$this->parser->addTrackingCategory( 'duplicate-args-category' );
885
					}
886
					$namedArgs[$name] = $bits['value'];
887
					unset( $numberedArgs[$name] );
888
				}
889
			}
890
		}
891
		return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
892
	}
893
894
	/**
895
	 * @throws MWException
896
	 * @param string|int $key
897
	 * @param string|PPNode $root
898
	 * @param int $flags
899
	 * @return string
900
	 */
901
	public function cachedExpand( $key, $root, $flags = 0 ) {
902
		// we don't have a parent, so we don't have a cache
903
		return $this->expand( $root, $flags );
904
	}
905
906
	/**
907
	 * @throws MWException
908
	 * @param string|PPNode $root
909
	 * @param int $flags
910
	 * @return string
911
	 */
912
	public function expand( $root, $flags = 0 ) {
913
		static $expansionDepth = 0;
914
		if ( is_string( $root ) ) {
915
			return $root;
916
		}
917
918 View Code Duplication
		if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
919
			$this->parser->limitationWarn( 'node-count-exceeded',
920
					$this->parser->mPPNodeCount,
921
					$this->parser->mOptions->getMaxPPNodeCount()
922
			);
923
			return '<span class="error">Node-count limit exceeded</span>';
924
		}
925 View Code Duplication
		if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
926
			$this->parser->limitationWarn( 'expansion-depth-exceeded',
927
					$expansionDepth,
928
					$this->parser->mOptions->getMaxPPExpandDepth()
929
			);
930
			return '<span class="error">Expansion depth limit exceeded</span>';
931
		}
932
		++$expansionDepth;
933
		if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
934
			$this->parser->mHighestExpansionDepth = $expansionDepth;
935
		}
936
937
		$outStack = [ '', '' ];
938
		$iteratorStack = [ false, $root ];
939
		$indexStack = [ 0, 0 ];
940
941
		while ( count( $iteratorStack ) > 1 ) {
942
			$level = count( $outStack ) - 1;
943
			$iteratorNode =& $iteratorStack[$level];
944
			$out =& $outStack[$level];
945
			$index =& $indexStack[$level];
946
947 View Code Duplication
			if ( is_array( $iteratorNode ) ) {
948
				if ( $index >= count( $iteratorNode ) ) {
949
					// All done with this iterator
950
					$iteratorStack[$level] = false;
951
					$contextNode = false;
952
				} else {
953
					$contextNode = $iteratorNode[$index];
954
					$index++;
955
				}
956
			} elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
957
				if ( $index >= $iteratorNode->getLength() ) {
958
					// All done with this iterator
959
					$iteratorStack[$level] = false;
960
					$contextNode = false;
961
				} else {
962
					$contextNode = $iteratorNode->item( $index );
963
					$index++;
964
				}
965
			} else {
966
				// Copy to $contextNode and then delete from iterator stack,
967
				// because this is not an iterator but we do have to execute it once
968
				$contextNode = $iteratorStack[$level];
969
				$iteratorStack[$level] = false;
970
			}
971
972
			$newIterator = false;
973
			$contextName = false;
974
			$contextChildren = false;
975
976
			if ( $contextNode === false ) {
977
				// nothing to do
978
			} elseif ( is_string( $contextNode ) ) {
979
				$out .= $contextNode;
980
			} elseif ( $contextNode instanceof PPNode_Hash_Array ) {
981
				$newIterator = $contextNode;
982
			} elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
983
				// No output
984
			} elseif ( $contextNode instanceof PPNode_Hash_Text ) {
985
				$out .= $contextNode->value;
986
			} elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
987
				$contextName = $contextNode->name;
988
				$contextChildren = $contextNode->getRawChildren();
989
			} elseif ( is_array( $contextNode ) ) {
990
				// Node descriptor array
991
				if ( count( $contextNode ) !== 2 ) {
992
					throw new MWException( __METHOD__.
993
						': found an array where a node descriptor should be' );
994
				}
995
				list( $contextName, $contextChildren ) = $contextNode;
996
			} else {
997
				throw new MWException( __METHOD__ . ': Invalid parameter type' );
998
			}
999
1000
			// Handle node descriptor array or tree object
1001
			if ( $contextName === false ) {
1002
				// Not a node, already handled above
1003
			} elseif ( $contextName[0] === '@' ) {
1004
				// Attribute: no output
1005
			} elseif ( $contextName === 'template' ) {
1006
				# Double-brace expansion
1007
				$bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
1008 View Code Duplication
				if ( $flags & PPFrame::NO_TEMPLATES ) {
1009
					$newIterator = $this->virtualBracketedImplode(
1010
						'{{', '|', '}}',
1011
						$bits['title'],
1012
						$bits['parts']
1013
					);
1014
				} else {
1015
					$ret = $this->parser->braceSubstitution( $bits, $this );
1016
					if ( isset( $ret['object'] ) ) {
1017
						$newIterator = $ret['object'];
1018
					} else {
1019
						$out .= $ret['text'];
1020
					}
1021
				}
1022
			} elseif ( $contextName === 'tplarg' ) {
1023
				# Triple-brace expansion
1024
				$bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
1025 View Code Duplication
				if ( $flags & PPFrame::NO_ARGS ) {
1026
					$newIterator = $this->virtualBracketedImplode(
1027
						'{{{', '|', '}}}',
1028
						$bits['title'],
1029
						$bits['parts']
1030
					);
1031
				} else {
1032
					$ret = $this->parser->argSubstitution( $bits, $this );
1033
					if ( isset( $ret['object'] ) ) {
1034
						$newIterator = $ret['object'];
1035
					} else {
1036
						$out .= $ret['text'];
1037
					}
1038
				}
1039
			} elseif ( $contextName === 'comment' ) {
1040
				# HTML-style comment
1041
				# Remove it in HTML, pre+remove and STRIP_COMMENTS modes
1042
				# Not in RECOVER_COMMENTS mode (msgnw) though.
1043
				if ( ( $this->parser->ot['html']
1044
					|| ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
1045
					|| ( $flags & PPFrame::STRIP_COMMENTS )
1046
					) && !( $flags & PPFrame::RECOVER_COMMENTS )
1047
				) {
1048
					$out .= '';
1049
				} elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
1050
					# Add a strip marker in PST mode so that pstPass2() can
1051
					# run some old-fashioned regexes on the result.
1052
					# Not in RECOVER_COMMENTS mode (extractSections) though.
1053
					$out .= $this->parser->insertStripItem( $contextChildren[0] );
1054
				} else {
1055
					# Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
1056
					$out .= $contextChildren[0];
1057
				}
1058 View Code Duplication
			} elseif ( $contextName === 'ignore' ) {
1059
				# Output suppression used by <includeonly> etc.
1060
				# OT_WIKI will only respect <ignore> in substed templates.
1061
				# The other output types respect it unless NO_IGNORE is set.
1062
				# extractSections() sets NO_IGNORE and so never respects it.
1063
				if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
1064
					|| ( $flags & PPFrame::NO_IGNORE )
1065
				) {
1066
					$out .= $contextChildren[0];
1067
				} else {
1068
					// $out .= '';
1069
				}
1070
			} elseif ( $contextName === 'ext' ) {
1071
				# Extension tag
1072
				$bits = PPNode_Hash_Tree::splitRawExt( $contextChildren ) +
1073
					[ 'attr' => null, 'inner' => null, 'close' => null ];
1074
				if ( $flags & PPFrame::NO_TAGS ) {
1075
					$s = '<' . $bits['name']->getFirstChild()->value;
1076
					if ( $bits['attr'] ) {
1077
						$s .= $bits['attr']->getFirstChild()->value;
1078
					}
1079
					if ( $bits['inner'] ) {
1080
						$s .= '>' . $bits['inner']->getFirstChild()->value;
1081
						if ( $bits['close'] ) {
1082
							$s .= $bits['close']->getFirstChild()->value;
1083
						}
1084
					} else {
1085
						$s .= '/>';
1086
					}
1087
					$out .= $s;
1088
				} else {
1089
					$out .= $this->parser->extensionSubstitution( $bits, $this );
1090
				}
1091
			} elseif ( $contextName === 'h' ) {
1092
				# Heading
1093
				if ( $this->parser->ot['html'] ) {
1094
					# Expand immediately and insert heading index marker
1095
					$s = $this->expand( $contextChildren, $flags );
1096
					$bits = PPNode_Hash_Tree::splitRawHeading( $contextChildren );
1097
					$titleText = $this->title->getPrefixedDBkey();
1098
					$this->parser->mHeadings[] = [ $titleText, $bits['i'] ];
1099
					$serial = count( $this->parser->mHeadings ) - 1;
1100
					$marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
1101
					$s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
1102
					$this->parser->mStripState->addGeneral( $marker, '' );
1103
					$out .= $s;
1104
				} else {
1105
					# Expand in virtual stack
1106
					$newIterator = $contextChildren;
1107
				}
1108
			} else {
1109
				# Generic recursive expansion
1110
				$newIterator = $contextChildren;
1111
			}
1112
1113
			if ( $newIterator !== false ) {
1114
				$outStack[] = '';
1115
				$iteratorStack[] = $newIterator;
1116
				$indexStack[] = 0;
1117 View Code Duplication
			} elseif ( $iteratorStack[$level] === false ) {
1118
				// Return accumulated value to parent
1119
				// With tail recursion
1120
				while ( $iteratorStack[$level] === false && $level > 0 ) {
1121
					$outStack[$level - 1] .= $out;
1122
					array_pop( $outStack );
1123
					array_pop( $iteratorStack );
1124
					array_pop( $indexStack );
1125
					$level--;
1126
				}
1127
			}
1128
		}
1129
		--$expansionDepth;
1130
		return $outStack[0];
1131
	}
1132
1133
	/**
1134
	 * @param string $sep
1135
	 * @param int $flags
1136
	 * @param string|PPNode $args,...
1137
	 * @return string
1138
	 */
1139 View Code Duplication
	public function implodeWithFlags( $sep, $flags /*, ... */ ) {
1140
		$args = array_slice( func_get_args(), 2 );
1141
1142
		$first = true;
1143
		$s = '';
1144
		foreach ( $args as $root ) {
1145
			if ( $root instanceof PPNode_Hash_Array ) {
1146
				$root = $root->value;
1147
			}
1148
			if ( !is_array( $root ) ) {
1149
				$root = [ $root ];
1150
			}
1151
			foreach ( $root as $node ) {
1152
				if ( $first ) {
1153
					$first = false;
1154
				} else {
1155
					$s .= $sep;
1156
				}
1157
				$s .= $this->expand( $node, $flags );
1158
			}
1159
		}
1160
		return $s;
1161
	}
1162
1163
	/**
1164
	 * Implode with no flags specified
1165
	 * This previously called implodeWithFlags but has now been inlined to reduce stack depth
1166
	 * @param string $sep
1167
	 * @param string|PPNode $args,...
1168
	 * @return string
1169
	 */
1170 View Code Duplication
	public function implode( $sep /*, ... */ ) {
1171
		$args = array_slice( func_get_args(), 1 );
1172
1173
		$first = true;
1174
		$s = '';
1175
		foreach ( $args as $root ) {
1176
			if ( $root instanceof PPNode_Hash_Array ) {
1177
				$root = $root->value;
1178
			}
1179
			if ( !is_array( $root ) ) {
1180
				$root = [ $root ];
1181
			}
1182
			foreach ( $root as $node ) {
1183
				if ( $first ) {
1184
					$first = false;
1185
				} else {
1186
					$s .= $sep;
1187
				}
1188
				$s .= $this->expand( $node );
1189
			}
1190
		}
1191
		return $s;
1192
	}
1193
1194
	/**
1195
	 * Makes an object that, when expand()ed, will be the same as one obtained
1196
	 * with implode()
1197
	 *
1198
	 * @param string $sep
1199
	 * @param string|PPNode $args,...
1200
	 * @return PPNode_Hash_Array
1201
	 */
1202 View Code Duplication
	public function virtualImplode( $sep /*, ... */ ) {
1203
		$args = array_slice( func_get_args(), 1 );
1204
		$out = [];
1205
		$first = true;
1206
1207
		foreach ( $args as $root ) {
1208
			if ( $root instanceof PPNode_Hash_Array ) {
1209
				$root = $root->value;
1210
			}
1211
			if ( !is_array( $root ) ) {
1212
				$root = [ $root ];
1213
			}
1214
			foreach ( $root as $node ) {
1215
				if ( $first ) {
1216
					$first = false;
1217
				} else {
1218
					$out[] = $sep;
1219
				}
1220
				$out[] = $node;
1221
			}
1222
		}
1223
		return new PPNode_Hash_Array( $out );
1224
	}
1225
1226
	/**
1227
	 * Virtual implode with brackets
1228
	 *
1229
	 * @param string $start
1230
	 * @param string $sep
1231
	 * @param string $end
1232
	 * @param string|PPNode $args,...
1233
	 * @return PPNode_Hash_Array
1234
	 */
1235 View Code Duplication
	public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
1236
		$args = array_slice( func_get_args(), 3 );
1237
		$out = [ $start ];
1238
		$first = true;
1239
1240
		foreach ( $args as $root ) {
1241
			if ( $root instanceof PPNode_Hash_Array ) {
1242
				$root = $root->value;
1243
			}
1244
			if ( !is_array( $root ) ) {
1245
				$root = [ $root ];
1246
			}
1247
			foreach ( $root as $node ) {
1248
				if ( $first ) {
1249
					$first = false;
1250
				} else {
1251
					$out[] = $sep;
1252
				}
1253
				$out[] = $node;
1254
			}
1255
		}
1256
		$out[] = $end;
1257
		return new PPNode_Hash_Array( $out );
1258
	}
1259
1260
	public function __toString() {
1261
		return 'frame{}';
1262
	}
1263
1264
	/**
1265
	 * @param bool $level
1266
	 * @return array|bool|string
1267
	 */
1268 View Code Duplication
	public function getPDBK( $level = false ) {
1269
		if ( $level === false ) {
1270
			return $this->title->getPrefixedDBkey();
1271
		} else {
1272
			return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
1273
		}
1274
	}
1275
1276
	/**
1277
	 * @return array
1278
	 */
1279
	public function getArguments() {
1280
		return [];
1281
	}
1282
1283
	/**
1284
	 * @return array
1285
	 */
1286
	public function getNumberedArguments() {
1287
		return [];
1288
	}
1289
1290
	/**
1291
	 * @return array
1292
	 */
1293
	public function getNamedArguments() {
1294
		return [];
1295
	}
1296
1297
	/**
1298
	 * Returns true if there are no arguments in this frame
1299
	 *
1300
	 * @return bool
1301
	 */
1302
	public function isEmpty() {
1303
		return true;
1304
	}
1305
1306
	/**
1307
	 * @param int|string $name
1308
	 * @return bool Always false in this implementation.
1309
	 */
1310
	public function getArgument( $name ) {
1311
		return false;
1312
	}
1313
1314
	/**
1315
	 * Returns true if the infinite loop check is OK, false if a loop is detected
1316
	 *
1317
	 * @param Title $title
1318
	 *
1319
	 * @return bool
1320
	 */
1321
	public function loopCheck( $title ) {
1322
		return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1323
	}
1324
1325
	/**
1326
	 * Return true if the frame is a template frame
1327
	 *
1328
	 * @return bool
1329
	 */
1330
	public function isTemplate() {
1331
		return false;
1332
	}
1333
1334
	/**
1335
	 * Get a title of frame
1336
	 *
1337
	 * @return Title
1338
	 */
1339
	public function getTitle() {
1340
		return $this->title;
1341
	}
1342
1343
	/**
1344
	 * Set the volatile flag
1345
	 *
1346
	 * @param bool $flag
1347
	 */
1348
	public function setVolatile( $flag = true ) {
1349
		$this->volatile = $flag;
1350
	}
1351
1352
	/**
1353
	 * Get the volatile flag
1354
	 *
1355
	 * @return bool
1356
	 */
1357
	public function isVolatile() {
1358
		return $this->volatile;
1359
	}
1360
1361
	/**
1362
	 * Set the TTL
1363
	 *
1364
	 * @param int $ttl
1365
	 */
1366
	public function setTTL( $ttl ) {
1367 View Code Duplication
		if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
1368
			$this->ttl = $ttl;
1369
		}
1370
	}
1371
1372
	/**
1373
	 * Get the TTL
1374
	 *
1375
	 * @return int|null
1376
	 */
1377
	public function getTTL() {
1378
		return $this->ttl;
1379
	}
1380
}
1381
1382
/**
1383
 * Expansion frame with template arguments
1384
 * @ingroup Parser
1385
 */
1386
// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1387 View Code Duplication
class PPTemplateFrame_Hash extends PPFrame_Hash {
1388
	// @codingStandardsIgnoreEnd
1389
1390
	public $numberedArgs, $namedArgs, $parent;
1391
	public $numberedExpansionCache, $namedExpansionCache;
1392
1393
	/**
1394
	 * @param Preprocessor $preprocessor
1395
	 * @param bool|PPFrame $parent
1396
	 * @param array $numberedArgs
1397
	 * @param array $namedArgs
1398
	 * @param bool|Title $title
1399
	 */
1400
	public function __construct( $preprocessor, $parent = false, $numberedArgs = [],
1401
		$namedArgs = [], $title = false
1402
	) {
1403
		parent::__construct( $preprocessor );
1404
1405
		$this->parent = $parent;
1406
		$this->numberedArgs = $numberedArgs;
1407
		$this->namedArgs = $namedArgs;
1408
		$this->title = $title;
1409
		$pdbk = $title ? $title->getPrefixedDBkey() : false;
1410
		$this->titleCache = $parent->titleCache;
1411
		$this->titleCache[] = $pdbk;
1412
		$this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1413
		if ( $pdbk !== false ) {
1414
			$this->loopCheckHash[$pdbk] = true;
1415
		}
1416
		$this->depth = $parent->depth + 1;
1417
		$this->numberedExpansionCache = $this->namedExpansionCache = [];
1418
	}
1419
1420
	public function __toString() {
1421
		$s = 'tplframe{';
1422
		$first = true;
1423
		$args = $this->numberedArgs + $this->namedArgs;
1424
		foreach ( $args as $name => $value ) {
1425
			if ( $first ) {
1426
				$first = false;
1427
			} else {
1428
				$s .= ', ';
1429
			}
1430
			$s .= "\"$name\":\"" .
1431
				str_replace( '"', '\\"', $value->__toString() ) . '"';
1432
		}
1433
		$s .= '}';
1434
		return $s;
1435
	}
1436
1437
	/**
1438
	 * @throws MWException
1439
	 * @param string|int $key
1440
	 * @param string|PPNode $root
1441
	 * @param int $flags
1442
	 * @return string
1443
	 */
1444
	public function cachedExpand( $key, $root, $flags = 0 ) {
1445
		if ( isset( $this->parent->childExpansionCache[$key] ) ) {
1446
			return $this->parent->childExpansionCache[$key];
1447
		}
1448
		$retval = $this->expand( $root, $flags );
1449
		if ( !$this->isVolatile() ) {
1450
			$this->parent->childExpansionCache[$key] = $retval;
1451
		}
1452
		return $retval;
1453
	}
1454
1455
	/**
1456
	 * Returns true if there are no arguments in this frame
1457
	 *
1458
	 * @return bool
1459
	 */
1460
	public function isEmpty() {
1461
		return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1462
	}
1463
1464
	/**
1465
	 * @return array
1466
	 */
1467
	public function getArguments() {
1468
		$arguments = [];
1469
		foreach ( array_merge(
1470
				array_keys( $this->numberedArgs ),
1471
				array_keys( $this->namedArgs ) ) as $key ) {
1472
			$arguments[$key] = $this->getArgument( $key );
1473
		}
1474
		return $arguments;
1475
	}
1476
1477
	/**
1478
	 * @return array
1479
	 */
1480
	public function getNumberedArguments() {
1481
		$arguments = [];
1482
		foreach ( array_keys( $this->numberedArgs ) as $key ) {
1483
			$arguments[$key] = $this->getArgument( $key );
1484
		}
1485
		return $arguments;
1486
	}
1487
1488
	/**
1489
	 * @return array
1490
	 */
1491
	public function getNamedArguments() {
1492
		$arguments = [];
1493
		foreach ( array_keys( $this->namedArgs ) as $key ) {
1494
			$arguments[$key] = $this->getArgument( $key );
1495
		}
1496
		return $arguments;
1497
	}
1498
1499
	/**
1500
	 * @param int $index
1501
	 * @return string|bool
1502
	 */
1503
	public function getNumberedArgument( $index ) {
1504
		if ( !isset( $this->numberedArgs[$index] ) ) {
1505
			return false;
1506
		}
1507
		if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1508
			# No trimming for unnamed arguments
1509
			$this->numberedExpansionCache[$index] = $this->parent->expand(
1510
				$this->numberedArgs[$index],
1511
				PPFrame::STRIP_COMMENTS
1512
			);
1513
		}
1514
		return $this->numberedExpansionCache[$index];
1515
	}
1516
1517
	/**
1518
	 * @param string $name
1519
	 * @return string|bool
1520
	 */
1521
	public function getNamedArgument( $name ) {
1522
		if ( !isset( $this->namedArgs[$name] ) ) {
1523
			return false;
1524
		}
1525
		if ( !isset( $this->namedExpansionCache[$name] ) ) {
1526
			# Trim named arguments post-expand, for backwards compatibility
1527
			$this->namedExpansionCache[$name] = trim(
1528
				$this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
1529
		}
1530
		return $this->namedExpansionCache[$name];
1531
	}
1532
1533
	/**
1534
	 * @param int|string $name
1535
	 * @return string|bool
1536
	 */
1537
	public function getArgument( $name ) {
1538
		$text = $this->getNumberedArgument( $name );
1539
		if ( $text === false ) {
1540
			$text = $this->getNamedArgument( $name );
1541
		}
1542
		return $text;
1543
	}
1544
1545
	/**
1546
	 * Return true if the frame is a template frame
1547
	 *
1548
	 * @return bool
1549
	 */
1550
	public function isTemplate() {
1551
		return true;
1552
	}
1553
1554
	public function setVolatile( $flag = true ) {
1555
		parent::setVolatile( $flag );
1556
		$this->parent->setVolatile( $flag );
1557
	}
1558
1559
	public function setTTL( $ttl ) {
1560
		parent::setTTL( $ttl );
1561
		$this->parent->setTTL( $ttl );
1562
	}
1563
}
1564
1565
/**
1566
 * Expansion frame with custom arguments
1567
 * @ingroup Parser
1568
 */
1569
// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1570 View Code Duplication
class PPCustomFrame_Hash extends PPFrame_Hash {
1571
	// @codingStandardsIgnoreEnd
1572
1573
	public $args;
1574
1575
	public function __construct( $preprocessor, $args ) {
1576
		parent::__construct( $preprocessor );
1577
		$this->args = $args;
1578
	}
1579
1580
	public function __toString() {
1581
		$s = 'cstmframe{';
1582
		$first = true;
1583
		foreach ( $this->args as $name => $value ) {
1584
			if ( $first ) {
1585
				$first = false;
1586
			} else {
1587
				$s .= ', ';
1588
			}
1589
			$s .= "\"$name\":\"" .
1590
				str_replace( '"', '\\"', $value->__toString() ) . '"';
1591
		}
1592
		$s .= '}';
1593
		return $s;
1594
	}
1595
1596
	/**
1597
	 * @return bool
1598
	 */
1599
	public function isEmpty() {
1600
		return !count( $this->args );
1601
	}
1602
1603
	/**
1604
	 * @param int|string $index
1605
	 * @return string|bool
1606
	 */
1607
	public function getArgument( $index ) {
1608
		if ( !isset( $this->args[$index] ) ) {
1609
			return false;
1610
		}
1611
		return $this->args[$index];
1612
	}
1613
1614
	public function getArguments() {
1615
		return $this->args;
1616
	}
1617
}
1618
1619
/**
1620
 * @ingroup Parser
1621
 */
1622
// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1623
class PPNode_Hash_Tree implements PPNode {
1624
	// @codingStandardsIgnoreEnd
1625
1626
	public $name;
1627
1628
	/**
1629
	 * The store array for children of this node. It is "raw" in the sense that
1630
	 * nodes are two-element arrays ("descriptors") rather than PPNode_Hash_*
1631
	 * objects.
1632
	 */
1633
	private $rawChildren;
1634
1635
	/**
1636
	 * The store array for the siblings of this node, including this node itself.
1637
	 */
1638
	private $store;
1639
1640
	/**
1641
	 * The index into $this->store which contains the descriptor of this node.
1642
	 */
1643
	private $index;
1644
1645
	/**
1646
	 * The offset of the name within descriptors, used in some places for
1647
	 * readability.
1648
	 */
1649
	const NAME = 0;
1650
1651
	/**
1652
	 * The offset of the child list within descriptors, used in some places for
1653
	 * readability.
1654
	 */
1655
	const CHILDREN = 1;
1656
1657
	/**
1658
	 * Construct an object using the data from $store[$index]. The rest of the
1659
	 * store array can be accessed via getNextSibling().
1660
	 *
1661
	 * @param array $store
1662
	 * @param integer $index
1663
	 */
1664
	public function __construct( array $store, $index ) {
1665
		$this->store = $store;
1666
		$this->index = $index;
1667
		list( $this->name, $this->rawChildren ) = $this->store[$index];
1668
	}
1669
1670
	/**
1671
	 * Construct an appropriate PPNode_Hash_* object with a class that depends
1672
	 * on what is at the relevant store index.
1673
	 *
1674
	 * @param array $store
1675
	 * @param integer $index
1676
	 * @return PPNode_Hash_Tree|PPNode_Hash_Attr|PPNode_Hash_Text
1677
	 */
1678
	public static function factory( array $store, $index ) {
1679
		if ( !isset( $store[$index] ) ) {
1680
			return false;
1681
		}
1682
1683
		$descriptor = $store[$index];
1684
		if ( is_string( $descriptor ) ) {
1685
			$class = 'PPNode_Hash_Text';
1686
		} elseif ( is_array( $descriptor ) ) {
1687
			if ( $descriptor[self::NAME][0] === '@' ) {
1688
				$class = 'PPNode_Hash_Attr';
1689
			} else {
1690
				$class = 'PPNode_Hash_Tree';
1691
			}
1692
		} else {
1693
			throw new MWException( __METHOD__.': invalid node descriptor' );
1694
		}
1695
		return new $class( $store, $index );
1696
	}
1697
1698
	/**
1699
	 * Convert a node to XML, for debugging
1700
	 */
1701
	public function __toString() {
1702
		$inner = '';
1703
		$attribs = '';
1704
		for ( $node = $this->getFirstChild(); $node; $node = $node->getNextSibling() ) {
1705
			if ( $node instanceof PPNode_Hash_Attr ) {
1706
				$attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
1707
			} else {
1708
				$inner .= $node->__toString();
1709
			}
1710
		}
1711
		if ( $inner === '' ) {
1712
			return "<{$this->name}$attribs/>";
1713
		} else {
1714
			return "<{$this->name}$attribs>$inner</{$this->name}>";
1715
		}
1716
	}
1717
1718
	/**
1719
	 * @return PPNode_Hash_Array
1720
	 */
1721
	public function getChildren() {
1722
		$children = [];
1723
		foreach ( $this->rawChildren as $i => $child ) {
1724
			$children[] = self::factory( $this->rawChildren, $i );
1725
		}
1726
		return new PPNode_Hash_Array( $children );
1727
	}
1728
1729
	/**
1730
	 * Get the first child, or false if there is none. Note that this will
1731
	 * return a temporary proxy object: different instances will be returned
1732
	 * if this is called more than once on the same node.
1733
	 *
1734
	 * @return PPNode_Hash_Tree|PPNode_Hash_Attr|PPNode_Hash_Text|boolean
1735
	 */
1736
	public function getFirstChild() {
1737
		if ( !isset( $this->rawChildren[0] ) ) {
1738
			return false;
1739
		} else {
1740
			return self::factory( $this->rawChildren, 0 );
1741
		}
1742
	}
1743
1744
	/**
1745
	 * Get the next sibling, or false if there is none. Note that this will
1746
	 * return a temporary proxy object: different instances will be returned
1747
	 * if this is called more than once on the same node.
1748
	 *
1749
	 * @return PPNode_Hash_Tree|PPNode_Hash_Attr|PPNode_Hash_Text|boolean
1750
	 */
1751
	public function getNextSibling() {
1752
		return self::factory( $this->store, $this->index + 1 );
1753
	}
1754
1755
	/**
1756
	 * Get an array of the children with a given node name
1757
	 *
1758
	 * @param string $name
1759
	 * @return PPNode_Hash_Array
1760
	 */
1761
	public function getChildrenOfType( $name ) {
1762
		$children = [];
1763
		foreach ( $this->rawChildren as $i => $child ) {
1764
			if ( is_array( $child ) && $child[self::NAME] === $name ) {
1765
				$children[] = self::factory( $this->rawChildren, $i );
1766
			}
1767
		}
1768
		return new PPNode_Hash_Array( $children );
1769
	}
1770
1771
	/**
1772
	 * Get the raw child array. For internal use.
1773
	 * @return array
1774
	 */
1775
	public function getRawChildren() {
1776
		return $this->rawChildren;
1777
	}
1778
1779
	/**
1780
	 * @return bool
1781
	 */
1782
	public function getLength() {
1783
		return false;
1784
	}
1785
1786
	/**
1787
	 * @param int $i
1788
	 * @return bool
1789
	 */
1790
	public function item( $i ) {
1791
		return false;
1792
	}
1793
1794
	/**
1795
	 * @return string
1796
	 */
1797
	public function getName() {
1798
		return $this->name;
1799
	}
1800
1801
	/**
1802
	 * Split a "<part>" node into an associative array containing:
1803
	 *  - name          PPNode name
1804
	 *  - index         String index
1805
	 *  - value         PPNode value
1806
	 *
1807
	 * @throws MWException
1808
	 * @return array
1809
	 */
1810
	public function splitArg() {
1811
		return self::splitRawArg( $this->rawChildren );
1812
	}
1813
1814
	/**
1815
	 * Like splitArg() but for a raw child array. For internal use only.
1816
	 */
1817
	public static function splitRawArg( array $children ) {
1818
		$bits = [];
1819
		foreach ( $children as $i => $child ) {
1820
			if ( !is_array( $child ) ) {
1821
				continue;
1822
			}
1823
			if ( $child[self::NAME] === 'name' ) {
1824
				$bits['name'] = new self( $children, $i );
1825
				if ( isset( $child[self::CHILDREN][0][self::NAME] )
1826
					&& $child[self::CHILDREN][0][self::NAME] === '@index'
1827
				) {
1828
					$bits['index'] = $child[self::CHILDREN][0][self::CHILDREN][0];
1829
				}
1830
			} elseif ( $child[self::NAME] === 'value' ) {
1831
				$bits['value'] = new self( $children, $i );
1832
			}
1833
		}
1834
1835
		if ( !isset( $bits['name'] ) ) {
1836
			throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
1837
		}
1838
		if ( !isset( $bits['index'] ) ) {
1839
			$bits['index'] = '';
1840
		}
1841
		return $bits;
1842
	}
1843
1844
	/**
1845
	 * Split an "<ext>" node into an associative array containing name, attr, inner and close
1846
	 * All values in the resulting array are PPNodes. Inner and close are optional.
1847
	 *
1848
	 * @throws MWException
1849
	 * @return array
1850
	 */
1851
	public function splitExt() {
1852
		return self::splitRawExt( $this->rawChildren );
1853
	}
1854
1855
	/**
1856
	 * Like splitExt() but for a raw child array. For internal use only.
1857
	 */
1858
	public static function splitRawExt( array $children ) {
1859
		$bits = [];
1860
		foreach ( $children as $i => $child ) {
1861
			if ( !is_array( $child ) ) {
1862
				continue;
1863
			}
1864
			switch ( $child[self::NAME] ) {
1865
			case 'name':
1866
				$bits['name'] = new self( $children, $i );
1867
				break;
1868
			case 'attr':
1869
				$bits['attr'] = new self( $children, $i );
1870
				break;
1871
			case 'inner':
1872
				$bits['inner'] = new self( $children, $i );
1873
				break;
1874
			case 'close':
1875
				$bits['close'] = new self( $children, $i );
1876
				break;
1877
			}
1878
		}
1879
		if ( !isset( $bits['name'] ) ) {
1880
			throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
1881
		}
1882
		return $bits;
1883
	}
1884
1885
	/**
1886
	 * Split an "<h>" node
1887
	 *
1888
	 * @throws MWException
1889
	 * @return array
1890
	 */
1891
	public function splitHeading() {
1892
		if ( $this->name !== 'h' ) {
1893
			throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1894
		}
1895
		return self::splitRawHeading( $this->rawChildren );
1896
	}
1897
1898
	/**
1899
	 * Like splitHeading() but for a raw child array. For internal use only.
1900
	 */
1901
	public static function splitRawHeading( array $children ) {
1902
		$bits = [];
1903
		foreach ( $children as $i => $child ) {
1904
			if ( !is_array( $child ) ) {
1905
				continue;
1906
			}
1907
			if ( $child[self::NAME] === '@i' ) {
1908
				$bits['i'] = $child[self::CHILDREN][0];
1909
			} elseif ( $child[self::NAME] === '@level' ) {
1910
				$bits['level'] = $child[self::CHILDREN][0];
1911
			}
1912
		}
1913
		if ( !isset( $bits['i'] ) ) {
1914
			throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1915
		}
1916
		return $bits;
1917
	}
1918
1919
	/**
1920
	 * Split a "<template>" or "<tplarg>" node
1921
	 *
1922
	 * @throws MWException
1923
	 * @return array
1924
	 */
1925
	public function splitTemplate() {
1926
		return self::splitRawTemplate( $this->rawChildren );
1927
	}
1928
1929
	/**
1930
	 * Like splitTemplate() but for a raw child array. For internal use only.
1931
	 */
1932
	public static function splitRawTemplate( array $children ) {
1933
		$parts = [];
1934
		$bits = [ 'lineStart' => '' ];
1935
		foreach ( $children as $i => $child ) {
1936
			if ( !is_array( $child ) ) {
1937
				continue;
1938
			}
1939
			switch ( $child[self::NAME] ) {
1940
			case 'title':
1941
				$bits['title'] = new self( $children, $i );
1942
				break;
1943
			case 'part':
1944
				$parts[] = new self( $children, $i );
1945
				break;
1946
			case '@lineStart':
1947
				$bits['lineStart'] = '1';
1948
				break;
1949
			}
1950
		}
1951
		if ( !isset( $bits['title'] ) ) {
1952
			throw new MWException( 'Invalid node passed to ' . __METHOD__ );
1953
		}
1954
		$bits['parts'] = new PPNode_Hash_Array( $parts );
1955
		return $bits;
1956
	}
1957
}
1958
1959
/**
1960
 * @ingroup Parser
1961
 */
1962
// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1963
class PPNode_Hash_Text implements PPNode {
1964
	// @codingStandardsIgnoreEnd
1965
1966
	public $value;
1967
	private $store, $index;
1968
1969
	/**
1970
	 * Construct an object using the data from $store[$index]. The rest of the
1971
	 * store array can be accessed via getNextSibling().
1972
	 *
1973
	 * @param array $store
1974
	 * @param integer $index
1975
	 */
1976
	public function __construct( array $store, $index ) {
1977
		$this->value = $store[$index];
1978
		if ( !is_scalar( $this->value ) ) {
1979
			throw new MWException( __CLASS__ . ' given object instead of string' );
1980
		}
1981
		$this->store = $store;
1982
		$this->index = $index;
1983
	}
1984
1985
	public function __toString() {
1986
		return htmlspecialchars( $this->value );
1987
	}
1988
1989
	public function getNextSibling() {
1990
		return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
1991
	}
1992
1993
	public function getChildren() {
1994
		return false;
1995
	}
1996
1997
	public function getFirstChild() {
1998
		return false;
1999
	}
2000
2001
	public function getChildrenOfType( $name ) {
2002
		return false;
2003
	}
2004
2005
	public function getLength() {
2006
		return false;
2007
	}
2008
2009
	public function item( $i ) {
2010
		return false;
2011
	}
2012
2013
	public function getName() {
2014
		return '#text';
2015
	}
2016
2017
	public function splitArg() {
2018
		throw new MWException( __METHOD__ . ': not supported' );
2019
	}
2020
2021
	public function splitExt() {
2022
		throw new MWException( __METHOD__ . ': not supported' );
2023
	}
2024
2025
	public function splitHeading() {
2026
		throw new MWException( __METHOD__ . ': not supported' );
2027
	}
2028
}
2029
2030
/**
2031
 * @ingroup Parser
2032
 */
2033
// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
2034
class PPNode_Hash_Array implements PPNode {
2035
	// @codingStandardsIgnoreEnd
2036
2037
	public $value;
2038
2039
	public function __construct( $value ) {
2040
		$this->value = $value;
2041
	}
2042
2043
	public function __toString() {
2044
		return var_export( $this, true );
2045
	}
2046
2047
	public function getLength() {
2048
		return count( $this->value );
2049
	}
2050
2051
	public function item( $i ) {
2052
		return $this->value[$i];
2053
	}
2054
2055
	public function getName() {
2056
		return '#nodelist';
2057
	}
2058
2059
	public function getNextSibling() {
2060
		return false;
2061
	}
2062
2063
	public function getChildren() {
2064
		return false;
2065
	}
2066
2067
	public function getFirstChild() {
2068
		return false;
2069
	}
2070
2071
	public function getChildrenOfType( $name ) {
2072
		return false;
2073
	}
2074
2075
	public function splitArg() {
2076
		throw new MWException( __METHOD__ . ': not supported' );
2077
	}
2078
2079
	public function splitExt() {
2080
		throw new MWException( __METHOD__ . ': not supported' );
2081
	}
2082
2083
	public function splitHeading() {
2084
		throw new MWException( __METHOD__ . ': not supported' );
2085
	}
2086
}
2087
2088
/**
2089
 * @ingroup Parser
2090
 */
2091
// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
2092
class PPNode_Hash_Attr implements PPNode {
2093
	// @codingStandardsIgnoreEnd
2094
2095
	public $name, $value;
2096
	private $store, $index;
2097
2098
	/**
2099
	 * Construct an object using the data from $store[$index]. The rest of the
2100
	 * store array can be accessed via getNextSibling().
2101
	 *
2102
	 * @param array $store
2103
	 * @param integer $index
2104
	 */
2105
	public function __construct( array $store, $index ) {
2106
		$descriptor = $store[$index];
2107
		if ( $descriptor[PPNode_Hash_Tree::NAME][0] !== '@' ) {
2108
			throw new MWException( __METHOD__.': invalid name in attribute descriptor' );
2109
		}
2110
		$this->name = substr( $descriptor[PPNode_Hash_Tree::NAME], 1 );
2111
		$this->value = $descriptor[PPNode_Hash_Tree::CHILDREN][0];
2112
		$this->store = $store;
2113
		$this->index = $index;
2114
	}
2115
2116
	public function __toString() {
2117
		return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
2118
	}
2119
2120
	public function getName() {
2121
		return $this->name;
2122
	}
2123
2124
	public function getNextSibling() {
2125
		return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
2126
	}
2127
2128
	public function getChildren() {
2129
		return false;
2130
	}
2131
2132
	public function getFirstChild() {
2133
		return false;
2134
	}
2135
2136
	public function getChildrenOfType( $name ) {
2137
		return false;
2138
	}
2139
2140
	public function getLength() {
2141
		return false;
2142
	}
2143
2144
	public function item( $i ) {
2145
		return false;
2146
	}
2147
2148
	public function splitArg() {
2149
		throw new MWException( __METHOD__ . ': not supported' );
2150
	}
2151
2152
	public function splitExt() {
2153
		throw new MWException( __METHOD__ . ': not supported' );
2154
	}
2155
2156
	public function splitHeading() {
2157
		throw new MWException( __METHOD__ . ': not supported' );
2158
	}
2159
}
2160