/** * @throws MWException * @param string|PPNode $root * @param int $flags * @return string */ public function expand($root, $flags = 0) { static $expansionDepth = 0; if (is_string($root)) { return $root; } if (++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount()) { $this->parser->limitationWarn('node-count-exceeded', $this->parser->mPPNodeCount, $this->parser->mOptions->getMaxPPNodeCount()); return '<span class="error">Node-count limit exceeded</span>'; } if ($expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth()) { $this->parser->limitationWarn('expansion-depth-exceeded', $expansionDepth, $this->parser->mOptions->getMaxPPExpandDepth()); return '<span class="error">Expansion depth limit exceeded</span>'; } ++$expansionDepth; if ($expansionDepth > $this->parser->mHighestExpansionDepth) { $this->parser->mHighestExpansionDepth = $expansionDepth; } $outStack = ['', '']; $iteratorStack = [false, $root]; $indexStack = [0, 0]; while (count($iteratorStack) > 1) { $level = count($outStack) - 1; $iteratorNode =& $iteratorStack[$level]; $out =& $outStack[$level]; $index =& $indexStack[$level]; if (is_array($iteratorNode)) { if ($index >= count($iteratorNode)) { // All done with this iterator $iteratorStack[$level] = false; $contextNode = false; } else { $contextNode = $iteratorNode[$index]; $index++; } } elseif ($iteratorNode instanceof PPNode_Hash_Array) { if ($index >= $iteratorNode->getLength()) { // All done with this iterator $iteratorStack[$level] = false; $contextNode = false; } else { $contextNode = $iteratorNode->item($index); $index++; } } else { // Copy to $contextNode and then delete from iterator stack, // because this is not an iterator but we do have to execute it once $contextNode = $iteratorStack[$level]; $iteratorStack[$level] = false; } $newIterator = false; $contextName = false; $contextChildren = false; if ($contextNode === false) { // nothing to do } elseif (is_string($contextNode)) { $out .= $contextNode; } elseif ($contextNode instanceof PPNode_Hash_Array) { $newIterator = $contextNode; } elseif ($contextNode instanceof PPNode_Hash_Attr) { // No output } elseif ($contextNode instanceof PPNode_Hash_Text) { $out .= $contextNode->value; } elseif ($contextNode instanceof PPNode_Hash_Tree) { $contextName = $contextNode->name; $contextChildren = $contextNode->getRawChildren(); } elseif (is_array($contextNode)) { // Node descriptor array if (count($contextNode) !== 2) { throw new MWException(__METHOD__ . ': found an array where a node descriptor should be'); } list($contextName, $contextChildren) = $contextNode; } else { throw new MWException(__METHOD__ . ': Invalid parameter type'); } // Handle node descriptor array or tree object if ($contextName === false) { // Not a node, already handled above } elseif ($contextName[0] === '@') { // Attribute: no output } elseif ($contextName === 'template') { # Double-brace expansion $bits = PPNode_Hash_Tree::splitRawTemplate($contextChildren); if ($flags & PPFrame::NO_TEMPLATES) { $newIterator = $this->virtualBracketedImplode('{{', '|', '}}', $bits['title'], $bits['parts']); } else { $ret = $this->parser->braceSubstitution($bits, $this); if (isset($ret['object'])) { $newIterator = $ret['object']; } else { $out .= $ret['text']; } } } elseif ($contextName === 'tplarg') { # Triple-brace expansion $bits = PPNode_Hash_Tree::splitRawTemplate($contextChildren); if ($flags & PPFrame::NO_ARGS) { $newIterator = $this->virtualBracketedImplode('{{{', '|', '}}}', $bits['title'], $bits['parts']); } else { $ret = $this->parser->argSubstitution($bits, $this); if (isset($ret['object'])) { $newIterator = $ret['object']; } else { $out .= $ret['text']; } } } elseif ($contextName === 'comment') { # HTML-style comment # Remove it in HTML, pre+remove and STRIP_COMMENTS modes # Not in RECOVER_COMMENTS mode (msgnw) though. if (($this->parser->ot['html'] || $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() || $flags & PPFrame::STRIP_COMMENTS) && !($flags & PPFrame::RECOVER_COMMENTS)) { $out .= ''; } elseif ($this->parser->ot['wiki'] && !($flags & PPFrame::RECOVER_COMMENTS)) { # Add a strip marker in PST mode so that pstPass2() can # run some old-fashioned regexes on the result. # Not in RECOVER_COMMENTS mode (extractSections) though. $out .= $this->parser->insertStripItem($contextChildren[0]); } else { # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove $out .= $contextChildren[0]; } } elseif ($contextName === 'ignore') { # Output suppression used by <includeonly> etc. # OT_WIKI will only respect <ignore> in substed templates. # The other output types respect it unless NO_IGNORE is set. # extractSections() sets NO_IGNORE and so never respects it. if (!isset($this->parent) && $this->parser->ot['wiki'] || $flags & PPFrame::NO_IGNORE) { $out .= $contextChildren[0]; } else { // $out .= ''; } } elseif ($contextName === 'ext') { # Extension tag $bits = PPNode_Hash_Tree::splitRawExt($contextChildren) + ['attr' => null, 'inner' => null, 'close' => null]; if ($flags & PPFrame::NO_TAGS) { $s = '<' . $bits['name']->getFirstChild()->value; if ($bits['attr']) { $s .= $bits['attr']->getFirstChild()->value; } if ($bits['inner']) { $s .= '>' . $bits['inner']->getFirstChild()->value; if ($bits['close']) { $s .= $bits['close']->getFirstChild()->value; } } else { $s .= '/>'; } $out .= $s; } else { $out .= $this->parser->extensionSubstitution($bits, $this); } } elseif ($contextName === 'h') { # Heading if ($this->parser->ot['html']) { # Expand immediately and insert heading index marker $s = $this->expand($contextChildren, $flags); $bits = PPNode_Hash_Tree::splitRawHeading($contextChildren); $titleText = $this->title->getPrefixedDBkey(); $this->parser->mHeadings[] = [$titleText, $bits['i']]; $serial = count($this->parser->mHeadings) - 1; $marker = Parser::MARKER_PREFIX . "-h-{$serial}-" . Parser::MARKER_SUFFIX; $s = substr($s, 0, $bits['level']) . $marker . substr($s, $bits['level']); $this->parser->mStripState->addGeneral($marker, ''); $out .= $s; } else { # Expand in virtual stack $newIterator = $contextChildren; } } else { # Generic recursive expansion $newIterator = $contextChildren; } if ($newIterator !== false) { $outStack[] = ''; $iteratorStack[] = $newIterator; $indexStack[] = 0; } elseif ($iteratorStack[$level] === false) { // Return accumulated value to parent // With tail recursion while ($iteratorStack[$level] === false && $level > 0) { $outStack[$level - 1] .= $out; array_pop($outStack); array_pop($iteratorStack); array_pop($indexStack); $level--; } } } --$expansionDepth; return $outStack[0]; }