00001 <?php
00046 class Parser
00047 {
00053 const VERSION = '1.6.4';
00054
00055 # Flags for Parser::setFunctionHook
00056 # Also available as global constants from Defines.php
00057 const SFH_NO_HASH = 1;
00058 const SFH_OBJECT_ARGS = 2;
00059
00060 # Constants needed for external link processing
00061 # Everything except bracket, space, or control characters
00062 const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
00063 const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
00064 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
00065
00066
00067 const COLON_STATE_TEXT = 0;
00068 const COLON_STATE_TAG = 1;
00069 const COLON_STATE_TAGSTART = 2;
00070 const COLON_STATE_CLOSETAG = 3;
00071 const COLON_STATE_TAGSLASH = 4;
00072 const COLON_STATE_COMMENT = 5;
00073 const COLON_STATE_COMMENTDASH = 6;
00074 const COLON_STATE_COMMENTDASHDASH = 7;
00075
00076
00077 const PTD_FOR_INCLUSION = 1;
00078
00079
00080
00081 const OT_HTML = 1;
00082 const OT_WIKI = 2;
00083 const OT_PREPROCESS = 3;
00084 const OT_MSG = 3;
00085
00086
00087 const MARKER_SUFFIX = "-QINU\x7f";
00088
00092 # Persistent:
00093 var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
00094 $mSubstWords, $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex,
00095 $mPreprocessor, $mExtLinkBracketedRegex, $mUrlProtocols, $mDefaultStripList,
00096 $mVarCache, $mConf, $mFunctionTagHooks;
00097
00098
00099 # Cleared with clearState():
00100 var $mOutput, $mAutonumber, $mDTopen, $mStripState;
00101 var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
00102 var $mLinkHolders, $mLinkID;
00103 var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
00104 var $mTplExpandCache;
00105 var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
00106 var $mExpensiveFunctionCount;
00107
00108 # Temporary
00109 # These are variables reset at least once per parse regardless of $clearState
00110 var $mOptions,
00111 $mTitle,
00112 $mOutputType,
00113 $ot,
00114 $mRevisionId,
00115 $mRevisionTimestamp,
00116 $mRevIdForTs;
00117
00125 function __construct( $conf = array() ) {
00126 $this->mConf = $conf;
00127 $this->mTagHooks = array();
00128 $this->mTransparentTagHooks = array();
00129 $this->mFunctionHooks = array();
00130 $this->mFunctionTagHooks = array();
00131 $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
00132 $this->mDefaultStripList = $this->mStripList = array();
00133 $this->mUrlProtocols = wfUrlProtocols();
00134 $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
00135 '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
00136 $this->mVarCache = array();
00137 if ( isset( $conf['preprocessorClass'] ) ) {
00138 $this->mPreprocessorClass = $conf['preprocessorClass'];
00139 } elseif ( extension_loaded( 'domxml' ) ) {
00140
00141 wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
00142 $this->mPreprocessorClass = 'Preprocessor_Hash';
00143 } elseif ( extension_loaded( 'dom' ) ) {
00144 $this->mPreprocessorClass = 'Preprocessor_DOM';
00145 } else {
00146 $this->mPreprocessorClass = 'Preprocessor_Hash';
00147 }
00148 $this->mMarkerIndex = 0;
00149 $this->mFirstCall = true;
00150 }
00151
00155 function __destruct() {
00156 if ( isset( $this->mLinkHolders ) ) {
00157 $this->mLinkHolders->__destruct();
00158 }
00159 foreach ( $this as $name => $value ) {
00160 unset( $this->$name );
00161 }
00162 }
00163
00167 function firstCallInit() {
00168 if ( !$this->mFirstCall ) {
00169 return;
00170 }
00171 $this->mFirstCall = false;
00172
00173 wfProfileIn( __METHOD__ );
00174
00175 CoreParserFunctions::register( $this );
00176 CoreTagHooks::register( $this );
00177 $this->initialiseVariables();
00178
00179 wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
00180 wfProfileOut( __METHOD__ );
00181 }
00182
00188 function clearState() {
00189 wfProfileIn( __METHOD__ );
00190 if ( $this->mFirstCall ) {
00191 $this->firstCallInit();
00192 }
00193 $this->mOutput = new ParserOutput;
00194 $this->mAutonumber = 0;
00195 $this->mLastSection = '';
00196 $this->mDTopen = false;
00197 $this->mIncludeCount = array();
00198 $this->mStripState = new StripState;
00199 $this->mArgStack = false;
00200 $this->mInPre = false;
00201 $this->mLinkHolders = new LinkHolderArray( $this );
00202 $this->mLinkID = 0;
00203 $this->mRevisionTimestamp = $this->mRevisionId = null;
00204 $this->mVarCache = array();
00205
00216 #$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
00217 # Changed to \x7f to allow XML double-parsing -- TS
00218 $this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString();
00219
00220
00221 # Clear these on every parse, bug 4549
00222 $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array();
00223
00224 $this->mShowToc = true;
00225 $this->mForceTocPosition = false;
00226 $this->mIncludeSizes = array(
00227 'post-expand' => 0,
00228 'arg' => 0,
00229 );
00230 $this->mPPNodeCount = 0;
00231 $this->mDefaultSort = false;
00232 $this->mHeadings = array();
00233 $this->mDoubleUnderscores = array();
00234 $this->mExpensiveFunctionCount = 0;
00235
00236 # Fix cloning
00237 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
00238 $this->mPreprocessor = null;
00239 }
00240
00241 wfRunHooks( 'ParserClearState', array( &$this ) );
00242 wfProfileOut( __METHOD__ );
00243 }
00244
00245 function setOutputType( $ot ) {
00246 $this->mOutputType = $ot;
00247
00248 $this->ot = array(
00249 'html' => $ot == self::OT_HTML,
00250 'wiki' => $ot == self::OT_WIKI,
00251 'pre' => $ot == self::OT_PREPROCESS,
00252 );
00253 }
00254
00258 function setTitle( $t ) {
00259 if ( !$t || $t instanceof FakeTitle ) {
00260 $t = Title::newFromText( 'NO TITLE' );
00261 }
00262
00263 if ( strval( $t->getFragment() ) !== '' ) {
00264 # Strip the fragment to avoid various odd effects
00265 $this->mTitle = clone $t;
00266 $this->mTitle->setFragment( '' );
00267 } else {
00268 $this->mTitle = $t;
00269 }
00270 }
00271
00277 function uniqPrefix() {
00278 if( !isset( $this->mUniqPrefix ) ) {
00279
00280
00281
00282
00283
00284 return '';
00285
00286 }
00287 return $this->mUniqPrefix;
00288 }
00289
00302 public function parse( $text, Title $title, ParserOptions $options, $linestart = true, $clearState = true, $revid = null ) {
00308 global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang, $wgDisableLangConversion, $wgDisableTitleConversion;
00309 $fname = __METHOD__.'-' . wfGetCaller();
00310 wfProfileIn( __METHOD__ );
00311 wfProfileIn( $fname );
00312
00313 if ( $clearState ) {
00314 $this->clearState();
00315 }
00316
00317 $this->mOptions = $options;
00318 $this->setTitle( $title );
00319
00320 $oldRevisionId = $this->mRevisionId;
00321 $oldRevisionTimestamp = $this->mRevisionTimestamp;
00322 if( $revid !== null ) {
00323 $this->mRevisionId = $revid;
00324 $this->mRevisionTimestamp = null;
00325 }
00326 $this->setOutputType( self::OT_HTML );
00327 wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
00328 # No more strip!
00329 wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
00330 $text = $this->internalParse( $text );
00331
00332 $text = $this->mStripState->unstripGeneral( $text );
00333
00334 # Clean up special characters, only run once, next-to-last before doBlockLevels
00335 $fixtags = array(
00336 # french spaces, last one Guillemet-left
00337 # only if there is something before the space
00338 '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1 \\2',
00339 # french spaces, Guillemet-right
00340 '/(\\302\\253) /' => '\\1 ',
00341 '/ (!\s*important)/' => ' \\1', #Beware of CSS magic word !important, bug #11874.
00342 );
00343 $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
00344
00345 $text = $this->doBlockLevels( $text, $linestart );
00346
00347 $this->replaceLinkHolders( $text );
00348
00355 if ( !( $wgDisableLangConversion
00356 || isset( $this->mDoubleUnderscores['nocontentconvert'] )
00357 || $this->mTitle->isConversionTable() ) ) {
00358
00359 # The position of the convert() call should not be changed. it
00360 # assumes that the links are all replaced and the only thing left
00361 # is the <nowiki> mark.
00362
00363 $text = $wgContLang->convert( $text );
00364 }
00365
00380 if ( !( $wgDisableLangConversion
00381 || $wgDisableTitleConversion
00382 || isset( $this->mDoubleUnderscores['nocontentconvert'] )
00383 || isset( $this->mDoubleUnderscores['notitleconvert'] )
00384 || $this->mOutput->getDisplayTitle() !== false ) )
00385 {
00386 $convruletitle = $wgContLang->getConvRuleTitle();
00387 if ( $convruletitle ) {
00388 $this->mOutput->setTitleText( $convruletitle );
00389 } else {
00390 $titleText = $wgContLang->convertTitle( $title );
00391 $this->mOutput->setTitleText( $titleText );
00392 }
00393 }
00394
00395 $text = $this->mStripState->unstripNoWiki( $text );
00396
00397 wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
00398
00400
00401 $uniq_prefix = $this->mUniqPrefix;
00402 $matches = array();
00403 $elements = array_keys( $this->mTransparentTagHooks );
00404 $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
00405
00406 foreach( $matches as $marker => $data ) {
00407 list( $element, $content, $params, $tag ) = $data;
00408 $tagName = strtolower( $element );
00409 if( isset( $this->mTransparentTagHooks[$tagName] ) ) {
00410 $output = call_user_func_array( $this->mTransparentTagHooks[$tagName],
00411 array( $content, $params, $this ) );
00412 } else {
00413 $output = $tag;
00414 }
00415 $this->mStripState->general->setPair( $marker, $output );
00416 }
00417 $text = $this->mStripState->unstripGeneral( $text );
00418
00419 $text = Sanitizer::normalizeCharReferences( $text );
00420
00421 if ( ( $wgUseTidy && $this->mOptions->mTidy ) || $wgAlwaysUseTidy ) {
00422 $text = MWTidy::tidy( $text );
00423 } else {
00424 # attempt to sanitize at least some nesting problems
00425 # (bug #2702 and quite a few others)
00426 $tidyregs = array(
00427 # ''Something [http:
00428 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
00429 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
00430 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
00431 # fix up an anchor inside another anchor, only
00432 # at least for a single single nested link (bug 3695)
00433 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
00434 '\\1\\2</a>\\3</a>\\1\\4</a>',
00435 # fix div inside inline elements- doBlockLevels won't wrap a line which
00436 # contains a div, so fix it up here; replace
00437 # div with escaped text
00438 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
00439 '\\1\\3<div\\5>\\6</div>\\8\\9',
00440 # remove empty italic or bold tag pairs, some
00441 # introduced by rules above
00442 '/<([bi])><\/\\1>/' => '',
00443 );
00444
00445 $text = preg_replace(
00446 array_keys( $tidyregs ),
00447 array_values( $tidyregs ),
00448 $text );
00449 }
00450 global $wgExpensiveParserFunctionLimit;
00451 if ( $this->mExpensiveFunctionCount > $wgExpensiveParserFunctionLimit ) {
00452 $this->limitationWarn( 'expensive-parserfunction', $this->mExpensiveFunctionCount, $wgExpensiveParserFunctionLimit );
00453 }
00454
00455 wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
00456
00457 # Information on include size limits, for the benefit of users who try to skirt them
00458 if ( $this->mOptions->getEnableLimitReport() ) {
00459 $max = $this->mOptions->getMaxIncludeSize();
00460 $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/$wgExpensiveParserFunctionLimit\n";
00461 $limitReport =
00462 "NewPP limit report\n" .
00463 "Preprocessor node count: {$this->mPPNodeCount}/{$this->mOptions->mMaxPPNodeCount}\n" .
00464 "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
00465 "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n".
00466 $PFreport;
00467 wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
00468 $text .= "\n<!-- \n$limitReport-->\n";
00469 }
00470 $this->mOutput->setText( $text );
00471
00472 $this->mRevisionId = $oldRevisionId;
00473 $this->mRevisionTimestamp = $oldRevisionTimestamp;
00474 wfProfileOut( $fname );
00475 wfProfileOut( __METHOD__ );
00476
00477 return $this->mOutput;
00478 }
00479
00489 function recursiveTagParse( $text, $frame=false ) {
00490 wfProfileIn( __METHOD__ );
00491 wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
00492 wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
00493 $text = $this->internalParse( $text, false, $frame );
00494 wfProfileOut( __METHOD__ );
00495 return $text;
00496 }
00497
00502 function preprocess( $text, $title, $options, $revid = null ) {
00503 wfProfileIn( __METHOD__ );
00504 $this->clearState();
00505 $this->setOutputType( self::OT_PREPROCESS );
00506 $this->mOptions = $options;
00507 $this->setTitle( $title );
00508 if( $revid !== null ) {
00509 $this->mRevisionId = $revid;
00510 }
00511 wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
00512 wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
00513 $text = $this->replaceVariables( $text );
00514 $text = $this->mStripState->unstripBoth( $text );
00515 wfProfileOut( __METHOD__ );
00516 return $text;
00517 }
00518
00525 function getRandomString() {
00526 return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
00527 }
00528
00529 function &getTitle() { return $this->mTitle; }
00530 function getOptions() { return $this->mOptions; }
00531 function getRevisionId() { return $this->mRevisionId; }
00532 function getOutput() { return $this->mOutput; }
00533 function nextLinkID() { return $this->mLinkID++; }
00534
00535 function getFunctionLang() {
00536 global $wgLang, $wgContLang;
00537
00538 $target = $this->mOptions->getTargetLanguage();
00539 if ( $target !== null ) {
00540 return $target;
00541 } else {
00542 return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
00543 }
00544 }
00545
00549 function getPreprocessor() {
00550 if ( !isset( $this->mPreprocessor ) ) {
00551 $class = $this->mPreprocessorClass;
00552 $this->mPreprocessor = new $class( $this );
00553 }
00554 return $this->mPreprocessor;
00555 }
00556
00575 function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
00576 static $n = 1;
00577 $stripped = '';
00578 $matches = array();
00579
00580 $taglist = implode( '|', $elements );
00581 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
00582
00583 while ( $text != '' ) {
00584 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
00585 $stripped .= $p[0];
00586 if( count( $p ) < 5 ) {
00587 break;
00588 }
00589 if( count( $p ) > 5 ) {
00590 // comment
00591 $element = $p[4];
00592 $attributes = '';
00593 $close = '';
00594 $inside = $p[5];
00595 } else {
00596 // tag
00597 $element = $p[1];
00598 $attributes = $p[2];
00599 $close = $p[3];
00600 $inside = $p[4];
00601 }
00602
00603 $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . self::MARKER_SUFFIX;
00604 $stripped .= $marker;
00605
00606 if ( $close === '/>' ) {
00607 // Empty element tag, <tag />
00608 $content = null;
00609 $text = $inside;
00610 $tail = null;
00611 } else {
00612 if( $element === '!--' ) {
00613 $end = '/(-->)/';
00614 } else {
00615 $end = "/(<\\/$element\\s*>)/i";
00616 }
00617 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
00618 $content = $q[0];
00619 if( count( $q ) < 3 ) {
00620 # No end tag -- let it run out to the end of the text.
00621 $tail = '';
00622 $text = '';
00623 } else {
00624 $tail = $q[1];
00625 $text = $q[2];
00626 }
00627 }
00628
00629 $matches[$marker] = array( $element,
00630 $content,
00631 Sanitizer::decodeTagAttributes( $attributes ),
00632 "<$element$attributes$close$content$tail" );
00633 }
00634 return $stripped;
00635 }
00636
00640 function getStripList() {
00641 return $this->mStripList;
00642 }
00643
00647 function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
00648 return $text;
00649 }
00650
00658 function unstrip( $text, $state ) {
00659 return $state->unstripGeneral( $text );
00660 }
00661
00668 function unstripNoWiki( $text, $state ) {
00669 return $state->unstripNoWiki( $text );
00670 }
00671
00675 function unstripForHTML( $text ) {
00676 return $this->mStripState->unstripBoth( $text );
00677 }
00678
00686 function insertStripItem( $text ) {
00687 $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
00688 $this->mMarkerIndex++;
00689 $this->mStripState->general->setPair( $rnd, $text );
00690 return $rnd;
00691 }
00692
00697 public static function tidy( $text ) {
00698 wfDeprecated( __METHOD__ );
00699 return MWTidy::tidy( $text );
00700 }
00701
00707 function doTableStuff ( $text ) {
00708 wfProfileIn( __METHOD__ );
00709
00710 $lines = StringUtils::explode( "\n", $text );
00711 $out = '';
00712 $td_history = array (); // Is currently a td tag open?
00713 $last_tag_history = array (); // Save history of last lag activated (td, th or caption)
00714 $tr_history = array (); // Is currently a tr tag open?
00715 $tr_attributes = array (); // history of tr attributes
00716 $has_opened_tr = array(); // Did this table open a <tr> element?
00717 $indent_level = 0; // indent level of the table
00718
00719 foreach ( $lines as $outLine ) {
00720 $line = trim( $outLine );
00721
00722 if( $line == '' ) { // empty line, go to next line
00723 $out .= $outLine."\n";
00724 continue;
00725 }
00726 $first_character = $line[0];
00727 $matches = array();
00728
00729 if ( preg_match( '/^(:*)\{\|(.*)$/', $line , $matches ) ) {
00730 // First check if we are starting a new table
00731 $indent_level = strlen( $matches[1] );
00732
00733 $attributes = $this->mStripState->unstripBoth( $matches[2] );
00734 $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' );
00735
00736 $outLine = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
00737 array_push( $td_history , false );
00738 array_push( $last_tag_history , '' );
00739 array_push( $tr_history , false );
00740 array_push( $tr_attributes , '' );
00741 array_push( $has_opened_tr , false );
00742 } else if ( count ( $td_history ) == 0 ) {
00743 // Don't do any of the following
00744 $out .= $outLine."\n";
00745 continue;
00746 } else if ( substr ( $line , 0 , 2 ) === '|}' ) {
00747
00748 $line = '</table>' . substr ( $line , 2 );
00749 $last_tag = array_pop ( $last_tag_history );
00750
00751 if ( !array_pop ( $has_opened_tr ) ) {
00752 $line = "<tr><td></td></tr>{$line}";
00753 }
00754
00755 if ( array_pop ( $tr_history ) ) {
00756 $line = "</tr>{$line}";
00757 }
00758
00759 if ( array_pop ( $td_history ) ) {
00760 $line = "</{$last_tag}>{$line}";
00761 }
00762 array_pop ( $tr_attributes );
00763 $outLine = $line . str_repeat( '</dd></dl>' , $indent_level );
00764 } else if ( substr ( $line , 0 , 2 ) === '|-' ) {
00765
00766 $line = preg_replace( '#^\|-+#', '', $line );
00767
00768
00769 $attributes = $this->mStripState->unstripBoth( $line );
00770 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
00771 array_pop( $tr_attributes );
00772 array_push( $tr_attributes, $attributes );
00773
00774 $line = '';
00775 $last_tag = array_pop ( $last_tag_history );
00776 array_pop ( $has_opened_tr );
00777 array_push ( $has_opened_tr , true );
00778
00779 if ( array_pop ( $tr_history ) ) {
00780 $line = '</tr>';
00781 }
00782
00783 if ( array_pop ( $td_history ) ) {
00784 $line = "</{$last_tag}>{$line}";
00785 }
00786
00787 $outLine = $line;
00788 array_push ( $tr_history , false );
00789 array_push ( $td_history , false );
00790 array_push ( $last_tag_history , '' );
00791 }
00792 else if ( $first_character === '|' || $first_character === '!' || substr ( $line , 0 , 2 ) === '|+' ) {
00793
00794 if ( substr ( $line , 0 , 2 ) === '|+' ) {
00795 $first_character = '+';
00796 $line = substr ( $line , 1 );
00797 }
00798
00799 $line = substr ( $line , 1 );
00800
00801 if ( $first_character === '!' ) {
00802 $line = str_replace ( '!!' , '||' , $line );
00803 }
00804
00805
00806
00807
00808
00809 $cells = StringUtils::explodeMarkup( '||' , $line );
00810
00811 $outLine = '';
00812
00813
00814 foreach ( $cells as $cell )
00815 {
00816 $previous = '';
00817 if ( $first_character !== '+' )
00818 {
00819 $tr_after = array_pop ( $tr_attributes );
00820 if ( !array_pop ( $tr_history ) ) {
00821 $previous = "<tr{$tr_after}>\n";
00822 }
00823 array_push ( $tr_history , true );
00824 array_push ( $tr_attributes , '' );
00825 array_pop ( $has_opened_tr );
00826 array_push ( $has_opened_tr , true );
00827 }
00828
00829 $last_tag = array_pop ( $last_tag_history );
00830
00831 if ( array_pop ( $td_history ) ) {
00832 $previous = "</{$last_tag}>{$previous}";
00833 }
00834
00835 if ( $first_character === '|' ) {
00836 $last_tag = 'td';
00837 } else if ( $first_character === '!' ) {
00838 $last_tag = 'th';
00839 } else if ( $first_character === '+' ) {
00840 $last_tag = 'caption';
00841 } else {
00842 $last_tag = '';
00843 }
00844
00845 array_push ( $last_tag_history , $last_tag );
00846
00847
00848 $cell_data = explode ( '|' , $cell , 2 );
00849
00850
00851
00852 if ( strpos( $cell_data[0], '[[' ) !== false ) {
00853 $cell = "{$previous}<{$last_tag}>{$cell}";
00854 } else if ( count ( $cell_data ) == 1 )
00855 $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
00856 else {
00857 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
00858 $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag );
00859 $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
00860 }
00861
00862 $outLine .= $cell;
00863 array_push ( $td_history , true );
00864 }
00865 }
00866 $out .= $outLine . "\n";
00867 }
00868
00869
00870 while ( count ( $td_history ) > 0 )
00871 {
00872 if ( array_pop ( $td_history ) ) {
00873 $out .= "</td>\n";
00874 }
00875 if ( array_pop ( $tr_history ) ) {
00876 $out .= "</tr>\n";
00877 }
00878 if ( !array_pop ( $has_opened_tr ) ) {
00879 $out .= "<tr><td></td></tr>\n" ;
00880 }
00881
00882 $out .= "</table>\n";
00883 }
00884
00885
00886 if ( substr( $out, -1 ) === "\n" ) {
00887 $out = substr( $out, 0, -1 );
00888 }
00889
00890
00891 if( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
00892 $out = '';
00893 }
00894
00895 wfProfileOut( __METHOD__ );
00896
00897 return $out;
00898 }
00899
00906 function internalParse( $text, $isMain = true, $frame=false ) {
00907 wfProfileIn( __METHOD__ );
00908
00909 $origText = $text;
00910
00911 # Hook to suspend the parser in this state
00912 if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
00913 wfProfileOut( __METHOD__ );
00914 return $text ;
00915 }
00916
00917
00918 if ($frame) {
00919
00920
00921 if( !$frame->depth )
00922 $flag = 0;
00923 else
00924 $flag = Parser::PTD_FOR_INCLUSION;
00925 $dom = $this->preprocessToDom( $text, $flag );
00926 $text = $frame->expand( $dom );
00927 }
00928
00929 else {
00930 $text = $this->replaceVariables( $text );
00931 }
00932
00933 $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) );
00934 wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
00935
00936
00937
00938
00939
00940 $text = $this->doTableStuff( $text );
00941
00942 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
00943
00944 $text = $this->doDoubleUnderscore( $text );
00945
00946 $text = $this->doHeadings( $text );
00947 if( $this->mOptions->getUseDynamicDates() ) {
00948 $df = DateFormatter::getInstance();
00949 $text = $df->reformat( $this->mOptions->getDateFormat(), $text );
00950 }
00951 $text = $this->doAllQuotes( $text );
00952 $text = $this->replaceInternalLinks( $text );
00953 $text = $this->replaceExternalLinks( $text );
00954
00955 # replaceInternalLinks may sometimes leave behind
00956 # absolute URLs, which have to be masked to hide them from replaceExternalLinks
00957 $text = str_replace($this->mUniqPrefix.'NOPARSE', '', $text);
00958
00959 $text = $this->doMagicLinks( $text );
00960 $text = $this->formatHeadings( $text, $origText, $isMain );
00961
00962 wfProfileOut( __METHOD__ );
00963 return $text;
00964 }
00965
00973 function doMagicLinks( $text ) {
00974 wfProfileIn( __METHOD__ );
00975 $prots = $this->mUrlProtocols;
00976 $urlChar = self::EXT_LINK_URL_CLASS;
00977 $text = preg_replace_callback(
00978 '!(?: # Start cases
00979 (<a.*?</a>) | # m[1]: Skip link text
00980 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
00981 (\\b(?:$prots)$urlChar+) | # m[3]: Free external links" . '
00982 (?:RFC|PMID)\s+([0-9]+) | # m[4]: RFC or PMID, capture number
00983 ISBN\s+(\b # m[5]: ISBN, capture number
00984 (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix
00985 (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters
00986 [0-9Xx] # check digit
00987 \b)
00988 )!x', array( &$this, 'magicLinkCallback' ), $text );
00989 wfProfileOut( __METHOD__ );
00990 return $text;
00991 }
00992
00993 function magicLinkCallback( $m ) {
00994 if ( isset( $m[1] ) && $m[1] !== '' ) {
00995 # Skip anchor
00996 return $m[0];
00997 } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
00998 # Skip HTML element
00999 return $m[0];
01000 } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
01001 # Free external link
01002 return $this->makeFreeExternalLink( $m[0] );
01003 } elseif ( isset( $m[4] ) && $m[4] !== '' ) {
01004 # RFC or PMID
01005 $CssClass = '';
01006 if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
01007 $keyword = 'RFC';
01008 $urlmsg = 'rfcurl';
01009 $CssClass = 'mw-magiclink-rfc';
01010 $id = $m[4];
01011 } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
01012 $keyword = 'PMID';
01013 $urlmsg = 'pubmedurl';
01014 $CssClass = 'mw-magiclink-pmid';
01015 $id = $m[4];
01016 } else {
01017 throw new MWException( __METHOD__.': unrecognised match type "' .
01018 substr($m[0], 0, 20 ) . '"' );
01019 }
01020 $url = wfMsg( $urlmsg, $id);
01021 $sk = $this->mOptions->getSkin();
01022 $la = $sk->getExternalLinkAttributes( "external $CssClass" );
01023 return "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
01024 } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
01025 # ISBN
01026 $isbn = $m[5];
01027 $num = strtr( $isbn, array(
01028 '-' => '',
01029 ' ' => '',
01030 'x' => 'X',
01031 ));
01032 $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
01033 return'<a href="' .
01034 $titleObj->escapeLocalUrl() .
01035 "\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
01036 } else {
01037 return $m[0];
01038 }
01039 }
01040
01046 function makeFreeExternalLink( $url ) {
01047 global $wgContLang;
01048 wfProfileIn( __METHOD__ );
01049
01050 $sk = $this->mOptions->getSkin();
01051 $trail = '';
01052
01053 # The characters '<' and '>' (which were escaped by
01054 # removeHTMLtags()) should not be included in
01055 # URLs, per RFC 2396.
01056 $m2 = array();
01057 if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
01058 $trail = substr($url, $m2[0][1]) . $trail;
01059 $url = substr($url, 0, $m2[0][1]);
01060 }
01061
01062 # Move trailing punctuation to $trail
01063 $sep = ',;\.:!?';
01064 # If there is no left bracket, then consider right brackets fair game too
01065 if ( strpos( $url, '(' ) === false ) {
01066 $sep .= ')';
01067 }
01068
01069 $numSepChars = strspn( strrev( $url ), $sep );
01070 if ( $numSepChars ) {
01071 $trail = substr( $url, -$numSepChars ) . $trail;
01072 $url = substr( $url, 0, -$numSepChars );
01073 }
01074
01075 $url = Sanitizer::cleanUrl( $url );
01076
01077 # Is this an external image?
01078 $text = $this->maybeMakeExternalImage( $url );
01079 if ( $text === false ) {
01080 # Not an image, make a link
01081 $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free',
01082 $this->getExternalLinkAttribs( $url ) );
01083 # Register it in the output object...
01084 # Replace unnecessary URL escape codes with their equivalent characters
01085 $pasteurized = self::replaceUnusualEscapes( $url );
01086 $this->mOutput->addExternalLink( $pasteurized );
01087 }
01088 wfProfileOut( __METHOD__ );
01089 return $text . $trail;
01090 }
01091
01092
01098 function doHeadings( $text ) {
01099 wfProfileIn( __METHOD__ );
01100 for ( $i = 6; $i >= 1; --$i ) {
01101 $h = str_repeat( '=', $i );
01102 $text = preg_replace( "/^$h(.+)$h\\s*$/m",
01103 "<h$i>\\1</h$i>", $text );
01104 }
01105 wfProfileOut( __METHOD__ );
01106 return $text;
01107 }
01108
01114 function doAllQuotes( $text ) {
01115 wfProfileIn( __METHOD__ );
01116 $outtext = '';
01117 $lines = StringUtils::explode( "\n", $text );
01118 foreach ( $lines as $line ) {
01119 $outtext .= $this->doQuotes( $line ) . "\n";
01120 }
01121 $outtext = substr($outtext, 0,-1);
01122 wfProfileOut( __METHOD__ );
01123 return $outtext;
01124 }
01125
01129 public function doQuotes( $text ) {
01130 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
01131 if ( count( $arr ) == 1 )
01132 return $text;
01133 else
01134 {
01135 # First, do some preliminary work. This may shift some apostrophes from
01136 # being mark-up to being text. It also counts the number of occurrences
01137 # of bold and italics mark-ups.
01138 $i = 0;
01139 $numbold = 0;
01140 $numitalics = 0;
01141 foreach ( $arr as $r )
01142 {
01143 if ( ( $i % 2 ) == 1 )
01144 {
01145 # If there are ever four apostrophes, assume the first is supposed to
01146 # be text, and the remaining three constitute mark-up for bold text.
01147 if ( strlen( $arr[$i] ) == 4 )
01148 {
01149 $arr[$i-1] .= "'";
01150 $arr[$i] = "'''";
01151 }
01152 # If there are more than 5 apostrophes in a row, assume they're all
01153 # text except for the last 5.
01154 else if ( strlen( $arr[$i] ) > 5 )
01155 {
01156 $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 );
01157 $arr[$i] = "'''''";
01158 }
01159 # Count the number of occurrences of bold and italics mark-ups.
01160 # We are not counting sequences of five apostrophes.
01161 if ( strlen( $arr[$i] ) == 2 ) { $numitalics++; }
01162 else if ( strlen( $arr[$i] ) == 3 ) { $numbold++; }
01163 else if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; }
01164 }
01165 $i++;
01166 }
01167
01168 # If there is an odd number of both bold and italics, it is likely
01169 # that one of the bold ones was meant to be an apostrophe followed
01170 # by italics. Which one we cannot know for certain, but it is more
01171 # likely to be one that has a single-letter word before it.
01172 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) )
01173 {
01174 $i = 0;
01175 $firstsingleletterword = -1;
01176 $firstmultiletterword = -1;
01177 $firstspace = -1;
01178 foreach ( $arr as $r )
01179 {
01180 if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) )
01181 {
01182 $x1 = substr ($arr[$i-1], -1);
01183 $x2 = substr ($arr[$i-1], -2, 1);
01184 if ($x1 === ' ') {
01185 if ($firstspace == -1) $firstspace = $i;
01186 } else if ($x2 === ' ') {
01187 if ($firstsingleletterword == -1) $firstsingleletterword = $i;
01188 } else {
01189 if ($firstmultiletterword == -1) $firstmultiletterword = $i;
01190 }
01191 }
01192 $i++;
01193 }
01194
01195 # If there is a single-letter word, use it!
01196 if ($firstsingleletterword > -1)
01197 {
01198 $arr [ $firstsingleletterword ] = "''";
01199 $arr [ $firstsingleletterword-1 ] .= "'";
01200 }
01201 # If not, but there's a multi-letter word, use that one.
01202 else if ($firstmultiletterword > -1)
01203 {
01204 $arr [ $firstmultiletterword ] = "''";
01205 $arr [ $firstmultiletterword-1 ] .= "'";
01206 }
01207 # ... otherwise use the first one that has neither.
01208 # (notice that it is possible for all three to be -1 if, for example,
01209 # there is only one pentuple-apostrophe in the line)
01210 else if ($firstspace > -1)
01211 {
01212 $arr [ $firstspace ] = "''";
01213 $arr [ $firstspace-1 ] .= "'";
01214 }
01215 }
01216
01217 # Now let's actually convert our apostrophic mush to HTML!
01218 $output = '';
01219 $buffer = '';
01220 $state = '';
01221 $i = 0;
01222 foreach ($arr as $r)
01223 {
01224 if (($i % 2) == 0)
01225 {
01226 if ($state === 'both')
01227 $buffer .= $r;
01228 else
01229 $output .= $r;
01230 }
01231 else
01232 {
01233 if (strlen ($r) == 2)
01234 {
01235 if ($state === 'i')
01236 { $output .= '</i>'; $state = ''; }
01237 else if ($state === 'bi')
01238 { $output .= '</i>'; $state = 'b'; }
01239 else if ($state === 'ib')
01240 { $output .= '</b></i><b>'; $state = 'b'; }
01241 else if ($state === 'both')
01242 { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; }
01243 else # $state can be 'b' or ''
01244 { $output .= '<i>'; $state .= 'i'; }
01245 }
01246 else if (strlen ($r) == 3)
01247 {
01248 if ($state === 'b')
01249 { $output .= '</b>'; $state = ''; }
01250 else if ($state === 'bi')
01251 { $output .= '</i></b><i>'; $state = 'i'; }
01252 else if ($state === 'ib')
01253 { $output .= '</b>'; $state = 'i'; }
01254 else if ($state === 'both')
01255 { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; }
01256 else # $state can be 'i' or ''
01257 { $output .= '<b>'; $state .= 'b'; }
01258 }
01259 else if (strlen ($r) == 5)
01260 {
01261 if ($state === 'b')
01262 { $output .= '</b><i>'; $state = 'i'; }
01263 else if ($state === 'i')
01264 { $output .= '</i><b>'; $state = 'b'; }
01265 else if ($state === 'bi')
01266 { $output .= '</i></b>'; $state = ''; }
01267 else if ($state === 'ib')
01268 { $output .= '</b></i>'; $state = ''; }
01269 else if ($state === 'both')
01270 { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; }
01271 else # ($state == '')
01272 { $buffer = ''; $state = 'both'; }
01273 }
01274 }
01275 $i++;
01276 }
01277 # Now close all remaining tags. Notice that the order is important.
01278 if ($state === 'b' || $state === 'ib')
01279 $output .= '</b>';
01280 if ($state === 'i' || $state === 'bi' || $state === 'ib')
01281 $output .= '</i>';
01282 if ($state === 'bi')
01283 $output .= '</b>';
01284 # There might be lonely ''''', so make sure we have a buffer
01285 if ($state === 'both' && $buffer)
01286 $output .= '<b><i>'.$buffer.'</i></b>';
01287 return $output;
01288 }
01289 }
01290
01299 function replaceExternalLinks( $text ) {
01300 global $wgContLang;
01301 wfProfileIn( __METHOD__ );
01302
01303 $sk = $this->mOptions->getSkin();
01304
01305 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
01306 $s = array_shift( $bits );
01307
01308 $i = 0;
01309 while ( $i<count( $bits ) ) {
01310 $url = $bits[$i++];
01311 $protocol = $bits[$i++];
01312 $text = $bits[$i++];
01313 $trail = $bits[$i++];
01314
01315 # The characters '<' and '>' (which were escaped by
01316 # removeHTMLtags()) should not be included in
01317 # URLs, per RFC 2396.
01318 $m2 = array();
01319 if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
01320 $text = substr($url, $m2[0][1]) . ' ' . $text;
01321 $url = substr($url, 0, $m2[0][1]);
01322 }
01323
01324 # If the link text is an image URL, replace it with an <img> tag
01325 # This happened by accident in the original parser, but some people used it extensively
01326 $img = $this->maybeMakeExternalImage( $text );
01327 if ( $img !== false ) {
01328 $text = $img;
01329 }
01330
01331 $dtrail = '';
01332
01333 # Set linktype for CSS - if URL==text, link is essentially free
01334 $linktype = ($text === $url) ? 'free' : 'text';
01335
01336 # No link text, e.g. [http://domain.tld/some.link]
01337 if ( $text == '' ) {
01338 # Autonumber if allowed. See bug #5918
01339 if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) {
01340 $langObj = $this->getFunctionLang();
01341 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
01342 $linktype = 'autonumber';
01343 } else {
01344 # Otherwise just use the URL
01345 $text = htmlspecialchars( $url );
01346 $linktype = 'free';
01347 }
01348 } else {
01349 # Have link text, e.g. [http://domain.tld/some.link text]s
01350 # Check for trail
01351 list( $dtrail, $trail ) = Linker::splitTrail( $trail );
01352 }
01353
01354 $text = $wgContLang->markNoConversion($text);
01355
01356 $url = Sanitizer::cleanUrl( $url );
01357
01358 # Use the encoded URL
01359 # This means that users can paste URLs directly into the text
01360 # Funny characters like ö aren't valid in URLs anyway
01361 # This was changed in August 2004
01362 $s .= $sk->makeExternalLink( $url, $text, false, $linktype,
01363 $this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
01364
01365 # Register link in the output object.
01366 # Replace unnecessary URL escape codes with the referenced character
01367 # This prevents spammers from hiding links from the filters
01368 $pasteurized = self::replaceUnusualEscapes( $url );
01369 $this->mOutput->addExternalLink( $pasteurized );
01370 }
01371
01372 wfProfileOut( __METHOD__ );
01373 return $s;
01374 }
01375
01386 function getExternalLinkAttribs( $url = false ) {
01387 $attribs = array();
01388 global $wgNoFollowLinks, $wgNoFollowNsExceptions;
01389 $ns = $this->mTitle->getNamespace();
01390 if( $wgNoFollowLinks && !in_array($ns, $wgNoFollowNsExceptions) ) {
01391 $attribs['rel'] = 'nofollow';
01392
01393 global $wgNoFollowDomainExceptions;
01394 if ( $wgNoFollowDomainExceptions ) {
01395 $bits = wfParseUrl( $url );
01396 if ( is_array( $bits ) && isset( $bits['host'] ) ) {
01397 foreach ( $wgNoFollowDomainExceptions as $domain ) {
01398 if( substr( $bits['host'], -strlen( $domain ) )
01399 == $domain ) {
01400 unset( $attribs['rel'] );
01401 break;
01402 }
01403 }
01404 }
01405 }
01406 }
01407 if ( $this->mOptions->getExternalLinkTarget() ) {
01408 $attribs['target'] = $this->mOptions->getExternalLinkTarget();
01409 }
01410 return $attribs;
01411 }
01412
01413
01424 static function replaceUnusualEscapes( $url ) {
01425 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
01426 array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url );
01427 }
01428
01435 private static function replaceUnusualEscapesCallback( $matches ) {
01436 $char = urldecode( $matches[0] );
01437 $ord = ord( $char );
01438
01439 if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) {
01440
01441 return $char;
01442 } else {
01443
01444 return $matches[0];
01445 }
01446 }
01447
01453 function maybeMakeExternalImage( $url ) {
01454 $sk = $this->mOptions->getSkin();
01455 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
01456 $imagesexception = !empty($imagesfrom);
01457 $text = false;
01458 # $imagesfrom could be either a single string or an array of strings, parse out the latter
01459 if( $imagesexception && is_array( $imagesfrom ) ) {
01460 $imagematch = false;
01461 foreach( $imagesfrom as $match ) {
01462 if( strpos( $url, $match ) === 0 ) {
01463 $imagematch = true;
01464 break;
01465 }
01466 }
01467 } elseif( $imagesexception ) {
01468 $imagematch = (strpos( $url, $imagesfrom ) === 0);
01469 } else {
01470 $imagematch = false;
01471 }
01472 if ( $this->mOptions->getAllowExternalImages()
01473 || ( $imagesexception && $imagematch ) ) {
01474 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
01475 # Image found
01476 $text = $sk->makeExternalImage( $url );
01477 }
01478 }
01479 if( !$text && $this->mOptions->getEnableImageWhitelist()
01480 && preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
01481 $whitelist = explode( "\n", wfMsgForContent( 'external_image_whitelist' ) );
01482 foreach( $whitelist as $entry ) {
01483 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
01484 if( strpos( $entry, '#' ) === 0 || $entry === '' )
01485 continue;
01486 if( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
01487 # Image matches a whitelist entry
01488 $text = $sk->makeExternalImage( $url );
01489 break;
01490 }
01491 }
01492 }
01493 return $text;
01494 }
01495
01502 function replaceInternalLinks( $s ) {
01503 $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
01504 return $s;
01505 }
01506
01513 function replaceInternalLinks2( &$s ) {
01514 global $wgContLang;
01515
01516 wfProfileIn( __METHOD__ );
01517
01518 wfProfileIn( __METHOD__.'-setup' );
01519 static $tc = FALSE, $e1, $e1_img;
01520 # the % is needed to support urlencoded titles as well
01521 if ( !$tc ) {
01522 $tc = Title::legalChars() . '#%';
01523 # Match a link having the form [[namespace:link|alternate]]trail
01524 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
01525 # Match cases where there is no "]]", which might still be images
01526 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
01527 }
01528
01529 $sk = $this->mOptions->getSkin();
01530 $holders = new LinkHolderArray( $this );
01531
01532 #split the entire text string on occurences of [[
01533 $a = StringUtils::explode( '[[', ' ' . $s );
01534 #get the first element (all text up to first [[), and remove the space we added
01535 $s = $a->current();
01536 $a->next();
01537 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
01538 $s = substr( $s, 1 );
01539
01540 $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
01541 $e2 = null;
01542 if ( $useLinkPrefixExtension ) {
01543 # Match the end of a line for a word that's not followed by whitespace,
01544 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
01545 $e2 = wfMsgForContent( 'linkprefix' );
01546 }
01547
01548 if( is_null( $this->mTitle ) ) {
01549 wfProfileOut( __METHOD__.'-setup' );
01550 wfProfileOut( __METHOD__ );
01551 throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
01552 }
01553 $nottalk = !$this->mTitle->isTalkPage();
01554
01555 if ( $useLinkPrefixExtension ) {
01556 $m = array();
01557 if ( preg_match( $e2, $s, $m ) ) {
01558 $first_prefix = $m[2];
01559 } else {
01560 $first_prefix = false;
01561 }
01562 } else {
01563 $prefix = '';
01564 }
01565
01566 if($wgContLang->hasVariants()) {
01567 $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText());
01568 } else {
01569 $selflink = array($this->mTitle->getPrefixedText());
01570 }
01571 $useSubpages = $this->areSubpagesAllowed();
01572 wfProfileOut( __METHOD__.'-setup' );
01573
01574 # Loop for each link
01575 for ( ; $line !== false && $line !== null ; $a->next(), $line = $a->current() ) {
01576 # Check for excessive memory usage
01577 if ( $holders->isBig() ) {
01578 # Too big
01579 # Do the existence check, replace the link holders and clear the array
01580 $holders->replace( $s );
01581 $holders->clear();
01582 }
01583
01584 if ( $useLinkPrefixExtension ) {
01585 wfProfileIn( __METHOD__.'-prefixhandling' );
01586 if ( preg_match( $e2, $s, $m ) ) {
01587 $prefix = $m[2];
01588 $s = $m[1];
01589 } else {
01590 $prefix='';
01591 }
01592 # first link
01593 if($first_prefix) {
01594 $prefix = $first_prefix;
01595 $first_prefix = false;
01596 }
01597 wfProfileOut( __METHOD__.'-prefixhandling' );
01598 }
01599
01600 $might_be_img = false;
01601
01602 wfProfileIn( __METHOD__."-e1" );
01603 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
01604 $text = $m[2];
01605 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
01606 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
01607 # the real problem is with the $e1 regex
01608 # See bug 1300.
01609 #
01610 # Still some problems for cases where the ] is meant to be outside punctuation,
01611 # and no image is in sight. See bug 2095.
01612 #
01613 if( $text !== '' &&
01614 substr( $m[3], 0, 1 ) === ']' &&
01615 strpos($text, '[') !== false
01616 )
01617 {
01618 $text .= ']'; # so that replaceExternalLinks($text) works later
01619 $m[3] = substr( $m[3], 1 );
01620 }
01621 # fix up urlencoded title texts
01622 if( strpos( $m[1], '%' ) !== false ) {
01623 # Should anchors '#' also be rejected?
01624 $m[1] = str_replace( array('<', '>'), array('<', '>'), urldecode($m[1]) );
01625 }
01626 $trail = $m[3];
01627 } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption
01628 $might_be_img = true;
01629 $text = $m[2];
01630 if ( strpos( $m[1], '%' ) !== false ) {
01631 $m[1] = urldecode($m[1]);
01632 }
01633 $trail = "";
01634 } else { # Invalid form; output directly
01635 $s .= $prefix . '[[' . $line ;
01636 wfProfileOut( __METHOD__."-e1" );
01637 continue;
01638 }
01639 wfProfileOut( __METHOD__."-e1" );
01640 wfProfileIn( __METHOD__."-misc" );
01641
01642 # Don't allow internal links to pages containing
01643 # PROTO: where PROTO is a valid URL protocol; these
01644 # should be external links.
01645 if ( preg_match( '/^\b(?:' . wfUrlProtocols() . ')/', $m[1] ) ) {
01646 $s .= $prefix . '[[' . $line ;
01647 wfProfileOut( __METHOD__."-misc" );
01648 continue;
01649 }
01650
01651 # Make subpage if necessary
01652 if ( $useSubpages ) {
01653 $link = $this->maybeDoSubpageLink( $m[1], $text );
01654 } else {
01655 $link = $m[1];
01656 }
01657
01658 $noforce = (substr( $m[1], 0, 1 ) !== ':');
01659 if (!$noforce) {
01660 # Strip off leading ':'
01661 $link = substr( $link, 1 );
01662 }
01663
01664 wfProfileOut( __METHOD__."-misc" );
01665 wfProfileIn( __METHOD__."-title" );
01666 $nt = Title::newFromText( $this->mStripState->unstripNoWiki( $link ) );
01667 if ( $nt === null ) {
01668 $s .= $prefix . '[[' . $line;
01669 wfProfileOut( __METHOD__."-title" );
01670 continue;
01671 }
01672
01673 $ns = $nt->getNamespace();
01674 $iw = $nt->getInterWiki();
01675 wfProfileOut( __METHOD__."-title" );
01676
01677 if ( $might_be_img ) { # if this is actually an invalid link
01678 wfProfileIn( __METHOD__."-might_be_img" );
01679 if ( $ns == NS_FILE && $noforce ) { #but might be an image
01680 $found = false;
01681 while ( true ) {
01682 #look at the next 'line' to see if we can close it there
01683 $a->next();
01684 $next_line = $a->current();
01685 if ( $next_line === false || $next_line === null ) {
01686 break;
01687 }
01688 $m = explode( ']]', $next_line, 3 );
01689 if ( count( $m ) == 3 ) {
01690 # the first ]] closes the inner link, the second the image
01691 $found = true;
01692 $text .= "[[{$m[0]}]]{$m[1]}";
01693 $trail = $m[2];
01694 break;
01695 } elseif ( count( $m ) == 2 ) {
01696 #if there's exactly one ]] that's fine, we'll keep looking
01697 $text .= "[[{$m[0]}]]{$m[1]}";
01698 } else {
01699 #if $next_line is invalid too, we need look no further
01700 $text .= '[[' . $next_line;
01701 break;
01702 }
01703 }
01704 if ( !$found ) {
01705 # we couldn't find the end of this imageLink, so output it raw
01706 #but don't ignore what might be perfectly normal links in the text we've examined
01707 $holders->merge( $this->replaceInternalLinks2( $text ) );
01708 $s .= "{$prefix}[[$link|$text";
01709 # note: no $trail, because without an end, there *is* no trail
01710 wfProfileOut( __METHOD__."-might_be_img" );
01711 continue;
01712 }
01713 } else { #it's not an image, so output it raw
01714 $s .= "{$prefix}[[$link|$text";
01715 # note: no $trail, because without an end, there *is* no trail
01716 wfProfileOut( __METHOD__."-might_be_img" );
01717 continue;
01718 }
01719 wfProfileOut( __METHOD__."-might_be_img" );
01720 }
01721
01722 $wasblank = ( $text == '' );
01723 if ( $wasblank ) $text = $link;
01724
01725 # Link not escaped by : , create the various objects
01726 if ( $noforce ) {
01727
01728 # Interwikis
01729 wfProfileIn( __METHOD__."-interwiki" );
01730 if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
01731 $this->mOutput->addLanguageLink( $nt->getFullText() );
01732 $s = rtrim($s . $prefix);
01733 $s .= trim($trail, "\n") == '' ? '': $prefix . $trail;
01734 wfProfileOut( __METHOD__."-interwiki" );
01735 continue;
01736 }
01737 wfProfileOut( __METHOD__."-interwiki" );
01738
01739 if ( $ns == NS_FILE ) {
01740 wfProfileIn( __METHOD__."-image" );
01741 if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
01742 if ( $wasblank ) {
01743 # if no parameters were passed, $text
01744 # becomes something like "File:Foo.png",
01745 # which we don't want to pass on to the
01746 # image generator
01747 $text = '';
01748 } else {
01749 # recursively parse links inside the image caption
01750 # actually, this will parse them in any other parameters, too,
01751 # but it might be hard to fix that, and it doesn't matter ATM
01752 $text = $this->replaceExternalLinks($text);
01753 $holders->merge( $this->replaceInternalLinks2( $text ) );
01754 }
01755 # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
01756 $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text, $holders ) ) . $trail;
01757 } else {
01758 $s .= $prefix . $trail;
01759 }
01760 $this->mOutput->addImage( $nt->getDBkey() );
01761 wfProfileOut( __METHOD__."-image" );
01762 continue;
01763
01764 }
01765
01766 if ( $ns == NS_CATEGORY ) {
01767 wfProfileIn( __METHOD__."-category" );
01768 $s = rtrim($s . "\n"); # bug 87
01769
01770 if ( $wasblank ) {
01771 $sortkey = $this->getDefaultSort();
01772 } else {
01773 $sortkey = $text;
01774 }
01775 $sortkey = Sanitizer::decodeCharReferences( $sortkey );
01776 $sortkey = str_replace( "\n", '', $sortkey );
01777 $sortkey = $wgContLang->convertCategoryKey( $sortkey );
01778 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
01779
01784 $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
01785
01786 wfProfileOut( __METHOD__."-category" );
01787 continue;
01788 }
01789 }
01790
01791 # Self-link checking
01792 if( $nt->getFragment() === '' && $ns != NS_SPECIAL ) {
01793 if( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
01794 $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
01795 continue;
01796 }
01797 }
01798
01799 # NS_MEDIA is a pseudo-namespace for linking directly to a file
01800 # FIXME: Should do batch file existence checks, see comment below
01801 if( $ns == NS_MEDIA ) {
01802 wfProfileIn( __METHOD__."-media" );
01803 # Give extensions a chance to select the file revision for us
01804 $skip = $time = false;
01805 wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$nt, &$skip, &$time ) );
01806 if ( $skip ) {
01807 $link = $sk->link( $nt );
01808 } else {
01809 $link = $sk->makeMediaLinkObj( $nt, $text, $time );
01810 }
01811 # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
01812 $s .= $prefix . $this->armorLinks( $link ) . $trail;
01813 $this->mOutput->addImage( $nt->getDBkey() );
01814 wfProfileOut( __METHOD__."-media" );
01815 continue;
01816 }
01817
01818 wfProfileIn( __METHOD__."-always_known" );
01819 # Some titles, such as valid special pages or files in foreign repos, should
01820 # be shown as bluelinks even though they're not included in the page table
01821 #
01822 # FIXME: isAlwaysKnown() can be expensive for file links; we should really do
01823 # batch file existence checks for NS_FILE and NS_MEDIA
01824 if( $iw == '' && $nt->isAlwaysKnown() ) {
01825 $this->mOutput->addLink( $nt );
01826 $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
01827 } else {
01828 # Links will be added to the output link list after checking
01829 $s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
01830 }
01831 wfProfileOut( __METHOD__."-always_known" );
01832 }
01833 wfProfileOut( __METHOD__ );
01834 return $holders;
01835 }
01836
01845 function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
01846 return $this->mLinkHolders->makeHolder( $nt, $text, $query, $trail, $prefix );
01847 }
01848
01863 function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
01864 list( $inside, $trail ) = Linker::splitTrail( $trail );
01865 $sk = $this->mOptions->getSkin();
01866
01867 $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix );
01868 return $this->armorLinks( $link ) . $trail;
01869 }
01870
01881 function armorLinks( $text ) {
01882 return preg_replace( '/\b(' . wfUrlProtocols() . ')/',
01883 "{$this->mUniqPrefix}NOPARSE$1", $text );
01884 }
01885
01890 function areSubpagesAllowed() {
01891 # Some namespaces don't allow subpages
01892 return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
01893 }
01894
01902 function maybeDoSubpageLink($target, &$text) {
01903 return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
01904 }
01905
01910 function closeParagraph() {
01911 $result = '';
01912 if ( $this->mLastSection != '' ) {
01913 $result = '</' . $this->mLastSection . ">\n";
01914 }
01915 $this->mInPre = false;
01916 $this->mLastSection = '';
01917 return $result;
01918 }
01919 # getCommon() returns the length of the longest common substring
01920 # of both arguments, starting at the beginning of both.
01921 #
01922 function getCommon( $st1, $st2 ) {
01923 $fl = strlen( $st1 );
01924 $shorter = strlen( $st2 );
01925 if ( $fl < $shorter ) { $shorter = $fl; }
01926
01927 for ( $i = 0; $i < $shorter; ++$i ) {
01928 if ( $st1{$i} != $st2{$i} ) { break; }
01929 }
01930 return $i;
01931 }
01932 # These next three functions open, continue, and close the list
01933 # element appropriate to the prefix character passed into them.
01934 #
01935 function openList( $char ) {
01936 $result = $this->closeParagraph();
01937
01938 if ( '*' === $char ) { $result .= '<ul><li>'; }
01939 elseif ( '#' === $char ) { $result .= '<ol><li>'; }
01940 elseif ( ':' === $char ) { $result .= '<dl><dd>'; }
01941 elseif ( ';' === $char ) {
01942 $result .= '<dl><dt>';
01943 $this->mDTopen = true;
01944 }
01945 else { $result = '<!-- ERR 1 -->'; }
01946
01947 return $result;
01948 }
01949
01950 function nextItem( $char ) {
01951 if ( '*' === $char || '#' === $char ) { return '</li><li>'; }
01952 elseif ( ':' === $char || ';' === $char ) {
01953 $close = '</dd>';
01954 if ( $this->mDTopen ) { $close = '</dt>'; }
01955 if ( ';' === $char ) {
01956 $this->mDTopen = true;
01957 return $close . '<dt>';
01958 } else {
01959 $this->mDTopen = false;
01960 return $close . '<dd>';
01961 }
01962 }
01963 return '<!-- ERR 2 -->';
01964 }
01965
01966 function closeList( $char ) {
01967 if ( '*' === $char ) { $text = '</li></ul>'; }
01968 elseif ( '#' === $char ) { $text = '</li></ol>'; }
01969 elseif ( ':' === $char ) {
01970 if ( $this->mDTopen ) {
01971 $this->mDTopen = false;
01972 $text = '</dt></dl>';
01973 } else {
01974 $text = '</dd></dl>';
01975 }
01976 }
01977 else { return '<!-- ERR 3 -->'; }
01978 return $text."\n";
01979 }
01989 function doBlockLevels( $text, $linestart ) {
01990 wfProfileIn( __METHOD__ );
01991
01992 # Parsing through the text line by line. The main thing
01993 # happening here is handling of block-level elements p, pre,
01994 # and making lists from lines starting with * # : etc.
01995 #
01996 $textLines = StringUtils::explode( "\n", $text );
01997
01998 $lastPrefix = $output = '';
01999 $this->mDTopen = $inBlockElem = false;
02000 $prefixLength = 0;
02001 $paragraphStack = false;
02002
02003 foreach ( $textLines as $oLine ) {
02004 # Fix up $linestart
02005 if ( !$linestart ) {
02006 $output .= $oLine;
02007 $linestart = true;
02008 continue;
02009 }
02010
02011
02012
02013
02014
02015 $lastPrefixLength = strlen( $lastPrefix );
02016 $preCloseMatch = preg_match('/<\\/pre/i', $oLine );
02017 $preOpenMatch = preg_match('/<pre/i', $oLine );
02018
02019 if ( !$this->mInPre ) {
02020 # Multiple prefixes may abut each other for nested lists.
02021 $prefixLength = strspn( $oLine, '*#:;' );
02022 $prefix = substr( $oLine, 0, $prefixLength );
02023
02024 # eh?
02025
02026
02027
02028 $prefix2 = str_replace( ';', ':', $prefix );
02029 $t = substr( $oLine, $prefixLength );
02030 $this->mInPre = (bool)$preOpenMatch;
02031 } else {
02032 # Don't interpret any other prefixes in preformatted text
02033 $prefixLength = 0;
02034 $prefix = $prefix2 = '';
02035 $t = $oLine;
02036 }
02037
02038 # List generation
02039 if( $prefixLength && $lastPrefix === $prefix2 ) {
02040 # Same as the last item, so no need to deal with nesting or opening stuff
02041 $output .= $this->nextItem( substr( $prefix, -1 ) );
02042 $paragraphStack = false;
02043
02044 if ( substr( $prefix, -1 ) === ';') {
02045 # The one nasty exception: definition lists work like this:
02046 # ; title : definition text
02047 # So we check for : in the remainder text to split up the
02048 # title and definition, without b0rking links.
02049 $term = $t2 = '';
02050 if ($this->findColonNoLinks($t, $term, $t2) !== false) {
02051 $t = $t2;
02052 $output .= $term . $this->nextItem( ':' );
02053 }
02054 }
02055 } elseif( $prefixLength || $lastPrefixLength ) {
02056
02057
02058 # Either open or close a level...
02059 $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
02060 $paragraphStack = false;
02061
02062
02063 while( $commonPrefixLength < $lastPrefixLength ) {
02064 $output .= $this->closeList( $lastPrefix[$lastPrefixLength-1] );
02065 --$lastPrefixLength;
02066 }
02067
02068
02069 if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
02070 $output .= $this->nextItem( $prefix[$commonPrefixLength-1] );
02071 }
02072
02073
02074 while ( $prefixLength > $commonPrefixLength ) {
02075 $char = substr( $prefix, $commonPrefixLength, 1 );
02076 $output .= $this->openList( $char );
02077
02078 if ( ';' === $char ) {
02079 # FIXME: This is dupe of code above
02080 if ($this->findColonNoLinks($t, $term, $t2) !== false) {
02081 $t = $t2;
02082 $output .= $term . $this->nextItem( ':' );
02083 }
02084 }
02085 ++$commonPrefixLength;
02086 }
02087 $lastPrefix = $prefix2;
02088 }
02089
02090
02091 if( 0 == $prefixLength ) {
02092 wfProfileIn( __METHOD__."-paragraph" );
02093 # No prefix (not in list)--go to paragraph mode
02094
02095 $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
02096 $closematch = preg_match(
02097 '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
02098 '<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t );
02099 if ( $openmatch or $closematch ) {
02100 $paragraphStack = false;
02101 #Â TODO bug 5718: paragraph closed
02102 $output .= $this->closeParagraph();
02103 if ( $preOpenMatch and !$preCloseMatch ) {
02104 $this->mInPre = true;
02105 }
02106 if ( $closematch ) {
02107 $inBlockElem = false;
02108 } else {
02109 $inBlockElem = true;
02110 }
02111 } else if ( !$inBlockElem && !$this->mInPre ) {
02112 if ( ' ' == substr( $t, 0, 1 ) and ( $this->mLastSection === 'pre' or trim($t) != '' ) ) {
02113
02114 if ($this->mLastSection !== 'pre') {
02115 $paragraphStack = false;
02116 $output .= $this->closeParagraph().'<pre>';
02117 $this->mLastSection = 'pre';
02118 }
02119 $t = substr( $t, 1 );
02120 } else {
02121
02122 if ( trim($t) == '' ) {
02123 if ( $paragraphStack ) {
02124 $output .= $paragraphStack.'<br />';
02125 $paragraphStack = false;
02126 $this->mLastSection = 'p';
02127 } else {
02128 if ($this->mLastSection !== 'p' ) {
02129 $output .= $this->closeParagraph();
02130 $this->mLastSection = '';
02131 $paragraphStack = '<p>';
02132 } else {
02133 $paragraphStack = '</p><p>';
02134 }
02135 }
02136 } else {
02137 if ( $paragraphStack ) {
02138 $output .= $paragraphStack;
02139 $paragraphStack = false;
02140 $this->mLastSection = 'p';
02141 } else if ($this->mLastSection !== 'p') {
02142 $output .= $this->closeParagraph().'<p>';
02143 $this->mLastSection = 'p';
02144 }
02145 }
02146 }
02147 }
02148 wfProfileOut( __METHOD__."-paragraph" );
02149 }
02150
02151 if($preCloseMatch && $this->mInPre) {
02152 $this->mInPre = false;
02153 }
02154 if ($paragraphStack === false) {
02155 $output .= $t."\n";
02156 }
02157 }
02158 while ( $prefixLength ) {
02159 $output .= $this->closeList( $prefix2[$prefixLength-1] );
02160 --$prefixLength;
02161 }
02162 if ( $this->mLastSection != '' ) {
02163 $output .= '</' . $this->mLastSection . '>';
02164 $this->mLastSection = '';
02165 }
02166
02167 wfProfileOut( __METHOD__ );
02168 return $output;
02169 }
02170
02179 function findColonNoLinks($str, &$before, &$after) {
02180 wfProfileIn( __METHOD__ );
02181
02182 $pos = strpos( $str, ':' );
02183 if( $pos === false ) {
02184
02185 wfProfileOut( __METHOD__ );
02186 return false;
02187 }
02188
02189 $lt = strpos( $str, '<' );
02190 if( $lt === false || $lt > $pos ) {
02191
02192 $before = substr( $str, 0, $pos );
02193 $after = substr( $str, $pos+1 );
02194 wfProfileOut( __METHOD__ );
02195 return $pos;
02196 }
02197
02198
02199 $state = self::COLON_STATE_TEXT;
02200 $stack = 0;
02201 $len = strlen( $str );
02202 for( $i = 0; $i < $len; $i++ ) {
02203 $c = $str{$i};
02204
02205 switch( $state ) {
02206
02207 case 0:
02208 switch( $c ) {
02209 case "<":
02210
02211 $state = self::COLON_STATE_TAGSTART;
02212 break;
02213 case ":":
02214 if( $stack == 0 ) {
02215
02216 $before = substr( $str, 0, $i );
02217 $after = substr( $str, $i + 1 );
02218 wfProfileOut( __METHOD__ );
02219 return $i;
02220 }
02221
02222 break;
02223 default:
02224
02225 $colon = strpos( $str, ':', $i );
02226 if( $colon === false ) {
02227
02228 wfProfileOut( __METHOD__ );
02229 return false;
02230 }
02231 $lt = strpos( $str, '<', $i );
02232 if( $stack === 0 ) {
02233 if( $lt === false || $colon < $lt ) {
02234
02235 $before = substr( $str, 0, $colon );
02236 $after = substr( $str, $colon + 1 );
02237 wfProfileOut( __METHOD__ );
02238 return $i;
02239 }
02240 }
02241 if( $lt === false ) {
02242
02243
02244 break 2;
02245 }
02246
02247 $i = $lt;
02248 $state = self::COLON_STATE_TAGSTART;
02249 }
02250 break;
02251 case 1:
02252
02253 switch( $c ) {
02254 case ">":
02255 $stack++;
02256 $state = self::COLON_STATE_TEXT;
02257 break;
02258 case "/":
02259
02260 $state = self::COLON_STATE_TAGSLASH;
02261 break;
02262 default:
02263
02264 }
02265 break;
02266 case 2:
02267 switch( $c ) {
02268 case "/":
02269 $state = self::COLON_STATE_CLOSETAG;
02270 break;
02271 case "!":
02272 $state = self::COLON_STATE_COMMENT;
02273 break;
02274 case ">":
02275
02276 $state = self::COLON_STATE_TEXT;
02277 break;
02278 default:
02279 $state = self::COLON_STATE_TAG;
02280 }
02281 break;
02282 case 3:
02283
02284 if( $c === ">" ) {
02285 $stack--;
02286 if( $stack < 0 ) {
02287 wfDebug( __METHOD__.": Invalid input; too many close tags\n" );
02288 wfProfileOut( __METHOD__ );
02289 return false;
02290 }
02291 $state = self::COLON_STATE_TEXT;
02292 }
02293 break;
02294 case self::COLON_STATE_TAGSLASH:
02295 if( $c === ">" ) {
02296
02297 $state = self::COLON_STATE_TEXT;
02298 } else {
02299
02300 $state = self::COLON_STATE_TAG;
02301 }
02302 break;
02303 case 5:
02304 if( $c === "-" ) {
02305 $state = self::COLON_STATE_COMMENTDASH;
02306 }
02307 break;
02308 case self::COLON_STATE_COMMENTDASH:
02309 if( $c === "-" ) {
02310 $state = self::COLON_STATE_COMMENTDASHDASH;
02311 } else {
02312 $state = self::COLON_STATE_COMMENT;
02313 }
02314 break;
02315 case self::COLON_STATE_COMMENTDASHDASH:
02316 if( $c === ">" ) {
02317 $state = self::COLON_STATE_TEXT;
02318 } else {
02319 $state = self::COLON_STATE_COMMENT;
02320 }
02321 break;
02322 default:
02323 throw new MWException( "State machine error in " . __METHOD__ );
02324 }
02325 }
02326 if( $stack > 0 ) {
02327 wfDebug( __METHOD__.": Invalid input; not enough close tags (stack $stack, state $state)\n" );
02328 return false;
02329 }
02330 wfProfileOut( __METHOD__ );
02331 return false;
02332 }
02333
02339 function getVariableValue( $index, $frame=false ) {
02340 global $wgContLang, $wgSitename, $wgServer, $wgServerName;
02341 global $wgScriptPath, $wgStylePath;
02342
02347 if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$this->mVarCache ) ) ) {
02348 if ( isset( $this->mVarCache[$index] ) ) {
02349 return $this->mVarCache[$index];
02350 }
02351 }
02352
02353 $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
02354 wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
02355
02356 # Use the time zone
02357 global $wgLocaltimezone;
02358 if ( isset( $wgLocaltimezone ) ) {
02359 $oldtz = date_default_timezone_get();
02360 date_default_timezone_set( $wgLocaltimezone );
02361 }
02362
02363 $localTimestamp = date( 'YmdHis', $ts );
02364 $localMonth = date( 'm', $ts );
02365 $localMonth1 = date( 'n', $ts );
02366 $localMonthName = date( 'n', $ts );
02367 $localDay = date( 'j', $ts );
02368 $localDay2 = date( 'd', $ts );
02369 $localDayOfWeek = date( 'w', $ts );
02370 $localWeek = date( 'W', $ts );
02371 $localYear = date( 'Y', $ts );
02372 $localHour = date( 'H', $ts );
02373 if ( isset( $wgLocaltimezone ) ) {
02374 date_default_timezone_set( $oldtz );
02375 }
02376
02377 switch ( $index ) {
02378 case 'currentmonth':
02379 $value = $wgContLang->formatNum( gmdate( 'm', $ts ) );
02380 break;
02381 case 'currentmonth1':
02382 $value = $wgContLang->formatNum( gmdate( 'n', $ts ) );
02383 break;
02384 case 'currentmonthname':
02385 $value = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
02386 break;
02387 case 'currentmonthnamegen':
02388 $value = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
02389 break;
02390 case 'currentmonthabbrev':
02391 $value = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
02392 break;
02393 case 'currentday':
02394 $value = $wgContLang->formatNum( gmdate( 'j', $ts ) );
02395 break;
02396 case 'currentday2':
02397 $value = $wgContLang->formatNum( gmdate( 'd', $ts ) );
02398 break;
02399 case 'localmonth':
02400 $value = $wgContLang->formatNum( $localMonth );
02401 break;
02402 case 'localmonth1':
02403 $value = $wgContLang->formatNum( $localMonth1 );
02404 break;
02405 case 'localmonthname':
02406 $value = $wgContLang->getMonthName( $localMonthName );
02407 break;
02408 case 'localmonthnamegen':
02409 $value = $wgContLang->getMonthNameGen( $localMonthName );
02410 break;
02411 case 'localmonthabbrev':
02412 $value = $wgContLang->getMonthAbbreviation( $localMonthName );
02413 break;
02414 case 'localday':
02415 $value = $wgContLang->formatNum( $localDay );
02416 break;
02417 case 'localday2':
02418 $value = $wgContLang->formatNum( $localDay2 );
02419 break;
02420 case 'pagename':
02421 $value = wfEscapeWikiText( $this->mTitle->getText() );
02422 break;
02423 case 'pagenamee':
02424 $value = $this->mTitle->getPartialURL();
02425 break;
02426 case 'fullpagename':
02427 $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
02428 break;
02429 case 'fullpagenamee':
02430 $value = $this->mTitle->getPrefixedURL();
02431 break;
02432 case 'subpagename':
02433 $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
02434 break;
02435 case 'subpagenamee':
02436 $value = $this->mTitle->getSubpageUrlForm();
02437 break;
02438 case 'basepagename':
02439 $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
02440 break;
02441 case 'basepagenamee':
02442 $value = wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
02443 break;
02444 case 'talkpagename':
02445 if( $this->mTitle->canTalk() ) {
02446 $talkPage = $this->mTitle->getTalkPage();
02447 $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
02448 } else {
02449 $value = '';
02450 }
02451 break;
02452 case 'talkpagenamee':
02453 if( $this->mTitle->canTalk() ) {
02454 $talkPage = $this->mTitle->getTalkPage();
02455 $value = $talkPage->getPrefixedUrl();
02456 } else {
02457 $value = '';
02458 }
02459 break;
02460 case 'subjectpagename':
02461 $subjPage = $this->mTitle->getSubjectPage();
02462 $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
02463 break;
02464 case 'subjectpagenamee':
02465 $subjPage = $this->mTitle->getSubjectPage();
02466 $value = $subjPage->getPrefixedUrl();
02467 break;
02468 case 'revisionid':
02469
02470
02471 $this->mOutput->setFlag( 'vary-revision' );
02472 wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
02473 $value = $this->mRevisionId;
02474 break;
02475 case 'revisionday':
02476
02477
02478 $this->mOutput->setFlag( 'vary-revision' );
02479 wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
02480 $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
02481 break;
02482 case 'revisionday2':
02483
02484
02485 $this->mOutput->setFlag( 'vary-revision' );
02486 wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
02487 $value = substr( $this->getRevisionTimestamp(), 6, 2 );
02488 break;
02489 case 'revisionmonth':
02490
02491
02492 $this->mOutput->setFlag( 'vary-revision' );
02493 wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
02494 $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
02495 break;
02496 case 'revisionyear':
02497
02498
02499 $this->mOutput->setFlag( 'vary-revision' );
02500 wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
02501 $value = substr( $this->getRevisionTimestamp(), 0, 4 );
02502 break;
02503 case 'revisiontimestamp':
02504
02505
02506 $this->mOutput->setFlag( 'vary-revision' );
02507 wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
02508 $value = $this->getRevisionTimestamp();
02509 break;
02510 case 'revisionuser':
02511
02512
02513 $this->mOutput->setFlag( 'vary-revision' );
02514 wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
02515 $value = $this->getRevisionUser();
02516 break;
02517 case 'namespace':
02518 $value = str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
02519 break;
02520 case 'namespacee':
02521 $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
02522 break;
02523 case 'talkspace':
02524 $value = $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : '';
02525 break;
02526 case 'talkspacee':
02527 $value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
02528 break;
02529 case 'subjectspace':
02530 $value = $this->mTitle->getSubjectNsText();
02531 break;
02532 case 'subjectspacee':
02533 $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
02534 break;
02535 case 'currentdayname':
02536 $value = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
02537 break;
02538 case 'currentyear':
02539 $value = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
02540 break;
02541 case 'currenttime':
02542 $value = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
02543 break;
02544 case 'currenthour':
02545 $value = $wgContLang->formatNum( gmdate( 'H', $ts ), true );
02546 break;
02547 case 'currentweek':
02548
02549
02550 $value = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
02551 break;
02552 case 'currentdow':
02553 $value = $wgContLang->formatNum( gmdate( 'w', $ts ) );
02554 break;
02555 case 'localdayname':
02556 $value = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
02557 break;
02558 case 'localyear':
02559 $value = $wgContLang->formatNum( $localYear, true );
02560 break;
02561 case 'localtime':
02562 $value = $wgContLang->time( $localTimestamp, false, false );
02563 break;
02564 case 'localhour':
02565 $value = $wgContLang->formatNum( $localHour, true );
02566 break;
02567 case 'localweek':
02568
02569
02570 $value = $wgContLang->formatNum( (int)$localWeek );
02571 break;
02572 case 'localdow':
02573 $value = $wgContLang->formatNum( $localDayOfWeek );
02574 break;
02575 case 'numberofarticles':
02576 $value = $wgContLang->formatNum( SiteStats::articles() );
02577 break;
02578 case 'numberoffiles':
02579 $value = $wgContLang->formatNum( SiteStats::images() );
02580 break;
02581 case 'numberofusers':
02582 $value = $wgContLang->formatNum( SiteStats::users() );
02583 break;
02584 case 'numberofactiveusers':
02585 $value = $wgContLang->formatNum( SiteStats::activeUsers() );
02586 break;
02587 case 'numberofpages':
02588 $value = $wgContLang->formatNum( SiteStats::pages() );
02589 break;
02590 case 'numberofadmins':
02591 $value = $wgContLang->formatNum( SiteStats::numberingroup('sysop') );
02592 break;
02593 case 'numberofedits':
02594 $value = $wgContLang->formatNum( SiteStats::edits() );
02595 break;
02596 case 'numberofviews':
02597 $value = $wgContLang->formatNum( SiteStats::views() );
02598 break;
02599 case 'currenttimestamp':
02600 $value = wfTimestamp( TS_MW, $ts );
02601 break;
02602 case 'localtimestamp':
02603 $value = $localTimestamp;
02604 break;
02605 case 'currentversion':
02606 $value = SpecialVersion::getVersion();
02607 break;
02608 case 'sitename':
02609 return $wgSitename;
02610 case 'server':
02611 return $wgServer;
02612 case 'servername':
02613 return $wgServerName;
02614 case 'scriptpath':
02615 return $wgScriptPath;
02616 case 'stylepath':
02617 return $wgStylePath;
02618 case 'directionmark':
02619 return $wgContLang->getDirMark();
02620 case 'contentlanguage':
02621 global $wgContLanguageCode;
02622 return $wgContLanguageCode;
02623 default:
02624 $ret = null;
02625 if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret, &$frame ) ) )
02626 return $ret;
02627 else
02628 return null;
02629 }
02630
02631 if ( $index )
02632 $this->mVarCache[$index] = $value;
02633
02634 return $value;
02635 }
02636
02642 function initialiseVariables() {
02643 wfProfileIn( __METHOD__ );
02644 $variableIDs = MagicWord::getVariableIDs();
02645 $substIDs = MagicWord::getSubstIDs();
02646
02647 $this->mVariables = new MagicWordArray( $variableIDs );
02648 $this->mSubstWords = new MagicWordArray( $substIDs );
02649 wfProfileOut( __METHOD__ );
02650 }
02651
02674 function preprocessToDom ( $text, $flags = 0 ) {
02675 $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
02676 return $dom;
02677 }
02678
02679
02680
02681
02682 public static function splitWhitespace( $s ) {
02683 $ltrimmed = ltrim( $s );
02684 $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
02685 $trimmed = rtrim( $ltrimmed );
02686 $diff = strlen( $ltrimmed ) - strlen( $trimmed );
02687 if ( $diff > 0 ) {
02688 $w2 = substr( $ltrimmed, -$diff );
02689 } else {
02690 $w2 = '';
02691 }
02692 return array( $w1, $trimmed, $w2 );
02693 }
02694
02712 function replaceVariables( $text, $frame = false, $argsOnly = false ) {
02713 # Is there any text? Also, Prevent too big inclusions!
02714 if ( strlen( $text ) < 1 || strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
02715 return $text;
02716 }
02717 wfProfileIn( __METHOD__ );
02718
02719 if ( $frame === false ) {
02720 $frame = $this->getPreprocessor()->newFrame();
02721 } elseif ( !( $frame instanceof PPFrame ) ) {
02722 wfDebug( __METHOD__." called using plain parameters instead of a PPFrame instance. Creating custom frame.\n" );
02723 $frame = $this->getPreprocessor()->newCustomFrame($frame);
02724 }
02725
02726 $dom = $this->preprocessToDom( $text );
02727 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
02728 $text = $frame->expand( $dom, $flags );
02729
02730 wfProfileOut( __METHOD__ );
02731 return $text;
02732 }
02733
02735 static function createAssocArgs( $args ) {
02736 $assocArgs = array();
02737 $index = 1;
02738 foreach( $args as $arg ) {
02739 $eqpos = strpos( $arg, '=' );
02740 if ( $eqpos === false ) {
02741 $assocArgs[$index++] = $arg;
02742 } else {
02743 $name = trim( substr( $arg, 0, $eqpos ) );
02744 $value = trim( substr( $arg, $eqpos+1 ) );
02745 if ( $value === false ) {
02746 $value = '';
02747 }
02748 if ( $name !== false ) {
02749 $assocArgs[$name] = $value;
02750 }
02751 }
02752 }
02753
02754 return $assocArgs;
02755 }
02756
02768 function limitationWarn( $limitationType, $current=null, $max=null) {
02769
02770 $warning = wfMsgExt( "$limitationType-warning", array( 'parsemag', 'escape' ), $current, $max );
02771 $this->mOutput->addWarning( $warning );
02772 $this->addTrackingCategory( "$limitationType-category" );
02773 }
02774
02787 function braceSubstitution( $piece, $frame ) {
02788 global $wgContLang, $wgNonincludableNamespaces;
02789 wfProfileIn( __METHOD__ );
02790 wfProfileIn( __METHOD__.'-setup' );
02791
02792 # Flags
02793 $found = false; # $text has been filled
02794 $nowiki = false; # wiki markup in $text should be escaped
02795 $isHTML = false; # $text is HTML, armour it against wikitext transformation
02796 $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered
02797 $isChildObj = false; # $text is a DOM node needing expansion in a child frame
02798 $isLocalObj = false; # $text is a DOM node needing expansion in the current frame
02799
02800 # Title object, where $text came from
02801 $title = null;
02802
02803 # $part1 is the bit before the first |, and must contain only title characters.
02804 # Various prefixes will be stripped from it later.
02805 $titleWithSpaces = $frame->expand( $piece['title'] );
02806 $part1 = trim( $titleWithSpaces );
02807 $titleText = false;
02808
02809 # Original title text preserved for various purposes
02810 $originalTitle = $part1;
02811
02812 # $args is a list of argument nodes, starting from index 0, not including $part1
02813 $args = (null == $piece['parts']) ? array() : $piece['parts'];
02814 wfProfileOut( __METHOD__.'-setup' );
02815
02816 # SUBST
02817 wfProfileIn( __METHOD__.'-modifiers' );
02818 if ( !$found ) {
02819
02820 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
02821
02822 # Possibilities for substMatch: "subst", "safesubst" or FALSE
02823 # Decide whether to expand template or keep wikitext as-is.
02824 if ( $this->ot['wiki'] ) {
02825 if ( $substMatch === false ) {
02826 $literal = true; # literal when in PST with no prefix
02827 } else {
02828 $literal = false; # expand when in PST with subst: or safesubst:
02829 }
02830 } else {
02831 if ( $substMatch == 'subst' ) {
02832 $literal = true; # literal when not in PST with plain subst:
02833 } else {
02834 $literal = false; # expand when not in PST with safesubst: or no prefix
02835 }
02836 }
02837 if ( $literal ) {
02838 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
02839 $isLocalObj = true;
02840 $found = true;
02841 }
02842 }
02843
02844 # Variables
02845 if ( !$found && $args->getLength() == 0 ) {
02846 $id = $this->mVariables->matchStartToEnd( $part1 );
02847 if ( $id !== false ) {
02848 $text = $this->getVariableValue( $id, $frame );
02849 if (MagicWord::getCacheTTL($id)>-1)
02850 $this->mOutput->mContainsOldMagic = true;
02851 $found = true;
02852 }
02853 }
02854
02855 # MSG, MSGNW and RAW
02856 if ( !$found ) {
02857 # Check for MSGNW:
02858 $mwMsgnw = MagicWord::get( 'msgnw' );
02859 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
02860 $nowiki = true;
02861 } else {
02862 # Remove obsolete MSG:
02863 $mwMsg = MagicWord::get( 'msg' );
02864 $mwMsg->matchStartAndRemove( $part1 );
02865 }
02866
02867 # Check for RAW:
02868 $mwRaw = MagicWord::get( 'raw' );
02869 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
02870 $forceRawInterwiki = true;
02871 }
02872 }
02873 wfProfileOut( __METHOD__.'-modifiers' );
02874
02875 # Parser functions
02876 if ( !$found ) {
02877 wfProfileIn( __METHOD__ . '-pfunc' );
02878
02879 $colonPos = strpos( $part1, ':' );
02880 if ( $colonPos !== false ) {
02881 # Case sensitive functions
02882 $function = substr( $part1, 0, $colonPos );
02883 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
02884 $function = $this->mFunctionSynonyms[1][$function];
02885 } else {
02886 # Case insensitive functions
02887 $function = $wgContLang->lc( $function );
02888 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
02889 $function = $this->mFunctionSynonyms[0][$function];
02890 } else {
02891 $function = false;
02892 }
02893 }
02894 if ( $function ) {
02895 list( $callback, $flags ) = $this->mFunctionHooks[$function];
02896 $initialArgs = array( &$this );
02897 $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) );
02898 if ( $flags & SFH_OBJECT_ARGS ) {
02899 # Add a frame parameter, and pass the arguments as an array
02900 $allArgs = $initialArgs;
02901 $allArgs[] = $frame;
02902 for ( $i = 0; $i < $args->getLength(); $i++ ) {
02903 $funcArgs[] = $args->item( $i );
02904 }
02905 $allArgs[] = $funcArgs;
02906 } else {
02907 # Convert arguments to plain text
02908 for ( $i = 0; $i < $args->getLength(); $i++ ) {
02909 $funcArgs[] = trim( $frame->expand( $args->item( $i ) ) );
02910 }
02911 $allArgs = array_merge( $initialArgs, $funcArgs );
02912 }
02913
02914 # Workaround for PHP bug 35229 and similar
02915 if ( !is_callable( $callback ) ) {
02916 wfProfileOut( __METHOD__ . '-pfunc' );
02917 wfProfileOut( __METHOD__ );
02918 throw new MWException( "Tag hook for $function is not callable\n" );
02919 }
02920 $result = call_user_func_array( $callback, $allArgs );
02921 $found = true;
02922 $noparse = true;
02923 $preprocessFlags = 0;
02924
02925 if ( is_array( $result ) ) {
02926 if ( isset( $result[0] ) ) {
02927 $text = $result[0];
02928 unset( $result[0] );
02929 }
02930
02931
02932
02933 extract( $result );
02934 } else {
02935 $text = $result;
02936 }
02937 if ( !$noparse ) {
02938 $text = $this->preprocessToDom( $text, $preprocessFlags );
02939 $isChildObj = true;
02940 }
02941 }
02942 }
02943 wfProfileOut( __METHOD__ . '-pfunc' );
02944 }
02945
02946 # Finish mangling title and then check for loops.
02947 # Set $title to a Title object and $titleText to the PDBK
02948 if ( !$found ) {
02949 $ns = NS_TEMPLATE;
02950 # Split the title into page and subpage
02951 $subpage = '';
02952 $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
02953 if ($subpage !== '') {
02954 $ns = $this->mTitle->getNamespace();
02955 }
02956 $title = Title::newFromText( $part1, $ns );
02957 if ( $title ) {
02958 $titleText = $title->getPrefixedText();
02959 # Check for language variants if the template is not found
02960 if($wgContLang->hasVariants() && $title->getArticleID() == 0){
02961 $wgContLang->findVariantLink( $part1, $title, true );
02962 }
02963 # Do recursion depth check
02964 $limit = $this->mOptions->getMaxTemplateDepth();
02965 if ( $frame->depth >= $limit ) {
02966 $found = true;
02967 $text = '<span class="error">' . wfMsgForContent( 'parser-template-recursion-depth-warning', $limit ) . '</span>';
02968 }
02969 }
02970 }
02971
02972 # Load from database
02973 if ( !$found && $title ) {
02974 wfProfileIn( __METHOD__ . '-loadtpl' );
02975 if ( !$title->isExternal() ) {
02976 if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) {
02977 $text = SpecialPage::capturePath( $title );
02978 if ( is_string( $text ) ) {
02979 $found = true;
02980 $isHTML = true;
02981 $this->disableCache();
02982 }
02983 } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) {
02984 $found = false;
02985 wfDebug( __METHOD__.": template inclusion denied for " . $title->getPrefixedDBkey() );
02986 } else {
02987 list( $text, $title ) = $this->getTemplateDom( $title );
02988 if ( $text !== false ) {
02989 $found = true;
02990 $isChildObj = true;
02991 }
02992 }
02993
02994 # If the title is valid but undisplayable, make a link to it
02995 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
02996 $text = "[[:$titleText]]";
02997 $found = true;
02998 }
02999 } elseif ( $title->isTrans() ) {
03000
03001 if ( $this->ot['html'] && !$forceRawInterwiki ) {
03002 $text = $this->interwikiTransclude( $title, 'render' );
03003 $isHTML = true;
03004 } else {
03005 $text = $this->interwikiTransclude( $title, 'raw' );
03006
03007 $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
03008 $isChildObj = true;
03009 }
03010 $found = true;
03011 }
03012
03013 # Do infinite loop check
03014 # This has to be done after redirect resolution to avoid infinite loops via redirects
03015 if ( !$frame->loopCheck( $title ) ) {
03016 $found = true;
03017 $text = '<span class="error">' . wfMsgForContent( 'parser-template-loop-warning', $titleText ) . '</span>';
03018 wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
03019 }
03020 wfProfileOut( __METHOD__ . '-loadtpl' );
03021 }
03022
03023 # If we haven't found text to substitute by now, we're done
03024 # Recover the source wikitext and return it
03025 if ( !$found ) {
03026 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
03027 wfProfileOut( __METHOD__ );
03028 return array( 'object' => $text );
03029 }
03030
03031 # Expand DOM-style return values in a child frame
03032 if ( $isChildObj ) {
03033 # Clean up argument array
03034 $newFrame = $frame->newChild( $args, $title );
03035
03036 if ( $nowiki ) {
03037 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
03038 } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
03039 # Expansion is eligible for the empty-frame cache
03040 if ( isset( $this->mTplExpandCache[$titleText] ) ) {
03041 $text = $this->mTplExpandCache[$titleText];
03042 } else {
03043 $text = $newFrame->expand( $text );
03044 $this->mTplExpandCache[$titleText] = $text;
03045 }
03046 } else {
03047 # Uncached expansion
03048 $text = $newFrame->expand( $text );
03049 }
03050 }
03051 if ( $isLocalObj && $nowiki ) {
03052 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
03053 $isLocalObj = false;
03054 }
03055
03056 # Replace raw HTML by a placeholder
03057 # Add a blank line preceding, to prevent it from mucking up
03058 # immediately preceding headings
03059 if ( $isHTML ) {
03060 $text = "\n\n" . $this->insertStripItem( $text );
03061 }
03062 # Escape nowiki-style return values
03063 elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
03064 $text = wfEscapeWikiText( $text );
03065 }
03066 # Bug 529: if the template begins with a table or block-level
03067 # element, it should be treated as beginning a new line.
03068 # This behaviour is somewhat controversial.
03069 elseif ( is_string( $text ) && !$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) {
03070 $text = "\n" . $text;
03071 }
03072
03073 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
03074 # Error, oversize inclusion
03075 $text = "[[$originalTitle]]" .
03076 $this->insertStripItem( '<!-- WARNING: template omitted, post-expand include size too large -->' );
03077 $this->limitationWarn( 'post-expand-template-inclusion' );
03078 }
03079
03080 if ( $isLocalObj ) {
03081 $ret = array( 'object' => $text );
03082 } else {
03083 $ret = array( 'text' => $text );
03084 }
03085
03086 wfProfileOut( __METHOD__ );
03087 return $ret;
03088 }
03089
03094 function getTemplateDom( $title ) {
03095 $cacheTitle = $title;
03096 $titleText = $title->getPrefixedDBkey();
03097
03098 if ( isset( $this->mTplRedirCache[$titleText] ) ) {
03099 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
03100 $title = Title::makeTitle( $ns, $dbk );
03101 $titleText = $title->getPrefixedDBkey();
03102 }
03103 if ( isset( $this->mTplDomCache[$titleText] ) ) {
03104 return array( $this->mTplDomCache[$titleText], $title );
03105 }
03106
03107
03108 list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
03109
03110 if ( $text === false ) {
03111 $this->mTplDomCache[$titleText] = false;
03112 return array( false, $title );
03113 }
03114
03115 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
03116 $this->mTplDomCache[ $titleText ] = $dom;
03117
03118 if (! $title->equals($cacheTitle)) {
03119 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
03120 array( $title->getNamespace(),$cdb = $title->getDBkey() );
03121 }
03122
03123 return array( $dom, $title );
03124 }
03125
03129 function fetchTemplateAndTitle( $title ) {
03130 $templateCb = $this->mOptions->getTemplateCallback();
03131 $stuff = call_user_func( $templateCb, $title, $this );
03132 $text = $stuff['text'];
03133 $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
03134 if ( isset( $stuff['deps'] ) ) {
03135 foreach ( $stuff['deps'] as $dep ) {
03136 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
03137 }
03138 }
03139 return array($text,$finalTitle);
03140 }
03141
03142 function fetchTemplate( $title ) {
03143 $rv = $this->fetchTemplateAndTitle($title);
03144 return $rv[0];
03145 }
03146
03151 static function statelessFetchTemplate( $title, $parser=false ) {
03152 $text = $skip = false;
03153 $finalTitle = $title;
03154 $deps = array();
03155
03156
03157 for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
03158 # Give extensions a chance to select the revision instead
03159 $id = false;
03160 wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( $parser, &$title, &$skip, &$id ) );
03161
03162 if( $skip ) {
03163 $text = false;
03164 $deps[] = array(
03165 'title' => $title,
03166 'page_id' => $title->getArticleID(),
03167 'rev_id' => null );
03168 break;
03169 }
03170 $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title );
03171 $rev_id = $rev ? $rev->getId() : 0;
03172
03173 if( $id === false && !$rev ) {
03174 $linkCache = LinkCache::singleton();
03175 $linkCache->addBadLinkObj( $title );
03176 }
03177
03178 $deps[] = array(
03179 'title' => $title,
03180 'page_id' => $title->getArticleID(),
03181 'rev_id' => $rev_id );
03182
03183 if( $rev ) {
03184 $text = $rev->getText();
03185 } elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
03186 global $wgContLang;
03187 $message = $wgContLang->lcfirst( $title->getText() );
03188 $text = wfMsgForContentNoTrans( $message );
03189 if( wfEmptyMsg( $message, $text ) ) {
03190 $text = false;
03191 break;
03192 }
03193 } else {
03194 break;
03195 }
03196 if ( $text === false ) {
03197 break;
03198 }
03199
03200 $finalTitle = $title;
03201 $title = Title::newFromRedirect( $text );
03202 }
03203 return array(
03204 'text' => $text,
03205 'finalTitle' => $finalTitle,
03206 'deps' => $deps );
03207 }
03208
03212 function interwikiTransclude( $title, $action ) {
03213 global $wgEnableScaryTranscluding;
03214
03215 if (!$wgEnableScaryTranscluding)
03216 return wfMsg('scarytranscludedisabled');
03217
03218 $url = $title->getFullUrl( "action=$action" );
03219
03220 if (strlen($url) > 255)
03221 return wfMsg('scarytranscludetoolong');
03222 return $this->fetchScaryTemplateMaybeFromCache($url);
03223 }
03224
03225 function fetchScaryTemplateMaybeFromCache($url) {
03226 global $wgTranscludeCacheExpiry;
03227 $dbr = wfGetDB(DB_SLAVE);
03228 $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
03229 $obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'),
03230 array('tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ) );
03231 if ($obj) {
03232 return $obj->tc_contents;
03233 }
03234
03235 $text = Http::get($url);
03236 if (!$text)
03237 return wfMsg('scarytranscludefailed', $url);
03238
03239 $dbw = wfGetDB(DB_MASTER);
03240 $dbw->replace('transcache', array('tc_url'), array(
03241 'tc_url' => $url,
03242 'tc_time' => $dbw->timestamp( time() ),
03243 'tc_contents' => $text));
03244 return $text;
03245 }
03246
03247
03252 function argSubstitution( $piece, $frame ) {
03253 wfProfileIn( __METHOD__ );
03254
03255 $error = false;
03256 $parts = $piece['parts'];
03257 $nameWithSpaces = $frame->expand( $piece['title'] );
03258 $argName = trim( $nameWithSpaces );
03259 $object = false;
03260 $text = $frame->getArgument( $argName );
03261 if ( $text === false && $parts->getLength() > 0
03262 && (
03263 $this->ot['html']
03264 || $this->ot['pre']
03265 || ( $this->ot['wiki'] && $frame->isTemplate() )
03266 )
03267 ) {
03268 # No match in frame, use the supplied default
03269 $object = $parts->item( 0 )->getChildren();
03270 }
03271 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
03272 $error = '<!-- WARNING: argument omitted, expansion size too large -->';
03273 $this->limitationWarn( 'post-expand-template-argument' );
03274 }
03275
03276 if ( $text === false && $object === false ) {
03277 # No match anywhere
03278 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
03279 }
03280 if ( $error !== false ) {
03281 $text .= $error;
03282 }
03283 if ( $object !== false ) {
03284 $ret = array( 'object' => $object );
03285 } else {
03286 $ret = array( 'text' => $text );
03287 }
03288
03289 wfProfileOut( __METHOD__ );
03290 return $ret;
03291 }
03292
03305 function extensionSubstitution( $params, $frame ) {
03306 global $wgRawHtml, $wgContLang;
03307
03308 $name = $frame->expand( $params['name'] );
03309 $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
03310 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
03311 $marker = "{$this->mUniqPrefix}-$name-" . sprintf('%08X', $this->mMarkerIndex++) . self::MARKER_SUFFIX;
03312
03313 $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower($name)] ) &&
03314 ( $this->ot['html'] || $this->ot['pre'] );
03315 if ( $isFunctionTag ) {
03316 $markerType = 'none';
03317 } else {
03318 $markerType = 'general';
03319 }
03320 if ( $this->ot['html'] || $isFunctionTag ) {
03321 $name = strtolower( $name );
03322 $attributes = Sanitizer::decodeTagAttributes( $attrText );
03323 if ( isset( $params['attributes'] ) ) {
03324 $attributes = $attributes + $params['attributes'];
03325 }
03326
03327 if( isset( $this->mTagHooks[$name] ) ) {
03328 # Workaround for PHP bug 35229 and similar
03329 if ( !is_callable( $this->mTagHooks[$name] ) ) {
03330 throw new MWException( "Tag hook for $name is not callable\n" );
03331 }
03332 $output = call_user_func_array( $this->mTagHooks[$name],
03333 array( $content, $attributes, $this, $frame ) );
03334 } elseif( isset( $this->mFunctionTagHooks[$name] ) ) {
03335 list( $callback, $flags ) = $this->mFunctionTagHooks[$name];
03336 if( !is_callable( $callback ) )
03337 throw new MWException( "Tag hook for $name is not callable\n" );
03338
03339 $output = call_user_func_array( $callback,
03340 array( &$this, $frame, $content, $attributes ) );
03341 } else {
03342 $output = '<span class="error">Invalid tag extension name: ' .
03343 htmlspecialchars( $name ) . '</span>';
03344 }
03345
03346 if ( is_array( $output ) ) {
03347
03348 $flags = $output;
03349 $output = $flags[0];
03350 unset( $flags[0] );
03351 extract( $flags );
03352 }
03353 } else {
03354 if ( is_null( $attrText ) ) {
03355 $attrText = '';
03356 }
03357 if ( isset( $params['attributes'] ) ) {
03358 foreach ( $params['attributes'] as $attrName => $attrValue ) {
03359 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
03360 htmlspecialchars( $attrValue ) . '"';
03361 }
03362 }
03363 if ( $content === null ) {
03364 $output = "<$name$attrText/>";
03365 } else {
03366 $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
03367 $output = "<$name$attrText>$content$close";
03368 }
03369 }
03370
03371 if( $markerType === 'none' ) {
03372 return $output;
03373 } elseif ( $markerType === 'nowiki' ) {
03374 $this->mStripState->nowiki->setPair( $marker, $output );
03375 } elseif ( $markerType === 'general' ) {
03376 $this->mStripState->general->setPair( $marker, $output );
03377 } else {
03378 throw new MWException( __METHOD__.': invalid marker type' );
03379 }
03380 return $marker;
03381 }
03382
03390 function incrementIncludeSize( $type, $size ) {
03391 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize( $type ) ) {
03392 return false;
03393 } else {
03394 $this->mIncludeSizes[$type] += $size;
03395 return true;
03396 }
03397 }
03398
03404 function incrementExpensiveFunctionCount() {
03405 global $wgExpensiveParserFunctionLimit;
03406 $this->mExpensiveFunctionCount++;
03407 if($this->mExpensiveFunctionCount <= $wgExpensiveParserFunctionLimit) {
03408 return true;
03409 }
03410 return false;
03411 }
03412
03417 function doDoubleUnderscore( $text ) {
03418 wfProfileIn( __METHOD__ );
03419
03420
03421 $mw = MagicWord::get( 'toc' );
03422 if( $mw->match( $text ) ) {
03423 $this->mShowToc = true;
03424 $this->mForceTocPosition = true;
03425
03426
03427 $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
03428
03429
03430 $text = $mw->replace( '', $text );
03431 }
03432
03433
03434 $mwa = MagicWord::getDoubleUnderscoreArray();
03435 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
03436
03437 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
03438 $this->mOutput->mNoGallery = true;
03439 }
03440 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
03441 $this->mShowToc = false;
03442 }
03443 if ( isset( $this->mDoubleUnderscores['hiddencat'] ) && $this->mTitle->getNamespace() == NS_CATEGORY ) {
03444 $this->mOutput->setProperty( 'hiddencat', 'y' );
03445 $this->addTrackingCategory( 'hidden-category-category' );
03446 }
03447 # (bug 8068) Allow control over whether robots index a page.
03448 #
03449 # FIXME (bug 14899): __INDEX__ always overrides __NOINDEX__ here! This
03450 # is not desirable, the last one on the page should win.
03451 if( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
03452 $this->mOutput->setIndexPolicy( 'noindex' );
03453 $this->addTrackingCategory( 'noindex-category' );
03454 }
03455 if( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ){
03456 $this->mOutput->setIndexPolicy( 'index' );
03457 $this->addTrackingCategory( 'index-category' );
03458 }
03459
03460 wfProfileOut( __METHOD__ );
03461 return $text;
03462 }
03463
03470 protected function addTrackingCategory( $msg ){
03471 $cat = wfMsgForContent( $msg );
03472
03473 # Allow tracking categories to be disabled by setting them to "-"
03474 if( $cat === '-' ) return false;
03475
03476 $containerCategory = Title::makeTitleSafe( NS_CATEGORY, $cat );
03477 if ( $containerCategory ) {
03478 $this->mOutput->addCategory( $containerCategory->getDBkey(), $this->getDefaultSort() );
03479 return true;
03480 } else {
03481 wfDebug( __METHOD__.": [[MediaWiki:$msg]] is not a valid title!\n" );
03482 return false;
03483 }
03484 }
03485
03501 function formatHeadings( $text, $origText, $isMain=true ) {
03502 global $wgMaxTocLevel, $wgContLang, $wgHtml5, $wgExperimentalHtmlIds;
03503
03504 $doNumberHeadings = $this->mOptions->getNumberHeadings();
03505 $showEditLink = $this->mOptions->getEditSection();
03506
03507
03508 if( $showEditLink && !$this->mTitle->quickUserCan( 'edit' ) ) {
03509 $showEditLink = 0;
03510 }
03511
03512 # Inhibit editsection links if requested in the page
03513 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) || $this->mOptions->getIsPrintable() ) {
03514 $showEditLink = 0;
03515 }
03516
03517 # Get all headlines for numbering them and adding funky stuff like [edit]
03518 # links - this is for later, but we need the number of headlines right now
03519 $matches = array();
03520 $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?'.'>)(?P<header>.*?)<\/H[1-6] *>/i', $text, $matches );
03521
03522 # if there are fewer than 4 headlines in the article, do not show TOC
03523 # unless it's been explicitly enabled.
03524 $enoughToc = $this->mShowToc &&
03525 (($numMatches >= 4) || $this->mForceTocPosition);
03526
03527 # Allow user to stipulate that a page should have a "new section"
03528 # link added via __NEWSECTIONLINK__
03529 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
03530 $this->mOutput->setNewSection( true );
03531 }
03532
03533 # Allow user to remove the "new section"
03534 # link via __NONEWSECTIONLINK__
03535 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
03536 $this->mOutput->hideNewSection( true );
03537 }
03538
03539 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
03540 # override above conditions and always show TOC above first header
03541 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
03542 $this->mShowToc = true;
03543 $enoughToc = true;
03544 }
03545
03546 # We need this to perform operations on the HTML
03547 $sk = $this->mOptions->getSkin();
03548
03549 # headline counter
03550 $headlineCount = 0;
03551 $numVisible = 0;
03552
03553 # Ugh .. the TOC should have neat indentation levels which can be
03554 # passed to the skin functions. These are determined here
03555 $toc = '';
03556 $full = '';
03557 $head = array();
03558 $sublevelCount = array();
03559 $levelCount = array();
03560 $toclevel = 0;
03561 $level = 0;
03562 $prevlevel = 0;
03563 $toclevel = 0;
03564 $prevtoclevel = 0;
03565 $markerRegex = "{$this->mUniqPrefix}-h-(\d+)-" . self::MARKER_SUFFIX;
03566 $baseTitleText = $this->mTitle->getPrefixedDBkey();
03567 $oldType = $this->mOutputType;
03568 $this->setOutputType( self::OT_WIKI );
03569 $frame = $this->getPreprocessor()->newFrame();
03570 $root = $this->preprocessToDom( $origText );
03571 $node = $root->getFirstChild();
03572 $byteOffset = 0;
03573 $tocraw = array();
03574
03575 foreach( $matches[3] as $headline ) {
03576 $isTemplate = false;
03577 $titleText = false;
03578 $sectionIndex = false;
03579 $numbering = '';
03580 $markerMatches = array();
03581 if (preg_match("/^$markerRegex/", $headline, $markerMatches)) {
03582 $serial = $markerMatches[1];
03583 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
03584 $isTemplate = ($titleText != $baseTitleText);
03585 $headline = preg_replace("/^$markerRegex/", "", $headline);
03586 }
03587
03588 if( $toclevel ) {
03589 $prevlevel = $level;
03590 $prevtoclevel = $toclevel;
03591 }
03592 $level = $matches[1][$headlineCount];
03593
03594 if ( $level > $prevlevel ) {
03595 # Increase TOC level
03596 $toclevel++;
03597 $sublevelCount[$toclevel] = 0;
03598 if( $toclevel<$wgMaxTocLevel ) {
03599 $prevtoclevel = $toclevel;
03600 $toc .= $sk->tocIndent();
03601 $numVisible++;
03602 }
03603 }
03604 elseif ( $level < $prevlevel && $toclevel > 1 ) {
03605 # Decrease TOC level, find level to jump to
03606
03607 for ($i = $toclevel; $i > 0; $i--) {
03608 if ( $levelCount[$i] == $level ) {
03609 # Found last matching level
03610 $toclevel = $i;
03611 break;
03612 }
03613 elseif ( $levelCount[$i] < $level ) {
03614 # Found first matching level below current level
03615 $toclevel = $i + 1;
03616 break;
03617 }
03618 }
03619 if( $i == 0 ) $toclevel = 1;
03620 if( $toclevel<$wgMaxTocLevel ) {
03621 if($prevtoclevel < $wgMaxTocLevel) {
03622 # Unindent only if the previous toc level was shown :p
03623 $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
03624 $prevtoclevel = $toclevel;
03625 } else {
03626 $toc .= $sk->tocLineEnd();
03627 }
03628 }
03629 }
03630 else {
03631 # No change in level, end TOC line
03632 if( $toclevel<$wgMaxTocLevel ) {
03633 $toc .= $sk->tocLineEnd();
03634 }
03635 }
03636
03637 $levelCount[$toclevel] = $level;
03638
03639 # count number of headlines for each level
03640 @$sublevelCount[$toclevel]++;
03641 $dot = 0;
03642 for( $i = 1; $i <= $toclevel; $i++ ) {
03643 if( !empty( $sublevelCount[$i] ) ) {
03644 if( $dot ) {
03645 $numbering .= '.';
03646 }
03647 $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
03648 $dot = 1;
03649 }
03650 }
03651
03652 # The safe header is a version of the header text safe to use for links
03653 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
03654 $safeHeadline = $this->mStripState->unstripBoth( $headline );
03655
03656 # Remove link placeholders by the link text.
03657 # <!--LINK number-->
03658 # turns into
03659 # link text with suffix
03660 $safeHeadline = $this->replaceLinkHoldersText( $safeHeadline );
03661
03662 # Strip out HTML (other than plain <sup> and <sub>: bug 8393)
03663 $tocline = preg_replace(
03664 array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ),
03665 array( '', '<$1>'),
03666 $safeHeadline
03667 );
03668 $tocline = trim( $tocline );
03669
03670 # For the anchor, strip out HTML-y stuff period
03671 $safeHeadline = preg_replace( '/<.*?'.'>/', '', $safeHeadline );
03672 $safeHeadline = preg_replace( '/[ _]+/', ' ', $safeHeadline );
03673 $safeHeadline = trim( $safeHeadline );
03674
03675 # Save headline for section edit hint before it's escaped
03676 $headlineHint = $safeHeadline;
03677
03678 if ( $wgHtml5 && $wgExperimentalHtmlIds ) {
03679 # For reverse compatibility, provide an id that's
03680 # HTML4-compatible, like we used to.
03681 #
03682 # It may be worth noting, academically, that it's possible for
03683 # the legacy anchor to conflict with a non-legacy headline
03684 # anchor on the page. In this case likely the "correct" thing
03685 # would be to either drop the legacy anchors or make sure
03686 # they're numbered first. However, this would require people
03687 # to type in section names like "abc_.D7.93.D7.90.D7.A4"
03688 # manually, so let's not bother worrying about it.
03689 $legacyHeadline = Sanitizer::escapeId( $safeHeadline,
03690 array( 'noninitial', 'legacy' ) );
03691 $safeHeadline = Sanitizer::escapeId( $safeHeadline );
03692
03693 if ( $legacyHeadline == $safeHeadline ) {
03694 # No reason to have both (in fact, we can't)
03695 $legacyHeadline = false;
03696 }
03697 } else {
03698 $legacyHeadline = false;
03699 $safeHeadline = Sanitizer::escapeId( $safeHeadline,
03700 'noninitial' );
03701 }
03702
03703 # HTML names must be case-insensitively unique (bug 10721). FIXME:
03704 # Does this apply to Unicode characters? Because we aren't
03705 # handling those here.
03706 $arrayKey = strtolower( $safeHeadline );
03707 if ( $legacyHeadline === false ) {
03708 $legacyArrayKey = false;
03709 } else {
03710 $legacyArrayKey = strtolower( $legacyHeadline );
03711 }
03712
03713 # count how many in assoc. array so we can track dupes in anchors
03714 if ( isset( $refers[$arrayKey] ) ) {
03715 $refers[$arrayKey]++;
03716 } else {
03717 $refers[$arrayKey] = 1;
03718 }
03719 if ( isset( $refers[$legacyArrayKey] ) ) {
03720 $refers[$legacyArrayKey]++;
03721 } else {
03722 $refers[$legacyArrayKey] = 1;
03723 }
03724
03725 # Don't number the heading if it is the only one (looks silly)
03726 if( $doNumberHeadings && count( $matches[3] ) > 1) {
03727 # the two are different if the line contains a link
03728 $headline = $numbering . ' ' . $headline;
03729 }
03730
03731 # Create the anchor for linking from the TOC to the section
03732 $anchor = $safeHeadline;
03733 $legacyAnchor = $legacyHeadline;
03734 if ( $refers[$arrayKey] > 1 ) {
03735 $anchor .= '_' . $refers[$arrayKey];
03736 }
03737 if ( $legacyHeadline !== false && $refers[$legacyArrayKey] > 1 ) {
03738 $legacyAnchor .= '_' . $refers[$legacyArrayKey];
03739 }
03740 if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
03741 $toc .= $sk->tocLine($anchor, $tocline,
03742 $numbering, $toclevel, ($isTemplate ? false : $sectionIndex));
03743 }
03744
03745 # Add the section to the section tree
03746 # Find the DOM node for this header
03747 while ( $node && !$isTemplate ) {
03748 if ( $node->getName() === 'h' ) {
03749 $bits = $node->splitHeading();
03750 if ( $bits['i'] == $sectionIndex )
03751 break;
03752 }
03753 $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
03754 $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
03755 $node = $node->getNextSibling();
03756 }
03757 $tocraw[] = array(
03758 'toclevel' => $toclevel,
03759 'level' => $level,
03760 'line' => $tocline,
03761 'number' => $numbering,
03762 'index' => ($isTemplate ? 'T-' : '' ) . $sectionIndex,
03763 'fromtitle' => $titleText,
03764 'byteoffset' => ( $isTemplate ? null : $byteOffset ),
03765 'anchor' => $anchor,
03766 );
03767
03768 # give headline the correct <h#> tag
03769 if( $showEditLink && $sectionIndex !== false ) {
03770 if( $isTemplate ) {
03771 # Put a T flag in the section identifier, to indicate to extractSections()
03772 # that sections inside <includeonly> should be counted.
03773 $editlink = $sk->doEditSectionLink(Title::newFromText( $titleText ), "T-$sectionIndex");
03774 } else {
03775 $editlink = $sk->doEditSectionLink($this->mTitle, $sectionIndex, $headlineHint);
03776 }
03777 } else {
03778 $editlink = '';
03779 }
03780 $head[$headlineCount] = $sk->makeHeadline( $level,
03781 $matches['attrib'][$headlineCount], $anchor, $headline,
03782 $editlink, $legacyAnchor );
03783
03784 $headlineCount++;
03785 }
03786
03787 $this->setOutputType( $oldType );
03788
03789 # Never ever show TOC if no headers
03790 if( $numVisible < 1 ) {
03791 $enoughToc = false;
03792 }
03793
03794 if( $enoughToc ) {
03795 if( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
03796 $toc .= $sk->tocUnindent( $prevtoclevel - 1 );
03797 }
03798 $toc = $sk->tocList( $toc );
03799 $this->mOutput->setTOCHTML( $toc );
03800 }
03801
03802 if ( $isMain ) {
03803 $this->mOutput->setSections( $tocraw );
03804 }
03805
03806 # split up and insert constructed headlines
03807
03808 $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
03809 $i = 0;
03810
03811 foreach( $blocks as $block ) {
03812 if( $showEditLink && $headlineCount > 0 && $i == 0 && $block !== "\n" ) {
03813 # This is the [edit] link that appears for the top block of text when
03814 # section editing is enabled
03815
03816 # Disabled because it broke block formatting
03817 # For example, a bullet point in the top line
03818 # $full .= $sk->editSectionLink(0);
03819 }
03820 $full .= $block;
03821 if( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) {
03822 # Top anchor now in skin
03823 $full = $full.$toc;
03824 }
03825
03826 if( !empty( $head[$i] ) ) {
03827 $full .= $head[$i];
03828 }
03829 $i++;
03830 }
03831 if( $this->mForceTocPosition ) {
03832 return str_replace( '<!--MWTOC-->', $toc, $full );
03833 } else {
03834 return $full;
03835 }
03836 }
03837
03850 public static function mergeSectionTrees( $tree1, $tree2, $section, $title, $len2 ) {
03851 global $wgContLang;
03852 $newTree = array();
03853 $targetLevel = false;
03854 $merged = false;
03855 $lastLevel = 1;
03856 $nextIndex = 1;
03857 $numbering = array( 0 );
03858 $titletext = $title->getPrefixedDBkey();
03859 foreach ( $tree1 as $s ) {
03860 if ( $targetLevel !== false ) {
03861 if ( $s['level'] <= $targetLevel )
03862
03863 $targetLevel = false;
03864 else
03865 continue;
03866 }
03867 if ( $s['index'] != $section ||
03868 $s['fromtitle'] != $titletext ) {
03869 self::incrementNumbering( $numbering,
03870 $s['toclevel'], $lastLevel );
03871
03872
03873 if ( $s['fromtitle'] == $titletext ) {
03874 $s['index'] = $nextIndex++;
03875 if ( $merged )
03876 $s['byteoffset'] += $len2;
03877 }
03878 $s['number'] = implode( '.', array_map(
03879 array( $wgContLang, 'formatnum' ),
03880 $numbering ) );
03881 $lastLevel = $s['toclevel'];
03882 $newTree[] = $s;
03883 } else {
03884
03885
03886 foreach ( $tree2 as $s2 ) {
03887
03888
03889 $s2['toclevel'] += $s['toclevel'] - 1;
03890 $s2['level'] += $s['level'] - 1;
03891 $s2['index'] = $nextIndex++;
03892 $s2['byteoffset'] += $s['byteoffset'];
03893
03894 self::incrementNumbering( $numbering,
03895 $s2['toclevel'], $lastLevel );
03896 $s2['number'] = implode( '.', array_map(
03897 array( $wgContLang, 'formatnum' ),
03898 $numbering ) );
03899 $lastLevel = $s2['toclevel'];
03900 $newTree[] = $s2;
03901 }
03902
03903 $targetLevel = $s['level'];
03904 $merged = true;
03905 }
03906 }
03907 return $newTree;
03908 }
03909
03916 private static function incrementNumbering( &$number, $level, $lastLevel ) {
03917 if ( $level > $lastLevel )
03918 $number[$level - 1] = 1;
03919 else if ( $level < $lastLevel ) {
03920 foreach ( $number as $key => $unused )
03921 if ( $key >= $level )
03922 unset( $number[$key] );
03923 $number[$level - 1]++;
03924 } else
03925 $number[$level - 1]++;
03926 }
03927
03940 function preSaveTransform( $text, Title $title, $user, $options, $clearState = true ) {
03941 $this->mOptions = $options;
03942 $this->setTitle( $title );
03943 $this->setOutputType( self::OT_WIKI );
03944
03945 if ( $clearState ) {
03946 $this->clearState();
03947 }
03948
03949 $pairs = array(
03950 "\r\n" => "\n",
03951 );
03952 $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
03953 $text = $this->pstPass2( $text, $user );
03954 $text = $this->mStripState->unstripBoth( $text );
03955 return $text;
03956 }
03957
03962 function pstPass2( $text, $user ) {
03963 global $wgContLang, $wgLocaltimezone;
03964
03965
03966
03967
03968
03969
03970
03971
03972 $ts = $this->mOptions->getTimestamp();
03973 if ( isset( $wgLocaltimezone ) ) {
03974 $tz = $wgLocaltimezone;
03975 } else {
03976 $tz = date_default_timezone_get();
03977 }
03978
03979 $unixts = wfTimestamp( TS_UNIX, $ts );
03980 $oldtz = date_default_timezone_get();
03981 date_default_timezone_set( $tz );
03982 $ts = date( 'YmdHis', $unixts );
03983 $tzMsg = date( 'T', $unixts ); # might vary on DST changeover!
03984
03985
03986
03987
03988
03989 $key = 'timezone-' . strtolower( trim( $tzMsg ) );
03990 $value = wfMsgForContent( $key );
03991 if ( !wfEmptyMsg( $key, $value ) ) $tzMsg = $value;
03992
03993 date_default_timezone_set( $oldtz );
03994
03995 $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
03996
03997 # Variable replacement
03998 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
03999 $text = $this->replaceVariables( $text );
04000
04001 # Signatures
04002 $sigText = $this->getUserSig( $user );
04003 $text = strtr( $text, array(
04004 '~~~~~' => $d,
04005 '~~~~' => "$sigText $d",
04006 '~~~' => $sigText
04007 ) );
04008
04009 # Context links: [[|name]] and [[name (context)|]]
04010 #
04011 global $wgLegalTitleChars;
04012 $tc = "[$wgLegalTitleChars]";
04013 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
04014
04015 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/"; # [[ns:page (context)|]]
04016 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)(($tc+))\\|]]/"; # [[ns:page(context)|]]
04017 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/"; # [[ns:page (context), context|]]
04018 $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]]
04019
04020 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
04021 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
04022 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
04023 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
04024
04025 $t = $this->mTitle->getText();
04026 $m = array();
04027 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
04028 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
04029 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
04030 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
04031 } else {
04032 # if there's no context, don't bother duplicating the title
04033 $text = preg_replace( $p2, '[[\\1]]', $text );
04034 }
04035
04036 # Trim trailing whitespace
04037 $text = rtrim( $text );
04038
04039 return $text;
04040 }
04041
04051 function getUserSig( &$user, $nickname = false, $fancySig = null ) {
04052 global $wgMaxSigChars;
04053
04054 $username = $user->getName();
04055
04056
04057 if ( $nickname === false )
04058 $nickname = $user->getOption( 'nickname' );
04059
04060 if ( is_null( $fancySig ) )
04061 $fancySig = $user->getBoolOption( 'fancysig' );
04062
04063 $nickname = $nickname == null ? $username : $nickname;
04064
04065 if( mb_strlen( $nickname ) > $wgMaxSigChars ) {
04066 $nickname = $username;
04067 wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
04068 } elseif( $fancySig !== false ) {
04069 # Sig. might contain markup; validate this
04070 if( $this->validateSig( $nickname ) !== false ) {
04071 # Validated; clean up (if needed) and return it
04072 return $this->cleanSig( $nickname, true );
04073 } else {
04074 # Failed to validate; fall back to the default
04075 $nickname = $username;
04076 wfDebug( __METHOD__.": $username has bad XML tags in signature.\n" );
04077 }
04078 }
04079
04080
04081 $nickname = $this->cleanSigInSig( $nickname );
04082
04083 # If we're still here, make it a link to the user page
04084 $userText = wfEscapeWikiText( $username );
04085 $nickText = wfEscapeWikiText( $nickname );
04086 if ( $user->isAnon() ) {
04087 return wfMsgExt( 'signature-anon', array( 'content', 'parsemag' ), $userText, $nickText );
04088 } else {
04089 return wfMsgExt( 'signature', array( 'content', 'parsemag' ), $userText, $nickText );
04090 }
04091 }
04092
04099 function validateSig( $text ) {
04100 return( Xml::isWellFormedXmlFragment( $text ) ? $text : false );
04101 }
04102
04113 function cleanSig( $text, $parsing = false ) {
04114 if ( !$parsing ) {
04115 global $wgTitle;
04116 $this->clearState();
04117 $this->setTitle( $wgTitle );
04118 $this->mOptions = new ParserOptions;
04119 $this->setOutputType = self::OT_PREPROCESS;
04120 }
04121
04122 # Option to disable this feature
04123 if ( !$this->mOptions->getCleanSignatures() ) {
04124 return $text;
04125 }
04126
04127 # FIXME: regex doesn't respect extension tags or nowiki
04128 # => Move this logic to braceSubstitution()
04129 $substWord = MagicWord::get( 'subst' );
04130 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
04131 $substText = '{{' . $substWord->getSynonym( 0 );
04132
04133 $text = preg_replace( $substRegex, $substText, $text );
04134 $text = $this->cleanSigInSig( $text );
04135 $dom = $this->preprocessToDom( $text );
04136 $frame = $this->getPreprocessor()->newFrame();
04137 $text = $frame->expand( $dom );
04138
04139 if ( !$parsing ) {
04140 $text = $this->mStripState->unstripBoth( $text );
04141 }
04142
04143 return $text;
04144 }
04145
04151 function cleanSigInSig( $text ) {
04152 $text = preg_replace( '/~{3,5}/', '', $text );
04153 return $text;
04154 }
04155
04161 function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
04162 $this->setTitle( $title );
04163 $this->mOptions = $options;
04164 $this->setOutputType( $outputType );
04165 if ( $clearState ) {
04166 $this->clearState();
04167 }
04168 }
04169
04178 function transformMsg( $text, $options ) {
04179 global $wgTitle;
04180 static $executing = false;
04181
04182 # Guard against infinite recursion
04183 if ( $executing ) {
04184 return $text;
04185 }
04186 $executing = true;
04187
04188 wfProfileIn(__METHOD__);
04189 $text = $this->preprocess( $text, $wgTitle, $options );
04190
04191 $executing = false;
04192 wfProfileOut(__METHOD__);
04193 return $text;
04194 }
04195
04211 function setHook( $tag, $callback ) {
04212 $tag = strtolower( $tag );
04213 $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
04214 $this->mTagHooks[$tag] = $callback;
04215 if( !in_array( $tag, $this->mStripList ) ) {
04216 $this->mStripList[] = $tag;
04217 }
04218
04219 return $oldVal;
04220 }
04221
04222 function setTransparentTagHook( $tag, $callback ) {
04223 $tag = strtolower( $tag );
04224 $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
04225 $this->mTransparentTagHooks[$tag] = $callback;
04226
04227 return $oldVal;
04228 }
04229
04233 function clearTagHooks() {
04234 $this->mTagHooks = array();
04235 $this->mStripList = $this->mDefaultStripList;
04236 }
04237
04282 function setFunctionHook( $id, $callback, $flags = 0 ) {
04283 global $wgContLang;
04284
04285 $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
04286 $this->mFunctionHooks[$id] = array( $callback, $flags );
04287
04288 # Add to function cache
04289 $mw = MagicWord::get( $id );
04290 if( !$mw )
04291 throw new MWException( __METHOD__.'() expecting a magic word identifier.' );
04292
04293 $synonyms = $mw->getSynonyms();
04294 $sensitive = intval( $mw->isCaseSensitive() );
04295
04296 foreach ( $synonyms as $syn ) {
04297 # Case
04298 if ( !$sensitive ) {
04299 $syn = $wgContLang->lc( $syn );
04300 }
04301 # Add leading hash
04302 if ( !( $flags & SFH_NO_HASH ) ) {
04303 $syn = '#' . $syn;
04304 }
04305 # Remove trailing colon
04306 if ( substr( $syn, -1, 1 ) === ':' ) {
04307 $syn = substr( $syn, 0, -1 );
04308 }
04309 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
04310 }
04311 return $oldVal;
04312 }
04313
04319 function getFunctionHooks() {
04320 return array_keys( $this->mFunctionHooks );
04321 }
04322
04328 function setFunctionTagHook( $tag, $callback, $flags ) {
04329 $tag = strtolower( $tag );
04330 $old = isset( $this->mFunctionTagHooks[$tag] ) ?
04331 $this->mFunctionTagHooks[$tag] : null;
04332 $this->mFunctionTagHooks[$tag] = array( $callback, $flags );
04333
04334 if( !in_array( $tag, $this->mStripList ) ) {
04335 $this->mStripList[] = $tag;
04336 }
04337
04338 return $old;
04339 }
04340
04347 function replaceLinkHolders( &$text, $options = 0 ) {
04348 return $this->mLinkHolders->replace( $text );
04349 }
04350
04357 function replaceLinkHoldersText( $text ) {
04358 return $this->mLinkHolders->replaceText( $text );
04359 }
04360
04370 function renderImageGallery( $text, $params ) {
04371 $ig = new ImageGallery();
04372 $ig->setContextTitle( $this->mTitle );
04373 $ig->setShowBytes( false );
04374 $ig->setShowFilename( false );
04375 $ig->setParser( $this );
04376 $ig->setHideBadImages();
04377 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
04378 $ig->useSkin( $this->mOptions->getSkin() );
04379 $ig->mRevisionId = $this->mRevisionId;
04380
04381 if( isset( $params['caption'] ) ) {
04382 $caption = $params['caption'];
04383 $caption = htmlspecialchars( $caption );
04384 $caption = $this->replaceInternalLinks( $caption );
04385 $ig->setCaptionHtml( $caption );
04386 }
04387 if( isset( $params['perrow'] ) ) {
04388 $ig->setPerRow( $params['perrow'] );
04389 }
04390 if( isset( $params['widths'] ) ) {
04391 $ig->setWidths( $params['widths'] );
04392 }
04393 if( isset( $params['heights'] ) ) {
04394 $ig->setHeights( $params['heights'] );
04395 }
04396
04397 wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
04398
04399 $lines = StringUtils::explode( "\n", $text );
04400 foreach ( $lines as $line ) {
04401 # match lines like these:
04402 # Image:someimage.jpg|This is some image
04403 $matches = array();
04404 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
04405 # Skip empty lines
04406 if ( count( $matches ) == 0 ) {
04407 continue;
04408 }
04409
04410 if ( strpos( $matches[0], '%' ) !== false )
04411 $matches[1] = urldecode( $matches[1] );
04412 $tp = Title::newFromText( $matches[1] );
04413 $nt =& $tp;
04414 if( is_null( $nt ) ) {
04415 # Bogus title. Ignore these so we don't bomb out later.
04416 continue;
04417 }
04418 if ( isset( $matches[3] ) ) {
04419 $label = $matches[3];
04420 } else {
04421 $label = '';
04422 }
04423
04424 $html = $this->recursiveTagParse( trim( $label ) );
04425
04426 $ig->add( $nt, $html );
04427
04428 # Only add real images (bug #5586)
04429 if ( $nt->getNamespace() == NS_FILE ) {
04430 $this->mOutput->addImage( $nt->getDBkey() );
04431 }
04432 }
04433 return $ig->toHTML();
04434 }
04435
04436 function getImageParams( $handler ) {
04437 if ( $handler ) {
04438 $handlerClass = get_class( $handler );
04439 } else {
04440 $handlerClass = '';
04441 }
04442 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
04443
04444 static $internalParamNames = array(
04445 'horizAlign' => array( 'left', 'right', 'center', 'none' ),
04446 'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
04447 'bottom', 'text-bottom' ),
04448 'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
04449 'upright', 'border', 'link', 'alt' ),
04450 );
04451 static $internalParamMap;
04452 if ( !$internalParamMap ) {
04453 $internalParamMap = array();
04454 foreach ( $internalParamNames as $type => $names ) {
04455 foreach ( $names as $name ) {
04456 $magicName = str_replace( '-', '_', "img_$name" );
04457 $internalParamMap[$magicName] = array( $type, $name );
04458 }
04459 }
04460 }
04461
04462
04463 $paramMap = $internalParamMap;
04464 if ( $handler ) {
04465 $handlerParamMap = $handler->getParamMap();
04466 foreach ( $handlerParamMap as $magic => $paramName ) {
04467 $paramMap[$magic] = array( 'handler', $paramName );
04468 }
04469 }
04470 $this->mImageParams[$handlerClass] = $paramMap;
04471 $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
04472 }
04473 return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
04474 }
04475
04482 function makeImage( $title, $options, $holders = false ) {
04483 # Check if the options text is of the form "options|alt text"
04484 # Options are:
04485 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
04486 # * left no resizing, just left align. label is used for alt= only
04487 # * right same, but right aligned
04488 # * none same, but not aligned
04489 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
04490 # * center center the image
04491 # * frame Keep original image size, no magnify-button.
04492 # * framed Same as "frame"
04493 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
04494 # * upright reduce width for upright images, rounded to full __0 px
04495 # * border draw a 1px border around the image
04496 # * alt Text for HTML alt attribute (defaults to empty)
04497 # * link Set the target of the image link. Can be external, interwiki, or local
04498 # vertical-align values (no % or length right now):
04499 # * baseline
04500 # * sub
04501 # * super
04502 # * top
04503 # * text-top
04504 # * middle
04505 # * bottom
04506 # * text-bottom
04507
04508 $parts = StringUtils::explode( "|", $options );
04509 $sk = $this->mOptions->getSkin();
04510
04511 # Give extensions a chance to select the file revision for us
04512 $skip = $time = $descQuery = false;
04513 wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time, &$descQuery ) );
04514
04515 if ( $skip ) {
04516 return $sk->link( $title );
04517 }
04518
04519 # Get the file
04520 $imagename = $title->getDBkey();
04521 $file = wfFindFile( $title, array( 'time' => $time ) );
04522 # Get parameter map
04523 $handler = $file ? $file->getHandler() : false;
04524
04525 list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
04526
04527 # Process the input parameters
04528 $caption = '';
04529 $params = array( 'frame' => array(), 'handler' => array(),
04530 'horizAlign' => array(), 'vertAlign' => array() );
04531 foreach( $parts as $part ) {
04532 $part = trim( $part );
04533 list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
04534 $validated = false;
04535 if( isset( $paramMap[$magicName] ) ) {
04536 list( $type, $paramName ) = $paramMap[$magicName];
04537
04538
04539 if( $type === 'handler' && $paramName === 'width' ) {
04540 $m = array();
04541 # (bug 13500) In both cases (width/height and width only),
04542 # permit trailing "px" for backward compatibility.
04543 if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
04544 $width = intval( $m[1] );
04545 $height = intval( $m[2] );
04546 if ( $handler->validateParam( 'width', $width ) ) {
04547 $params[$type]['width'] = $width;
04548 $validated = true;
04549 }
04550 if ( $handler->validateParam( 'height', $height ) ) {
04551 $params[$type]['height'] = $height;
04552 $validated = true;
04553 }
04554 } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
04555 $width = intval( $value );
04556 if ( $handler->validateParam( 'width', $width ) ) {
04557 $params[$type]['width'] = $width;
04558 $validated = true;
04559 }
04560 }
04561 } else {
04562 if ( $type === 'handler' ) {
04563 # Validate handler parameter
04564 $validated = $handler->validateParam( $paramName, $value );
04565 } else {
04566 # Validate internal parameters
04567 switch( $paramName ) {
04568 case 'manualthumb':
04569 case 'alt':
04570
04571
04572
04573 $validated = true;
04574 $value = $this->stripAltText( $value, $holders );
04575 break;
04576 case 'link':
04577 $chars = self::EXT_LINK_URL_CLASS;
04578 $prots = $this->mUrlProtocols;
04579 if ( $value === '' ) {
04580 $paramName = 'no-link';
04581 $value = true;
04582 $validated = true;
04583 } elseif ( preg_match( "/^$prots/", $value ) ) {
04584 if ( preg_match( "/^($prots)$chars+$/", $value, $m ) ) {
04585 $paramName = 'link-url';
04586 $this->mOutput->addExternalLink( $value );
04587 $validated = true;
04588 }
04589 } else {
04590 $linkTitle = Title::newFromText( $value );
04591 if ( $linkTitle ) {
04592 $paramName = 'link-title';
04593 $value = $linkTitle;
04594 $this->mOutput->addLink( $linkTitle );
04595 $validated = true;
04596 }
04597 }
04598 break;
04599 default:
04600
04601 $validated = ( $value === false || is_numeric( trim( $value ) ) );
04602 }
04603 }
04604
04605 if ( $validated ) {
04606 $params[$type][$paramName] = $value;
04607 }
04608 }
04609 }
04610 if ( !$validated ) {
04611 $caption = $part;
04612 }
04613 }
04614
04615 # Process alignment parameters
04616 if ( $params['horizAlign'] ) {
04617 $params['frame']['align'] = key( $params['horizAlign'] );
04618 }
04619 if ( $params['vertAlign'] ) {
04620 $params['frame']['valign'] = key( $params['vertAlign'] );
04621 }
04622
04623 $params['frame']['caption'] = $caption;
04624
04625 # Will the image be presented in a frame, with the caption below?
04626 $imageIsFramed = isset( $params['frame']['frame'] ) ||
04627 isset( $params['frame']['framed'] ) ||
04628 isset( $params['frame']['thumbnail'] ) ||
04629 isset( $params['frame']['manualthumb'] );
04630
04631 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
04632 # came to also set the caption, ordinary text after the image -- which
04633 # makes no sense, because that just repeats the text multiple times in
04634 # screen readers. It *also* came to set the title attribute.
04635 #
04636 # Now that we have an alt attribute, we should not set the alt text to
04637 # equal the caption: that's worse than useless, it just repeats the
04638 # text. This is the framed/thumbnail case. If there's no caption, we
04639 # use the unnamed parameter for alt text as well, just for the time be-
04640 # ing, if the unnamed param is set and the alt param is not.
04641 #
04642 # For the future, we need to figure out if we want to tweak this more,
04643 # e.g., introducing a title= parameter for the title; ignoring the un-
04644 # named parameter entirely for images without a caption; adding an ex-
04645 # plicit caption= parameter and preserving the old magic unnamed para-
04646 # meter for BC; ...
04647 if ( $imageIsFramed ) { # Framed image
04648 if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
04649 # No caption or alt text, add the filename as the alt text so
04650 # that screen readers at least get some description of the image
04651 $params['frame']['alt'] = $title->getText();
04652 }
04653 # Do not set $params['frame']['title'] because tooltips don't make sense
04654 # for framed images
04655 } else { # Inline image
04656 if ( !isset( $params['frame']['alt'] ) ) {
04657 # No alt text, use the "caption" for the alt text
04658 if ( $caption !== '') {
04659 $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
04660 } else {
04661 # No caption, fall back to using the filename for the
04662 # alt text
04663 $params['frame']['alt'] = $title->getText();
04664 }
04665 }
04666 # Use the "caption" for the tooltip text
04667 $params['frame']['title'] = $this->stripAltText( $caption, $holders );
04668 }
04669
04670 wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) );
04671
04672 # Linker does the rest
04673 $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'], $time, $descQuery );
04674
04675 # Give the handler a chance to modify the parser object
04676 if ( $handler ) {
04677 $handler->parserTransformHook( $this, $file );
04678 }
04679
04680 return $ret;
04681 }
04682
04683 protected function stripAltText( $caption, $holders ) {
04684 # Strip bad stuff out of the title (tooltip). We can't just use
04685 # replaceLinkHoldersText() here, because if this function is called
04686 # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
04687 if ( $holders ) {
04688 $tooltip = $holders->replaceText( $caption );
04689 } else {
04690 $tooltip = $this->replaceLinkHoldersText( $caption );
04691 }
04692
04693 # make sure there are no placeholders in thumbnail attributes
04694 # that are later expanded to html- so expand them now and
04695 # remove the tags
04696 $tooltip = $this->mStripState->unstripBoth( $tooltip );
04697 $tooltip = Sanitizer::stripAllTags( $tooltip );
04698
04699 return $tooltip;
04700 }
04701
04706 function disableCache() {
04707 wfDebug( "Parser output marked as uncacheable.\n" );
04708 $this->mOutput->mCacheTime = -1;
04709 }
04710
04719 function attributeStripCallback( &$text, $frame = false ) {
04720 $text = $this->replaceVariables( $text, $frame );
04721 $text = $this->mStripState->unstripBoth( $text );
04722 return $text;
04723 }
04724
04730 function Title( $x = null ) { return wfSetVar( $this->mTitle, $x ); }
04731 function Options( $x = null ) { return wfSetVar( $this->mOptions, $x ); }
04732 function OutputType( $x = null ) { return wfSetVar( $this->mOutputType, $x ); }
04738 function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); }
04767 private function extractSections( $text, $section, $mode, $newText='' ) {
04768 global $wgTitle;
04769 $this->clearState();
04770 $this->setTitle( $wgTitle );
04771 $this->mOptions = new ParserOptions;
04772 $this->setOutputType( self::OT_WIKI );
04773 $outText = '';
04774 $frame = $this->getPreprocessor()->newFrame();
04775
04776
04777 $flags = 0;
04778 $sectionParts = explode( '-', $section );
04779 $sectionIndex = array_pop( $sectionParts );
04780 foreach ( $sectionParts as $part ) {
04781 if ( $part === 'T' ) {
04782 $flags |= self::PTD_FOR_INCLUSION;
04783 }
04784 }
04785
04786 $root = $this->preprocessToDom( $text, $flags );
04787
04788
04789
04790 $node = $root->getFirstChild();
04791
04792
04793 if ( $sectionIndex == 0 ) {
04794
04795 $targetLevel = 1000;
04796 } else {
04797 while ( $node ) {
04798 if ( $node->getName() === 'h' ) {
04799 $bits = $node->splitHeading();
04800 if ( $bits['i'] == $sectionIndex ) {
04801 $targetLevel = $bits['level'];
04802 break;
04803 }
04804 }
04805 if ( $mode === 'replace' ) {
04806 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
04807 }
04808 $node = $node->getNextSibling();
04809 }
04810 }
04811
04812 if ( !$node ) {
04813
04814 if ( $mode === 'get' ) {
04815 return $newText;
04816 } else {
04817 return $text;
04818 }
04819 }
04820
04821
04822 do {
04823 if ( $node->getName() === 'h' ) {
04824 $bits = $node->splitHeading();
04825 $curLevel = $bits['level'];
04826 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
04827 break;
04828 }
04829 }
04830 if ( $mode === 'get' ) {
04831 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
04832 }
04833 $node = $node->getNextSibling();
04834 } while ( $node );
04835
04836
04837 if ( $mode === 'replace' ) {
04838
04839
04840
04841
04842 if($newText != "") {
04843 $outText .= $newText . "\n\n";
04844 }
04845
04846 while ( $node ) {
04847 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
04848 $node = $node->getNextSibling();
04849 }
04850 }
04851
04852 if ( is_string( $outText ) ) {
04853
04854 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
04855 }
04856
04857 return $outText;
04858 }
04859
04872 public function getSection( $text, $section, $deftext='' ) {
04873 return $this->extractSections( $text, $section, "get", $deftext );
04874 }
04875
04876 public function replaceSection( $oldtext, $section, $text ) {
04877 return $this->extractSections( $oldtext, $section, "replace", $text );
04878 }
04879
04884 function getRevisionTimestamp() {
04885 if ( is_null( $this->mRevisionTimestamp ) ) {
04886 wfProfileIn( __METHOD__ );
04887 global $wgContLang;
04888 $dbr = wfGetDB( DB_SLAVE );
04889 $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
04890 array( 'rev_id' => $this->mRevisionId ), __METHOD__ );
04891
04892
04893
04894
04895
04896 $timestamp = wfTimestamp( TS_MW, $timestamp );
04897
04898
04899
04900
04901
04902
04903
04904 $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
04905
04906 wfProfileOut( __METHOD__ );
04907 }
04908 return $this->mRevisionTimestamp;
04909 }
04910
04914 function getRevisionUser() {
04915
04916
04917 if( $this->mRevisionId ) {
04918 $revision = Revision::newFromId( $this->mRevisionId );
04919 $revuser = $revision->getUserText();
04920 } else {
04921 global $wgUser;
04922 $revuser = $wgUser->getName();
04923 }
04924 return $revuser;
04925 }
04926
04932 public function setDefaultSort( $sort ) {
04933 $this->mDefaultSort = $sort;
04934 }
04935
04942 public function getDefaultSort() {
04943 global $wgCategoryPrefixedDefaultSortkey;
04944 if( $this->mDefaultSort !== false ) {
04945 return $this->mDefaultSort;
04946 } elseif ($this->mTitle->getNamespace() == NS_CATEGORY ||
04947 !$wgCategoryPrefixedDefaultSortkey) {
04948 return $this->mTitle->getText();
04949 } else {
04950 return $this->mTitle->getPrefixedText();
04951 }
04952 }
04953
04960 public function getCustomDefaultSort() {
04961 return $this->mDefaultSort;
04962 }
04963
04969 public function guessSectionNameFromWikiText( $text ) {
04970 # Strip out wikitext links(they break the anchor)
04971 $text = $this->stripSectionName( $text );
04972 $headline = Sanitizer::decodeCharReferences( $text );
04973 # strip out HTML
04974 $headline = StringUtils::delimiterReplace( '<', '>', '', $headline );
04975 $headline = trim( $headline );
04976 $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) );
04977 $replacearray = array(
04978 '%3A' => ':',
04979 '%' => '.'
04980 );
04981 return str_replace(
04982 array_keys( $replacearray ),
04983 array_values( $replacearray ),
04984 $sectionanchor );
04985 }
04986
05001 public function stripSectionName( $text ) {
05002 # Strip internal link markup
05003 $text = preg_replace('/\[\[:?([^[|]+)\|([^[]+)\]\]/','$2',$text);
05004 $text = preg_replace('/\[\[:?([^[]+)\|?\]\]/','$1',$text);
05005
05006 # Strip external link markup (FIXME: Not Tolerant to blank link text
05007 # I.E. [http://www.mediawiki.org] will render as [1] or something depending
05008 # on how many empty links there are on the page - need to figure that out.
05009 $text = preg_replace('/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/','$2',$text);
05010
05011 # Parse wikitext quotes (italics & bold)
05012 $text = $this->doQuotes($text);
05013
05014 # Strip HTML tags
05015 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
05016 return $text;
05017 }
05018
05019 function srvus( $text ) {
05020 return $this->testSrvus( $text, $this->mOutputType );
05021 }
05022
05026 function testSrvus( $text, $title, $options, $outputType = self::OT_HTML ) {
05027 $this->clearState();
05028 if ( ! ( $title instanceof Title ) ) {
05029 $title = Title::newFromText( $title );
05030 }
05031 $this->mTitle = $title;
05032 $this->mOptions = $options;
05033 $this->setOutputType( $outputType );
05034 $text = $this->replaceVariables( $text );
05035 $text = $this->mStripState->unstripBoth( $text );
05036 $text = Sanitizer::removeHTMLtags( $text );
05037 return $text;
05038 }
05039
05040 function testPst( $text, $title, $options ) {
05041 global $wgUser;
05042 if ( ! ( $title instanceof Title ) ) {
05043 $title = Title::newFromText( $title );
05044 }
05045 return $this->preSaveTransform( $text, $title, $wgUser, $options );
05046 }
05047
05048 function testPreprocess( $text, $title, $options ) {
05049 if ( ! ( $title instanceof Title ) ) {
05050 $title = Title::newFromText( $title );
05051 }
05052 return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
05053 }
05054
05055 function markerSkipCallback( $s, $callback ) {
05056 $i = 0;
05057 $out = '';
05058 while ( $i < strlen( $s ) ) {
05059 $markerStart = strpos( $s, $this->mUniqPrefix, $i );
05060 if ( $markerStart === false ) {
05061 $out .= call_user_func( $callback, substr( $s, $i ) );
05062 break;
05063 } else {
05064 $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
05065 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
05066 if ( $markerEnd === false ) {
05067 $out .= substr( $s, $markerStart );
05068 break;
05069 } else {
05070 $markerEnd += strlen( self::MARKER_SUFFIX );
05071 $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
05072 $i = $markerEnd;
05073 }
05074 }
05075 }
05076 return $out;
05077 }
05078
05079 function serialiseHalfParsedText( $text ) {
05080 $data = array();
05081 $data['text'] = $text;
05082
05083
05084
05085 $stripState = new StripState;
05086 $pos = 0;
05087 while( ( $start_pos = strpos( $text, $this->mUniqPrefix, $pos ) ) && ( $end_pos = strpos( $text, self::MARKER_SUFFIX, $pos ) ) ) {
05088 $end_pos += strlen( self::MARKER_SUFFIX );
05089 $marker = substr( $text, $start_pos, $end_pos-$start_pos );
05090
05091 if ( !empty( $this->mStripState->general->data[$marker] ) ) {
05092 $replaceArray = $stripState->general;
05093 $stripText = $this->mStripState->general->data[$marker];
05094 } elseif ( !empty( $this->mStripState->nowiki->data[$marker] ) ) {
05095 $replaceArray = $stripState->nowiki;
05096 $stripText = $this->mStripState->nowiki->data[$marker];
05097 } else {
05098 throw new MWException( "Hanging strip marker: '$marker'." );
05099 }
05100
05101 $replaceArray->setPair( $marker, $stripText );
05102 $pos = $end_pos;
05103 }
05104 $data['stripstate'] = $stripState;
05105
05106
05107
05108 $links = array( 'internal' => array(), 'interwiki' => array() );
05109 $pos = 0;
05110
05111
05112 while( ( $start_pos = strpos( $text, '<!--LINK ', $pos ) ) ) {
05113 list( $ns, $trail ) = explode( ':', substr( $text, $start_pos + strlen( '<!--LINK ' ) ), 2 );
05114
05115 $ns = trim($ns);
05116 if (empty( $links['internal'][$ns] )) {
05117 $links['internal'][$ns] = array();
05118 }
05119
05120 $key = trim( substr( $trail, 0, strpos( $trail, '-->' ) ) );
05121 $links['internal'][$ns][] = $this->mLinkHolders->internals[$ns][$key];
05122 $pos = $start_pos + strlen( "<!--LINK $ns:$key-->" );
05123 }
05124
05125 $pos = 0;
05126
05127
05128 while( ( $start_pos = strpos( $text, '<!--IWLINK ', $pos ) ) ) {
05129 $data = substr( $text, $start_pos );
05130 $key = trim( substr( $data, 0, strpos( $data, '-->' ) ) );
05131 $links['interwiki'][] = $this->mLinkHolders->interwiki[$key];
05132 $pos = $start_pos + strlen( "<!--IWLINK $key-->" );
05133 }
05134
05135 $data['linkholder'] = $links;
05136
05137 return $data;
05138 }
05139
05140 function unserialiseHalfParsedText( $data, $intPrefix = null ) {
05141 if (!$intPrefix)
05142 $intPrefix = $this->getRandomString();
05143
05144
05145 $stripState = $data['stripstate'];
05146 $this->mStripState->general->merge( $stripState->general );
05147 $this->mStripState->nowiki->merge( $stripState->nowiki );
05148
05149
05150 $text = $data['text'];
05151 $links = $data['linkholder'];
05152
05153
05154 foreach( $links['internal'] as $ns => $nsLinks ) {
05155 foreach( $nsLinks as $key => $entry ) {
05156 $newKey = $intPrefix . '-' . $key;
05157 $this->mLinkHolders->internals[$ns][$newKey] = $entry;
05158
05159 $text = str_replace( "<!--LINK $ns:$key-->", "<!--LINK $ns:$newKey-->", $text );
05160 }
05161 }
05162
05163
05164 foreach( $links['interwiki'] as $key => $entry ) {
05165 $newKey = "$intPrefix-$key";
05166 $this->mLinkHolders->interwikis[$newKey] = $entry;
05167
05168 $text = str_replace( "<!--IWLINK $key-->", "<!--IWLINK $newKey-->", $text );
05169 }
05170
05171
05172 return $text;
05173 }
05174 }
05175
05180 class StripState {
05181 var $general, $nowiki;
05182
05183 function __construct() {
05184 $this->general = new ReplacementArray;
05185 $this->nowiki = new ReplacementArray;
05186 }
05187
05188 function unstripGeneral( $text ) {
05189 wfProfileIn( __METHOD__ );
05190 do {
05191 $oldText = $text;
05192 $text = $this->general->replace( $text );
05193 } while ( $text !== $oldText );
05194 wfProfileOut( __METHOD__ );
05195 return $text;
05196 }
05197
05198 function unstripNoWiki( $text ) {
05199 wfProfileIn( __METHOD__ );
05200 do {
05201 $oldText = $text;
05202 $text = $this->nowiki->replace( $text );
05203 } while ( $text !== $oldText );
05204 wfProfileOut( __METHOD__ );
05205 return $text;
05206 }
05207
05208 function unstripBoth( $text ) {
05209 wfProfileIn( __METHOD__ );
05210 do {
05211 $oldText = $text;
05212 $text = $this->general->replace( $text );
05213 $text = $this->nowiki->replace( $text );
05214 } while ( $text !== $oldText );
05215 wfProfileOut( __METHOD__ );
05216 return $text;
05217 }
05218 }
05219
05224 class OnlyIncludeReplacer {
05225 var $output = '';
05226
05227 function replace( $matches ) {
05228 if ( substr( $matches[1], -1 ) === "\n" ) {
05229 $this->output .= substr( $matches[1], 0, -1 );
05230 } else {
05231 $this->output .= $matches[1];
05232 }
05233 }
05234 }