73 const VERSION =
'1.6.4';
79 const HALF_PARSED_VERSION = 2;
81 # Flags for Parser::setFunctionHook
82 # Also available as global constants from Defines.php
86 # Constants needed for external link processing
87 # Everything except bracket, space, or control characters
88 # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
89 # as well as U+3000 is IDEOGRAPHIC SPACE for bug 19052
90 const EXT_LINK_URL_CLASS =
'[^][<>"\\x00-\\x20\\x7F\p{Zs}]';
91 const EXT_IMAGE_REGEX =
'/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F\p{Zs}]+)
92 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
94 # State constants for the definition list colon extraction
95 const COLON_STATE_TEXT = 0;
96 const COLON_STATE_TAG = 1;
97 const COLON_STATE_TAGSTART = 2;
98 const COLON_STATE_CLOSETAG = 3;
99 const COLON_STATE_TAGSLASH = 4;
100 const COLON_STATE_COMMENT = 5;
101 const COLON_STATE_COMMENTDASH = 6;
102 const COLON_STATE_COMMENTDASHDASH = 7;
104 # Flags for preprocessToDom
105 const PTD_FOR_INCLUSION = 1;
107 # Allowed values for $this->mOutputType
108 # Parameter to startExternalParse().
110 const
OT_WIKI = 2;
# like preSaveTransform()
113 const
OT_PLAIN = 4;
# like extractSections() - portions of the original are returned unchanged.
115 # Marker Suffix needs to be accessible staticly.
116 const MARKER_SUFFIX =
"-QINU\x7f";
118 # Markers used for wrapping the table of contents
119 const TOC_START =
'<mw:toc>';
120 const TOC_END =
'</mw:toc>';
123 var $mTagHooks =
array();
124 var $mTransparentTagHooks =
array();
125 var $mFunctionHooks =
array();
127 var $mFunctionTagHooks =
array();
128 var $mStripList =
array();
129 var $mDefaultStripList =
array();
130 var $mVarCache =
array();
131 var $mImageParams =
array();
132 var $mImageParamsMagicArray =
array();
133 var $mMarkerIndex = 0;
134 var $mFirstCall =
true;
136 # Initialised by initialiseVariables()
147 var $mConf, $mPreprocessor, $mExtLinkBracketedRegex, $mUrlProtocols; # Initialised
in constructor
149 # Cleared with clearState():
154 var $mAutonumber, $mDTopen;
161 var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
168 var $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
170 var $mTplExpandCache; # empty-frame expansion
cache
171 var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
172 var $mExpensiveFunctionCount; # number
of expensive parser
function calls
173 var $mShowToc, $mForceTocPosition;
181 # These are variables reset at least once per parse regardless of $clearState
191 var $mTitle; #
Title context,
used for self-link rendering and similar
things
192 var $mOutputType; # Output
type, one
of the OT_xxx constants
193 var $ot; # Shortcut alias,
see setOutputType()
194 var $mRevisionObject;
# The revision object of the specified revision ID
195 var $mRevisionId; #
ID to display
in {{REVISIONID}}
tags
196 var $mRevisionTimestamp; # The timestamp
of the specified revision
ID
197 var $mRevisionUser; #
User to display
in {{REVISIONUSER}} tag
198 var $mRevisionSize; # Size to display
in {{REVISIONSIZE}}
variable
199 var $mRevIdForTs; # The revision
ID which was
used to fetch the timestamp
200 var $mInputSize =
false; # For {{PAGESIZE}}
on current
page.
212 var $mLangLinkLanguages;
219 public function __construct( $conf =
array() ) {
220 $this->mConf = $conf;
222 $this->mExtLinkBracketedRegex =
'/\[(((?i)' . $this->mUrlProtocols .
')' .
223 self::EXT_LINK_URL_CLASS .
'+)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
224 if ( isset( $conf[
'preprocessorClass'] ) ) {
225 $this->mPreprocessorClass = $conf[
'preprocessorClass'];
226 } elseif ( defined(
'HPHP_VERSION' ) ) {
227 # Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
228 $this->mPreprocessorClass =
'Preprocessor_Hash';
229 } elseif ( extension_loaded(
'domxml' ) ) {
230 # PECL extension that conflicts with the core DOM extension (bug 13770)
231 wfDebug(
"Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
232 $this->mPreprocessorClass =
'Preprocessor_Hash';
233 } elseif ( extension_loaded(
'dom' ) ) {
234 $this->mPreprocessorClass =
'Preprocessor_DOM';
236 $this->mPreprocessorClass =
'Preprocessor_Hash';
238 wfDebug( __CLASS__ .
": using preprocessor: {$this->mPreprocessorClass}\n" );
244 function __destruct() {
245 if ( isset( $this->mLinkHolders ) ) {
246 unset( $this->mLinkHolders );
249 unset( $this->$name );
263 function firstCallInit() {
264 if ( !$this->mFirstCall ) {
267 $this->mFirstCall =
false;
273 $this->initialiseVariables();
284 function clearState() {
286 if ( $this->mFirstCall ) {
287 $this->firstCallInit();
290 $this->mOptions->registerWatcher(
array( $this->mOutput,
'recordOption' ) );
291 $this->mAutonumber = 0;
292 $this->mLastSection =
'';
293 $this->mDTopen =
false;
294 $this->mIncludeCount =
array();
295 $this->mArgStack =
false;
296 $this->mInPre =
false;
299 $this->mRevisionObject = $this->mRevisionTimestamp =
300 $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize =
null;
301 $this->mVarCache =
array();
303 $this->mLangLinkLanguages =
array();
315 $this->mUniqPrefix =
"\x7fUNIQ" . self::getRandomString();
316 $this->mStripState =
new StripState( $this->mUniqPrefix );
318 # Clear these on every parse, bug 4549
319 $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache =
array();
321 $this->mShowToc =
true;
322 $this->mForceTocPosition =
false;
323 $this->mIncludeSizes =
array(
327 $this->mPPNodeCount = 0;
328 $this->mGeneratedPPNodeCount = 0;
329 $this->mHighestExpansionDepth = 0;
330 $this->mDefaultSort =
false;
331 $this->mHeadings =
array();
332 $this->mDoubleUnderscores =
array();
333 $this->mExpensiveFunctionCount = 0;
336 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
337 $this->mPreprocessor =
null;
362 global $wgUseTidy, $wgAlwaysUseTidy, $wgShowHostnames;
369 $this->mInputSize = strlen( $text );
370 if ( $this->mOptions->getEnableLimitReport() ) {
371 $this->mOutput->resetParseStartTime();
374 # Remove the strip marker tag prefix from the input, if present.
376 $text = str_replace( $this->mUniqPrefix,
'', $text );
379 $oldRevisionId = $this->mRevisionId;
380 $oldRevisionObject = $this->mRevisionObject;
381 $oldRevisionTimestamp = $this->mRevisionTimestamp;
382 $oldRevisionUser = $this->mRevisionUser;
383 $oldRevisionSize = $this->mRevisionSize;
384 if ( $revid !==
null ) {
385 $this->mRevisionId = $revid;
386 $this->mRevisionObject =
null;
387 $this->mRevisionTimestamp =
null;
388 $this->mRevisionUser =
null;
389 $this->mRevisionSize =
null;
392 wfRunHooks(
'ParserBeforeStrip',
array( &$this, &$text, &$this->mStripState ) );
394 wfRunHooks(
'ParserAfterStrip',
array( &$this, &$text, &$this->mStripState ) );
395 $text = $this->internalParse( $text );
396 wfRunHooks(
'ParserAfterParse',
array( &$this, &$text, &$this->mStripState ) );
398 $text = $this->mStripState->unstripGeneral( $text );
400 # Clean up special characters, only run once, next-to-last before doBlockLevels
402 # french spaces, last one Guillemet-left
404 '/(.) (?=\\?|:|;|!|%|\\302\\273)/' =>
'\\1 ',
405 # french spaces, Guillemet-right
406 '/(\\302\\253) /' =>
'\\1 ',
407 '/ (!\s*important)/' =>
' \\1', # Beware
of CSS magic word !important, bug #11874.
409 $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
411 $text = $this->doBlockLevels( $text, $linestart );
413 $this->replaceLinkHolders( $text );
422 if ( !(
$options->getDisableContentConversion()
423 || isset( $this->mDoubleUnderscores[
'nocontentconvert'] ) )
425 if ( !$this->mOptions->getInterfaceMessage() ) {
426 # The position of the convert() call should not be changed. it
427 # assumes that the links are all replaced and the only thing left
428 # is the <nowiki> mark.
429 $text = $this->getConverterLanguage()->convert( $text );
440 if ( !(
$options->getDisableTitleConversion()
441 || isset( $this->mDoubleUnderscores[
'nocontentconvert'] )
442 || isset( $this->mDoubleUnderscores[
'notitleconvert'] )
443 || $this->mOutput->getDisplayTitle() !==
false )
445 $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
446 if ( $convruletitle ) {
447 $this->mOutput->setTitleText( $convruletitle );
449 $titleText = $this->getConverterLanguage()->convertTitle(
$title );
450 $this->mOutput->setTitleText( $titleText );
454 $text = $this->mStripState->unstripNoWiki( $text );
458 $text = $this->replaceTransparentTags( $text );
459 $text = $this->mStripState->unstripGeneral( $text );
463 if ( ( $wgUseTidy && $this->mOptions->getTidy() ) || $wgAlwaysUseTidy ) {
466 # attempt to sanitize at least some nesting problems
467 # (bug #2702 and quite a few others)
470 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
471 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
472 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
473 # fix
up an anchor inside another anchor,
only
474 # at least
for a single single nested link (bug 3695)
475 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
476 '\\1\\2</a>\\3</a>\\1\\4</a>',
477 # fix div inside
inline elements- doBlockLevels won
't wrap a line which
478 # contains a div, so fix it up here; replace
479 # div with escaped text
480 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/
' =>
481 '\\1\\3&
lt;div\\5&
gt;\\6&
lt;/div&
gt;\\8\\9
',
482 # remove empty italic or bold tag pairs, some
483 # introduced by rules above
484 '/<([bi])><\/\\1>/
' => '',
487 $text = preg_replace(
488 array_keys( $tidyregs ),
489 array_values( $tidyregs ),
493 if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
494 $this->limitationWarn( 'expensive-parserfunction
',
495 $this->mExpensiveFunctionCount,
496 $this->mOptions->getExpensiveParserFunctionLimit()
500 wfRunHooks( 'ParserAfterTidy
', array( &$this, &$text ) );
502 # Information on include size limits, for the benefit of users who try to skirt them
503 if ( $this->mOptions->getEnableLimitReport() ) {
504 $max = $this->mOptions->getMaxIncludeSize();
506 $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu
' );
507 if ( $cpuTime !== null ) {
508 $this->mOutput->setLimitReportData( 'limitreport-cputime
',
509 sprintf( "%.3f", $cpuTime )
513 $wallTime = $this->mOutput->getTimeSinceStart( 'wall
' );
514 $this->mOutput->setLimitReportData( 'limitreport-walltime
',
515 sprintf( "%.3f", $wallTime )
518 $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes
',
519 array( $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() )
521 $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes
',
522 array( $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() )
524 $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize
',
525 array( $this->mIncludeSizes['post-expand
'], $max )
527 $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize
',
528 array( $this->mIncludeSizes['arg
'], $max )
530 $this->mOutput->setLimitReportData( 'limitreport-expansiondepth
',
531 array( $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() )
533 $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount
',
534 array( $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() )
538 $limitReport = "NewPP limit report\n";
539 if ( $wgShowHostnames ) {
540 $limitReport .= 'Parsed by
' . wfHostname() . "\n";
542 foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
543 if ( wfRunHooks( 'ParserLimitReportFormat
',
544 array( $key, &$value, &$limitReport, false, false )
546 $keyMsg = wfMessage( $key )->inLanguage( 'en
' )->useDatabase( false );
547 $valueMsg = wfMessage( array( "$key-value-text", "$key-value" ) )
548 ->inLanguage( 'en
' )->useDatabase( false );
549 if ( !$valueMsg->exists() ) {
550 $valueMsg = new RawMessage( '$1
' );
552 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
553 $valueMsg->params( $value );
554 $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
558 // Since we're not really outputting HTML, decode the entities and
560 $limitReport = htmlspecialchars_decode( $limitReport );
565 $limitReport = str_replace(
array(
'-',
'&' ),
array(
'‐',
'&' ), $limitReport );
566 $text .=
"\n<!-- \n$limitReport-->\n";
568 if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
569 wfDebugLog(
'generated-pp-node-count', $this->mGeneratedPPNodeCount .
' ' .
570 $this->mTitle->getPrefixedDBkey() );
573 $this->mOutput->setText( $text );
575 $this->mRevisionId = $oldRevisionId;
576 $this->mRevisionObject = $oldRevisionObject;
577 $this->mRevisionTimestamp = $oldRevisionTimestamp;
578 $this->mRevisionUser = $oldRevisionUser;
579 $this->mRevisionSize = $oldRevisionSize;
580 $this->mInputSize =
false;
584 return $this->mOutput;
598 function recursiveTagParse( $text, $frame =
false ) {
600 wfRunHooks(
'ParserBeforeStrip',
array( &$this, &$text, &$this->mStripState ) );
601 wfRunHooks(
'ParserAfterStrip',
array( &$this, &$text, &$this->mStripState ) );
602 $text = $this->internalParse( $text,
false, $frame );
615 if ( $revid !==
null ) {
616 $this->mRevisionId = $revid;
618 wfRunHooks(
'ParserBeforeStrip',
array( &$this, &$text, &$this->mStripState ) );
619 wfRunHooks(
'ParserAfterStrip',
array( &$this, &$text, &$this->mStripState ) );
620 $text = $this->replaceVariables( $text );
621 $text = $this->mStripState->unstripBoth( $text );
635 public function recursivePreprocess( $text, $frame =
false ) {
637 $text = $this->replaceVariables( $text, $frame );
638 $text = $this->mStripState->unstripBoth( $text );
657 $msg =
new RawMessage( $text );
658 $text = $msg->params(
$params )->plain();
660 # Parser (re)initialisation
664 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
665 $text = $this->getPreprocessor()->newFrame()->expand( $dom,
$flags );
666 $text = $this->mStripState->unstripBoth( $text );
675 public static function getRandomString() {
685 function setUser(
$user ) {
686 $this->mUser =
$user;
694 public function uniqPrefix() {
695 if ( !isset( $this->mUniqPrefix ) ) {
696 # @todo FIXME: This is probably *horribly wrong*
697 # LanguageConverter seems to want $wgParser's uniqPrefix, however
698 # if this is called for a parser cache hit, the parser may not
699 # have ever been initialized in the first place.
700 # Not really sure what the heck is supposed to be going on here.
702 # throw new MWException( "Accessing uninitialized mUniqPrefix" );
704 return $this->mUniqPrefix;
712 function setTitle(
$t ) {
717 if (
$t->hasFragment() ) {
718 # Strip the fragment to avoid various odd effects
719 $this->mTitle = clone
$t;
720 $this->mTitle->setFragment(
'' );
731 function getTitle() {
732 return $this->mTitle;
741 function Title( $x =
null ) {
742 return wfSetVar( $this->mTitle, $x );
750 function setOutputType( $ot ) {
751 $this->mOutputType = $ot;
767 function OutputType( $x =
null ) {
768 return wfSetVar( $this->mOutputType, $x );
776 function getOutput() {
777 return $this->mOutput;
785 function getOptions() {
786 return $this->mOptions;
795 function Options( $x =
null ) {
796 return wfSetVar( $this->mOptions, $x );
802 function nextLinkID() {
803 return $this->mLinkID++;
809 function setLinkID( $id ) {
810 $this->mLinkID = $id;
817 function getFunctionLang() {
818 return $this->getTargetLanguage();
830 public function getTargetLanguage() {
831 $target = $this->mOptions->getTargetLanguage();
833 if ( $target !==
null ) {
835 } elseif ( $this->mOptions->getInterfaceMessage() ) {
836 return $this->mOptions->getUserLangObj();
837 } elseif ( is_null( $this->mTitle ) ) {
838 throw new MWException( __METHOD__ .
': $this->mTitle is null' );
841 return $this->mTitle->getPageLanguage();
847 function getConverterLanguage() {
848 return $this->getTargetLanguage();
858 if ( !is_null( $this->mUser ) ) {
861 return $this->mOptions->getUser();
869 function getPreprocessor() {
870 if ( !isset( $this->mPreprocessor ) ) {
871 $class = $this->mPreprocessorClass;
872 $this->mPreprocessor =
new $class( $this );
874 return $this->mPreprocessor;
897 public static function extractTagsAndParams( $elements, $text, &
$matches, $uniq_prefix =
'' ) {
902 $taglist = implode(
'|', $elements );
903 $start =
"/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" .
">)|<(!--)/i";
905 while ( $text !=
'' ) {
906 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
908 if ( count( $p ) < 5 ) {
911 if ( count( $p ) > 5 ) {
925 $marker =
"$uniq_prefix-$element-" . sprintf(
'%08X',
$n++ ) . self::MARKER_SUFFIX;
926 $stripped .= $marker;
928 if ( $close ===
'/>' ) {
929 # Empty element tag, <tag />
934 if ( $element ===
'!--' ) {
937 $end =
"/(<\\/$element\\s*>)/i";
939 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
941 if ( count( $q ) < 3 ) {
942 # No end tag -- let it run out to the end of the text.
954 "<$element$attributes$close$content$tail" );
964 function getStripList() {
965 return $this->mStripList;
977 function insertStripItem( $text ) {
978 $rnd =
"{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
979 $this->mMarkerIndex++;
980 $this->mStripState->addGeneral( $rnd, $text );
990 function doTableStuff( $text ) {
995 $td_history =
array(); # Is currently a td tag
open?
996 $last_tag_history =
array(); # Save history
of last lag activated (td, th or caption)
997 $tr_history =
array(); # Is currently a tr tag
open?
998 $tr_attributes =
array(); # history
of tr attributes
1000 $indent_level = 0; # indent level
of the
table
1003 $line = trim( $outLine );
1006 $out .= $outLine .
"\n";
1010 $first_character =
$line[0];
1014 # First check if we are starting a new table
1015 $indent_level = strlen(
$matches[1] );
1017 $attributes = $this->mStripState->unstripBoth(
$matches[2] );
1020 $outLine = str_repeat(
'<dl><dd>', $indent_level ) .
"<table{$attributes}>";
1021 array_push( $td_history,
false );
1022 array_push( $last_tag_history,
'' );
1023 array_push( $tr_history,
false );
1024 array_push( $tr_attributes,
'' );
1025 array_push( $has_opened_tr,
false );
1026 } elseif ( count( $td_history ) == 0 ) {
1027 # Don't do any of the following
1028 $out .= $outLine .
"\n";
1030 } elseif ( substr(
$line, 0, 2 ) ===
'|}' ) {
1031 # We are ending a table
1033 $last_tag = array_pop( $last_tag_history );
1035 if ( !array_pop( $has_opened_tr ) ) {
1036 $line =
"<tr><td></td></tr>{$line}";
1039 if ( array_pop( $tr_history ) ) {
1040 $line =
"</tr>{$line}";
1043 if ( array_pop( $td_history ) ) {
1044 $line =
"</{$last_tag}>{$line}";
1046 array_pop( $tr_attributes );
1047 $outLine =
$line . str_repeat(
'</dd></dl>', $indent_level );
1048 } elseif ( substr(
$line, 0, 2 ) ===
'|-' ) {
1049 # Now we have a table row
1050 $line = preg_replace(
'#^\|-+#',
'',
$line );
1052 # Whats after the tag is now only attributes
1053 $attributes = $this->mStripState->unstripBoth(
$line );
1055 array_pop( $tr_attributes );
1056 array_push( $tr_attributes, $attributes );
1059 $last_tag = array_pop( $last_tag_history );
1060 array_pop( $has_opened_tr );
1061 array_push( $has_opened_tr,
true );
1063 if ( array_pop( $tr_history ) ) {
1067 if ( array_pop( $td_history ) ) {
1068 $line =
"</{$last_tag}>{$line}";
1072 array_push( $tr_history,
false );
1073 array_push( $td_history,
false );
1074 array_push( $last_tag_history,
'' );
1075 } elseif ( $first_character ===
'|' || $first_character ===
'!' || substr(
$line, 0, 2 ) ===
'|+' ) {
1076 # This might be cell elements, td, th or captions
1077 if ( substr(
$line, 0, 2 ) ===
'|+' ) {
1078 $first_character =
'+';
1084 if ( $first_character ===
'!' ) {
1088 # Split up multiple cells on the same line.
1089 # FIXME : This can result in improper nesting of tags processed
1090 # by earlier parser steps, but should avoid splitting up eg
1091 # attribute values containing literal "||".
1096 # Loop through each table cell
1097 foreach ( $cells
as $cell ) {
1099 if ( $first_character !==
'+' ) {
1100 $tr_after = array_pop( $tr_attributes );
1101 if ( !array_pop( $tr_history ) ) {
1102 $previous =
"<tr{$tr_after}>\n";
1104 array_push( $tr_history,
true );
1105 array_push( $tr_attributes,
'' );
1106 array_pop( $has_opened_tr );
1107 array_push( $has_opened_tr,
true );
1110 $last_tag = array_pop( $last_tag_history );
1112 if ( array_pop( $td_history ) ) {
1113 $previous =
"</{$last_tag}>\n{$previous}";
1116 if ( $first_character ===
'|' ) {
1118 } elseif ( $first_character ===
'!' ) {
1120 } elseif ( $first_character ===
'+' ) {
1121 $last_tag =
'caption';
1126 array_push( $last_tag_history, $last_tag );
1128 # A cell could contain both parameters and data
1129 $cell_data = explode(
'|', $cell, 2 );
1131 # Bug 553: Note that a '|' inside an invalid link should not
1132 # be mistaken as delimiting cell parameters
1133 if ( strpos( $cell_data[0],
'[[' ) !==
false ) {
1134 $cell =
"{$previous}<{$last_tag}>{$cell}";
1135 } elseif ( count( $cell_data ) == 1 ) {
1136 $cell =
"{$previous}<{$last_tag}>{$cell_data[0]}";
1138 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1140 $cell =
"{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
1144 array_push( $td_history,
true );
1147 $out .= $outLine .
"\n";
1150 # Closing open td, tr && table
1151 while ( count( $td_history ) > 0 ) {
1152 if ( array_pop( $td_history ) ) {
1155 if ( array_pop( $tr_history ) ) {
1158 if ( !array_pop( $has_opened_tr ) ) {
1159 $out .=
"<tr><td></td></tr>\n";
1162 $out .=
"</table>\n";
1165 # Remove trailing line-ending (b/c)
1166 if ( substr(
$out, -1 ) ===
"\n" ) {
1170 # special case: don't return empty table
1171 if (
$out ===
"<table>\n<tr><td></td></tr>\n</table>" ) {
1192 function internalParse( $text, $isMain =
true, $frame =
false ) {
1197 # Hook to suspend the parser in this state
1198 if ( !
wfRunHooks(
'ParserBeforeInternalParse',
array( &$this, &$text, &$this->mStripState ) ) ) {
1203 # if $frame is provided, then use $frame for replacing any variables
1205 # use frame depth to infer how include/noinclude tags should be handled
1206 # depth=0 means this is the top-level document; otherwise it's an included document
1207 if ( !$frame->depth ) {
1210 $flag = Parser::PTD_FOR_INCLUSION;
1212 $dom = $this->preprocessToDom( $text, $flag );
1213 $text = $frame->expand( $dom );
1215 # if $frame is not provided, then use old-style replaceVariables
1216 $text = $this->replaceVariables( $text );
1219 wfRunHooks(
'InternalParseBeforeSanitize',
array( &$this, &$text, &$this->mStripState ) );
1221 wfRunHooks(
'InternalParseBeforeLinks',
array( &$this, &$text, &$this->mStripState ) );
1223 # Tables need to come after variable replacement for things to work
1224 # properly; putting them before other transformations should keep
1225 # exciting things like link expansions from showing up in surprising
1227 $text = $this->doTableStuff( $text );
1229 $text = preg_replace(
'/(^|\n)-----*/',
'\\1<hr />', $text );
1231 $text = $this->doDoubleUnderscore( $text );
1233 $text = $this->doHeadings( $text );
1234 $text = $this->replaceInternalLinks( $text );
1235 $text = $this->doAllQuotes( $text );
1236 $text = $this->replaceExternalLinks( $text );
1238 # replaceInternalLinks may sometimes leave behind
1239 # absolute URLs, which have to be masked to hide them from replaceExternalLinks
1240 $text = str_replace( $this->mUniqPrefix .
'NOPARSE',
'', $text );
1242 $text = $this->doMagicLinks( $text );
1243 $text = $this->formatHeadings( $text, $origText, $isMain );
1260 function doMagicLinks( $text ) {
1263 $urlChar = self::EXT_LINK_URL_CLASS;
1264 $text = preg_replace_callback(
1266 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1267 (<.*?>) | # m[2]: Skip stuff inside HTML elements' .
"
1268 (\\b(?i:$prots)$urlChar+) | # m[3]: Free external links" .
'
1269 (?:RFC|PMID)\s+([0-9]+) | # m[4]: RFC or PMID, capture number
1270 ISBN\s+(\b # m[5]: ISBN, capture number
1271 (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix
1272 (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters
1273 [0-9Xx] # check digit
1275 )!xu',
array( &$this,
'magicLinkCallback' ), $text );
1285 function magicLinkCallback( $m ) {
1286 if ( isset( $m[1] ) && $m[1] !==
'' ) {
1289 } elseif ( isset( $m[2] ) && $m[2] !==
'' ) {
1292 } elseif ( isset( $m[3] ) && $m[3] !==
'' ) {
1293 # Free external link
1294 return $this->makeFreeExternalLink( $m[0] );
1295 } elseif ( isset( $m[4] ) && $m[4] !==
'' ) {
1297 if ( substr( $m[0], 0, 3 ) ===
'RFC' ) {
1300 $cssClass =
'mw-magiclink-rfc';
1302 } elseif ( substr( $m[0], 0, 4 ) ===
'PMID' ) {
1304 $urlmsg =
'pubmedurl';
1305 $cssClass =
'mw-magiclink-pmid';
1308 throw new MWException( __METHOD__ .
': unrecognised match type "' .
1309 substr( $m[0], 0, 20 ) .
'"' );
1311 $url =
wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1313 } elseif ( isset( $m[5] ) && $m[5] !==
'' ) {
1316 $num = strtr( $isbn,
array(
1322 return '<a href="' .
1323 htmlspecialchars( $titleObj->getLocalURL() ) .
1324 "\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
1338 function makeFreeExternalLink( $url ) {
1343 # The characters '<' and '>' (which were escaped by
1344 # removeHTMLtags()) should not be included in
1345 # URLs, per RFC 2396.
1347 if ( preg_match(
'/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1348 $trail = substr( $url, $m2[0][1] ) . $trail;
1349 $url = substr( $url, 0, $m2[0][1] );
1352 # Move trailing punctuation to $trail
1354 # If there is no left bracket, then consider right brackets fair game too
1355 if ( strpos( $url,
'(' ) ===
false ) {
1359 $numSepChars = strspn( strrev( $url ), $sep );
1360 if ( $numSepChars ) {
1361 $trail = substr( $url, -$numSepChars ) . $trail;
1362 $url = substr( $url, 0, -$numSepChars );
1367 # Is this an external image?
1368 $text = $this->maybeMakeExternalImage( $url );
1369 if ( $text ===
false ) {
1370 # Not an image, make a link
1372 $this->getConverterLanguage()->markNoConversion( $url,
true ),
1374 $this->getExternalLinkAttribs( $url ) );
1375 # Register it in the output object...
1376 # Replace unnecessary URL escape codes with their equivalent characters
1377 $pasteurized = self::replaceUnusualEscapes( $url );
1378 $this->mOutput->addExternalLink( $pasteurized );
1381 return $text . $trail;
1393 function doHeadings( $text ) {
1395 for ( $i = 6; $i >= 1; --$i ) {
1396 $h = str_repeat(
'=', $i );
1397 $text = preg_replace(
"/^$h(.+)$h\\s*$/m",
"<h$i>\\1</h$i>", $text );
1411 function doAllQuotes( $text ) {
1416 $outtext .= $this->doQuotes(
$line ) .
"\n";
1418 $outtext = substr( $outtext, 0, -1 );
1430 public function doQuotes( $text ) {
1431 $arr = preg_split(
"/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1432 $countarr = count( $arr );
1433 if ( $countarr == 1 ) {
1442 for ( $i = 1; $i < $countarr; $i += 2 ) {
1443 $thislen = strlen( $arr[$i] );
1447 if ( $thislen == 4 ) {
1448 $arr[$i - 1] .=
"'";
1451 } elseif ( $thislen > 5 ) {
1455 $arr[$i - 1] .= str_repeat(
"'", $thislen - 5 );
1460 if ( $thislen == 2 ) {
1462 } elseif ( $thislen == 3 ) {
1464 } elseif ( $thislen == 5 ) {
1474 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1475 $firstsingleletterword = -1;
1476 $firstmultiletterword = -1;
1478 for ( $i = 1; $i < $countarr; $i += 2 ) {
1479 if ( strlen( $arr[$i] ) == 3 ) {
1480 $x1 = substr( $arr[$i - 1], -1 );
1481 $x2 = substr( $arr[$i - 1], -2, 1 );
1482 if ( $x1 ===
' ' ) {
1483 if ( $firstspace == -1 ) {
1486 } elseif ( $x2 ===
' ' ) {
1487 if ( $firstsingleletterword == -1 ) {
1488 $firstsingleletterword = $i;
1494 if ( $firstmultiletterword == -1 ) {
1495 $firstmultiletterword = $i;
1502 if ( $firstsingleletterword > -1 ) {
1503 $arr[$firstsingleletterword] =
"''";
1504 $arr[$firstsingleletterword - 1] .=
"'";
1505 } elseif ( $firstmultiletterword > -1 ) {
1507 $arr[$firstmultiletterword] =
"''";
1508 $arr[$firstmultiletterword - 1] .=
"'";
1509 } elseif ( $firstspace > -1 ) {
1513 $arr[$firstspace] =
"''";
1514 $arr[$firstspace - 1] .=
"'";
1523 foreach ( $arr
as $r ) {
1524 if ( ( $i % 2 ) == 0 ) {
1525 if ( $state ===
'both' ) {
1531 $thislen = strlen( $r );
1532 if ( $thislen == 2 ) {
1533 if ( $state ===
'i' ) {
1536 } elseif ( $state ===
'bi' ) {
1539 } elseif ( $state ===
'ib' ) {
1542 } elseif ( $state ===
'both' ) {
1543 $output .=
'<b><i>' . $buffer .
'</i>';
1549 } elseif ( $thislen == 3 ) {
1550 if ( $state ===
'b' ) {
1553 } elseif ( $state ===
'bi' ) {
1556 } elseif ( $state ===
'ib' ) {
1559 } elseif ( $state ===
'both' ) {
1560 $output .=
'<i><b>' . $buffer .
'</b>';
1566 } elseif ( $thislen == 5 ) {
1567 if ( $state ===
'b' ) {
1570 } elseif ( $state ===
'i' ) {
1573 } elseif ( $state ===
'bi' ) {
1576 } elseif ( $state ===
'ib' ) {
1579 } elseif ( $state ===
'both' ) {
1580 $output .=
'<i><b>' . $buffer .
'</b></i>';
1591 if ( $state ===
'b' || $state ===
'ib' ) {
1594 if ( $state ===
'i' || $state ===
'bi' || $state ===
'ib' ) {
1597 if ( $state ===
'bi' ) {
1601 if ( $state ===
'both' && $buffer ) {
1602 $output .=
'<b><i>' . $buffer .
'</i></b>';
1620 function replaceExternalLinks( $text ) {
1623 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1624 if ( $bits ===
false ) {
1626 throw new MWException(
"PCRE needs to be compiled with --enable-unicode-properties in order for MediaWiki to function" );
1628 $s = array_shift( $bits );
1631 while ( $i < count( $bits ) ) {
1634 $text = $bits[$i++];
1635 $trail = $bits[$i++];
1637 # The characters '<' and '>' (which were escaped by
1638 # removeHTMLtags()) should not be included in
1639 # URLs, per RFC 2396.
1641 if ( preg_match(
'/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1642 $text = substr( $url, $m2[0][1] ) .
' ' . $text;
1643 $url = substr( $url, 0, $m2[0][1] );
1646 # If the link text is an image URL, replace it with an <img> tag
1647 # This happened by accident in the original parser, but some people used it extensively
1648 $img = $this->maybeMakeExternalImage( $text );
1649 if ( $img !==
false ) {
1655 # Set linktype for CSS - if URL==text, link is essentially free
1656 $linktype = ( $text === $url ) ?
'free' :
'text';
1658 # No link text, e.g. [http://domain.tld/some.link]
1659 if ( $text ==
'' ) {
1661 $langObj = $this->getTargetLanguage();
1662 $text =
'[' . $langObj->formatNum( ++$this->mAutonumber ) .
']';
1663 $linktype =
'autonumber';
1665 # Have link text, e.g. [http://domain.tld/some.link text]s
1670 $text = $this->getConverterLanguage()->markNoConversion( $text );
1674 # Use the encoded URL
1675 # This means that users can paste URLs directly into the text
1676 # Funny characters like ö aren't valid in URLs anyway
1677 # This was changed in August 2004
1679 $this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
1681 # Register link in the output object.
1682 # Replace unnecessary URL escape codes with the referenced character
1683 # This prevents spammers from hiding links from the filters
1684 $pasteurized = self::replaceUnusualEscapes( $url );
1685 $this->mOutput->addExternalLink( $pasteurized );
1701 public static function getExternalLinkRel( $url =
false,
$title =
null ) {
1702 global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
1704 if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1722 function getExternalLinkAttribs( $url =
false ) {
1724 $attribs[
'rel'] = self::getExternalLinkRel( $url, $this->mTitle );
1726 if ( $this->mOptions->getExternalLinkTarget() ) {
1727 $attribs[
'target'] = $this->mOptions->getExternalLinkTarget();
1743 static function replaceUnusualEscapes( $url ) {
1744 return preg_replace_callback(
'/%[0-9A-Fa-f]{2}/',
1745 array( __CLASS__,
'replaceUnusualEscapesCallback' ), $url );
1756 private static function replaceUnusualEscapesCallback(
$matches ) {
1758 $ord = ord( $char );
1759 # Is it an unsafe or HTTP reserved character according to RFC 1738?
1760 if ( $ord > 32 && $ord < 127 && strpos(
'<>"#{}|\^~[]`;/?', $char ) ===
false ) {
1761 # No, shouldn't be escaped
1764 # Yes, leave it escaped
1778 function maybeMakeExternalImage( $url ) {
1779 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
1780 $imagesexception = !empty( $imagesfrom );
1782 # $imagesfrom could be either a single string or an array of strings, parse out the latter
1783 if ( $imagesexception && is_array( $imagesfrom ) ) {
1784 $imagematch =
false;
1785 foreach ( $imagesfrom
as $match ) {
1786 if ( strpos( $url, $match ) === 0 ) {
1791 } elseif ( $imagesexception ) {
1792 $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
1794 $imagematch =
false;
1796 if ( $this->mOptions->getAllowExternalImages()
1797 || ( $imagesexception && $imagematch ) ) {
1798 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
1803 if ( !$text && $this->mOptions->getEnableImageWhitelist()
1804 && preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
1805 $whitelist = explode(
"\n",
wfMessage(
'external_image_whitelist' )->inContentLanguage()->
text() );
1806 foreach ( $whitelist
as $entry ) {
1807 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
1808 if ( strpos( $entry,
'#' ) === 0 || $entry ===
'' ) {
1811 if ( preg_match(
'/' . str_replace(
'/',
'\\/', $entry ) .
'/i', $url ) ) {
1812 # Image matches a whitelist entry
1830 function replaceInternalLinks(
$s ) {
1831 $this->mLinkHolders->merge( $this->replaceInternalLinks2(
$s ) );
1843 function replaceInternalLinks2( &
$s ) {
1847 static $tc =
false, $e1, $e1_img;
1848 # the % is needed to support urlencoded titles as well
1851 # Match a link having the form [[namespace:link|alternate]]trail
1852 $e1 =
"/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
1853 # Match cases where there is no "]]", which might still be images
1854 $e1_img =
"/^([{$tc}]+)\\|(.*)\$/sD";
1859 # split the entire text string on occurrences of [[
1861 # get the first element (all text up to first [[), and remove the space we added
1864 $line = $a->current(); # Workaround
for broken ArrayIterator::next()
that returns "
void"
1865 $s = substr(
$s, 1 );
1867 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
1869 if ( $useLinkPrefixExtension ) {
1870 # Match the end of a line for a word that's not followed by whitespace,
1871 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
1874 $e2 =
"/^((?>.*[^$charset]|))(.+)$/sDu";
1877 if ( is_null( $this->mTitle ) ) {
1880 throw new MWException( __METHOD__ .
": \$this->mTitle is null\n" );
1882 $nottalk = !$this->mTitle->isTalkPage();
1884 if ( $useLinkPrefixExtension ) {
1886 if ( preg_match( $e2,
$s, $m ) ) {
1887 $first_prefix = $m[2];
1889 $first_prefix =
false;
1895 $useSubpages = $this->areSubpagesAllowed();
1898 # Loop for each link
1899 for ( ;
$line !==
false &&
$line !==
null; $a->next(),
$line = $a->current() ) {
1900 # Check for excessive memory usage
1901 if ( $holders->isBig() ) {
1903 # Do the existence check, replace the link holders and clear the array
1904 $holders->replace(
$s );
1908 if ( $useLinkPrefixExtension ) {
1910 if ( preg_match( $e2,
$s, $m ) ) {
1917 if ( $first_prefix ) {
1918 $prefix = $first_prefix;
1919 $first_prefix =
false;
1924 $might_be_img =
false;
1929 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
1930 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
1931 # the real problem is with the $e1 regex
1934 # Still some problems for cases where the ] is meant to be outside punctuation,
1935 # and no image is in sight. See bug 2095.
1938 && substr( $m[3], 0, 1 ) ===
']'
1939 && strpos( $text,
'[' ) !==
false
1941 $text .=
']'; #
so that replaceExternalLinks($text) works
later
1942 $m[3] = substr( $m[3], 1 );
1944 # fix up urlencoded title texts
1945 if ( strpos( $m[1],
'%' ) !==
false ) {
1946 # Should anchors '#' also be rejected?
1947 $m[1] = str_replace(
array(
'<',
'>' ),
array(
'<',
'>' ), rawurldecode( $m[1] ) );
1950 } elseif ( preg_match( $e1_img,
$line, $m ) ) { # Invalid, but might be an image with a link
in its caption
1951 $might_be_img =
true;
1953 if ( strpos( $m[1],
'%' ) !==
false ) {
1954 $m[1] = rawurldecode( $m[1] );
1965 # Don't allow internal links to pages containing
1966 # PROTO: where PROTO is a valid URL protocol; these
1967 # should be external links.
1968 if ( preg_match(
'/^(?i:' . $this->mUrlProtocols .
')/', $m[1] ) ) {
1974 # Make subpage if necessary
1975 if ( $useSubpages ) {
1976 $link = $this->maybeDoSubpageLink( $m[1], $text );
1981 $noforce = ( substr( $m[1], 0, 1 ) !==
':' );
1983 # Strip off leading ':'
1990 if ( $nt ===
null ) {
1996 $ns = $nt->getNamespace();
1997 $iw = $nt->getInterwiki();
2000 if ( $might_be_img ) { #
if this is actually an invalid link
2002 if ( $ns ==
NS_FILE && $noforce ) { # but might be an image
2005 # look at the next 'line' to see if we can close it there
2007 $next_line = $a->current();
2008 if ( $next_line ===
false || $next_line ===
null ) {
2011 $m = explode(
']]', $next_line, 3 );
2012 if ( count( $m ) == 3 ) {
2013 # the first ]] closes the inner link, the second the image
2015 $text .=
"[[{$m[0]}]]{$m[1]}";
2018 } elseif ( count( $m ) == 2 ) {
2019 # if there's exactly one ]] that's fine, we'll keep looking
2020 $text .=
"[[{$m[0]}]]{$m[1]}";
2022 # if $next_line is invalid too, we need look no further
2023 $text .=
'[[' . $next_line;
2028 # we couldn't find the end of this imageLink, so output it raw
2029 # but don't ignore what might be perfectly normal links in the text we've examined
2030 $holders->merge( $this->replaceInternalLinks2( $text ) );
2031 $s .=
"{$prefix}[[$link|$text";
2032 # note: no $trail, because without an end, there *is* no trail
2036 }
else { #
it's not an image, so output it raw
2037 $s .= "{$prefix}[[$link|$text";
2038 # note: no $trail, because without an end, there *is* no trail
2039 wfProfileOut( __METHOD__ . "-might_be_img" );
2042 wfProfileOut( __METHOD__ . "-might_be_img" );
2045 $wasblank = ( $text == '' );
2049 # Bug 4598 madness. Handle the quotes only if they come from the alternate part
2050 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2051 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2052 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2053 $text = $this->doQuotes( $text );
2056 # Link not escaped by : , create the various objects
2059 wfProfileIn( __METHOD__ . "-interwiki" );
2060 if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw
' ) ) {
2061 // XXX: the above check prevents links to sites with identifiers that are not language codes
2063 # Bug 24502: filter duplicates
2064 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2065 $this->mLangLinkLanguages[$iw] = true;
2066 $this->mOutput->addLanguageLink( $nt->getFullText() );
2069 $s = rtrim( $s . $prefix );
2070 $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
2071 wfProfileOut( __METHOD__ . "-interwiki" );
2074 wfProfileOut( __METHOD__ . "-interwiki" );
2076 if ( $ns == NS_FILE ) {
2077 wfProfileIn( __METHOD__ . "-image" );
2078 if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2080 # if no parameters were passed, $text
2081 # becomes something like "File:Foo.png",
2082 # which we don't want to pass
on to the
2086 # recursively parse links inside the image caption
2087 # actually, this will parse them in any other parameters, too,
2088 # but it might be hard to fix that, and it doesn't matter ATM
2089 $text = $this->replaceExternalLinks( $text );
2090 $holders->merge( $this->replaceInternalLinks2( $text ) );
2092 # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2093 $s .= $prefix . $this->armorLinks(
2094 $this->makeImage( $nt, $text, $holders ) ) . $trail;
2096 $s .= $prefix . $trail;
2104 $s = rtrim(
$s .
"\n" ); # bug 87
2107 $sortkey = $this->getDefaultSort();
2112 $sortkey = str_replace(
"\n",
'', $sortkey );
2113 $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2114 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2119 $s .= trim( $prefix . $trail,
"\n" ) ==
'' ?
'' : $prefix . $trail;
2126 # Self-link checking. For some languages, variants of the title are checked in
2127 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2128 # for linking to a different variant.
2129 if ( $ns !=
NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2134 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2135 # @todo FIXME: Should do batch file existence checks, see comment below
2138 # Give extensions a chance to select the file revision for us
2143 # Fetch and register the file (file title may be different via hooks)
2145 # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2146 $s .= $prefix . $this->armorLinks(
2153 # Some titles, such as valid special pages or files in foreign repos, should
2154 # be shown as bluelinks even though they're not included in the page table
2156 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2157 # batch file existence checks for NS_FILE and NS_MEDIA
2158 if ( $iw ==
'' && $nt->isAlwaysKnown() ) {
2159 $this->mOutput->addLink( $nt );
2160 $s .= $this->makeKnownLinkHolder( $nt, $text,
array(), $trail, $prefix );
2162 # Links will be added to the output link list after checking
2163 $s .= $holders->makeHolder( $nt, $text,
array(), $trail, $prefix );
2185 function makeKnownLinkHolder( $nt, $text =
'',
$query =
array(), $trail =
'', $prefix =
'' ) {
2188 if ( is_string(
$query ) ) {
2191 if ( $text ==
'' ) {
2192 $text = htmlspecialchars( $nt->getPrefixedText() );
2197 return $this->armorLinks(
$link ) . $trail;
2210 function armorLinks( $text ) {
2211 return preg_replace(
'/\b((?i)' . $this->mUrlProtocols .
')/',
2212 "{$this->mUniqPrefix}NOPARSE$1", $text );
2219 function areSubpagesAllowed() {
2220 # Some namespaces don't allow subpages
2232 function maybeDoSubpageLink( $target, &$text ) {
2242 function closeParagraph() {
2244 if ( $this->mLastSection !=
'' ) {
2245 $result =
'</' . $this->mLastSection .
">\n";
2247 $this->mInPre =
false;
2248 $this->mLastSection =
'';
2262 function getCommon( $st1, $st2 ) {
2263 $fl = strlen( $st1 );
2264 $shorter = strlen( $st2 );
2265 if ( $fl < $shorter ) {
2269 for ( $i = 0; $i < $shorter; ++$i ) {
2270 if ( $st1[$i] != $st2[$i] ) {
2286 function openList( $char ) {
2287 $result = $this->closeParagraph();
2289 if (
'*' === $char ) {
2291 } elseif (
'#' === $char ) {
2293 } elseif (
':' === $char ) {
2295 } elseif (
';' === $char ) {
2297 $this->mDTopen =
true;
2312 function nextItem( $char ) {
2313 if (
'*' === $char ||
'#' === $char ) {
2314 return "</li>\n<li>";
2315 } elseif (
':' === $char ||
';' === $char ) {
2317 if ( $this->mDTopen ) {
2320 if (
';' === $char ) {
2321 $this->mDTopen =
true;
2322 return $close .
'<dt>';
2324 $this->mDTopen =
false;
2325 return $close .
'<dd>';
2328 return '<!-- ERR 2 -->';
2338 function closeList( $char ) {
2339 if (
'*' === $char ) {
2340 $text =
"</li>\n</ul>";
2341 } elseif (
'#' === $char ) {
2342 $text =
"</li>\n</ol>";
2343 } elseif (
':' === $char ) {
2344 if ( $this->mDTopen ) {
2345 $this->mDTopen =
false;
2346 $text =
"</dt>\n</dl>";
2348 $text =
"</dd>\n</dl>";
2351 return '<!-- ERR 3 -->';
2353 return $text .
"\n";
2365 function doBlockLevels( $text, $linestart ) {
2368 # Parsing through the text line by line. The main thing
2369 # happening here is handling of block-level elements p, pre,
2370 # and making lists from lines starting with * # : etc.
2375 $this->mDTopen = $inBlockElem =
false;
2377 $paragraphStack =
false;
2378 $inBlockquote =
false;
2380 foreach ( $textLines
as $oLine ) {
2382 if ( !$linestart ) {
2392 $lastPrefixLength = strlen( $lastPrefix );
2393 $preCloseMatch = preg_match(
'/<\\/pre/i', $oLine );
2394 $preOpenMatch = preg_match(
'/<pre/i', $oLine );
2395 # If not in a <pre> element, scan for and figure out what prefixes are there.
2396 if ( !$this->mInPre ) {
2397 # Multiple prefixes may abut each other for nested lists.
2398 $prefixLength = strspn( $oLine,
'*#:;' );
2399 $prefix = substr( $oLine, 0, $prefixLength );
2402 # ; and : are both from definition-lists, so they're equivalent
2403 # for the purposes of determining whether or not we need to open/close
2405 $prefix2 = str_replace(
';',
':', $prefix );
2406 $t = substr( $oLine, $prefixLength );
2407 $this->mInPre = (bool)$preOpenMatch;
2409 # Don't interpret any other prefixes in preformatted text
2411 $prefix = $prefix2 =
'';
2416 if ( $prefixLength && $lastPrefix === $prefix2 ) {
2417 # Same as the last item, so no need to deal with nesting or opening stuff
2418 $output .= $this->nextItem( substr( $prefix, -1 ) );
2419 $paragraphStack =
false;
2421 if ( substr( $prefix, -1 ) ===
';' ) {
2422 # The one nasty exception: definition lists work like this:
2423 # ; title : definition text
2424 # So we check for : in the remainder text to split up the
2425 # title and definition, without b0rking links.
2427 if ( $this->findColonNoLinks(
$t,
$term, $t2 ) !==
false ) {
2432 } elseif ( $prefixLength || $lastPrefixLength ) {
2433 # We need to open or close prefixes, or both.
2435 # Either open or close a level...
2436 $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
2437 $paragraphStack =
false;
2439 # Close all the prefixes which aren't shared.
2440 while ( $commonPrefixLength < $lastPrefixLength ) {
2441 $output .= $this->closeList( $lastPrefix[$lastPrefixLength - 1] );
2442 --$lastPrefixLength;
2445 # Continue the current prefix if appropriate.
2446 if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
2447 $output .= $this->nextItem( $prefix[$commonPrefixLength - 1] );
2450 # Open prefixes where appropriate.
2451 while ( $prefixLength > $commonPrefixLength ) {
2452 $char = substr( $prefix, $commonPrefixLength, 1 );
2453 $output .= $this->openList( $char );
2455 if (
';' === $char ) {
2456 # @todo FIXME: This is dupe of code above
2457 if ( $this->findColonNoLinks(
$t,
$term, $t2 ) !==
false ) {
2462 ++$commonPrefixLength;
2464 $lastPrefix = $prefix2;
2467 # If we have no prefixes, go to paragraph mode.
2468 if ( 0 == $prefixLength ) {
2470 # No prefix (not in list)--go to paragraph mode
2471 # XXX: use a stack for nestable elements like span, table and div
2472 $openmatch = preg_match(
'/(?:<table|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<dl|<li|<\\/tr|<\\/td|<\\/th)/iS',
$t );
2473 $closematch = preg_match(
2474 '/(?:<\\/table|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|' .
2475 '<td|<th|<\\/?blockquote|<\\/?div|<hr|<\\/pre|<\\/p|<\\/mw:|' . $this->mUniqPrefix .
'-pre|<\\/li|<\\/ul|<\\/ol|<\\/dl|<\\/?center)/iS',
$t );
2476 if ( $openmatch or $closematch ) {
2477 $paragraphStack =
false;
2478 # TODO bug 5718: paragraph closed
2479 $output .= $this->closeParagraph();
2480 if ( $preOpenMatch and !$preCloseMatch ) {
2481 $this->mInPre =
true;
2484 while ( preg_match(
'/<(\\/?)blockquote[\s>]/i',
$t, $bqMatch, PREG_OFFSET_CAPTURE, $bqOffset ) ) {
2485 $inBlockquote = !$bqMatch[1][0];
2486 $bqOffset = $bqMatch[0][1] + strlen( $bqMatch[0][0] );
2488 $inBlockElem = !$closematch;
2489 } elseif ( !$inBlockElem && !$this->mInPre ) {
2490 if (
' ' == substr(
$t, 0, 1 ) and ( $this->mLastSection ===
'pre' || trim(
$t ) !=
'' ) and !$inBlockquote ) {
2492 if ( $this->mLastSection !==
'pre' ) {
2493 $paragraphStack =
false;
2494 $output .= $this->closeParagraph() .
'<pre>';
2495 $this->mLastSection =
'pre';
2497 $t = substr(
$t, 1 );
2500 if ( trim(
$t ) ===
'' ) {
2501 if ( $paragraphStack ) {
2502 $output .= $paragraphStack .
'<br />';
2503 $paragraphStack =
false;
2504 $this->mLastSection =
'p';
2506 if ( $this->mLastSection !==
'p' ) {
2507 $output .= $this->closeParagraph();
2508 $this->mLastSection =
'';
2509 $paragraphStack =
'<p>';
2511 $paragraphStack =
'</p><p>';
2515 if ( $paragraphStack ) {
2517 $paragraphStack =
false;
2518 $this->mLastSection =
'p';
2519 } elseif ( $this->mLastSection !==
'p' ) {
2520 $output .= $this->closeParagraph() .
'<p>';
2521 $this->mLastSection =
'p';
2528 # somewhere above we forget to get out of pre block (bug 785)
2529 if ( $preCloseMatch && $this->mInPre ) {
2530 $this->mInPre =
false;
2532 if ( $paragraphStack ===
false ) {
2536 while ( $prefixLength ) {
2537 $output .= $this->closeList( $prefix2[$prefixLength - 1] );
2540 if ( $this->mLastSection !=
'' ) {
2541 $output .=
'</' . $this->mLastSection .
'>';
2542 $this->mLastSection =
'';
2559 function findColonNoLinks( $str, &$before, &$after ) {
2562 $pos = strpos( $str,
':' );
2563 if ( $pos ===
false ) {
2569 $lt = strpos( $str,
'<' );
2570 if ( $lt ===
false || $lt > $pos ) {
2571 # Easy; no tag nesting to worry about
2572 $before = substr( $str, 0, $pos );
2573 $after = substr( $str, $pos + 1 );
2578 # Ugly state machine to walk through avoiding tags.
2579 $state = self::COLON_STATE_TEXT;
2581 $len = strlen( $str );
2582 for ( $i = 0; $i < $len; $i++ ) {
2586 # (Using the number is a performance hack for common cases)
2587 case 0: # self::COLON_STATE_TEXT:
2590 # Could be either a <start> tag or an </end> tag
2591 $state = self::COLON_STATE_TAGSTART;
2594 if ( $stack == 0 ) {
2596 $before = substr( $str, 0, $i );
2597 $after = substr( $str, $i + 1 );
2601 # Embedded in a tag; don't break it.
2604 # Skip ahead looking for something interesting
2605 $colon = strpos( $str,
':', $i );
2606 if ( $colon ===
false ) {
2607 # Nothing else interesting
2611 $lt = strpos( $str,
'<', $i );
2612 if ( $stack === 0 ) {
2613 if ( $lt ===
false || $colon < $lt ) {
2615 $before = substr( $str, 0, $colon );
2616 $after = substr( $str, $colon + 1 );
2621 if ( $lt ===
false ) {
2622 # Nothing else interesting to find; abort!
2623 # We're nested, but there's no close tags left. Abort!
2626 # Skip ahead to next tag start
2628 $state = self::COLON_STATE_TAGSTART;
2631 case 1: # self::COLON_STATE_TAG:
2636 $state = self::COLON_STATE_TEXT;
2639 # Slash may be followed by >?
2640 $state = self::COLON_STATE_TAGSLASH;
2646 case 2: # self::COLON_STATE_TAGSTART:
2649 $state = self::COLON_STATE_CLOSETAG;
2652 $state = self::COLON_STATE_COMMENT;
2655 # Illegal early close? This shouldn't happen D:
2656 $state = self::COLON_STATE_TEXT;
2659 $state = self::COLON_STATE_TAG;
2662 case 3: # self::COLON_STATE_CLOSETAG:
2667 wfDebug( __METHOD__ .
": Invalid input; too many close tags\n" );
2671 $state = self::COLON_STATE_TEXT;
2674 case self::COLON_STATE_TAGSLASH:
2676 # Yes, a self-closed tag <blah/>
2677 $state = self::COLON_STATE_TEXT;
2679 # Probably we're jumping the gun, and this is an attribute
2680 $state = self::COLON_STATE_TAG;
2683 case 5: # self::COLON_STATE_COMMENT:
2685 $state = self::COLON_STATE_COMMENTDASH;
2688 case self::COLON_STATE_COMMENTDASH:
2690 $state = self::COLON_STATE_COMMENTDASHDASH;
2692 $state = self::COLON_STATE_COMMENT;
2695 case self::COLON_STATE_COMMENTDASHDASH:
2697 $state = self::COLON_STATE_TEXT;
2699 $state = self::COLON_STATE_COMMENT;
2704 throw new MWException(
"State machine error in " . __METHOD__ );
2708 wfDebug( __METHOD__ .
": Invalid input; not enough close tags (stack $stack, state $state)\n" );
2727 function getVariableValue( $index, $frame =
false ) {
2731 if ( is_null( $this->mTitle ) ) {
2736 throw new MWException( __METHOD__ .
' Should only be '
2737 .
' called while parsing (no title set)' );
2744 if (
wfRunHooks(
'ParserGetVariableValueVarCache',
array( &$this, &$this->mVarCache ) ) ) {
2745 if ( isset( $this->mVarCache[$index] ) ) {
2746 return $this->mVarCache[$index];
2753 $pageLang = $this->getFunctionLang();
2756 case 'currentmonth':
2759 case 'currentmonth1':
2762 case 'currentmonthname':
2765 case 'currentmonthnamegen':
2768 case 'currentmonthabbrev':
2783 case 'localmonthname':
2786 case 'localmonthnamegen':
2789 case 'localmonthabbrev':
2804 case 'fullpagename':
2807 case 'fullpagenamee':
2813 case 'subpagenamee':
2816 case 'rootpagename':
2819 case 'rootpagenamee':
2822 case 'basepagename':
2825 case 'basepagenamee':
2828 case 'talkpagename':
2829 if ( $this->mTitle->canTalk() ) {
2830 $talkPage = $this->mTitle->getTalkPage();
2836 case 'talkpagenamee':
2837 if ( $this->mTitle->canTalk() ) {
2838 $talkPage = $this->mTitle->getTalkPage();
2844 case 'subjectpagename':
2845 $subjPage = $this->mTitle->getSubjectPage();
2848 case 'subjectpagenamee':
2849 $subjPage = $this->mTitle->getSubjectPage();
2853 $pageid = $this->getTitle()->getArticleID();
2854 if ( $pageid == 0 ) {
2855 # 0 means the page doesn't exist in the database,
2856 # which means the user is previewing a new page.
2857 # The vary-revision flag must be set, because the magic word
2858 # will have a different value once the page is saved.
2859 $this->mOutput->setFlag(
'vary-revision' );
2860 wfDebug( __METHOD__ .
": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2862 $value = $pageid ? $pageid :
null;
2865 # Let the edit saving system know we should parse the page
2866 # *after* a revision ID has been assigned.
2867 $this->mOutput->setFlag(
'vary-revision' );
2868 wfDebug( __METHOD__ .
": {{REVISIONID}} used, setting vary-revision...\n" );
2869 $value = $this->mRevisionId;
2872 # Let the edit saving system know we should parse the page
2873 # *after* a revision ID has been assigned. This is for null edits.
2874 $this->mOutput->setFlag(
'vary-revision' );
2875 wfDebug( __METHOD__ .
": {{REVISIONDAY}} used, setting vary-revision...\n" );
2876 $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
2878 case 'revisionday2':
2879 # Let the edit saving system know we should parse the page
2880 # *after* a revision ID has been assigned. This is for null edits.
2881 $this->mOutput->setFlag(
'vary-revision' );
2882 wfDebug( __METHOD__ .
": {{REVISIONDAY2}} used, setting vary-revision...\n" );
2883 $value = substr( $this->getRevisionTimestamp(), 6, 2 );
2885 case 'revisionmonth':
2886 # Let the edit saving system know we should parse the page
2887 # *after* a revision ID has been assigned. This is for null edits.
2888 $this->mOutput->setFlag(
'vary-revision' );
2889 wfDebug( __METHOD__ .
": {{REVISIONMONTH}} used, setting vary-revision...\n" );
2890 $value = substr( $this->getRevisionTimestamp(), 4, 2 );
2892 case 'revisionmonth1':
2893 # Let the edit saving system know we should parse the page
2894 # *after* a revision ID has been assigned. This is for null edits.
2895 $this->mOutput->setFlag(
'vary-revision' );
2896 wfDebug( __METHOD__ .
": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
2897 $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
2899 case 'revisionyear':
2900 # Let the edit saving system know we should parse the page
2901 # *after* a revision ID has been assigned. This is for null edits.
2902 $this->mOutput->setFlag(
'vary-revision' );
2903 wfDebug( __METHOD__ .
": {{REVISIONYEAR}} used, setting vary-revision...\n" );
2904 $value = substr( $this->getRevisionTimestamp(), 0, 4 );
2906 case 'revisiontimestamp':
2907 # Let the edit saving system know we should parse the page
2908 # *after* a revision ID has been assigned. This is for null edits.
2909 $this->mOutput->setFlag(
'vary-revision' );
2910 wfDebug( __METHOD__ .
": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2911 $value = $this->getRevisionTimestamp();
2913 case 'revisionuser':
2914 # Let the edit saving system know we should parse the page
2915 # *after* a revision ID has been assigned. This is for null edits.
2916 $this->mOutput->setFlag(
'vary-revision' );
2917 wfDebug( __METHOD__ .
": {{REVISIONUSER}} used, setting vary-revision...\n" );
2918 $value = $this->getRevisionUser();
2920 case 'revisionsize':
2921 # Let the edit saving system know we should parse the page
2922 # *after* a revision ID has been assigned. This is for null edits.
2923 $this->mOutput->setFlag(
'vary-revision' );
2924 wfDebug( __METHOD__ .
": {{REVISIONSIZE}} used, setting vary-revision...\n" );
2925 $value = $this->getRevisionSize();
2928 $value = str_replace(
'_',
' ',
$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2933 case 'namespacenumber':
2934 $value = $this->mTitle->getNamespace();
2937 $value = $this->mTitle->canTalk() ? str_replace(
'_',
' ', $this->mTitle->getTalkNsText() ) :
'';
2940 $value = $this->mTitle->canTalk() ?
wfUrlencode( $this->mTitle->getTalkNsText() ) :
'';
2942 case 'subjectspace':
2943 $value = str_replace(
'_',
' ', $this->mTitle->getSubjectNsText() );
2945 case 'subjectspacee':
2948 case 'currentdayname':
2961 # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2962 # int to remove the padding
2968 case 'localdayname':
2981 # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2982 # int to remove the padding
2988 case 'numberofarticles':
2991 case 'numberoffiles':
2994 case 'numberofusers':
2997 case 'numberofactiveusers':
3000 case 'numberofpages':
3003 case 'numberofadmins':
3006 case 'numberofedits':
3009 case 'numberofviews':
3010 global $wgDisableCounters;
3013 case 'currenttimestamp':
3016 case 'localtimestamp':
3019 case 'currentversion':
3030 return $serverParts && isset( $serverParts[
'host'] ) ? $serverParts[
'host'] : $wgServer;
3032 return $wgScriptPath;
3034 return $wgStylePath;
3035 case 'directionmark':
3036 return $pageLang->getDirMark();
3037 case 'contentlanguage':
3039 return $wgLanguageCode;
3040 case 'cascadingsources':
3045 wfRunHooks(
'ParserGetVariableValueSwitch',
array( &$this, &$this->mVarCache, &$index, &
$ret, &$frame ) );
3050 $this->mVarCache[$index] =
$value;
3061 function initialiseVariables() {
3093 function preprocessToDom( $text,
$flags = 0 ) {
3094 $dom = $this->getPreprocessor()->preprocessToObj( $text,
$flags );
3105 public static function splitWhitespace(
$s ) {
3106 $ltrimmed = ltrim(
$s );
3107 $w1 = substr(
$s, 0, strlen(
$s ) - strlen( $ltrimmed ) );
3108 $trimmed = rtrim( $ltrimmed );
3109 $diff = strlen( $ltrimmed ) - strlen( $trimmed );
3111 $w2 = substr( $ltrimmed, -$diff );
3115 return array( $w1, $trimmed, $w2 );
3137 function replaceVariables( $text, $frame =
false, $argsOnly =
false ) {
3138 # Is there any text? Also, Prevent too big inclusions!
3139 if ( strlen( $text ) < 1 || strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
3144 if ( $frame ===
false ) {
3145 $frame = $this->getPreprocessor()->newFrame();
3146 } elseif ( !( $frame instanceof
PPFrame ) ) {
3147 wfDebug( __METHOD__ .
" called using plain parameters instead of a PPFrame instance. Creating custom frame.\n" );
3148 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
3151 $dom = $this->preprocessToDom( $text );
3153 $text = $frame->expand( $dom,
$flags );
3166 static function createAssocArgs(
$args ) {
3167 $assocArgs =
array();
3170 $eqpos = strpos( $arg,
'=' );
3171 if ( $eqpos ===
false ) {
3172 $assocArgs[$index++] = $arg;
3174 $name = trim( substr( $arg, 0, $eqpos ) );
3175 $value = trim( substr( $arg, $eqpos + 1 ) );
3176 if (
$value ===
false ) {
3179 if (
$name !==
false ) {
3212 function limitationWarn( $limitationType, $current =
'', $max =
'' ) {
3213 # does no harm if $current and $max are present but are unnecessary for the message
3214 $warning =
wfMessage(
"$limitationType-warning" )->numParams( $current, $max )
3215 ->inLanguage( $this->mOptions->getUserLangObj() )->
text();
3216 $this->mOutput->addWarning( $warning );
3217 $this->addTrackingCategory(
"$limitationType-category" );
3233 function braceSubstitution( $piece, $frame ) {
3238 $found =
false; # $text has been filled
3239 $nowiki =
false; #
wiki markup
in $text should be escaped
3240 $isHTML =
false; # $text
is HTML, armour
it against wikitext transformation
3241 $forceRawInterwiki =
false; # Force interwiki transclusion to be done
in raw mode not rendered
3242 $isChildObj =
false; # $text
is a DOM node needing expansion
in a child frame
3243 $isLocalObj =
false; # $text
is a DOM node needing expansion
in the current frame
3245 # Title object, where $text came from
3248 # $part1 is the bit before the first |, and must contain only title characters.
3249 # Various prefixes will be stripped from it later.
3250 $titleWithSpaces = $frame->expand( $piece[
'title'] );
3251 $part1 = trim( $titleWithSpaces );
3254 # Original title text preserved for various purposes
3255 $originalTitle = $part1;
3257 # $args is a list of argument nodes, starting from index 0, not including $part1
3258 # @todo FIXME: If piece['parts'] is null then the call to getLength() below won't work b/c this $args isn't an object
3259 $args = (
null == $piece[
'parts'] ) ?
array() : $piece[
'parts'];
3262 $titleProfileIn =
null;
3268 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3270 # Possibilities for substMatch: "subst", "safesubst" or FALSE
3271 # Decide whether to expand template or keep wikitext as-is.
3272 if ( $this->ot[
'wiki'] ) {
3273 if ( $substMatch ===
false ) {
3274 $literal =
true; # literal when
in PST with no prefix
3276 $literal =
false; # expand when
in PST with subst: or safesubst:
3279 if ( $substMatch ==
'subst' ) {
3280 $literal =
true; # literal when not
in PST with plain subst:
3282 $literal =
false; # expand when not
in PST with safesubst: or no prefix
3286 $text = $frame->virtualBracketedImplode(
'{{',
'|',
'}}', $titleWithSpaces,
$args );
3293 if ( !$found &&
$args->getLength() == 0 ) {
3294 $id = $this->mVariables->matchStartToEnd( $part1 );
3295 if ( $id !==
false ) {
3296 $text = $this->getVariableValue( $id, $frame );
3304 # MSG, MSGNW and RAW
3308 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3311 # Remove obsolete MSG:
3313 $mwMsg->matchStartAndRemove( $part1 );
3318 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3319 $forceRawInterwiki =
true;
3328 $colonPos = strpos( $part1,
':' );
3329 if ( $colonPos !==
false ) {
3330 $func = substr( $part1, 0, $colonPos );
3331 $funcArgs =
array( trim( substr( $part1, $colonPos + 1 ) ) );
3332 for ( $i = 0; $i <
$args->getLength(); $i++ ) {
3333 $funcArgs[] =
$args->item( $i );
3336 $result = $this->callParserFunction( $frame, $func, $funcArgs );
3337 }
catch ( Exception $ex ) {
3343 # The interface for parser functions allows for extracting
3344 # flags into the local scope. Extract any forwarded flags
3351 # Finish mangling title and then check for loops.
3352 # Set $title to a Title object and $titleText to the PDBK
3355 # Split the title into page and subpage
3357 $relative = $this->maybeDoSubpageLink( $part1, $subpage );
3358 if ( $part1 !== $relative ) {
3360 $ns = $this->mTitle->getNamespace();
3364 $titleText =
$title->getPrefixedText();
3365 # Check for language variants if the template is not found
3366 if ( $this->getConverterLanguage()->hasVariants() &&
$title->getArticleID() == 0 ) {
3367 $this->getConverterLanguage()->findVariantLink( $part1,
$title,
true );
3369 # Do recursion depth check
3370 $limit = $this->mOptions->getMaxTemplateDepth();
3371 if ( $frame->depth >=
$limit ) {
3373 $text =
'<span class="error">'
3374 .
wfMessage(
'parser-template-recursion-depth-warning' )
3375 ->numParams(
$limit )->inContentLanguage()->text()
3381 # Load from database
3382 if ( !$found &&
$title ) {
3384 # Too many unique items can kill profiling DBs/collectors
3385 $titleProfileIn = __METHOD__ .
"-title-" .
$title->getPrefixedDBkey();
3389 if ( !
$title->isExternal() ) {
3390 if (
$title->isSpecialPage()
3391 && $this->mOptions->getAllowSpecialInclusion()
3392 && $this->ot[
'html']
3397 $pageArgs =
array();
3398 for ( $i = 0; $i <
$args->getLength(); $i++ ) {
3399 $bits =
$args->item( $i )->splitArg();
3400 if ( strval( $bits[
'index'] ) ===
'' ) {
3402 $value = trim( $frame->expand( $bits[
'value'] ) );
3410 $context->setRequest(
new FauxRequest( $pageArgs ) );
3411 $context->setUser( $this->getUser() );
3412 $context->setLanguage( $this->mOptions->getUserLangObj() );
3415 $text = $context->getOutput()->getHTML();
3416 $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3419 $this->disableCache();
3422 $found =
false; # access denied
3423 wfDebug( __METHOD__ .
": template inclusion denied for " .
3424 $title->getPrefixedDBkey() .
"\n" );
3427 if ( $text !==
false ) {
3433 # If the title is valid but undisplayable, make a link to it
3434 if ( !$found && ( $this->ot[
'html'] || $this->ot[
'pre'] ) ) {
3435 $text =
"[[:$titleText]]";
3438 } elseif (
$title->isTrans() ) {
3439 # Interwiki transclusion
3440 if ( $this->ot[
'html'] && !$forceRawInterwiki ) {
3441 $text = $this->interwikiTransclude(
$title,
'render' );
3444 $text = $this->interwikiTransclude(
$title,
'raw' );
3445 # Preprocess it like a template
3446 $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3452 # Do infinite loop check
3453 # This has to be done after redirect resolution to avoid infinite loops via redirects
3454 if ( !$frame->loopCheck(
$title ) ) {
3456 $text =
'<span class="error">'
3457 .
wfMessage(
'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3459 wfDebug( __METHOD__ .
": template loop broken at '$titleText'\n" );
3464 # If we haven't found text to substitute by now, we're done
3465 # Recover the source wikitext and return it
3467 $text = $frame->virtualBracketedImplode(
'{{',
'|',
'}}', $titleWithSpaces,
$args );
3468 if ( $titleProfileIn ) {
3472 return array(
'object' => $text );
3475 # Expand DOM-style return values in a child frame
3476 if ( $isChildObj ) {
3477 # Clean up argument array
3482 } elseif ( $titleText !==
false && $newFrame->isEmpty() ) {
3483 # Expansion is eligible for the empty-frame cache
3484 if ( isset( $this->mTplExpandCache[$titleText] ) ) {
3485 $text = $this->mTplExpandCache[$titleText];
3487 $text = $newFrame->expand( $text );
3488 $this->mTplExpandCache[$titleText] = $text;
3491 # Uncached expansion
3492 $text = $newFrame->expand( $text );
3495 if ( $isLocalObj && $nowiki ) {
3497 $isLocalObj =
false;
3500 if ( $titleProfileIn ) {
3504 # Replace raw HTML by a placeholder
3506 $text = $this->insertStripItem( $text );
3507 } elseif ( $nowiki && ( $this->ot[
'html'] || $this->ot[
'pre'] ) ) {
3508 # Escape nowiki-style return values
3510 } elseif ( is_string( $text )
3511 && !$piece[
'lineStart']
3512 && preg_match(
'/^(?:{\\||:|;|#|\*)/', $text )
3514 # Bug 529: if the template begins with a table or block-level
3515 # element, it should be treated as beginning a new line.
3516 # This behavior is somewhat controversial.
3517 $text =
"\n" . $text;
3520 if ( is_string( $text ) && !$this->incrementIncludeSize(
'post-expand', strlen( $text ) ) ) {
3521 # Error, oversize inclusion
3522 if ( $titleText !==
false ) {
3523 # Make a working, properly escaped link if possible (bug 23588)
3524 $text =
"[[:$titleText]]";
3526 # This will probably not be a working link, but at least it may
3527 # provide some hint of where the problem is
3528 preg_replace(
'/^:/',
'', $originalTitle );
3529 $text =
"[[:$originalTitle]]";
3531 $text .= $this->insertStripItem(
'<!-- WARNING: template omitted, post-expand include size too large -->' );
3532 $this->limitationWarn(
'post-expand-template-inclusion' );
3535 if ( $isLocalObj ) {
3563 public function callParserFunction( $frame, $function,
array $args =
array() ) {
3568 # Case sensitive functions
3569 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3570 $function = $this->mFunctionSynonyms[1][$function];
3572 # Case insensitive functions
3574 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3575 $function = $this->mFunctionSynonyms[0][$function];
3578 return array(
'found' =>
false );
3582 wfProfileIn( __METHOD__ .
'-pfunc-' . $function );
3583 list( $callback,
$flags ) = $this->mFunctionHooks[$function];
3585 # Workaround for PHP bug 35229 and similar
3586 if ( !is_callable( $callback ) ) {
3589 throw new MWException(
"Tag hook for $function is not callable\n" );
3592 $allArgs =
array( &$this );
3594 # Convert arguments to PPNodes and collect for appending to $allArgs
3595 $funcArgs =
array();
3596 foreach (
$args as $k => $v ) {
3597 if ( $v instanceof
PPNode || $k === 0 ) {
3600 $funcArgs[] = $this->mPreprocessor->newPartNodeArray(
array( $k => $v ) )->item( 0 );
3604 # Add a frame parameter, and pass the arguments as an array
3605 $allArgs[] = $frame;
3606 $allArgs[] = $funcArgs;
3608 # Convert arguments to plain text and append to $allArgs
3609 foreach (
$args as $k => $v ) {
3610 if ( $v instanceof
PPNode ) {
3611 $allArgs[] = trim( $frame->expand( $v ) );
3612 } elseif ( is_int( $k ) && $k >= 0 ) {
3613 $allArgs[] = trim( $v );
3615 $allArgs[] = trim(
"$k=$v" );
3620 $result = call_user_func_array( $callback, $allArgs );
3622 # The interface for function hooks allows them to return a wikitext
3623 # string or an array containing the string and any flags. This mungs
3624 # things around to match what this method should return.
3641 $preprocessFlags = 0;
3642 if ( isset(
$result[
'noparse'] ) ) {
3643 $noparse =
$result[
'noparse'];
3645 if ( isset(
$result[
'preprocessFlags'] ) ) {
3646 $preprocessFlags =
$result[
'preprocessFlags'];
3650 $result[
'text'] = $this->preprocessToDom(
$result[
'text'], $preprocessFlags );
3667 function getTemplateDom(
$title ) {
3669 $titleText =
$title->getPrefixedDBkey();
3671 if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3672 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3674 $titleText =
$title->getPrefixedDBkey();
3676 if ( isset( $this->mTplDomCache[$titleText] ) ) {
3677 return array( $this->mTplDomCache[$titleText],
$title );
3680 # Cache miss, go to the database
3683 if ( $text ===
false ) {
3684 $this->mTplDomCache[$titleText] =
false;
3688 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3689 $this->mTplDomCache[$titleText] = $dom;
3691 if ( !
$title->equals( $cacheTitle ) ) {
3692 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3704 function fetchTemplateAndTitle(
$title ) {
3705 $templateCb = $this->mOptions->getTemplateCallback(); # Defaults to Parser::statelessFetchTemplate()
3706 $stuff = call_user_func( $templateCb,
$title, $this );
3707 $text = $stuff['
text'];
3708 $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] :
$title;
3709 if ( isset( $stuff['deps'] ) ) {
3710 foreach ( $stuff[
'deps']
as $dep ) {
3711 $this->mOutput->addTemplate( $dep[
'title'], $dep[
'page_id'], $dep[
'rev_id'] );
3712 if ( $dep[
'title']->equals( $this->getTitle() ) ) {
3715 $this->mOutput->setFlag(
'vary-revision' );
3719 return array( $text, $finalTitle );
3727 function fetchTemplate(
$title ) {
3728 $rv = $this->fetchTemplateAndTitle(
$title );
3741 static function statelessFetchTemplate(
$title,
$parser =
false ) {
3742 $text = $skip =
false;
3746 # Loop to fetch the article, with up to 1 redirect
3747 for ( $i = 0; $i < 2 && is_object(
$title ); $i++ ) {
3748 # Give extensions a chance to select the revision instead
3749 $id =
false; # Assume current
3750 wfRunHooks(
'BeforeParserFetchTemplateAndtitle',
3757 'page_id' =>
$title->getArticleID(),
3766 $rev_id =
$rev ?
$rev->getId() : 0;
3767 # If there is no current revision, there is no page
3768 if ( $id ===
false && !
$rev ) {
3770 $linkCache->addBadLinkObj(
$title );
3775 'page_id' =>
$title->getArticleID(),
3776 'rev_id' => $rev_id );
3778 # We fetched a rev from a different title; register it too...
3780 'title' =>
$rev->getTitle(),
3781 'page_id' =>
$rev->getPage(),
3782 'rev_id' => $rev_id );
3786 $content =
$rev->getContent();
3787 $text = $content ? $content->getWikitextForTransclusion() :
null;
3789 if ( $text ===
false || $text ===
null ) {
3796 if ( !$message->exists() ) {
3800 $content = $message->content();
3801 $text = $message->plain();
3810 $title = $content->getRedirectTarget();
3814 'finalTitle' => $finalTitle,
3842 # Register the file as a dependency...
3843 $this->mOutput->addImage(
$title->getDBkey(),
$time, $sha1 );
3845 # Update fetched file title
3847 $this->mOutput->addImage(
$title->getDBkey(),
$time, $sha1 );
3863 if ( isset(
$options[
'broken'] ) ) {
3865 } elseif ( isset(
$options[
'sha1'] ) ) {
3881 function interwikiTransclude(
$title, $action ) {
3882 global $wgEnableScaryTranscluding;
3884 if ( !$wgEnableScaryTranscluding ) {
3885 return wfMessage(
'scarytranscludedisabled' )->inContentLanguage()->text();
3888 $url =
$title->getFullURL(
array(
'action' => $action ) );
3890 if ( strlen( $url ) > 255 ) {
3891 return wfMessage(
'scarytranscludetoolong' )->inContentLanguage()->text();
3893 return $this->fetchScaryTemplateMaybeFromCache( $url );
3900 function fetchScaryTemplateMaybeFromCache( $url ) {
3901 global $wgTranscludeCacheExpiry;
3903 $tsCond =
$dbr->timestamp( time() - $wgTranscludeCacheExpiry );
3904 $obj =
$dbr->selectRow(
'transcache',
array(
'tc_time',
'tc_contents' ),
3905 array(
'tc_url' => $url,
"tc_time >= " .
$dbr->addQuotes( $tsCond ) ) );
3907 return $obj->tc_contents;
3911 $status = $req->execute();
3912 if ( $status->isOK() ) {
3913 $text = $req->getContent();
3914 } elseif ( $req->getStatus() != 200 ) {
3915 return wfMessage(
'scarytranscludefailed-httpstatus', $url, $req->getStatus() )->inContentLanguage()->text();
3917 return wfMessage(
'scarytranscludefailed', $url )->inContentLanguage()->text();
3921 $dbw->replace(
'transcache',
array(
'tc_url' ),
array(
3923 'tc_time' => $dbw->timestamp( time() ),
3924 'tc_contents' => $text
3938 function argSubstitution( $piece, $frame ) {
3942 $parts = $piece[
'parts'];
3943 $nameWithSpaces = $frame->expand( $piece[
'title'] );
3944 $argName = trim( $nameWithSpaces );
3946 $text = $frame->getArgument( $argName );
3947 if ( $text ===
false && $parts->getLength() > 0
3948 && ( $this->ot[
'html']
3950 || ( $this->ot[
'wiki'] && $frame->isTemplate() )
3953 # No match in frame, use the supplied default
3954 $object = $parts->item( 0 )->getChildren();
3956 if ( !$this->incrementIncludeSize(
'arg', strlen( $text ) ) ) {
3957 $error =
'<!-- WARNING: argument omitted, expansion size too large -->';
3958 $this->limitationWarn(
'post-expand-template-argument' );
3961 if ( $text ===
false && $object ===
false ) {
3963 $object = $frame->virtualBracketedImplode(
'{{{',
'|',
'}}}', $nameWithSpaces, $parts );
3965 if (
$error !==
false ) {
3968 if ( $object !==
false ) {
3993 function extensionSubstitution(
$params, $frame ) {
3995 $attrText = !isset(
$params[
'attr'] ) ? null : $frame->expand(
$params[
'attr'] );
3996 $content = !isset(
$params[
'inner'] ) ? null : $frame->expand(
$params[
'inner'] );
3997 $marker =
"{$this->mUniqPrefix}-$name-" . sprintf(
'%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3999 $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower(
$name )] ) &&
4000 ( $this->ot[
'html'] || $this->ot[
'pre'] );
4001 if ( $isFunctionTag ) {
4002 $markerType =
'none';
4004 $markerType =
'general';
4006 if ( $this->ot[
'html'] || $isFunctionTag ) {
4009 if ( isset(
$params[
'attributes'] ) ) {
4010 $attributes = $attributes +
$params[
'attributes'];
4013 if ( isset( $this->mTagHooks[
$name] ) ) {
4014 # Workaround for PHP bug 35229 and similar
4015 if ( !is_callable( $this->mTagHooks[
$name] ) ) {
4016 throw new MWException(
"Tag hook for $name is not callable\n" );
4018 $output = call_user_func_array( $this->mTagHooks[
$name],
4019 array( $content, $attributes, $this, $frame ) );
4020 } elseif ( isset( $this->mFunctionTagHooks[
$name] ) ) {
4021 list( $callback, ) = $this->mFunctionTagHooks[
$name];
4022 if ( !is_callable( $callback ) ) {
4023 throw new MWException(
"Tag hook for $name is not callable\n" );
4026 $output = call_user_func_array( $callback,
array( &$this, $frame, $content, $attributes ) );
4028 $output =
'<span class="error">Invalid tag extension name: ' .
4029 htmlspecialchars(
$name ) .
'</span>';
4033 # Extract flags to local scope (to override $markerType)
4040 if ( is_null( $attrText ) ) {
4043 if ( isset(
$params[
'attributes'] ) ) {
4044 foreach (
$params[
'attributes']
as $attrName => $attrValue ) {
4045 $attrText .=
' ' . htmlspecialchars( $attrName ) .
'="' .
4046 htmlspecialchars( $attrValue ) .
'"';
4049 if ( $content ===
null ) {
4050 $output =
"<$name$attrText/>";
4052 $close = is_null(
$params[
'close'] ) ?
'' : $frame->expand(
$params[
'close'] );
4053 $output =
"<$name$attrText>$content$close";
4057 if ( $markerType ===
'none' ) {
4059 } elseif ( $markerType ===
'nowiki' ) {
4060 $this->mStripState->addNoWiki( $marker,
$output );
4061 } elseif ( $markerType ===
'general' ) {
4062 $this->mStripState->addGeneral( $marker,
$output );
4064 throw new MWException( __METHOD__ .
': invalid marker type' );
4076 function incrementIncludeSize(
$type,
$size ) {
4077 if ( $this->mIncludeSizes[
$type] +
$size > $this->mOptions->getMaxIncludeSize() ) {
4090 function incrementExpensiveFunctionCount() {
4091 $this->mExpensiveFunctionCount++;
4092 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4103 function doDoubleUnderscore( $text ) {
4106 # The position of __TOC__ needs to be recorded
4108 if ( $mw->match( $text ) ) {
4109 $this->mShowToc =
true;
4110 $this->mForceTocPosition =
true;
4112 # Set a placeholder. At the end we'll fill it in with the TOC.
4113 $text = $mw->replace(
'<!--MWTOC-->', $text, 1 );
4115 # Only keep the first one.
4116 $text = $mw->replace(
'', $text );
4119 # Now match and remove the rest of them
4121 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4123 if ( isset( $this->mDoubleUnderscores[
'nogallery'] ) ) {
4124 $this->mOutput->mNoGallery =
true;
4126 if ( isset( $this->mDoubleUnderscores[
'notoc'] ) && !$this->mForceTocPosition ) {
4127 $this->mShowToc =
false;
4129 if ( isset( $this->mDoubleUnderscores[
'hiddencat'] ) && $this->mTitle->getNamespace() ==
NS_CATEGORY ) {
4130 $this->addTrackingCategory(
'hidden-category-category' );
4132 # (bug 8068) Allow control over whether robots index a page.
4134 # @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here! This
4135 # is not desirable, the last one on the page should win.
4136 if ( isset( $this->mDoubleUnderscores[
'noindex'] ) && $this->mTitle->canUseNoindex() ) {
4137 $this->mOutput->setIndexPolicy(
'noindex' );
4138 $this->addTrackingCategory(
'noindex-category' );
4140 if ( isset( $this->mDoubleUnderscores[
'index'] ) && $this->mTitle->canUseNoindex() ) {
4141 $this->mOutput->setIndexPolicy(
'index' );
4142 $this->addTrackingCategory(
'index-category' );
4145 # Cache all double underscores in the database
4146 foreach ( $this->mDoubleUnderscores
as $key => $val ) {
4147 $this->mOutput->setProperty( $key,
'' );
4165 public function addTrackingCategory( $msg ) {
4166 if ( $this->mTitle->getNamespace() ===
NS_SPECIAL ) {
4167 wfDebug( __METHOD__ .
": Not adding tracking category $msg to special page!\n" );
4172 ->title( $this->getTitle() )
4173 ->inContentLanguage()
4176 # Allow tracking categories to be disabled by setting them to "-"
4177 if ( $cat ===
'-' ) {
4182 if ( $containerCategory ) {
4183 $this->mOutput->addCategory( $containerCategory->getDBkey(), $this->getDefaultSort() );
4186 wfDebug( __METHOD__ .
": [[MediaWiki:$msg]] is not a valid title!\n" );
4207 function formatHeadings( $text, $origText, $isMain =
true ) {
4208 global $wgMaxTocLevel, $wgExperimentalHtmlIds;
4210 # Inhibit editsection links if requested in the page
4211 if ( isset( $this->mDoubleUnderscores[
'noeditsection'] ) ) {
4212 $maybeShowEditLink = $showEditLink =
false;
4214 $maybeShowEditLink =
true;
4215 $showEditLink = $this->mOptions->getEditSection();
4217 if ( $showEditLink ) {
4218 $this->mOutput->setEditSectionTokens(
true );
4221 # Get all headlines for numbering them and adding funky stuff like [edit]
4222 # links - this is for later, but we need the number of headlines right now
4224 $numMatches = preg_match_all(
'/<H(?P<level>[1-6])(?P<attrib>.*?' .
'>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i', $text,
$matches );
4226 # if there are fewer than 4 headlines in the article, do not show TOC
4227 # unless it's been explicitly enabled.
4228 $enoughToc = $this->mShowToc &&
4229 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4231 # Allow user to stipulate that a page should have a "new section"
4232 # link added via __NEWSECTIONLINK__
4233 if ( isset( $this->mDoubleUnderscores[
'newsectionlink'] ) ) {
4234 $this->mOutput->setNewSection(
true );
4237 # Allow user to remove the "new section"
4238 # link via __NONEWSECTIONLINK__
4239 if ( isset( $this->mDoubleUnderscores[
'nonewsectionlink'] ) ) {
4240 $this->mOutput->hideNewSection(
true );
4243 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4244 # override above conditions and always show TOC above first header
4245 if ( isset( $this->mDoubleUnderscores[
'forcetoc'] ) ) {
4246 $this->mShowToc =
true;
4254 # Ugh .. the TOC should have neat indentation levels which can be
4255 # passed to the skin functions. These are determined here
4259 $sublevelCount =
array();
4260 $levelCount =
array();
4265 $markerRegex =
"{$this->mUniqPrefix}-h-(\d+)-" . self::MARKER_SUFFIX;
4266 $baseTitleText = $this->mTitle->getPrefixedDBkey();
4267 $oldType = $this->mOutputType;
4269 $frame = $this->getPreprocessor()->newFrame();
4270 $root = $this->preprocessToDom( $origText );
4271 $node = $root->getFirstChild();
4277 $isTemplate =
false;
4279 $sectionIndex =
false;
4281 $markerMatches =
array();
4282 if ( preg_match(
"/^$markerRegex/", $headline, $markerMatches ) ) {
4283 $serial = $markerMatches[1];
4284 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4285 $isTemplate = ( $titleText != $baseTitleText );
4286 $headline = preg_replace(
"/^$markerRegex\\s*/",
"", $headline );
4290 $prevlevel = $level;
4292 $level =
$matches[1][$headlineCount];
4294 if ( $level > $prevlevel ) {
4295 # Increase TOC level
4297 $sublevelCount[$toclevel] = 0;
4298 if ( $toclevel < $wgMaxTocLevel ) {
4299 $prevtoclevel = $toclevel;
4303 } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4304 # Decrease TOC level, find level to jump to
4306 for ( $i = $toclevel; $i > 0; $i-- ) {
4307 if ( $levelCount[$i] == $level ) {
4308 # Found last matching level
4311 } elseif ( $levelCount[$i] < $level ) {
4312 # Found first matching level below current level
4320 if ( $toclevel < $wgMaxTocLevel ) {
4321 if ( $prevtoclevel < $wgMaxTocLevel ) {
4322 # Unindent only if the previous toc level was shown :p
4324 $prevtoclevel = $toclevel;
4330 # No change in level, end TOC line
4331 if ( $toclevel < $wgMaxTocLevel ) {
4336 $levelCount[$toclevel] = $level;
4338 # count number of headlines for each level
4339 $sublevelCount[$toclevel]++;
4341 for ( $i = 1; $i <= $toclevel; $i++ ) {
4342 if ( !empty( $sublevelCount[$i] ) ) {
4346 $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4351 # The safe header is a version of the header text safe to use for links
4353 # Remove link placeholders by the link text.
4354 # <!--LINK number-->
4356 # link text with suffix
4357 # Do this before unstrip since link text can contain strip markers
4358 $safeHeadline = $this->replaceLinkHoldersText( $headline );
4360 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4361 $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4363 # Strip out HTML (first regex removes any tag not allowed)
4365 # * <sup> and <sub> (bug 8393)
4368 # * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
4370 # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4371 # to allow setting directionality in toc items.
4372 $tocline = preg_replace(
4373 array(
'#<(?!/?(span|sup|sub|i|b)(?: [^>]*)?>).*?' .
'>#',
'#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|i|b))(?: .*?)?' .
'>#' ),
4374 array(
'',
'<$1>' ),
4377 $tocline = trim( $tocline );
4379 # For the anchor, strip out HTML-y stuff period
4380 $safeHeadline = preg_replace(
'/<.*?' .
'>/',
'', $safeHeadline );
4383 # Save headline for section edit hint before it's escaped
4384 $headlineHint = $safeHeadline;
4386 if ( $wgExperimentalHtmlIds ) {
4387 # For reverse compatibility, provide an id that's
4388 # HTML4-compatible, like we used to.
4390 # It may be worth noting, academically, that it's possible for
4391 # the legacy anchor to conflict with a non-legacy headline
4392 # anchor on the page. In this case likely the "correct" thing
4393 # would be to either drop the legacy anchors or make sure
4394 # they're numbered first. However, this would require people
4395 # to type in section names like "abc_.D7.93.D7.90.D7.A4"
4396 # manually, so let's not bother worrying about it.
4398 array(
'noninitial',
'legacy' ) );
4401 if ( $legacyHeadline == $safeHeadline ) {
4402 # No reason to have both (in fact, we can't)
4403 $legacyHeadline =
false;
4406 $legacyHeadline =
false;
4411 # HTML names must be case-insensitively unique (bug 10721).
4412 # This does not apply to Unicode characters per
4413 # http://dev.w3.org/html5/spec/infrastructure.html#case-sensitivity-and-string-comparison
4414 # @todo FIXME: We may be changing them depending on the current locale.
4415 $arrayKey = strtolower( $safeHeadline );
4416 if ( $legacyHeadline ===
false ) {
4417 $legacyArrayKey =
false;
4419 $legacyArrayKey = strtolower( $legacyHeadline );
4422 # count how many in assoc. array so we can track dupes in anchors
4423 if ( isset( $refers[$arrayKey] ) ) {
4424 $refers[$arrayKey]++;
4426 $refers[$arrayKey] = 1;
4428 if ( isset( $refers[$legacyArrayKey] ) ) {
4429 $refers[$legacyArrayKey]++;
4431 $refers[$legacyArrayKey] = 1;
4434 # Don't number the heading if it is the only one (looks silly)
4435 if ( count(
$matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4436 # the two are different if the line contains a link
4437 $headline =
Html::element(
'span',
array(
'class' =>
'mw-headline-number' ), $numbering ) .
' ' . $headline;
4440 # Create the anchor for linking from the TOC to the section
4441 $anchor = $safeHeadline;
4442 $legacyAnchor = $legacyHeadline;
4443 if ( $refers[$arrayKey] > 1 ) {
4444 $anchor .=
'_' . $refers[$arrayKey];
4446 if ( $legacyHeadline !==
false && $refers[$legacyArrayKey] > 1 ) {
4447 $legacyAnchor .=
'_' . $refers[$legacyArrayKey];
4449 if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4451 $numbering, $toclevel, ( $isTemplate ?
false : $sectionIndex ) );
4454 # Add the section to the section tree
4455 # Find the DOM node for this header
4456 $noOffset = ( $isTemplate || $sectionIndex ===
false );
4457 while ( $node && !$noOffset ) {
4458 if ( $node->getName() ===
'h' ) {
4459 $bits = $node->splitHeading();
4460 if ( $bits[
'i'] == $sectionIndex ) {
4464 $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4466 $node = $node->getNextSibling();
4469 'toclevel' => $toclevel,
4472 'number' => $numbering,
4473 'index' => ( $isTemplate ?
'T-' :
'' ) . $sectionIndex,
4474 'fromtitle' => $titleText,
4475 'byteoffset' => ( $noOffset ?
null : $byteOffset ),
4476 'anchor' => $anchor,
4479 # give headline the correct <h#> tag
4480 if ( $maybeShowEditLink && $sectionIndex !==
false ) {
4482 if ( $isTemplate ) {
4483 # Put a T flag in the section identifier, to indicate to extractSections()
4484 # that sections inside <includeonly> should be counted.
4485 $editlinkArgs =
array( $titleText,
"T-$sectionIndex" );
4487 $editlinkArgs =
array( $this->mTitle->getPrefixedText(), $sectionIndex, $headlineHint );
4496 $editlink =
'<mw:editsection page="' . htmlspecialchars( $editlinkArgs[0] );
4497 $editlink .=
'" section="' . htmlspecialchars( $editlinkArgs[1] ) .
'"';
4498 if ( isset( $editlinkArgs[2] ) ) {
4499 $editlink .=
'>' . $editlinkArgs[2] .
'</mw:editsection>';
4507 $matches[
'attrib'][$headlineCount], $anchor, $headline,
4508 $editlink, $legacyAnchor );
4513 $this->setOutputType( $oldType );
4515 # Never ever show TOC if no headers
4516 if ( $numVisible < 1 ) {
4521 if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4525 $this->mOutput->setTOCHTML( $toc );
4526 $toc = self::TOC_START . $toc . self::TOC_END;
4530 $this->mOutput->setSections( $tocraw );
4533 # split up and insert constructed headlines
4534 $blocks = preg_split(
'/<H[1-6].*?' .
'>[\s\S]*?<\/H[1-6]>/i', $text );
4538 $sections =
array();
4539 foreach ( $blocks
as $block ) {
4541 if ( empty( $head[$i - 1] ) ) {
4542 $sections[$i] = $block;
4544 $sections[$i] = $head[$i - 1] . $block;
4557 wfRunHooks(
'ParserSectionCreate',
array( $this, $i, &$sections[$i], $showEditLink ) );
4562 if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4565 $sections[0] = $sections[0] . $toc .
"\n";
4568 $full .= join(
'', $sections );
4570 if ( $this->mForceTocPosition ) {
4571 return str_replace(
'<!--MWTOC-->', $toc, $full );
4590 $this->setUser(
$user );
4595 $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
4596 if (
$options->getPreSaveTransform() ) {
4597 $text = $this->pstPass2( $text,
$user );
4599 $text = $this->mStripState->unstripBoth( $text );
4601 $this->setUser(
null ); #Reset
4614 private function pstPass2( $text,
$user ) {
4617 # Note: This is the timestamp saved as hardcoded wikitext to
4618 # the database, we use $wgContLang here in order to give
4619 # everyone the same signature and use the default one rather
4620 # than the one selected in each user's preferences.
4621 # (see also bug 12815)
4622 $ts = $this->mOptions->getTimestamp();
4625 $tzMsg =
$timestamp->format(
'T' ); # might vary
on DST changeover!
4627 # Allow translation of timezones through wiki. format() can return
4628 # whatever crap the system uses, localised or not, so we cannot
4629 # ship premade translations.
4630 $key =
'timezone-' . strtolower( trim( $tzMsg ) );
4631 $msg =
wfMessage( $key )->inContentLanguage();
4632 if ( $msg->exists() ) {
4633 $tzMsg = $msg->text();
4636 $d =
$wgContLang->timeanddate( $ts,
false,
false ) .
" ($tzMsg)";
4638 # Variable replacement
4639 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4640 $text = $this->replaceVariables( $text );
4642 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4643 # which may corrupt this parser instance via its wfMessage()->text() call-
4646 $sigText = $this->getUserSig(
$user );
4647 $text = strtr( $text,
array(
4649 '~~~~' =>
"$sigText $d",
4653 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4655 $nc =
'[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4657 $p1 =
"/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/"; # [[ns:
page (context)|]]
4658 $p4 =
"/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/"; # [[ns:page(context)|]] (
double-width brackets, added
in r40257)
4659 $p3 =
"/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/"; # [[ns:
page (context), context|]] (
using either single or
double-width comma)
4662 #
try $p1 first, to turn
"[[A, B (C)|]]" into
"[[A, B (C)|A, B]]"
4663 $text = preg_replace( $p1,
'[[\\1\\2\\3|\\2]]', $text );
4664 $text = preg_replace( $p4,
'[[\\1\\2\\3|\\2]]', $text );
4665 $text = preg_replace( $p3,
'[[\\1\\2\\3\\4|\\2]]', $text );
4667 $t = $this->mTitle->getText();
4669 if ( preg_match(
"/^($nc+:|)$tc+?( \\($tc+\\))$/",
$t, $m ) ) {
4670 $text = preg_replace( $p2,
"[[$m[1]\\1$m[2]|\\1]]", $text );
4671 } elseif ( preg_match(
"/^($nc+:|)$tc+?(, $tc+|)$/",
$t, $m ) &&
"$m[1]$m[2]" !=
'' ) {
4672 $text = preg_replace( $p2,
"[[$m[1]\\1$m[2]|\\1]]", $text );
4674 # if there's no context, don't bother duplicating the title
4675 $text = preg_replace( $p2,
'[[\\1]]', $text );
4678 # Trim trailing whitespace
4679 $text = rtrim( $text );
4698 function getUserSig( &
$user, $nickname =
false, $fancySig =
null ) {
4701 $username =
$user->getName();
4703 # If not given, retrieve from the user object.
4704 if ( $nickname ===
false ) {
4705 $nickname =
$user->getOption(
'nickname' );
4708 if ( is_null( $fancySig ) ) {
4709 $fancySig =
$user->getBoolOption(
'fancysig' );
4712 $nickname = $nickname ==
null ? $username : $nickname;
4714 if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4715 $nickname = $username;
4716 wfDebug( __METHOD__ .
": $username has overlong signature.\n" );
4717 } elseif ( $fancySig !==
false ) {
4718 # Sig. might contain markup; validate this
4719 if ( $this->validateSig( $nickname ) !==
false ) {
4720 # Validated; clean up (if needed) and return it
4721 return $this->cleanSig( $nickname,
true );
4723 # Failed to validate; fall back to the default
4724 $nickname = $username;
4725 wfDebug( __METHOD__ .
": $username has bad XML tags in signature.\n" );
4729 # Make sure nickname doesnt get a sig in a sig
4730 $nickname = self::cleanSigInSig( $nickname );
4732 # If we're still here, make it a link to the user page
4735 $msgName =
$user->isAnon() ?
'signature-anon' :
'signature';
4737 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()->title( $this->getTitle() )->text();
4746 function validateSig( $text ) {
4760 public function cleanSig( $text, $parsing =
false ) {
4766 # Option to disable this feature
4767 if ( !$this->mOptions->getCleanSignatures() ) {
4771 # @todo FIXME: Regex doesn't respect extension tags or nowiki
4772 # => Move this logic to braceSubstitution()
4774 $substRegex =
'/\{\{(?!(?:' . $substWord->getBaseRegex() .
'))/x' . $substWord->getRegexCase();
4775 $substText =
'{{' . $substWord->getSynonym( 0 );
4777 $text = preg_replace( $substRegex, $substText, $text );
4778 $text = self::cleanSigInSig( $text );
4779 $dom = $this->preprocessToDom( $text );
4780 $frame = $this->getPreprocessor()->newFrame();
4781 $text = $frame->expand( $dom );
4784 $text = $this->mStripState->unstripBoth( $text );
4796 public static function cleanSigInSig( $text ) {
4797 $text = preg_replace(
'/~{3,5}/',
'', $text );
4821 $this->setTitle(
$title );
4823 $this->setOutputType( $outputType );
4824 if ( $clearState ) {
4825 $this->clearState();
4838 static $executing =
false;
4840 # Guard against infinite recursion
4883 public function setHook( $tag, $callback ) {
4884 $tag = strtolower( $tag );
4885 if ( preg_match(
'/[<>\r\n]/', $tag, $m ) ) {
4886 throw new MWException(
"Invalid character {$m[0]} in setHook('$tag', ...) call" );
4888 $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] :
null;
4889 $this->mTagHooks[$tag] = $callback;
4890 if ( !in_array( $tag, $this->mStripList ) ) {
4891 $this->mStripList[] = $tag;
4914 function setTransparentTagHook( $tag, $callback ) {
4915 $tag = strtolower( $tag );
4916 if ( preg_match(
'/[<>\r\n]/', $tag, $m ) ) {
4917 throw new MWException(
"Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4919 $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] :
null;
4920 $this->mTransparentTagHooks[$tag] = $callback;
4928 function clearTagHooks() {
4929 $this->mTagHooks =
array();
4930 $this->mFunctionTagHooks =
array();
4931 $this->mStripList = $this->mDefaultStripList;
4977 public function setFunctionHook( $id, $callback,
$flags = 0 ) {
4980 $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] :
null;
4981 $this->mFunctionHooks[$id] =
array( $callback,
$flags );
4983 # Add to function cache
4986 throw new MWException( __METHOD__ .
'() expecting a magic word identifier.' );
4989 $synonyms = $mw->getSynonyms();
4990 $sensitive = intval( $mw->isCaseSensitive() );
4992 foreach ( $synonyms
as $syn ) {
4994 if ( !$sensitive ) {
5001 # Remove trailing colon
5002 if ( substr( $syn, -1, 1 ) ===
':' ) {
5003 $syn = substr( $syn, 0, -1 );
5005 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5015 function getFunctionHooks() {
5016 return array_keys( $this->mFunctionHooks );
5029 function setFunctionTagHook( $tag, $callback,
$flags ) {
5030 $tag = strtolower( $tag );
5031 if ( preg_match(
'/[<>\r\n]/', $tag, $m ) ) {
5032 throw new MWException(
"Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
5034 $old = isset( $this->mFunctionTagHooks[$tag] ) ?
5035 $this->mFunctionTagHooks[$tag] :
null;
5036 $this->mFunctionTagHooks[$tag] =
array( $callback,
$flags );
5038 if ( !in_array( $tag, $this->mStripList ) ) {
5039 $this->mStripList[] = $tag;
5055 function replaceLinkHolders( &$text,
$options = 0 ) {
5056 return $this->mLinkHolders->replace( $text );
5066 function replaceLinkHoldersText( $text ) {
5067 return $this->mLinkHolders->replaceText( $text );
5083 function renderImageGallery( $text,
$params ) {
5087 if ( isset(
$params[
'mode'] ) ) {
5098 $ig->setContextTitle( $this->mTitle );
5099 $ig->setShowBytes(
false );
5100 $ig->setShowFilename(
false );
5101 $ig->setParser( $this );
5102 $ig->setHideBadImages();
5105 if ( isset(
$params[
'showfilename'] ) ) {
5106 $ig->setShowFilename(
true );
5108 $ig->setShowFilename(
false );
5110 if ( isset(
$params[
'caption'] ) ) {
5111 $caption =
$params[
'caption'];
5112 $caption = htmlspecialchars( $caption );
5113 $caption = $this->replaceInternalLinks( $caption );
5114 $ig->setCaptionHtml( $caption );
5116 if ( isset(
$params[
'perrow'] ) ) {
5117 $ig->setPerRow(
$params[
'perrow'] );
5119 if ( isset(
$params[
'widths'] ) ) {
5120 $ig->setWidths(
$params[
'widths'] );
5122 if ( isset(
$params[
'heights'] ) ) {
5123 $ig->setHeights(
$params[
'heights'] );
5125 $ig->setAdditionalOptions(
$params );
5127 wfRunHooks(
'BeforeParserrenderImageGallery',
array( &$this, &$ig ) );
5131 # match lines like these:
5132 # Image:someimage.jpg|This is some image
5140 if ( strpos(
$matches[0],
'%' ) !==
false ) {
5144 if ( is_null(
$title ) ) {
5145 # Bogus title. Ignore these so we don't bomb out later.
5149 # We need to get what handler the file uses, to figure out parameters.
5150 # Note, a hook can overide the file name, and chose an entirely different
5151 # file (which potentially could be of a different type and have different handler).
5156 # Don't register it now, as ImageGallery does that later.
5158 $handler =
$file ?
$file->getHandler() :
false;
5162 'img_alt' =>
'gallery-internal-alt',
5163 'img_link' =>
'gallery-internal-link',
5166 $paramMap = $paramMap + $handler->getParamMap();
5169 unset( $paramMap[
'img_width'] );
5178 $handlerOptions =
array();
5189 foreach ( $parameterMatches
as $parameterMatch ) {
5190 list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5192 $paramName = $paramMap[$magicName];
5194 switch ( $paramName ) {
5195 case 'gallery-internal-alt':
5196 $alt = $this->stripAltText( $match,
false );
5198 case 'gallery-internal-link':
5199 $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
5200 $chars = self::EXT_LINK_URL_CLASS;
5201 $prots = $this->mUrlProtocols;
5203 if ( preg_match(
"/^($prots)$chars+$/u", $linkValue ) ) {
5207 if ( $localLinkTitle !==
null ) {
5208 $link = $localLinkTitle->getLocalURL();
5214 if ( $handler->validateParam( $paramName, $match ) ) {
5215 $handlerOptions[$paramName] = $match;
5218 wfDebug(
"$parameterMatch failed parameter validation\n" );
5219 $label .=
'|' . $parameterMatch;
5225 $label .=
'|' . $parameterMatch;
5229 $label = substr( $label, 1 );
5232 $ig->add(
$title, $label, $alt,
$link, $handlerOptions );
5234 $html = $ig->toHTML();
5243 function getImageParams( $handler ) {
5245 $handlerClass = get_class( $handler );
5249 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5250 # Initialise static lists
5251 static $internalParamNames =
array(
5252 'horizAlign' =>
array(
'left',
'right',
'center',
'none' ),
5253 'vertAlign' =>
array(
'baseline',
'sub',
'super',
'top',
'text-top',
'middle',
5254 'bottom',
'text-bottom' ),
5255 'frame' =>
array(
'thumbnail',
'manualthumb',
'framed',
'frameless',
5256 'upright',
'border',
'link',
'alt',
'class' ),
5258 static $internalParamMap;
5259 if ( !$internalParamMap ) {
5260 $internalParamMap =
array();
5261 foreach ( $internalParamNames
as $type => $names ) {
5263 $magicName = str_replace(
'-',
'_',
"img_$name" );
5269 # Add handler params
5270 $paramMap = $internalParamMap;
5272 $handlerParamMap = $handler->getParamMap();
5273 foreach ( $handlerParamMap
as $magic => $paramName ) {
5274 $paramMap[$magic] =
array(
'handler', $paramName );
5277 $this->mImageParams[$handlerClass] = $paramMap;
5278 $this->mImageParamsMagicArray[$handlerClass] =
new MagicWordArray( array_keys( $paramMap ) );
5280 return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
5292 # Check if the options text is of the form "options|alt text"
5294 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5295 # * left no resizing, just left align. label is used for alt= only
5296 # * right same, but right aligned
5297 # * none same, but not aligned
5298 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5299 # * center center the image
5300 # * frame Keep original image size, no magnify-button.
5301 # * framed Same as "frame"
5302 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5303 # * upright reduce width for upright images, rounded to full __0 px
5304 # * border draw a 1px border around the image
5305 # * alt Text for HTML alt attribute (defaults to empty)
5306 # * class Set a class for img node
5307 # * link Set the target of the image link. Can be external, interwiki, or local
5308 # vertical-align values (no % or length right now):
5320 # Give extensions a chance to select the file revision for us
5325 # Fetch and register the file (file title may be different via hooks)
5329 $handler =
$file ?
$file->getHandler() :
false;
5331 list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5334 $this->addTrackingCategory(
'broken-file-category' );
5337 # Process the input parameters
5340 'horizAlign' =>
array(),
'vertAlign' =>
array() );
5341 foreach ( $parts
as $part ) {
5342 $part = trim( $part );
5343 list( $magicName,
$value ) = $mwArray->matchVariableStartToEnd( $part );
5345 if ( isset( $paramMap[$magicName] ) ) {
5346 list(
$type, $paramName ) = $paramMap[$magicName];
5348 # Special case; width and height come in one variable together
5349 if (
$type ===
'handler' && $paramName ===
'width' ) {
5350 $parsedWidthParam = $this->parseWidthParam(
$value );
5351 if ( isset( $parsedWidthParam[
'width'] ) ) {
5352 $width = $parsedWidthParam[
'width'];
5353 if ( $handler->validateParam(
'width', $width ) ) {
5358 if ( isset( $parsedWidthParam[
'height'] ) ) {
5359 $height = $parsedWidthParam[
'height'];
5360 if ( $handler->validateParam(
'height', $height ) ) {
5365 # else no validation -- bug 13436
5367 if (
$type ===
'handler' ) {
5368 # Validate handler parameter
5369 $validated = $handler->validateParam( $paramName,
$value );
5371 # Validate internal parameters
5372 switch ( $paramName ) {
5376 # @todo FIXME: Possibly check validity here for
5377 # manualthumb? downstream behavior seems odd with
5378 # missing manual thumbs.
5383 $chars = self::EXT_LINK_URL_CLASS;
5384 $prots = $this->mUrlProtocols;
5386 $paramName =
'no-link';
5389 } elseif ( preg_match(
"/^(?i)$prots/",
$value ) ) {
5390 if ( preg_match(
"/^((?i)$prots)$chars+$/u",
$value, $m ) ) {
5391 $paramName =
'link-url';
5392 $this->mOutput->addExternalLink(
$value );
5393 if ( $this->mOptions->getExternalLinkTarget() ) {
5394 $params[
$type][
'link-target'] = $this->mOptions->getExternalLinkTarget();
5401 $paramName =
'link-title';
5403 $this->mOutput->addLink( $linkTitle );
5409 # Most other things appear to be empty or numeric...
5410 $validated = (
$value ===
false || is_numeric( trim(
$value ) ) );
5419 if ( !$validated ) {
5424 # Process alignment parameters
5425 if (
$params[
'horizAlign'] ) {
5432 $params[
'frame'][
'caption'] = $caption;
5434 # Will the image be presented in a frame, with the caption below?
5435 $imageIsFramed = isset(
$params[
'frame'][
'frame'] )
5436 || isset(
$params[
'frame'][
'framed'] )
5437 || isset(
$params[
'frame'][
'thumbnail'] )
5438 || isset(
$params[
'frame'][
'manualthumb'] );
5440 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5441 # came to also set the caption, ordinary text after the image -- which
5442 # makes no sense, because that just repeats the text multiple times in
5443 # screen readers. It *also* came to set the title attribute.
5445 # Now that we have an alt attribute, we should not set the alt text to
5446 # equal the caption: that's worse than useless, it just repeats the
5447 # text. This is the framed/thumbnail case. If there's no caption, we
5448 # use the unnamed parameter for alt text as well, just for the time be-
5449 # ing, if the unnamed param is set and the alt param is not.
5451 # For the future, we need to figure out if we want to tweak this more,
5452 # e.g., introducing a title= parameter for the title; ignoring the un-
5453 # named parameter entirely for images without a caption; adding an ex-
5454 # plicit caption= parameter and preserving the old magic unnamed para-
5456 if ( $imageIsFramed ) { # Framed image
5457 if ( $caption ===
'' && !isset(
$params[
'frame'][
'alt'] ) ) {
5458 # No caption or alt text, add the filename as the alt text so
5459 # that screen readers at least get some description of the image
5462 # Do not set $params['frame']['title'] because tooltips don't make sense
5464 }
else { # Inline image
5465 if ( !isset(
$params[
'frame'][
'alt'] ) ) {
5466 # No alt text, use the "caption" for the alt text
5467 if ( $caption !==
'' ) {
5468 $params[
'frame'][
'alt'] = $this->stripAltText( $caption, $holders );
5470 # No caption, fall back to using the filename for the
5475 # Use the "caption" for the tooltip text
5476 $params[
'frame'][
'title'] = $this->stripAltText( $caption, $holders );
5481 # Linker does the rest
5484 $time, $descQuery, $this->mOptions->getThumbSize() );
5486 # Give the handler a chance to modify the parser object
5488 $handler->parserTransformHook( $this,
$file );
5499 protected function stripAltText( $caption, $holders ) {
5500 # Strip bad stuff out of the title (tooltip). We can't just use
5501 # replaceLinkHoldersText() here, because if this function is called
5502 # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5504 $tooltip = $holders->replaceText( $caption );
5506 $tooltip = $this->replaceLinkHoldersText( $caption );
5509 # make sure there are no placeholders in thumbnail attributes
5510 # that are later expanded to html- so expand them now and
5512 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5522 function disableCache() {
5523 wfDebug(
"Parser output marked as uncacheable.\n" );
5524 if ( !$this->mOutput ) {
5526 " can only be called when actually parsing something" );
5528 $this->mOutput->setCacheTime( -1 );
5529 $this->mOutput->updateCacheExpiry( 0 );
5540 function attributeStripCallback( &$text, $frame =
false ) {
5541 $text = $this->replaceVariables( $text, $frame );
5542 $text = $this->mStripState->unstripBoth( $text );
5551 function getTags() {
5552 return array_merge( array_keys( $this->mTransparentTagHooks ), array_keys( $this->mTagHooks ), array_keys( $this->mFunctionTagHooks ) );
5565 function replaceTransparentTags( $text ) {
5567 $elements = array_keys( $this->mTransparentTagHooks );
5568 $text = self::extractTagsAndParams( $elements, $text,
$matches, $this->mUniqPrefix );
5569 $replacements =
array();
5572 list( $element, $content,
$params, $tag ) = $data;
5573 $tagName = strtolower( $element );
5574 if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5575 $output = call_user_func_array( $this->mTransparentTagHooks[$tagName],
array( $content,
$params, $this ) );
5579 $replacements[$marker] =
$output;
5581 return strtr( $text, $replacements );
5613 private function extractSections( $text,
$section, $mode, $newText =
'' ) {
5617 $frame = $this->getPreprocessor()->newFrame();
5619 # Process section extraction flags
5621 $sectionParts = explode(
'-',
$section );
5622 $sectionIndex = array_pop( $sectionParts );
5623 foreach ( $sectionParts
as $part ) {
5624 if ( $part ===
'T' ) {
5625 $flags |= self::PTD_FOR_INCLUSION;
5629 # Check for empty input
5630 if ( strval( $text ) ===
'' ) {
5631 # Only sections 0 and T-0 exist in an empty document
5632 if ( $sectionIndex == 0 ) {
5633 if ( $mode ===
'get' ) {
5639 if ( $mode ===
'get' ) {
5647 # Preprocess the text
5648 $root = $this->preprocessToDom( $text,
$flags );
5650 # <h> nodes indicate section breaks
5651 # They can only occur at the top level, so we can find them by iterating the root's children
5652 $node = $root->getFirstChild();
5654 # Find the target section
5655 if ( $sectionIndex == 0 ) {
5656 # Section zero doesn't nest, level=big
5657 $targetLevel = 1000;
5660 if ( $node->getName() ===
'h' ) {
5661 $bits = $node->splitHeading();
5662 if ( $bits[
'i'] == $sectionIndex ) {
5663 $targetLevel = $bits[
'level'];
5667 if ( $mode ===
'replace' ) {
5670 $node = $node->getNextSibling();
5676 if ( $mode ===
'get' ) {
5683 # Find the end of the section, including nested sections
5685 if ( $node->getName() ===
'h' ) {
5686 $bits = $node->splitHeading();
5687 $curLevel = $bits[
'level'];
5688 if ( $bits[
'i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5692 if ( $mode ===
'get' ) {
5695 $node = $node->getNextSibling();
5698 # Write out the remainder (in replace mode only)
5699 if ( $mode ===
'replace' ) {
5700 # Output the replacement text
5701 # Add two newlines on -- trailing whitespace in $newText is conventionally
5702 # stripped by the editor, so we need both newlines to restore the paragraph gap
5703 # Only add trailing whitespace if there is newText
5704 if ( $newText !=
"" ) {
5705 $outText .= $newText .
"\n\n";
5710 $node = $node->getNextSibling();
5714 if ( is_string( $outText ) ) {
5715 # Re-insert stripped tags
5716 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5734 public function getSection( $text,
$section, $deftext =
'' ) {
5735 return $this->extractSections( $text,
$section,
"get", $deftext );
5748 public function replaceSection( $oldtext,
$section, $text ) {
5749 return $this->extractSections( $oldtext,
$section,
"replace", $text );
5757 function getRevisionId() {
5758 return $this->mRevisionId;
5767 public function getRevisionObject() {
5768 if ( !is_null( $this->mRevisionObject ) ) {
5769 return $this->mRevisionObject;
5771 if ( is_null( $this->mRevisionId ) ) {
5776 return $this->mRevisionObject;
5783 function getRevisionTimestamp() {
5784 if ( is_null( $this->mRevisionTimestamp ) ) {
5789 $revObject = $this->getRevisionObject();
5792 # The cryptic '' timezone parameter tells to use the site-default
5793 # timezone offset instead of the user settings.
5795 # Since this value will be saved into the parser cache, served
5796 # to other users, and potentially even used inside links and such,
5797 # it needs to be consistent for all visitors.
5802 return $this->mRevisionTimestamp;
5810 function getRevisionUser() {
5811 if ( is_null( $this->mRevisionUser ) ) {
5812 $revObject = $this->getRevisionObject();
5814 # if this template is subst: the revision id will be blank,
5815 # so just use the current user's name
5817 $this->mRevisionUser = $revObject->getUserText();
5818 } elseif ( $this->ot[
'wiki'] || $this->mOptions->getIsPreview() ) {
5819 $this->mRevisionUser = $this->getUser()->getName();
5822 return $this->mRevisionUser;
5830 function getRevisionSize() {
5831 if ( is_null( $this->mRevisionSize ) ) {
5832 $revObject = $this->getRevisionObject();
5834 # if this variable is subst: the revision id will be blank,
5835 # so just use the parser input size, because the own substituation
5836 # will change the size.
5838 $this->mRevisionSize = $revObject->getSize();
5839 } elseif ( $this->ot[
'wiki'] || $this->mOptions->getIsPreview() ) {
5840 $this->mRevisionSize = $this->mInputSize;
5843 return $this->mRevisionSize;
5851 public function setDefaultSort(
$sort ) {
5852 $this->mDefaultSort =
$sort;
5853 $this->mOutput->setProperty(
'defaultsort',
$sort );
5866 public function getDefaultSort() {
5867 if ( $this->mDefaultSort !==
false ) {
5868 return $this->mDefaultSort;
5880 public function getCustomDefaultSort() {
5881 return $this->mDefaultSort;
5893 public function guessSectionNameFromWikiText( $text ) {
5894 # Strip out wikitext links(they break the anchor)
5895 $text = $this->stripSectionName( $text );
5908 public function guessLegacySectionNameFromWikiText( $text ) {
5909 # Strip out wikitext links(they break the anchor)
5910 $text = $this->stripSectionName( $text );
5929 public function stripSectionName( $text ) {
5930 # Strip internal link markup
5931 $text = preg_replace(
'/\[\[:?([^[|]+)\|([^[]+)\]\]/',
'$2', $text );
5932 $text = preg_replace(
'/\[\[:?([^[]+)\|?\]\]/',
'$1', $text );
5934 # Strip external link markup
5935 # @todo FIXME: Not tolerant to blank link text
5936 # I.E. [https://www.mediawiki.org] will render as [1] or something depending
5937 # on how many empty links there are on the page - need to figure that out.
5938 $text = preg_replace(
'/\[(?i:' . $this->mUrlProtocols .
')([^ ]+?) ([^[]+)\]/',
'$2', $text );
5940 # Parse wikitext quotes (italics & bold)
5941 $text = $this->doQuotes( $text );
5961 $text = $this->replaceVariables( $text );
5962 $text = $this->mStripState->unstripBoth( $text );
6003 function markerSkipCallback(
$s, $callback ) {
6006 while ( $i < strlen(
$s ) ) {
6007 $markerStart = strpos(
$s, $this->mUniqPrefix, $i );
6008 if ( $markerStart ===
false ) {
6009 $out .= call_user_func( $callback, substr(
$s, $i ) );
6012 $out .= call_user_func( $callback, substr(
$s, $i, $markerStart - $i ) );
6013 $markerEnd = strpos(
$s, self::MARKER_SUFFIX, $markerStart );
6014 if ( $markerEnd ===
false ) {
6015 $out .= substr(
$s, $markerStart );
6018 $markerEnd += strlen( self::MARKER_SUFFIX );
6019 $out .= substr(
$s, $markerStart, $markerEnd - $markerStart );
6033 function killMarkers( $text ) {
6034 return $this->mStripState->killMarkers( $text );
6053 function serializeHalfParsedText( $text ) {
6057 'version' => self::HALF_PARSED_VERSION,
6058 'stripState' => $this->mStripState->getSubState( $text ),
6059 'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6080 function unserializeHalfParsedText( $data ) {
6081 if ( !isset( $data[
'version'] ) || $data[
'version'] != self::HALF_PARSED_VERSION ) {
6082 throw new MWException( __METHOD__ .
': invalid version' );
6085 # First, extract the strip state.
6086 $texts =
array( $data[
'text'] );
6087 $texts = $this->mStripState->merge( $data[
'stripState'], $texts );
6089 # Now renumber links
6090 $texts = $this->mLinkHolders->mergeForeign( $data[
'linkHolders'], $texts );
6092 # Should be good to go.
6105 function isValidHalfParsedText( $data ) {
6106 return isset( $data[
'version'] ) && $data[
'version'] == self::HALF_PARSED_VERSION;
6117 public function parseWidthParam(
$value ) {
6118 $parsedWidthParam =
array();
6120 return $parsedWidthParam;
6123 # (bug 13500) In both cases (width/height and width only),
6124 # permit trailing "px" for backward compatibility.
6125 if ( preg_match(
'/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/',
$value, $m ) ) {
6126 $width = intval( $m[1] );
6127 $height = intval( $m[2] );
6128 $parsedWidthParam[
'width'] = $width;
6129 $parsedWidthParam[
'height'] = $height;
6130 } elseif ( preg_match(
'/^[0-9]*\s*(?:px)?\s*$/',
$value ) ) {
6131 $width = intval(
$value );
6132 $parsedWidthParam[
'width'] = $width;
6134 return $parsedWidthParam;