Preprocessor_Hash.php 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610
  1. <?php
  2. /**
  3. * Differences from DOM schema:
  4. * * attribute nodes are children
  5. * * <h> nodes that aren't at the top are replaced with <possible-h>
  6. * @ingroup Parser
  7. */
  8. class Preprocessor_Hash implements Preprocessor {
  9. var $parser;
  10. const CACHE_VERSION = 1;
  11. function __construct( $parser ) {
  12. $this->parser = $parser;
  13. }
  14. function newFrame() {
  15. return new PPFrame_Hash( $this );
  16. }
  17. function newCustomFrame( $args ) {
  18. return new PPCustomFrame_Hash( $this, $args );
  19. }
  20. /**
  21. * Preprocess some wikitext and return the document tree.
  22. * This is the ghost of Parser::replace_variables().
  23. *
  24. * @param string $text The text to parse
  25. * @param integer flags Bitwise combination of:
  26. * Parser::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being
  27. * included. Default is to assume a direct page view.
  28. *
  29. * The generated DOM tree must depend only on the input text and the flags.
  30. * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
  31. *
  32. * Any flag added to the $flags parameter here, or any other parameter liable to cause a
  33. * change in the DOM tree for a given text, must be passed through the section identifier
  34. * in the section edit link and thus back to extractSections().
  35. *
  36. * The output of this function is currently only cached in process memory, but a persistent
  37. * cache may be implemented at a later date which takes further advantage of these strict
  38. * dependency requirements.
  39. *
  40. * @private
  41. */
  42. function preprocessToObj( $text, $flags = 0 ) {
  43. wfProfileIn( __METHOD__ );
  44. // Check cache.
  45. global $wgMemc, $wgPreprocessorCacheThreshold;
  46. $cacheable = strlen( $text ) > $wgPreprocessorCacheThreshold;
  47. if ( $cacheable ) {
  48. wfProfileIn( __METHOD__.'-cacheable' );
  49. $cacheKey = wfMemcKey( 'preprocess-hash', md5($text), $flags );
  50. $cacheValue = $wgMemc->get( $cacheKey );
  51. if ( $cacheValue ) {
  52. $version = substr( $cacheValue, 0, 8 );
  53. if ( intval( $version ) == self::CACHE_VERSION ) {
  54. $hash = unserialize( substr( $cacheValue, 8 ) );
  55. // From the cache
  56. wfDebugLog( "Preprocessor",
  57. "Loaded preprocessor hash from memcached (key $cacheKey)" );
  58. wfProfileOut( __METHOD__.'-cacheable' );
  59. wfProfileOut( __METHOD__ );
  60. return $hash;
  61. }
  62. }
  63. wfProfileIn( __METHOD__.'-cache-miss' );
  64. }
  65. $rules = array(
  66. '{' => array(
  67. 'end' => '}',
  68. 'names' => array(
  69. 2 => 'template',
  70. 3 => 'tplarg',
  71. ),
  72. 'min' => 2,
  73. 'max' => 3,
  74. ),
  75. '[' => array(
  76. 'end' => ']',
  77. 'names' => array( 2 => null ),
  78. 'min' => 2,
  79. 'max' => 2,
  80. )
  81. );
  82. $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
  83. $xmlishElements = $this->parser->getStripList();
  84. $enableOnlyinclude = false;
  85. if ( $forInclusion ) {
  86. $ignoredTags = array( 'includeonly', '/includeonly' );
  87. $ignoredElements = array( 'noinclude' );
  88. $xmlishElements[] = 'noinclude';
  89. if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) {
  90. $enableOnlyinclude = true;
  91. }
  92. } else {
  93. $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' );
  94. $ignoredElements = array( 'includeonly' );
  95. $xmlishElements[] = 'includeonly';
  96. }
  97. $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
  98. // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
  99. $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
  100. $stack = new PPDStack_Hash;
  101. $searchBase = "[{<\n";
  102. $revText = strrev( $text ); // For fast reverse searches
  103. $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start
  104. $accum =& $stack->getAccum(); # Current accumulator
  105. $findEquals = false; # True to find equals signs in arguments
  106. $findPipe = false; # True to take notice of pipe characters
  107. $headingIndex = 1;
  108. $inHeading = false; # True if $i is inside a possible heading
  109. $noMoreGT = false; # True if there are no more greater-than (>) signs right of $i
  110. $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude>
  111. $fakeLineStart = true; # Do a line-start run without outputting an LF character
  112. while ( true ) {
  113. //$this->memCheck();
  114. if ( $findOnlyinclude ) {
  115. // Ignore all input up to the next <onlyinclude>
  116. $startPos = strpos( $text, '<onlyinclude>', $i );
  117. if ( $startPos === false ) {
  118. // Ignored section runs to the end
  119. $accum->addNodeWithText( 'ignore', substr( $text, $i ) );
  120. break;
  121. }
  122. $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
  123. $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i ) );
  124. $i = $tagEndPos;
  125. $findOnlyinclude = false;
  126. }
  127. if ( $fakeLineStart ) {
  128. $found = 'line-start';
  129. $curChar = '';
  130. } else {
  131. # Find next opening brace, closing brace or pipe
  132. $search = $searchBase;
  133. if ( $stack->top === false ) {
  134. $currentClosing = '';
  135. } else {
  136. $currentClosing = $stack->top->close;
  137. $search .= $currentClosing;
  138. }
  139. if ( $findPipe ) {
  140. $search .= '|';
  141. }
  142. if ( $findEquals ) {
  143. // First equals will be for the template
  144. $search .= '=';
  145. }
  146. $rule = null;
  147. # Output literal section, advance input counter
  148. $literalLength = strcspn( $text, $search, $i );
  149. if ( $literalLength > 0 ) {
  150. $accum->addLiteral( substr( $text, $i, $literalLength ) );
  151. $i += $literalLength;
  152. }
  153. if ( $i >= strlen( $text ) ) {
  154. if ( $currentClosing == "\n" ) {
  155. // Do a past-the-end run to finish off the heading
  156. $curChar = '';
  157. $found = 'line-end';
  158. } else {
  159. # All done
  160. break;
  161. }
  162. } else {
  163. $curChar = $text[$i];
  164. if ( $curChar == '|' ) {
  165. $found = 'pipe';
  166. } elseif ( $curChar == '=' ) {
  167. $found = 'equals';
  168. } elseif ( $curChar == '<' ) {
  169. $found = 'angle';
  170. } elseif ( $curChar == "\n" ) {
  171. if ( $inHeading ) {
  172. $found = 'line-end';
  173. } else {
  174. $found = 'line-start';
  175. }
  176. } elseif ( $curChar == $currentClosing ) {
  177. $found = 'close';
  178. } elseif ( isset( $rules[$curChar] ) ) {
  179. $found = 'open';
  180. $rule = $rules[$curChar];
  181. } else {
  182. # Some versions of PHP have a strcspn which stops on null characters
  183. # Ignore and continue
  184. ++$i;
  185. continue;
  186. }
  187. }
  188. }
  189. if ( $found == 'angle' ) {
  190. $matches = false;
  191. // Handle </onlyinclude>
  192. if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) {
  193. $findOnlyinclude = true;
  194. continue;
  195. }
  196. // Determine element name
  197. if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
  198. // Element name missing or not listed
  199. $accum->addLiteral( '<' );
  200. ++$i;
  201. continue;
  202. }
  203. // Handle comments
  204. if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
  205. // To avoid leaving blank lines, when a comment is both preceded
  206. // and followed by a newline (ignoring spaces), trim leading and
  207. // trailing spaces and one of the newlines.
  208. // Find the end
  209. $endPos = strpos( $text, '-->', $i + 4 );
  210. if ( $endPos === false ) {
  211. // Unclosed comment in input, runs to end
  212. $inner = substr( $text, $i );
  213. $accum->addNodeWithText( 'comment', $inner );
  214. $i = strlen( $text );
  215. } else {
  216. // Search backwards for leading whitespace
  217. $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0;
  218. // Search forwards for trailing whitespace
  219. // $wsEnd will be the position of the last space
  220. $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 );
  221. // Eat the line if possible
  222. // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
  223. // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
  224. // it's a possible beneficial b/c break.
  225. if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
  226. && substr( $text, $wsEnd + 1, 1 ) == "\n" )
  227. {
  228. $startPos = $wsStart;
  229. $endPos = $wsEnd + 1;
  230. // Remove leading whitespace from the end of the accumulator
  231. // Sanity check first though
  232. $wsLength = $i - $wsStart;
  233. if ( $wsLength > 0
  234. && $accum->lastNode instanceof PPNode_Hash_Text
  235. && substr( $accum->lastNode->value, -$wsLength ) === str_repeat( ' ', $wsLength ) )
  236. {
  237. $accum->lastNode->value = substr( $accum->lastNode->value, 0, -$wsLength );
  238. }
  239. // Do a line-start run next time to look for headings after the comment
  240. $fakeLineStart = true;
  241. } else {
  242. // No line to eat, just take the comment itself
  243. $startPos = $i;
  244. $endPos += 2;
  245. }
  246. if ( $stack->top ) {
  247. $part = $stack->top->getCurrentPart();
  248. if ( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) {
  249. // Comments abutting, no change in visual end
  250. $part->commentEnd = $wsEnd;
  251. } else {
  252. $part->visualEnd = $wsStart;
  253. $part->commentEnd = $endPos;
  254. }
  255. }
  256. $i = $endPos + 1;
  257. $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
  258. $accum->addNodeWithText( 'comment', $inner );
  259. }
  260. continue;
  261. }
  262. $name = $matches[1];
  263. $lowerName = strtolower( $name );
  264. $attrStart = $i + strlen( $name ) + 1;
  265. // Find end of tag
  266. $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
  267. if ( $tagEndPos === false ) {
  268. // Infinite backtrack
  269. // Disable tag search to prevent worst-case O(N^2) performance
  270. $noMoreGT = true;
  271. $accum->addLiteral( '<' );
  272. ++$i;
  273. continue;
  274. }
  275. // Handle ignored tags
  276. if ( in_array( $lowerName, $ignoredTags ) ) {
  277. $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i + 1 ) );
  278. $i = $tagEndPos + 1;
  279. continue;
  280. }
  281. $tagStartPos = $i;
  282. if ( $text[$tagEndPos-1] == '/' ) {
  283. // Short end tag
  284. $attrEnd = $tagEndPos - 1;
  285. $inner = null;
  286. $i = $tagEndPos + 1;
  287. $close = null;
  288. } else {
  289. $attrEnd = $tagEndPos;
  290. // Find closing tag
  291. if ( preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
  292. $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) )
  293. {
  294. $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
  295. $i = $matches[0][1] + strlen( $matches[0][0] );
  296. $close = $matches[0][0];
  297. } else {
  298. // No end tag -- let it run out to the end of the text.
  299. $inner = substr( $text, $tagEndPos + 1 );
  300. $i = strlen( $text );
  301. $close = null;
  302. }
  303. }
  304. // <includeonly> and <noinclude> just become <ignore> tags
  305. if ( in_array( $lowerName, $ignoredElements ) ) {
  306. $accum->addNodeWithText( 'ignore', substr( $text, $tagStartPos, $i - $tagStartPos ) );
  307. continue;
  308. }
  309. if ( $attrEnd <= $attrStart ) {
  310. $attr = '';
  311. } else {
  312. // Note that the attr element contains the whitespace between name and attribute,
  313. // this is necessary for precise reconstruction during pre-save transform.
  314. $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
  315. }
  316. $extNode = new PPNode_Hash_Tree( 'ext' );
  317. $extNode->addChild( PPNode_Hash_Tree::newWithText( 'name', $name ) );
  318. $extNode->addChild( PPNode_Hash_Tree::newWithText( 'attr', $attr ) );
  319. if ( $inner !== null ) {
  320. $extNode->addChild( PPNode_Hash_Tree::newWithText( 'inner', $inner ) );
  321. }
  322. if ( $close !== null ) {
  323. $extNode->addChild( PPNode_Hash_Tree::newWithText( 'close', $close ) );
  324. }
  325. $accum->addNode( $extNode );
  326. }
  327. elseif ( $found == 'line-start' ) {
  328. // Is this the start of a heading?
  329. // Line break belongs before the heading element in any case
  330. if ( $fakeLineStart ) {
  331. $fakeLineStart = false;
  332. } else {
  333. $accum->addLiteral( $curChar );
  334. $i++;
  335. }
  336. $count = strspn( $text, '=', $i, 6 );
  337. if ( $count == 1 && $findEquals ) {
  338. // DWIM: This looks kind of like a name/value separator
  339. // Let's let the equals handler have it and break the potential heading
  340. // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex.
  341. } elseif ( $count > 0 ) {
  342. $piece = array(
  343. 'open' => "\n",
  344. 'close' => "\n",
  345. 'parts' => array( new PPDPart_Hash( str_repeat( '=', $count ) ) ),
  346. 'startPos' => $i,
  347. 'count' => $count );
  348. $stack->push( $piece );
  349. $accum =& $stack->getAccum();
  350. extract( $stack->getFlags() );
  351. $i += $count;
  352. }
  353. }
  354. elseif ( $found == 'line-end' ) {
  355. $piece = $stack->top;
  356. // A heading must be open, otherwise \n wouldn't have been in the search list
  357. assert( $piece->open == "\n" );
  358. $part = $piece->getCurrentPart();
  359. // Search back through the input to see if it has a proper close
  360. // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient
  361. $wsLength = strspn( $revText, " \t", strlen( $text ) - $i );
  362. $searchStart = $i - $wsLength;
  363. if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
  364. // Comment found at line end
  365. // Search for equals signs before the comment
  366. $searchStart = $part->visualEnd;
  367. $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart );
  368. }
  369. $count = $piece->count;
  370. $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart );
  371. if ( $equalsLength > 0 ) {
  372. if ( $i - $equalsLength == $piece->startPos ) {
  373. // This is just a single string of equals signs on its own line
  374. // Replicate the doHeadings behaviour /={count}(.+)={count}/
  375. // First find out how many equals signs there really are (don't stop at 6)
  376. $count = $equalsLength;
  377. if ( $count < 3 ) {
  378. $count = 0;
  379. } else {
  380. $count = min( 6, intval( ( $count - 1 ) / 2 ) );
  381. }
  382. } else {
  383. $count = min( $equalsLength, $count );
  384. }
  385. if ( $count > 0 ) {
  386. // Normal match, output <h>
  387. $element = new PPNode_Hash_Tree( 'possible-h' );
  388. $element->addChild( new PPNode_Hash_Attr( 'level', $count ) );
  389. $element->addChild( new PPNode_Hash_Attr( 'i', $headingIndex++ ) );
  390. $element->lastChild->nextSibling = $accum->firstNode;
  391. $element->lastChild = $accum->lastNode;
  392. } else {
  393. // Single equals sign on its own line, count=0
  394. $element = $accum;
  395. }
  396. } else {
  397. // No match, no <h>, just pass down the inner text
  398. $element = $accum;
  399. }
  400. // Unwind the stack
  401. $stack->pop();
  402. $accum =& $stack->getAccum();
  403. extract( $stack->getFlags() );
  404. // Append the result to the enclosing accumulator
  405. if ( $element instanceof PPNode ) {
  406. $accum->addNode( $element );
  407. } else {
  408. $accum->addAccum( $element );
  409. }
  410. // Note that we do NOT increment the input pointer.
  411. // This is because the closing linebreak could be the opening linebreak of
  412. // another heading. Infinite loops are avoided because the next iteration MUST
  413. // hit the heading open case above, which unconditionally increments the
  414. // input pointer.
  415. }
  416. elseif ( $found == 'open' ) {
  417. # count opening brace characters
  418. $count = strspn( $text, $curChar, $i );
  419. # we need to add to stack only if opening brace count is enough for one of the rules
  420. if ( $count >= $rule['min'] ) {
  421. # Add it to the stack
  422. $piece = array(
  423. 'open' => $curChar,
  424. 'close' => $rule['end'],
  425. 'count' => $count,
  426. 'lineStart' => ($i > 0 && $text[$i-1] == "\n"),
  427. );
  428. $stack->push( $piece );
  429. $accum =& $stack->getAccum();
  430. extract( $stack->getFlags() );
  431. } else {
  432. # Add literal brace(s)
  433. $accum->addLiteral( str_repeat( $curChar, $count ) );
  434. }
  435. $i += $count;
  436. }
  437. elseif ( $found == 'close' ) {
  438. $piece = $stack->top;
  439. # lets check if there are enough characters for closing brace
  440. $maxCount = $piece->count;
  441. $count = strspn( $text, $curChar, $i, $maxCount );
  442. # check for maximum matching characters (if there are 5 closing
  443. # characters, we will probably need only 3 - depending on the rules)
  444. $matchingCount = 0;
  445. $rule = $rules[$piece->open];
  446. if ( $count > $rule['max'] ) {
  447. # The specified maximum exists in the callback array, unless the caller
  448. # has made an error
  449. $matchingCount = $rule['max'];
  450. } else {
  451. # Count is less than the maximum
  452. # Skip any gaps in the callback array to find the true largest match
  453. # Need to use array_key_exists not isset because the callback can be null
  454. $matchingCount = $count;
  455. while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
  456. --$matchingCount;
  457. }
  458. }
  459. if ($matchingCount <= 0) {
  460. # No matching element found in callback array
  461. # Output a literal closing brace and continue
  462. $accum->addLiteral( str_repeat( $curChar, $count ) );
  463. $i += $count;
  464. continue;
  465. }
  466. $name = $rule['names'][$matchingCount];
  467. if ( $name === null ) {
  468. // No element, just literal text
  469. $element = $piece->breakSyntax( $matchingCount );
  470. $element->addLiteral( str_repeat( $rule['end'], $matchingCount ) );
  471. } else {
  472. # Create XML element
  473. # Note: $parts is already XML, does not need to be encoded further
  474. $parts = $piece->parts;
  475. $titleAccum = $parts[0]->out;
  476. unset( $parts[0] );
  477. $element = new PPNode_Hash_Tree( $name );
  478. # The invocation is at the start of the line if lineStart is set in
  479. # the stack, and all opening brackets are used up.
  480. if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
  481. $element->addChild( new PPNode_Hash_Attr( 'lineStart', 1 ) );
  482. }
  483. $titleNode = new PPNode_Hash_Tree( 'title' );
  484. $titleNode->firstChild = $titleAccum->firstNode;
  485. $titleNode->lastChild = $titleAccum->lastNode;
  486. $element->addChild( $titleNode );
  487. $argIndex = 1;
  488. foreach ( $parts as $partIndex => $part ) {
  489. if ( isset( $part->eqpos ) ) {
  490. // Find equals
  491. $lastNode = false;
  492. for ( $node = $part->out->firstNode; $node; $node = $node->nextSibling ) {
  493. if ( $node === $part->eqpos ) {
  494. break;
  495. }
  496. $lastNode = $node;
  497. }
  498. if ( !$node ) {
  499. throw new MWException( __METHOD__. ': eqpos not found' );
  500. }
  501. if ( $node->name !== 'equals' ) {
  502. throw new MWException( __METHOD__ .': eqpos is not equals' );
  503. }
  504. $equalsNode = $node;
  505. // Construct name node
  506. $nameNode = new PPNode_Hash_Tree( 'name' );
  507. if ( $lastNode !== false ) {
  508. $lastNode->nextSibling = false;
  509. $nameNode->firstChild = $part->out->firstNode;
  510. $nameNode->lastChild = $lastNode;
  511. }
  512. // Construct value node
  513. $valueNode = new PPNode_Hash_Tree( 'value' );
  514. if ( $equalsNode->nextSibling !== false ) {
  515. $valueNode->firstChild = $equalsNode->nextSibling;
  516. $valueNode->lastChild = $part->out->lastNode;
  517. }
  518. $partNode = new PPNode_Hash_Tree( 'part' );
  519. $partNode->addChild( $nameNode );
  520. $partNode->addChild( $equalsNode->firstChild );
  521. $partNode->addChild( $valueNode );
  522. $element->addChild( $partNode );
  523. } else {
  524. $partNode = new PPNode_Hash_Tree( 'part' );
  525. $nameNode = new PPNode_Hash_Tree( 'name' );
  526. $nameNode->addChild( new PPNode_Hash_Attr( 'index', $argIndex++ ) );
  527. $valueNode = new PPNode_Hash_Tree( 'value' );
  528. $valueNode->firstChild = $part->out->firstNode;
  529. $valueNode->lastChild = $part->out->lastNode;
  530. $partNode->addChild( $nameNode );
  531. $partNode->addChild( $valueNode );
  532. $element->addChild( $partNode );
  533. }
  534. }
  535. }
  536. # Advance input pointer
  537. $i += $matchingCount;
  538. # Unwind the stack
  539. $stack->pop();
  540. $accum =& $stack->getAccum();
  541. # Re-add the old stack element if it still has unmatched opening characters remaining
  542. if ($matchingCount < $piece->count) {
  543. $piece->parts = array( new PPDPart_Hash );
  544. $piece->count -= $matchingCount;
  545. # do we still qualify for any callback with remaining count?
  546. $names = $rules[$piece->open]['names'];
  547. $skippedBraces = 0;
  548. $enclosingAccum =& $accum;
  549. while ( $piece->count ) {
  550. if ( array_key_exists( $piece->count, $names ) ) {
  551. $stack->push( $piece );
  552. $accum =& $stack->getAccum();
  553. break;
  554. }
  555. --$piece->count;
  556. $skippedBraces ++;
  557. }
  558. $enclosingAccum->addLiteral( str_repeat( $piece->open, $skippedBraces ) );
  559. }
  560. extract( $stack->getFlags() );
  561. # Add XML element to the enclosing accumulator
  562. if ( $element instanceof PPNode ) {
  563. $accum->addNode( $element );
  564. } else {
  565. $accum->addAccum( $element );
  566. }
  567. }
  568. elseif ( $found == 'pipe' ) {
  569. $findEquals = true; // shortcut for getFlags()
  570. $stack->addPart();
  571. $accum =& $stack->getAccum();
  572. ++$i;
  573. }
  574. elseif ( $found == 'equals' ) {
  575. $findEquals = false; // shortcut for getFlags()
  576. $accum->addNodeWithText( 'equals', '=' );
  577. $stack->getCurrentPart()->eqpos = $accum->lastNode;
  578. ++$i;
  579. }
  580. }
  581. # Output any remaining unclosed brackets
  582. foreach ( $stack->stack as $piece ) {
  583. $stack->rootAccum->addAccum( $piece->breakSyntax() );
  584. }
  585. # Enable top-level headings
  586. for ( $node = $stack->rootAccum->firstNode; $node; $node = $node->nextSibling ) {
  587. if ( isset( $node->name ) && $node->name === 'possible-h' ) {
  588. $node->name = 'h';
  589. }
  590. }
  591. $rootNode = new PPNode_Hash_Tree( 'root' );
  592. $rootNode->firstChild = $stack->rootAccum->firstNode;
  593. $rootNode->lastChild = $stack->rootAccum->lastNode;
  594. // Cache
  595. if ($cacheable) {
  596. $cacheValue = sprintf( "%08d", self::CACHE_VERSION ) . serialize( $rootNode );;
  597. $wgMemc->set( $cacheKey, $cacheValue, 86400 );
  598. wfProfileOut( __METHOD__.'-cache-miss' );
  599. wfProfileOut( __METHOD__.'-cacheable' );
  600. wfDebugLog( "Preprocessor", "Saved preprocessor Hash to memcached (key $cacheKey)" );
  601. }
  602. wfProfileOut( __METHOD__ );
  603. return $rootNode;
  604. }
  605. }
  606. /**
  607. * Stack class to help Preprocessor::preprocessToObj()
  608. * @ingroup Parser
  609. */
  610. class PPDStack_Hash extends PPDStack {
  611. function __construct() {
  612. $this->elementClass = 'PPDStackElement_Hash';
  613. parent::__construct();
  614. $this->rootAccum = new PPDAccum_Hash;
  615. }
  616. }
  617. /**
  618. * @ingroup Parser
  619. */
  620. class PPDStackElement_Hash extends PPDStackElement {
  621. function __construct( $data = array() ) {
  622. $this->partClass = 'PPDPart_Hash';
  623. parent::__construct( $data );
  624. }
  625. /**
  626. * Get the accumulator that would result if the close is not found.
  627. */
  628. function breakSyntax( $openingCount = false ) {
  629. if ( $this->open == "\n" ) {
  630. $accum = $this->parts[0]->out;
  631. } else {
  632. if ( $openingCount === false ) {
  633. $openingCount = $this->count;
  634. }
  635. $accum = new PPDAccum_Hash;
  636. $accum->addLiteral( str_repeat( $this->open, $openingCount ) );
  637. $first = true;
  638. foreach ( $this->parts as $part ) {
  639. if ( $first ) {
  640. $first = false;
  641. } else {
  642. $accum->addLiteral( '|' );
  643. }
  644. $accum->addAccum( $part->out );
  645. }
  646. }
  647. return $accum;
  648. }
  649. }
  650. /**
  651. * @ingroup Parser
  652. */
  653. class PPDPart_Hash extends PPDPart {
  654. function __construct( $out = '' ) {
  655. $accum = new PPDAccum_Hash;
  656. if ( $out !== '' ) {
  657. $accum->addLiteral( $out );
  658. }
  659. parent::__construct( $accum );
  660. }
  661. }
  662. /**
  663. * @ingroup Parser
  664. */
  665. class PPDAccum_Hash {
  666. var $firstNode, $lastNode;
  667. function __construct() {
  668. $this->firstNode = $this->lastNode = false;
  669. }
  670. /**
  671. * Append a string literal
  672. */
  673. function addLiteral( $s ) {
  674. if ( $this->lastNode === false ) {
  675. $this->firstNode = $this->lastNode = new PPNode_Hash_Text( $s );
  676. } elseif ( $this->lastNode instanceof PPNode_Hash_Text ) {
  677. $this->lastNode->value .= $s;
  678. } else {
  679. $this->lastNode->nextSibling = new PPNode_Hash_Text( $s );
  680. $this->lastNode = $this->lastNode->nextSibling;
  681. }
  682. }
  683. /**
  684. * Append a PPNode
  685. */
  686. function addNode( PPNode $node ) {
  687. if ( $this->lastNode === false ) {
  688. $this->firstNode = $this->lastNode = $node;
  689. } else {
  690. $this->lastNode->nextSibling = $node;
  691. $this->lastNode = $node;
  692. }
  693. }
  694. /**
  695. * Append a tree node with text contents
  696. */
  697. function addNodeWithText( $name, $value ) {
  698. $node = PPNode_Hash_Tree::newWithText( $name, $value );
  699. $this->addNode( $node );
  700. }
  701. /**
  702. * Append a PPAccum_Hash
  703. * Takes over ownership of the nodes in the source argument. These nodes may
  704. * subsequently be modified, especially nextSibling.
  705. */
  706. function addAccum( $accum ) {
  707. if ( $accum->lastNode === false ) {
  708. // nothing to add
  709. } elseif ( $this->lastNode === false ) {
  710. $this->firstNode = $accum->firstNode;
  711. $this->lastNode = $accum->lastNode;
  712. } else {
  713. $this->lastNode->nextSibling = $accum->firstNode;
  714. $this->lastNode = $accum->lastNode;
  715. }
  716. }
  717. }
  718. /**
  719. * An expansion frame, used as a context to expand the result of preprocessToObj()
  720. * @ingroup Parser
  721. */
  722. class PPFrame_Hash implements PPFrame {
  723. var $preprocessor, $parser, $title;
  724. var $titleCache;
  725. /**
  726. * Hashtable listing templates which are disallowed for expansion in this frame,
  727. * having been encountered previously in parent frames.
  728. */
  729. var $loopCheckHash;
  730. /**
  731. * Recursion depth of this frame, top = 0
  732. * Note that this is NOT the same as expansion depth in expand()
  733. */
  734. var $depth;
  735. /**
  736. * Construct a new preprocessor frame.
  737. * @param Preprocessor $preprocessor The parent preprocessor
  738. */
  739. function __construct( $preprocessor ) {
  740. $this->preprocessor = $preprocessor;
  741. $this->parser = $preprocessor->parser;
  742. $this->title = $this->parser->mTitle;
  743. $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false );
  744. $this->loopCheckHash = array();
  745. $this->depth = 0;
  746. }
  747. /**
  748. * Create a new child frame
  749. * $args is optionally a multi-root PPNode or array containing the template arguments
  750. */
  751. function newChild( $args = false, $title = false ) {
  752. $namedArgs = array();
  753. $numberedArgs = array();
  754. if ( $title === false ) {
  755. $title = $this->title;
  756. }
  757. if ( $args !== false ) {
  758. $xpath = false;
  759. if ( $args instanceof PPNode_Hash_Array ) {
  760. $args = $args->value;
  761. } elseif ( !is_array( $args ) ) {
  762. throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
  763. }
  764. foreach ( $args as $arg ) {
  765. $bits = $arg->splitArg();
  766. if ( $bits['index'] !== '' ) {
  767. // Numbered parameter
  768. $numberedArgs[$bits['index']] = $bits['value'];
  769. unset( $namedArgs[$bits['index']] );
  770. } else {
  771. // Named parameter
  772. $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
  773. $namedArgs[$name] = $bits['value'];
  774. unset( $numberedArgs[$name] );
  775. }
  776. }
  777. }
  778. return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
  779. }
  780. function expand( $root, $flags = 0 ) {
  781. static $expansionDepth = 0;
  782. if ( is_string( $root ) ) {
  783. return $root;
  784. }
  785. if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount )
  786. {
  787. return '<span class="error">Node-count limit exceeded</span>';
  788. }
  789. if ( $expansionDepth > $this->parser->mOptions->mMaxPPExpandDepth ) {
  790. return '<span class="error">Expansion depth limit exceeded</span>';
  791. }
  792. ++$expansionDepth;
  793. $outStack = array( '', '' );
  794. $iteratorStack = array( false, $root );
  795. $indexStack = array( 0, 0 );
  796. while ( count( $iteratorStack ) > 1 ) {
  797. $level = count( $outStack ) - 1;
  798. $iteratorNode =& $iteratorStack[ $level ];
  799. $out =& $outStack[$level];
  800. $index =& $indexStack[$level];
  801. if ( is_array( $iteratorNode ) ) {
  802. if ( $index >= count( $iteratorNode ) ) {
  803. // All done with this iterator
  804. $iteratorStack[$level] = false;
  805. $contextNode = false;
  806. } else {
  807. $contextNode = $iteratorNode[$index];
  808. $index++;
  809. }
  810. } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
  811. if ( $index >= $iteratorNode->getLength() ) {
  812. // All done with this iterator
  813. $iteratorStack[$level] = false;
  814. $contextNode = false;
  815. } else {
  816. $contextNode = $iteratorNode->item( $index );
  817. $index++;
  818. }
  819. } else {
  820. // Copy to $contextNode and then delete from iterator stack,
  821. // because this is not an iterator but we do have to execute it once
  822. $contextNode = $iteratorStack[$level];
  823. $iteratorStack[$level] = false;
  824. }
  825. $newIterator = false;
  826. if ( $contextNode === false ) {
  827. // nothing to do
  828. } elseif ( is_string( $contextNode ) ) {
  829. $out .= $contextNode;
  830. } elseif ( is_array( $contextNode ) || $contextNode instanceof PPNode_Hash_Array ) {
  831. $newIterator = $contextNode;
  832. } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
  833. // No output
  834. } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
  835. $out .= $contextNode->value;
  836. } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
  837. if ( $contextNode->name == 'template' ) {
  838. # Double-brace expansion
  839. $bits = $contextNode->splitTemplate();
  840. if ( $flags & self::NO_TEMPLATES ) {
  841. $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $bits['title'], $bits['parts'] );
  842. } else {
  843. $ret = $this->parser->braceSubstitution( $bits, $this );
  844. if ( isset( $ret['object'] ) ) {
  845. $newIterator = $ret['object'];
  846. } else {
  847. $out .= $ret['text'];
  848. }
  849. }
  850. } elseif ( $contextNode->name == 'tplarg' ) {
  851. # Triple-brace expansion
  852. $bits = $contextNode->splitTemplate();
  853. if ( $flags & self::NO_ARGS ) {
  854. $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $bits['title'], $bits['parts'] );
  855. } else {
  856. $ret = $this->parser->argSubstitution( $bits, $this );
  857. if ( isset( $ret['object'] ) ) {
  858. $newIterator = $ret['object'];
  859. } else {
  860. $out .= $ret['text'];
  861. }
  862. }
  863. } elseif ( $contextNode->name == 'comment' ) {
  864. # HTML-style comment
  865. # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
  866. if ( $this->parser->ot['html']
  867. || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
  868. || ( $flags & self::STRIP_COMMENTS ) )
  869. {
  870. $out .= '';
  871. }
  872. # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result
  873. # Not in RECOVER_COMMENTS mode (extractSections) though
  874. elseif ( $this->parser->ot['wiki'] && ! ( $flags & self::RECOVER_COMMENTS ) ) {
  875. $out .= $this->parser->insertStripItem( $contextNode->firstChild->value );
  876. }
  877. # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
  878. else {
  879. $out .= $contextNode->firstChild->value;
  880. }
  881. } elseif ( $contextNode->name == 'ignore' ) {
  882. # Output suppression used by <includeonly> etc.
  883. # OT_WIKI will only respect <ignore> in substed templates.
  884. # The other output types respect it unless NO_IGNORE is set.
  885. # extractSections() sets NO_IGNORE and so never respects it.
  886. if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & self::NO_IGNORE ) ) {
  887. $out .= $contextNode->firstChild->value;
  888. } else {
  889. //$out .= '';
  890. }
  891. } elseif ( $contextNode->name == 'ext' ) {
  892. # Extension tag
  893. $bits = $contextNode->splitExt() + array( 'attr' => null, 'inner' => null, 'close' => null );
  894. $out .= $this->parser->extensionSubstitution( $bits, $this );
  895. } elseif ( $contextNode->name == 'h' ) {
  896. # Heading
  897. if ( $this->parser->ot['html'] ) {
  898. # Expand immediately and insert heading index marker
  899. $s = '';
  900. for ( $node = $contextNode->firstChild; $node; $node = $node->nextSibling ) {
  901. $s .= $this->expand( $node, $flags );
  902. }
  903. $bits = $contextNode->splitHeading();
  904. $titleText = $this->title->getPrefixedDBkey();
  905. $this->parser->mHeadings[] = array( $titleText, $bits['i'] );
  906. $serial = count( $this->parser->mHeadings ) - 1;
  907. $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX;
  908. $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
  909. $this->parser->mStripState->general->setPair( $marker, '' );
  910. $out .= $s;
  911. } else {
  912. # Expand in virtual stack
  913. $newIterator = $contextNode->getChildren();
  914. }
  915. } else {
  916. # Generic recursive expansion
  917. $newIterator = $contextNode->getChildren();
  918. }
  919. } else {
  920. throw new MWException( __METHOD__.': Invalid parameter type' );
  921. }
  922. if ( $newIterator !== false ) {
  923. $outStack[] = '';
  924. $iteratorStack[] = $newIterator;
  925. $indexStack[] = 0;
  926. } elseif ( $iteratorStack[$level] === false ) {
  927. // Return accumulated value to parent
  928. // With tail recursion
  929. while ( $iteratorStack[$level] === false && $level > 0 ) {
  930. $outStack[$level - 1] .= $out;
  931. array_pop( $outStack );
  932. array_pop( $iteratorStack );
  933. array_pop( $indexStack );
  934. $level--;
  935. }
  936. }
  937. }
  938. --$expansionDepth;
  939. return $outStack[0];
  940. }
  941. function implodeWithFlags( $sep, $flags /*, ... */ ) {
  942. $args = array_slice( func_get_args(), 2 );
  943. $first = true;
  944. $s = '';
  945. foreach ( $args as $root ) {
  946. if ( $root instanceof PPNode_Hash_Array ) {
  947. $root = $root->value;
  948. }
  949. if ( !is_array( $root ) ) {
  950. $root = array( $root );
  951. }
  952. foreach ( $root as $node ) {
  953. if ( $first ) {
  954. $first = false;
  955. } else {
  956. $s .= $sep;
  957. }
  958. $s .= $this->expand( $node, $flags );
  959. }
  960. }
  961. return $s;
  962. }
  963. /**
  964. * Implode with no flags specified
  965. * This previously called implodeWithFlags but has now been inlined to reduce stack depth
  966. */
  967. function implode( $sep /*, ... */ ) {
  968. $args = array_slice( func_get_args(), 1 );
  969. $first = true;
  970. $s = '';
  971. foreach ( $args as $root ) {
  972. if ( $root instanceof PPNode_Hash_Array ) {
  973. $root = $root->value;
  974. }
  975. if ( !is_array( $root ) ) {
  976. $root = array( $root );
  977. }
  978. foreach ( $root as $node ) {
  979. if ( $first ) {
  980. $first = false;
  981. } else {
  982. $s .= $sep;
  983. }
  984. $s .= $this->expand( $node );
  985. }
  986. }
  987. return $s;
  988. }
  989. /**
  990. * Makes an object that, when expand()ed, will be the same as one obtained
  991. * with implode()
  992. */
  993. function virtualImplode( $sep /*, ... */ ) {
  994. $args = array_slice( func_get_args(), 1 );
  995. $out = array();
  996. $first = true;
  997. foreach ( $args as $root ) {
  998. if ( $root instanceof PPNode_Hash_Array ) {
  999. $root = $root->value;
  1000. }
  1001. if ( !is_array( $root ) ) {
  1002. $root = array( $root );
  1003. }
  1004. foreach ( $root as $node ) {
  1005. if ( $first ) {
  1006. $first = false;
  1007. } else {
  1008. $out[] = $sep;
  1009. }
  1010. $out[] = $node;
  1011. }
  1012. }
  1013. return new PPNode_Hash_Array( $out );
  1014. }
  1015. /**
  1016. * Virtual implode with brackets
  1017. */
  1018. function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
  1019. $args = array_slice( func_get_args(), 3 );
  1020. $out = array( $start );
  1021. $first = true;
  1022. foreach ( $args as $root ) {
  1023. if ( $root instanceof PPNode_Hash_Array ) {
  1024. $root = $root->value;
  1025. }
  1026. if ( !is_array( $root ) ) {
  1027. $root = array( $root );
  1028. }
  1029. foreach ( $root as $node ) {
  1030. if ( $first ) {
  1031. $first = false;
  1032. } else {
  1033. $out[] = $sep;
  1034. }
  1035. $out[] = $node;
  1036. }
  1037. }
  1038. $out[] = $end;
  1039. return new PPNode_Hash_Array( $out );
  1040. }
  1041. function __toString() {
  1042. return 'frame{}';
  1043. }
  1044. function getPDBK( $level = false ) {
  1045. if ( $level === false ) {
  1046. return $this->title->getPrefixedDBkey();
  1047. } else {
  1048. return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
  1049. }
  1050. }
  1051. /**
  1052. * Returns true if there are no arguments in this frame
  1053. */
  1054. function isEmpty() {
  1055. return true;
  1056. }
  1057. function getArgument( $name ) {
  1058. return false;
  1059. }
  1060. /**
  1061. * Returns true if the infinite loop check is OK, false if a loop is detected
  1062. */
  1063. function loopCheck( $title ) {
  1064. return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
  1065. }
  1066. /**
  1067. * Return true if the frame is a template frame
  1068. */
  1069. function isTemplate() {
  1070. return false;
  1071. }
  1072. }
  1073. /**
  1074. * Expansion frame with template arguments
  1075. * @ingroup Parser
  1076. */
  1077. class PPTemplateFrame_Hash extends PPFrame_Hash {
  1078. var $numberedArgs, $namedArgs, $parent;
  1079. var $numberedExpansionCache, $namedExpansionCache;
  1080. function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
  1081. $this->preprocessor = $preprocessor;
  1082. $this->parser = $preprocessor->parser;
  1083. $this->parent = $parent;
  1084. $this->numberedArgs = $numberedArgs;
  1085. $this->namedArgs = $namedArgs;
  1086. $this->title = $title;
  1087. $pdbk = $title ? $title->getPrefixedDBkey() : false;
  1088. $this->titleCache = $parent->titleCache;
  1089. $this->titleCache[] = $pdbk;
  1090. $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
  1091. if ( $pdbk !== false ) {
  1092. $this->loopCheckHash[$pdbk] = true;
  1093. }
  1094. $this->depth = $parent->depth + 1;
  1095. $this->numberedExpansionCache = $this->namedExpansionCache = array();
  1096. }
  1097. function __toString() {
  1098. $s = 'tplframe{';
  1099. $first = true;
  1100. $args = $this->numberedArgs + $this->namedArgs;
  1101. foreach ( $args as $name => $value ) {
  1102. if ( $first ) {
  1103. $first = false;
  1104. } else {
  1105. $s .= ', ';
  1106. }
  1107. $s .= "\"$name\":\"" .
  1108. str_replace( '"', '\\"', $value->__toString() ) . '"';
  1109. }
  1110. $s .= '}';
  1111. return $s;
  1112. }
  1113. /**
  1114. * Returns true if there are no arguments in this frame
  1115. */
  1116. function isEmpty() {
  1117. return !count( $this->numberedArgs ) && !count( $this->namedArgs );
  1118. }
  1119. function getArguments() {
  1120. $arguments = array();
  1121. foreach ( array_merge(
  1122. array_keys($this->numberedArgs),
  1123. array_keys($this->namedArgs)) as $key ) {
  1124. $arguments[$key] = $this->getArgument($key);
  1125. }
  1126. return $arguments;
  1127. }
  1128. function getNumberedArguments() {
  1129. $arguments = array();
  1130. foreach ( array_keys($this->numberedArgs) as $key ) {
  1131. $arguments[$key] = $this->getArgument($key);
  1132. }
  1133. return $arguments;
  1134. }
  1135. function getNamedArguments() {
  1136. $arguments = array();
  1137. foreach ( array_keys($this->namedArgs) as $key ) {
  1138. $arguments[$key] = $this->getArgument($key);
  1139. }
  1140. return $arguments;
  1141. }
  1142. function getNumberedArgument( $index ) {
  1143. if ( !isset( $this->numberedArgs[$index] ) ) {
  1144. return false;
  1145. }
  1146. if ( !isset( $this->numberedExpansionCache[$index] ) ) {
  1147. # No trimming for unnamed arguments
  1148. $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], self::STRIP_COMMENTS );
  1149. }
  1150. return $this->numberedExpansionCache[$index];
  1151. }
  1152. function getNamedArgument( $name ) {
  1153. if ( !isset( $this->namedArgs[$name] ) ) {
  1154. return false;
  1155. }
  1156. if ( !isset( $this->namedExpansionCache[$name] ) ) {
  1157. # Trim named arguments post-expand, for backwards compatibility
  1158. $this->namedExpansionCache[$name] = trim(
  1159. $this->parent->expand( $this->namedArgs[$name], self::STRIP_COMMENTS ) );
  1160. }
  1161. return $this->namedExpansionCache[$name];
  1162. }
  1163. function getArgument( $name ) {
  1164. $text = $this->getNumberedArgument( $name );
  1165. if ( $text === false ) {
  1166. $text = $this->getNamedArgument( $name );
  1167. }
  1168. return $text;
  1169. }
  1170. /**
  1171. * Return true if the frame is a template frame
  1172. */
  1173. function isTemplate() {
  1174. return true;
  1175. }
  1176. }
  1177. /**
  1178. * Expansion frame with custom arguments
  1179. * @ingroup Parser
  1180. */
  1181. class PPCustomFrame_Hash extends PPFrame_Hash {
  1182. var $args;
  1183. function __construct( $preprocessor, $args ) {
  1184. $this->preprocessor = $preprocessor;
  1185. $this->parser = $preprocessor->parser;
  1186. $this->args = $args;
  1187. }
  1188. function __toString() {
  1189. $s = 'cstmframe{';
  1190. $first = true;
  1191. foreach ( $this->args as $name => $value ) {
  1192. if ( $first ) {
  1193. $first = false;
  1194. } else {
  1195. $s .= ', ';
  1196. }
  1197. $s .= "\"$name\":\"" .
  1198. str_replace( '"', '\\"', $value->__toString() ) . '"';
  1199. }
  1200. $s .= '}';
  1201. return $s;
  1202. }
  1203. function isEmpty() {
  1204. return !count( $this->args );
  1205. }
  1206. function getArgument( $index ) {
  1207. if ( !isset( $this->args[$index] ) ) {
  1208. return false;
  1209. }
  1210. return $this->args[$index];
  1211. }
  1212. }
  1213. /**
  1214. * @ingroup Parser
  1215. */
  1216. class PPNode_Hash_Tree implements PPNode {
  1217. var $name, $firstChild, $lastChild, $nextSibling;
  1218. function __construct( $name ) {
  1219. $this->name = $name;
  1220. $this->firstChild = $this->lastChild = $this->nextSibling = false;
  1221. }
  1222. function __toString() {
  1223. $inner = '';
  1224. $attribs = '';
  1225. for ( $node = $this->firstChild; $node; $node = $node->nextSibling ) {
  1226. if ( $node instanceof PPNode_Hash_Attr ) {
  1227. $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
  1228. } else {
  1229. $inner .= $node->__toString();
  1230. }
  1231. }
  1232. if ( $inner === '' ) {
  1233. return "<{$this->name}$attribs/>";
  1234. } else {
  1235. return "<{$this->name}$attribs>$inner</{$this->name}>";
  1236. }
  1237. }
  1238. static function newWithText( $name, $text ) {
  1239. $obj = new self( $name );
  1240. $obj->addChild( new PPNode_Hash_Text( $text ) );
  1241. return $obj;
  1242. }
  1243. function addChild( $node ) {
  1244. if ( $this->lastChild === false ) {
  1245. $this->firstChild = $this->lastChild = $node;
  1246. } else {
  1247. $this->lastChild->nextSibling = $node;
  1248. $this->lastChild = $node;
  1249. }
  1250. }
  1251. function getChildren() {
  1252. $children = array();
  1253. for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
  1254. $children[] = $child;
  1255. }
  1256. return new PPNode_Hash_Array( $children );
  1257. }
  1258. function getFirstChild() {
  1259. return $this->firstChild;
  1260. }
  1261. function getNextSibling() {
  1262. return $this->nextSibling;
  1263. }
  1264. function getChildrenOfType( $name ) {
  1265. $children = array();
  1266. for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
  1267. if ( isset( $child->name ) && $child->name === $name ) {
  1268. $children[] = $name;
  1269. }
  1270. }
  1271. return $children;
  1272. }
  1273. function getLength() { return false; }
  1274. function item( $i ) { return false; }
  1275. function getName() {
  1276. return $this->name;
  1277. }
  1278. /**
  1279. * Split a <part> node into an associative array containing:
  1280. * name PPNode name
  1281. * index String index
  1282. * value PPNode value
  1283. */
  1284. function splitArg() {
  1285. $bits = array();
  1286. for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
  1287. if ( !isset( $child->name ) ) {
  1288. continue;
  1289. }
  1290. if ( $child->name === 'name' ) {
  1291. $bits['name'] = $child;
  1292. if ( $child->firstChild instanceof PPNode_Hash_Attr
  1293. && $child->firstChild->name === 'index' )
  1294. {
  1295. $bits['index'] = $child->firstChild->value;
  1296. }
  1297. } elseif ( $child->name === 'value' ) {
  1298. $bits['value'] = $child;
  1299. }
  1300. }
  1301. if ( !isset( $bits['name'] ) ) {
  1302. throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
  1303. }
  1304. if ( !isset( $bits['index'] ) ) {
  1305. $bits['index'] = '';
  1306. }
  1307. return $bits;
  1308. }
  1309. /**
  1310. * Split an <ext> node into an associative array containing name, attr, inner and close
  1311. * All values in the resulting array are PPNodes. Inner and close are optional.
  1312. */
  1313. function splitExt() {
  1314. $bits = array();
  1315. for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
  1316. if ( !isset( $child->name ) ) {
  1317. continue;
  1318. }
  1319. if ( $child->name == 'name' ) {
  1320. $bits['name'] = $child;
  1321. } elseif ( $child->name == 'attr' ) {
  1322. $bits['attr'] = $child;
  1323. } elseif ( $child->name == 'inner' ) {
  1324. $bits['inner'] = $child;
  1325. } elseif ( $child->name == 'close' ) {
  1326. $bits['close'] = $child;
  1327. }
  1328. }
  1329. if ( !isset( $bits['name'] ) ) {
  1330. throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
  1331. }
  1332. return $bits;
  1333. }
  1334. /**
  1335. * Split an <h> node
  1336. */
  1337. function splitHeading() {
  1338. if ( $this->name !== 'h' ) {
  1339. throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
  1340. }
  1341. $bits = array();
  1342. for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
  1343. if ( !isset( $child->name ) ) {
  1344. continue;
  1345. }
  1346. if ( $child->name == 'i' ) {
  1347. $bits['i'] = $child->value;
  1348. } elseif ( $child->name == 'level' ) {
  1349. $bits['level'] = $child->value;
  1350. }
  1351. }
  1352. if ( !isset( $bits['i'] ) ) {
  1353. throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
  1354. }
  1355. return $bits;
  1356. }
  1357. /**
  1358. * Split a <template> or <tplarg> node
  1359. */
  1360. function splitTemplate() {
  1361. $parts = array();
  1362. $bits = array( 'lineStart' => '' );
  1363. for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
  1364. if ( !isset( $child->name ) ) {
  1365. continue;
  1366. }
  1367. if ( $child->name == 'title' ) {
  1368. $bits['title'] = $child;
  1369. }
  1370. if ( $child->name == 'part' ) {
  1371. $parts[] = $child;
  1372. }
  1373. if ( $child->name == 'lineStart' ) {
  1374. $bits['lineStart'] = '1';
  1375. }
  1376. }
  1377. if ( !isset( $bits['title'] ) ) {
  1378. throw new MWException( 'Invalid node passed to ' . __METHOD__ );
  1379. }
  1380. $bits['parts'] = new PPNode_Hash_Array( $parts );
  1381. return $bits;
  1382. }
  1383. }
  1384. /**
  1385. * @ingroup Parser
  1386. */
  1387. class PPNode_Hash_Text implements PPNode {
  1388. var $value, $nextSibling;
  1389. function __construct( $value ) {
  1390. if ( is_object( $value ) ) {
  1391. throw new MWException( __CLASS__ . ' given object instead of string' );
  1392. }
  1393. $this->value = $value;
  1394. }
  1395. function __toString() {
  1396. return htmlspecialchars( $this->value );
  1397. }
  1398. function getNextSibling() {
  1399. return $this->nextSibling;
  1400. }
  1401. function getChildren() { return false; }
  1402. function getFirstChild() { return false; }
  1403. function getChildrenOfType( $name ) { return false; }
  1404. function getLength() { return false; }
  1405. function item( $i ) { return false; }
  1406. function getName() { return '#text'; }
  1407. function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
  1408. function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
  1409. function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
  1410. }
  1411. /**
  1412. * @ingroup Parser
  1413. */
  1414. class PPNode_Hash_Array implements PPNode {
  1415. var $value, $nextSibling;
  1416. function __construct( $value ) {
  1417. $this->value = $value;
  1418. }
  1419. function __toString() {
  1420. return var_export( $this, true );
  1421. }
  1422. function getLength() {
  1423. return count( $this->value );
  1424. }
  1425. function item( $i ) {
  1426. return $this->value[$i];
  1427. }
  1428. function getName() { return '#nodelist'; }
  1429. function getNextSibling() {
  1430. return $this->nextSibling;
  1431. }
  1432. function getChildren() { return false; }
  1433. function getFirstChild() { return false; }
  1434. function getChildrenOfType( $name ) { return false; }
  1435. function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
  1436. function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
  1437. function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
  1438. }
  1439. /**
  1440. * @ingroup Parser
  1441. */
  1442. class PPNode_Hash_Attr implements PPNode {
  1443. var $name, $value, $nextSibling;
  1444. function __construct( $name, $value ) {
  1445. $this->name = $name;
  1446. $this->value = $value;
  1447. }
  1448. function __toString() {
  1449. return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
  1450. }
  1451. function getName() {
  1452. return $this->name;
  1453. }
  1454. function getNextSibling() {
  1455. return $this->nextSibling;
  1456. }
  1457. function getChildren() { return false; }
  1458. function getFirstChild() { return false; }
  1459. function getChildrenOfType( $name ) { return false; }
  1460. function getLength() { return false; }
  1461. function item( $i ) { return false; }
  1462. function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
  1463. function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
  1464. function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
  1465. }