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 # Must have a character that needs escaping in attributes (since 1.25.6)
117 const MARKER_SUFFIX =
"-QINU`\"'\x7f";
119 # Markers used for wrapping the table of contents
120 const TOC_START =
'<mw:toc>';
121 const TOC_END =
'</mw:toc>';
124 var $mTagHooks =
array();
125 var $mTransparentTagHooks =
array();
126 var $mFunctionHooks =
array();
128 var $mFunctionTagHooks =
array();
129 var $mStripList =
array();
130 var $mDefaultStripList =
array();
131 var $mVarCache =
array();
132 var $mImageParams =
array();
133 var $mImageParamsMagicArray =
array();
134 var $mMarkerIndex = 0;
135 var $mFirstCall =
true;
137 # Initialised by initialiseVariables()
148 var $mConf, $mPreprocessor, $mExtLinkBracketedRegex, $mUrlProtocols; # Initialised
in constructor
150 # Cleared with clearState():
155 var $mAutonumber, $mDTopen;
162 var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
169 var $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
171 var $mTplExpandCache; # empty-frame expansion
cache
172 var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
173 var $mExpensiveFunctionCount; # number
of expensive parser
function calls
174 var $mShowToc, $mForceTocPosition;
182 # These are variables reset at least once per parse regardless of $clearState
192 var $mTitle; #
Title context,
used for self-link rendering and similar
things
193 var $mOutputType; # Output
type, one
of the OT_xxx constants
194 var $ot; # Shortcut alias,
see setOutputType()
195 var $mRevisionObject;
# The revision object of the specified revision ID
196 var $mRevisionId; #
ID to display
in {{REVISIONID}}
tags
197 var $mRevisionTimestamp; # The timestamp
of the specified revision
ID
198 var $mRevisionUser; #
User to display
in {{REVISIONUSER}} tag
199 var $mRevisionSize; # Size to display
in {{REVISIONSIZE}}
variable
200 var $mRevIdForTs; # The revision
ID which was
used to fetch the timestamp
201 var $mInputSize =
false; # For {{PAGESIZE}}
on current
page.
213 var $mLangLinkLanguages;
220 public function __construct( $conf =
array() ) {
221 $this->mConf = $conf;
223 $this->mExtLinkBracketedRegex =
'/\[(((?i)' . $this->mUrlProtocols .
')' .
224 self::EXT_LINK_URL_CLASS .
'+)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
225 if ( isset( $conf[
'preprocessorClass'] ) ) {
226 $this->mPreprocessorClass = $conf[
'preprocessorClass'];
227 } elseif ( defined(
'HPHP_VERSION' ) ) {
228 # Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
229 $this->mPreprocessorClass =
'Preprocessor_Hash';
230 } elseif ( extension_loaded(
'domxml' ) ) {
231 # PECL extension that conflicts with the core DOM extension (bug 13770)
232 wfDebug(
"Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
233 $this->mPreprocessorClass =
'Preprocessor_Hash';
234 } elseif ( extension_loaded(
'dom' ) ) {
235 $this->mPreprocessorClass =
'Preprocessor_DOM';
237 $this->mPreprocessorClass =
'Preprocessor_Hash';
239 wfDebug( __CLASS__ .
": using preprocessor: {$this->mPreprocessorClass}\n" );
245 function __destruct() {
246 if ( isset( $this->mLinkHolders ) ) {
247 unset( $this->mLinkHolders );
250 unset( $this->$name );
264 function firstCallInit() {
265 if ( !$this->mFirstCall ) {
268 $this->mFirstCall =
false;
274 $this->initialiseVariables();
285 function clearState() {
287 if ( $this->mFirstCall ) {
288 $this->firstCallInit();
291 $this->mOptions->registerWatcher(
array( $this->mOutput,
'recordOption' ) );
292 $this->mAutonumber = 0;
293 $this->mLastSection =
'';
294 $this->mDTopen =
false;
295 $this->mIncludeCount =
array();
296 $this->mArgStack =
false;
297 $this->mInPre =
false;
300 $this->mRevisionObject = $this->mRevisionTimestamp =
301 $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize =
null;
302 $this->mVarCache =
array();
304 $this->mLangLinkLanguages =
array();
316 $this->mUniqPrefix =
"\x7f'\"`UNIQ" . self::getRandomString();
317 $this->mStripState =
new StripState( $this->mUniqPrefix );
319 # Clear these on every parse, bug 4549
320 $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache =
array();
322 $this->mShowToc =
true;
323 $this->mForceTocPosition =
false;
324 $this->mIncludeSizes =
array(
328 $this->mPPNodeCount = 0;
329 $this->mGeneratedPPNodeCount = 0;
330 $this->mHighestExpansionDepth = 0;
331 $this->mDefaultSort =
false;
332 $this->mHeadings =
array();
333 $this->mDoubleUnderscores =
array();
334 $this->mExpensiveFunctionCount = 0;
337 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
338 $this->mPreprocessor =
null;
363 global $wgUseTidy, $wgAlwaysUseTidy, $wgShowHostnames;
370 $this->mInputSize = strlen( $text );
371 if ( $this->mOptions->getEnableLimitReport() ) {
372 $this->mOutput->resetParseStartTime();
375 # Remove the strip marker tag prefix from the input, if present.
377 $text = str_replace( $this->mUniqPrefix,
'', $text );
380 $oldRevisionId = $this->mRevisionId;
381 $oldRevisionObject = $this->mRevisionObject;
382 $oldRevisionTimestamp = $this->mRevisionTimestamp;
383 $oldRevisionUser = $this->mRevisionUser;
384 $oldRevisionSize = $this->mRevisionSize;
385 if ( $revid !==
null ) {
386 $this->mRevisionId = $revid;
387 $this->mRevisionObject =
null;
388 $this->mRevisionTimestamp =
null;
389 $this->mRevisionUser =
null;
390 $this->mRevisionSize =
null;
393 wfRunHooks(
'ParserBeforeStrip',
array( &$this, &$text, &$this->mStripState ) );
395 wfRunHooks(
'ParserAfterStrip',
array( &$this, &$text, &$this->mStripState ) );
396 $text = $this->internalParse( $text );
397 wfRunHooks(
'ParserAfterParse',
array( &$this, &$text, &$this->mStripState ) );
399 $text = $this->mStripState->unstripGeneral( $text );
401 # Clean up special characters, only run once, next-to-last before doBlockLevels
403 # french spaces, last one Guillemet-left
405 '/(.) (?=\\?|:|;|!|%|\\302\\273)/' =>
'\\1 ',
406 # french spaces, Guillemet-right
407 '/(\\302\\253) /' =>
'\\1 ',
408 '/ (!\s*important)/' =>
' \\1', # Beware
of CSS magic word !important, bug #11874.
410 $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
412 $text = $this->doBlockLevels( $text, $linestart );
414 $this->replaceLinkHolders( $text );
423 if ( !(
$options->getDisableContentConversion()
424 || isset( $this->mDoubleUnderscores[
'nocontentconvert'] ) )
426 if ( !$this->mOptions->getInterfaceMessage() ) {
427 # The position of the convert() call should not be changed. it
428 # assumes that the links are all replaced and the only thing left
429 # is the <nowiki> mark.
430 $text = $this->getConverterLanguage()->convert( $text );
441 if ( !(
$options->getDisableTitleConversion()
442 || isset( $this->mDoubleUnderscores[
'nocontentconvert'] )
443 || isset( $this->mDoubleUnderscores[
'notitleconvert'] )
444 || $this->mOutput->getDisplayTitle() !==
false )
446 $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
447 if ( $convruletitle ) {
448 $this->mOutput->setTitleText( $convruletitle );
450 $titleText = $this->getConverterLanguage()->convertTitle(
$title );
451 $this->mOutput->setTitleText( $titleText );
455 $text = $this->mStripState->unstripNoWiki( $text );
459 $text = $this->replaceTransparentTags( $text );
460 $text = $this->mStripState->unstripGeneral( $text );
464 if ( ( $wgUseTidy && $this->mOptions->getTidy() ) || $wgAlwaysUseTidy ) {
467 # attempt to sanitize at least some nesting problems
468 # (bug #2702 and quite a few others)
471 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
472 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
473 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
474 # fix
up an anchor inside another anchor,
only
475 # at least
for a single single nested link (bug 3695)
476 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
477 '\\1\\2</a>\\3</a>\\1\\4</a>',
478 # fix div inside
inline elements- doBlockLevels won
't wrap a line which
479 # contains a div, so fix it up here; replace
480 # div with escaped text
481 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/
' =>
482 '\\1\\3&
lt;div\\5&
gt;\\6&
lt;/div&
gt;\\8\\9
',
483 # remove empty italic or bold tag pairs, some
484 # introduced by rules above
485 '/<([bi])><\/\\1>/
' => '',
488 $text = preg_replace(
489 array_keys( $tidyregs ),
490 array_values( $tidyregs ),
494 if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
495 $this->limitationWarn( 'expensive-parserfunction
',
496 $this->mExpensiveFunctionCount,
497 $this->mOptions->getExpensiveParserFunctionLimit()
501 wfRunHooks( 'ParserAfterTidy
', array( &$this, &$text ) );
503 # Information on include size limits, for the benefit of users who try to skirt them
504 if ( $this->mOptions->getEnableLimitReport() ) {
505 $max = $this->mOptions->getMaxIncludeSize();
507 $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu
' );
508 if ( $cpuTime !== null ) {
509 $this->mOutput->setLimitReportData( 'limitreport-cputime
',
510 sprintf( "%.3f", $cpuTime )
514 $wallTime = $this->mOutput->getTimeSinceStart( 'wall
' );
515 $this->mOutput->setLimitReportData( 'limitreport-walltime
',
516 sprintf( "%.3f", $wallTime )
519 $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes
',
520 array( $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() )
522 $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes
',
523 array( $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() )
525 $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize
',
526 array( $this->mIncludeSizes['post-expand
'], $max )
528 $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize
',
529 array( $this->mIncludeSizes['arg
'], $max )
531 $this->mOutput->setLimitReportData( 'limitreport-expansiondepth
',
532 array( $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() )
534 $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount
',
535 array( $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() )
539 $limitReport = "NewPP limit report\n";
540 if ( $wgShowHostnames ) {
541 $limitReport .= 'Parsed by
' . wfHostname() . "\n";
543 foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
544 if ( wfRunHooks( 'ParserLimitReportFormat
',
545 array( $key, &$value, &$limitReport, false, false )
547 $keyMsg = wfMessage( $key )->inLanguage( 'en
' )->useDatabase( false );
548 $valueMsg = wfMessage( array( "$key-value-text", "$key-value" ) )
549 ->inLanguage( 'en
' )->useDatabase( false );
550 if ( !$valueMsg->exists() ) {
551 $valueMsg = new RawMessage( '$1
' );
553 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
554 $valueMsg->params( $value );
555 $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
559 // Since we're not really outputting HTML, decode the entities and
561 $limitReport = htmlspecialchars_decode( $limitReport );
566 $limitReport = str_replace(
array(
'-',
'&' ),
array(
'‐',
'&' ), $limitReport );
567 $text .=
"\n<!-- \n$limitReport-->\n";
569 if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
570 wfDebugLog(
'generated-pp-node-count', $this->mGeneratedPPNodeCount .
' ' .
571 $this->mTitle->getPrefixedDBkey() );
574 $this->mOutput->setText( $text );
576 $this->mRevisionId = $oldRevisionId;
577 $this->mRevisionObject = $oldRevisionObject;
578 $this->mRevisionTimestamp = $oldRevisionTimestamp;
579 $this->mRevisionUser = $oldRevisionUser;
580 $this->mRevisionSize = $oldRevisionSize;
581 $this->mInputSize =
false;
585 return $this->mOutput;
599 function recursiveTagParse( $text, $frame =
false ) {
601 wfRunHooks(
'ParserBeforeStrip',
array( &$this, &$text, &$this->mStripState ) );
602 wfRunHooks(
'ParserAfterStrip',
array( &$this, &$text, &$this->mStripState ) );
603 $text = $this->internalParse( $text,
false, $frame );
616 if ( $revid !==
null ) {
617 $this->mRevisionId = $revid;
619 wfRunHooks(
'ParserBeforeStrip',
array( &$this, &$text, &$this->mStripState ) );
620 wfRunHooks(
'ParserAfterStrip',
array( &$this, &$text, &$this->mStripState ) );
621 $text = $this->replaceVariables( $text );
622 $text = $this->mStripState->unstripBoth( $text );
636 public function recursivePreprocess( $text, $frame =
false ) {
638 $text = $this->replaceVariables( $text, $frame );
639 $text = $this->mStripState->unstripBoth( $text );
658 $msg =
new RawMessage( $text );
659 $text = $msg->params(
$params )->plain();
661 # Parser (re)initialisation
665 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
666 $text = $this->getPreprocessor()->newFrame()->expand( $dom,
$flags );
667 $text = $this->mStripState->unstripBoth( $text );
676 public static function getRandomString() {
686 function setUser(
$user ) {
687 $this->mUser =
$user;
695 public function uniqPrefix() {
696 if ( !isset( $this->mUniqPrefix ) ) {
697 # @todo FIXME: This is probably *horribly wrong*
698 # LanguageConverter seems to want $wgParser's uniqPrefix, however
699 # if this is called for a parser cache hit, the parser may not
700 # have ever been initialized in the first place.
701 # Not really sure what the heck is supposed to be going on here.
703 # throw new MWException( "Accessing uninitialized mUniqPrefix" );
705 return $this->mUniqPrefix;
713 function setTitle(
$t ) {
718 if (
$t->hasFragment() ) {
719 # Strip the fragment to avoid various odd effects
720 $this->mTitle = clone
$t;
721 $this->mTitle->setFragment(
'' );
732 function getTitle() {
733 return $this->mTitle;
742 function Title( $x =
null ) {
743 return wfSetVar( $this->mTitle, $x );
751 function setOutputType( $ot ) {
752 $this->mOutputType = $ot;
768 function OutputType( $x =
null ) {
769 return wfSetVar( $this->mOutputType, $x );
777 function getOutput() {
778 return $this->mOutput;
786 function getOptions() {
787 return $this->mOptions;
796 function Options( $x =
null ) {
797 return wfSetVar( $this->mOptions, $x );
803 function nextLinkID() {
804 return $this->mLinkID++;
810 function setLinkID( $id ) {
811 $this->mLinkID = $id;
818 function getFunctionLang() {
819 return $this->getTargetLanguage();
831 public function getTargetLanguage() {
832 $target = $this->mOptions->getTargetLanguage();
834 if ( $target !==
null ) {
836 } elseif ( $this->mOptions->getInterfaceMessage() ) {
837 return $this->mOptions->getUserLangObj();
838 } elseif ( is_null( $this->mTitle ) ) {
839 throw new MWException( __METHOD__ .
': $this->mTitle is null' );
842 return $this->mTitle->getPageLanguage();
848 function getConverterLanguage() {
849 return $this->getTargetLanguage();
859 if ( !is_null( $this->mUser ) ) {
862 return $this->mOptions->getUser();
870 function getPreprocessor() {
871 if ( !isset( $this->mPreprocessor ) ) {
872 $class = $this->mPreprocessorClass;
873 $this->mPreprocessor =
new $class( $this );
875 return $this->mPreprocessor;
898 public static function extractTagsAndParams( $elements, $text, &
$matches, $uniq_prefix =
'' ) {
903 $taglist = implode(
'|', $elements );
904 $start =
"/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" .
">)|<(!--)/i";
906 while ( $text !=
'' ) {
907 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
909 if ( count( $p ) < 5 ) {
912 if ( count( $p ) > 5 ) {
926 $marker =
"$uniq_prefix-$element-" . sprintf(
'%08X',
$n++ ) . self::MARKER_SUFFIX;
927 $stripped .= $marker;
929 if ( $close ===
'/>' ) {
930 # Empty element tag, <tag />
935 if ( $element ===
'!--' ) {
938 $end =
"/(<\\/$element\\s*>)/i";
940 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
942 if ( count( $q ) < 3 ) {
943 # No end tag -- let it run out to the end of the text.
955 "<$element$attributes$close$content$tail" );
965 function getStripList() {
966 return $this->mStripList;
978 function insertStripItem( $text ) {
979 $rnd =
"{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
980 $this->mMarkerIndex++;
981 $this->mStripState->addGeneral( $rnd, $text );
991 function doTableStuff( $text ) {
996 $td_history =
array(); # Is currently a td tag
open?
997 $last_tag_history =
array(); # Save history
of last lag activated (td, th or caption)
998 $tr_history =
array(); # Is currently a tr tag
open?
999 $tr_attributes =
array(); # history
of tr attributes
1001 $indent_level = 0; # indent level
of the
table
1004 $line = trim( $outLine );
1007 $out .= $outLine .
"\n";
1011 $first_character =
$line[0];
1015 # First check if we are starting a new table
1016 $indent_level = strlen(
$matches[1] );
1018 $attributes = $this->mStripState->unstripBoth(
$matches[2] );
1021 $outLine = str_repeat(
'<dl><dd>', $indent_level ) .
"<table{$attributes}>";
1022 array_push( $td_history,
false );
1023 array_push( $last_tag_history,
'' );
1024 array_push( $tr_history,
false );
1025 array_push( $tr_attributes,
'' );
1026 array_push( $has_opened_tr,
false );
1027 } elseif ( count( $td_history ) == 0 ) {
1028 # Don't do any of the following
1029 $out .= $outLine .
"\n";
1031 } elseif ( substr(
$line, 0, 2 ) ===
'|}' ) {
1032 # We are ending a table
1034 $last_tag = array_pop( $last_tag_history );
1036 if ( !array_pop( $has_opened_tr ) ) {
1037 $line =
"<tr><td></td></tr>{$line}";
1040 if ( array_pop( $tr_history ) ) {
1041 $line =
"</tr>{$line}";
1044 if ( array_pop( $td_history ) ) {
1045 $line =
"</{$last_tag}>{$line}";
1047 array_pop( $tr_attributes );
1048 $outLine =
$line . str_repeat(
'</dd></dl>', $indent_level );
1049 } elseif ( substr(
$line, 0, 2 ) ===
'|-' ) {
1050 # Now we have a table row
1051 $line = preg_replace(
'#^\|-+#',
'',
$line );
1053 # Whats after the tag is now only attributes
1054 $attributes = $this->mStripState->unstripBoth(
$line );
1056 array_pop( $tr_attributes );
1057 array_push( $tr_attributes, $attributes );
1060 $last_tag = array_pop( $last_tag_history );
1061 array_pop( $has_opened_tr );
1062 array_push( $has_opened_tr,
true );
1064 if ( array_pop( $tr_history ) ) {
1068 if ( array_pop( $td_history ) ) {
1069 $line =
"</{$last_tag}>{$line}";
1073 array_push( $tr_history,
false );
1074 array_push( $td_history,
false );
1075 array_push( $last_tag_history,
'' );
1076 } elseif ( $first_character ===
'|' || $first_character ===
'!' || substr(
$line, 0, 2 ) ===
'|+' ) {
1077 # This might be cell elements, td, th or captions
1078 if ( substr(
$line, 0, 2 ) ===
'|+' ) {
1079 $first_character =
'+';
1085 if ( $first_character ===
'!' ) {
1089 # Split up multiple cells on the same line.
1090 # FIXME : This can result in improper nesting of tags processed
1091 # by earlier parser steps, but should avoid splitting up eg
1092 # attribute values containing literal "||".
1097 # Loop through each table cell
1098 foreach ( $cells
as $cell ) {
1100 if ( $first_character !==
'+' ) {
1101 $tr_after = array_pop( $tr_attributes );
1102 if ( !array_pop( $tr_history ) ) {
1103 $previous =
"<tr{$tr_after}>\n";
1105 array_push( $tr_history,
true );
1106 array_push( $tr_attributes,
'' );
1107 array_pop( $has_opened_tr );
1108 array_push( $has_opened_tr,
true );
1111 $last_tag = array_pop( $last_tag_history );
1113 if ( array_pop( $td_history ) ) {
1114 $previous =
"</{$last_tag}>\n{$previous}";
1117 if ( $first_character ===
'|' ) {
1119 } elseif ( $first_character ===
'!' ) {
1121 } elseif ( $first_character ===
'+' ) {
1122 $last_tag =
'caption';
1127 array_push( $last_tag_history, $last_tag );
1129 # A cell could contain both parameters and data
1130 $cell_data = explode(
'|', $cell, 2 );
1132 # Bug 553: Note that a '|' inside an invalid link should not
1133 # be mistaken as delimiting cell parameters
1134 if ( strpos( $cell_data[0],
'[[' ) !==
false ) {
1135 $cell =
"{$previous}<{$last_tag}>{$cell}";
1136 } elseif ( count( $cell_data ) == 1 ) {
1137 $cell =
"{$previous}<{$last_tag}>{$cell_data[0]}";
1139 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1141 $cell =
"{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
1145 array_push( $td_history,
true );
1148 $out .= $outLine .
"\n";
1151 # Closing open td, tr && table
1152 while ( count( $td_history ) > 0 ) {
1153 if ( array_pop( $td_history ) ) {
1156 if ( array_pop( $tr_history ) ) {
1159 if ( !array_pop( $has_opened_tr ) ) {
1160 $out .=
"<tr><td></td></tr>\n";
1163 $out .=
"</table>\n";
1166 # Remove trailing line-ending (b/c)
1167 if ( substr(
$out, -1 ) ===
"\n" ) {
1171 # special case: don't return empty table
1172 if (
$out ===
"<table>\n<tr><td></td></tr>\n</table>" ) {
1193 function internalParse( $text, $isMain =
true, $frame =
false ) {
1198 # Hook to suspend the parser in this state
1199 if ( !
wfRunHooks(
'ParserBeforeInternalParse',
array( &$this, &$text, &$this->mStripState ) ) ) {
1204 # if $frame is provided, then use $frame for replacing any variables
1206 # use frame depth to infer how include/noinclude tags should be handled
1207 # depth=0 means this is the top-level document; otherwise it's an included document
1208 if ( !$frame->depth ) {
1211 $flag = Parser::PTD_FOR_INCLUSION;
1213 $dom = $this->preprocessToDom( $text, $flag );
1214 $text = $frame->expand( $dom );
1216 # if $frame is not provided, then use old-style replaceVariables
1217 $text = $this->replaceVariables( $text );
1220 wfRunHooks(
'InternalParseBeforeSanitize',
array( &$this, &$text, &$this->mStripState ) );
1222 wfRunHooks(
'InternalParseBeforeLinks',
array( &$this, &$text, &$this->mStripState ) );
1224 # Tables need to come after variable replacement for things to work
1225 # properly; putting them before other transformations should keep
1226 # exciting things like link expansions from showing up in surprising
1228 $text = $this->doTableStuff( $text );
1230 $text = preg_replace(
'/(^|\n)-----*/',
'\\1<hr />', $text );
1232 $text = $this->doDoubleUnderscore( $text );
1234 $text = $this->doHeadings( $text );
1235 $text = $this->replaceInternalLinks( $text );
1236 $text = $this->doAllQuotes( $text );
1237 $text = $this->replaceExternalLinks( $text );
1239 # replaceInternalLinks may sometimes leave behind
1240 # absolute URLs, which have to be masked to hide them from replaceExternalLinks
1241 $text = str_replace( $this->mUniqPrefix .
'NOPARSE',
'', $text );
1243 $text = $this->doMagicLinks( $text );
1244 $text = $this->formatHeadings( $text, $origText, $isMain );
1261 function doMagicLinks( $text ) {
1264 $urlChar = self::EXT_LINK_URL_CLASS;
1265 $text = preg_replace_callback(
1267 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1268 (<.*?>) | # m[2]: Skip stuff inside HTML elements' .
"
1269 (\\b(?i:$prots)$urlChar+) | # m[3]: Free external links" .
'
1270 (?:RFC|PMID)\s+([0-9]+) | # m[4]: RFC or PMID, capture number
1271 ISBN\s+(\b # m[5]: ISBN, capture number
1272 (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix
1273 (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters
1274 [0-9Xx] # check digit
1276 )!xu',
array( &$this,
'magicLinkCallback' ), $text );
1286 function magicLinkCallback( $m ) {
1287 if ( isset( $m[1] ) && $m[1] !==
'' ) {
1290 } elseif ( isset( $m[2] ) && $m[2] !==
'' ) {
1293 } elseif ( isset( $m[3] ) && $m[3] !==
'' ) {
1294 # Free external link
1295 return $this->makeFreeExternalLink( $m[0] );
1296 } elseif ( isset( $m[4] ) && $m[4] !==
'' ) {
1298 if ( substr( $m[0], 0, 3 ) ===
'RFC' ) {
1301 $cssClass =
'mw-magiclink-rfc';
1303 } elseif ( substr( $m[0], 0, 4 ) ===
'PMID' ) {
1305 $urlmsg =
'pubmedurl';
1306 $cssClass =
'mw-magiclink-pmid';
1309 throw new MWException( __METHOD__ .
': unrecognised match type "' .
1310 substr( $m[0], 0, 20 ) .
'"' );
1312 $url =
wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1314 } elseif ( isset( $m[5] ) && $m[5] !==
'' ) {
1317 $num = strtr( $isbn,
array(
1323 return '<a href="' .
1324 htmlspecialchars( $titleObj->getLocalURL() ) .
1325 "\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
1339 function makeFreeExternalLink( $url ) {
1344 # The characters '<' and '>' (which were escaped by
1345 # removeHTMLtags()) should not be included in
1346 # URLs, per RFC 2396.
1348 if ( preg_match(
'/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1349 $trail = substr( $url, $m2[0][1] ) . $trail;
1350 $url = substr( $url, 0, $m2[0][1] );
1353 # Move trailing punctuation to $trail
1355 # If there is no left bracket, then consider right brackets fair game too
1356 if ( strpos( $url,
'(' ) ===
false ) {
1360 $numSepChars = strspn( strrev( $url ), $sep );
1361 if ( $numSepChars ) {
1362 $trail = substr( $url, -$numSepChars ) . $trail;
1363 $url = substr( $url, 0, -$numSepChars );
1368 # Is this an external image?
1369 $text = $this->maybeMakeExternalImage( $url );
1370 if ( $text ===
false ) {
1371 # Not an image, make a link
1373 $this->getConverterLanguage()->markNoConversion( $url,
true ),
1375 $this->getExternalLinkAttribs( $url ) );
1376 # Register it in the output object...
1377 # Replace unnecessary URL escape codes with their equivalent characters
1378 $pasteurized = self::replaceUnusualEscapes( $url );
1379 $this->mOutput->addExternalLink( $pasteurized );
1382 return $text . $trail;
1394 function doHeadings( $text ) {
1396 for ( $i = 6; $i >= 1; --$i ) {
1397 $h = str_repeat(
'=', $i );
1398 $text = preg_replace(
"/^$h(.+)$h\\s*$/m",
"<h$i>\\1</h$i>", $text );
1412 function doAllQuotes( $text ) {
1417 $outtext .= $this->doQuotes(
$line ) .
"\n";
1419 $outtext = substr( $outtext, 0, -1 );
1431 public function doQuotes( $text ) {
1432 $arr = preg_split(
"/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1433 $countarr = count( $arr );
1434 if ( $countarr == 1 ) {
1443 for ( $i = 1; $i < $countarr; $i += 2 ) {
1444 $thislen = strlen( $arr[$i] );
1448 if ( $thislen == 4 ) {
1449 $arr[$i - 1] .=
"'";
1452 } elseif ( $thislen > 5 ) {
1456 $arr[$i - 1] .= str_repeat(
"'", $thislen - 5 );
1461 if ( $thislen == 2 ) {
1463 } elseif ( $thislen == 3 ) {
1465 } elseif ( $thislen == 5 ) {
1475 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1476 $firstsingleletterword = -1;
1477 $firstmultiletterword = -1;
1479 for ( $i = 1; $i < $countarr; $i += 2 ) {
1480 if ( strlen( $arr[$i] ) == 3 ) {
1481 $x1 = substr( $arr[$i - 1], -1 );
1482 $x2 = substr( $arr[$i - 1], -2, 1 );
1483 if ( $x1 ===
' ' ) {
1484 if ( $firstspace == -1 ) {
1487 } elseif ( $x2 ===
' ' ) {
1488 if ( $firstsingleletterword == -1 ) {
1489 $firstsingleletterword = $i;
1495 if ( $firstmultiletterword == -1 ) {
1496 $firstmultiletterword = $i;
1503 if ( $firstsingleletterword > -1 ) {
1504 $arr[$firstsingleletterword] =
"''";
1505 $arr[$firstsingleletterword - 1] .=
"'";
1506 } elseif ( $firstmultiletterword > -1 ) {
1508 $arr[$firstmultiletterword] =
"''";
1509 $arr[$firstmultiletterword - 1] .=
"'";
1510 } elseif ( $firstspace > -1 ) {
1514 $arr[$firstspace] =
"''";
1515 $arr[$firstspace - 1] .=
"'";
1524 foreach ( $arr
as $r ) {
1525 if ( ( $i % 2 ) == 0 ) {
1526 if ( $state ===
'both' ) {
1532 $thislen = strlen( $r );
1533 if ( $thislen == 2 ) {
1534 if ( $state ===
'i' ) {
1537 } elseif ( $state ===
'bi' ) {
1540 } elseif ( $state ===
'ib' ) {
1543 } elseif ( $state ===
'both' ) {
1544 $output .=
'<b><i>' . $buffer .
'</i>';
1550 } elseif ( $thislen == 3 ) {
1551 if ( $state ===
'b' ) {
1554 } elseif ( $state ===
'bi' ) {
1557 } elseif ( $state ===
'ib' ) {
1560 } elseif ( $state ===
'both' ) {
1561 $output .=
'<i><b>' . $buffer .
'</b>';
1567 } elseif ( $thislen == 5 ) {
1568 if ( $state ===
'b' ) {
1571 } elseif ( $state ===
'i' ) {
1574 } elseif ( $state ===
'bi' ) {
1577 } elseif ( $state ===
'ib' ) {
1580 } elseif ( $state ===
'both' ) {
1581 $output .=
'<i><b>' . $buffer .
'</b></i>';
1592 if ( $state ===
'b' || $state ===
'ib' ) {
1595 if ( $state ===
'i' || $state ===
'bi' || $state ===
'ib' ) {
1598 if ( $state ===
'bi' ) {
1602 if ( $state ===
'both' && $buffer ) {
1603 $output .=
'<b><i>' . $buffer .
'</i></b>';
1621 function replaceExternalLinks( $text ) {
1624 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1625 if ( $bits ===
false ) {
1627 throw new MWException(
"PCRE needs to be compiled with --enable-unicode-properties in order for MediaWiki to function" );
1629 $s = array_shift( $bits );
1632 while ( $i < count( $bits ) ) {
1635 $text = $bits[$i++];
1636 $trail = $bits[$i++];
1638 # The characters '<' and '>' (which were escaped by
1639 # removeHTMLtags()) should not be included in
1640 # URLs, per RFC 2396.
1642 if ( preg_match(
'/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1643 $text = substr( $url, $m2[0][1] ) .
' ' . $text;
1644 $url = substr( $url, 0, $m2[0][1] );
1647 # If the link text is an image URL, replace it with an <img> tag
1648 # This happened by accident in the original parser, but some people used it extensively
1649 $img = $this->maybeMakeExternalImage( $text );
1650 if ( $img !==
false ) {
1656 # Set linktype for CSS - if URL==text, link is essentially free
1657 $linktype = ( $text === $url ) ?
'free' :
'text';
1659 # No link text, e.g. [http://domain.tld/some.link]
1660 if ( $text ==
'' ) {
1662 $langObj = $this->getTargetLanguage();
1663 $text =
'[' . $langObj->formatNum( ++$this->mAutonumber ) .
']';
1664 $linktype =
'autonumber';
1666 # Have link text, e.g. [http://domain.tld/some.link text]s
1671 $text = $this->getConverterLanguage()->markNoConversion( $text );
1675 # Use the encoded URL
1676 # This means that users can paste URLs directly into the text
1677 # Funny characters like ö aren't valid in URLs anyway
1678 # This was changed in August 2004
1680 $this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
1682 # Register link in the output object.
1683 # Replace unnecessary URL escape codes with the referenced character
1684 # This prevents spammers from hiding links from the filters
1685 $pasteurized = self::replaceUnusualEscapes( $url );
1686 $this->mOutput->addExternalLink( $pasteurized );
1702 public static function getExternalLinkRel( $url =
false,
$title =
null ) {
1703 global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
1705 if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1723 function getExternalLinkAttribs( $url =
false ) {
1725 $rel = self::getExternalLinkRel( $url, $this->mTitle );
1727 $target = $this->mOptions->getExternalLinkTarget();
1730 if ( !in_array( $target,
array(
'_self',
'_parent',
'_top' ) ) ) {
1734 if ( $rel !==
'' ) {
1737 $rel .=
'noreferrer noopener';
1755 static function replaceUnusualEscapes( $url ) {
1756 return preg_replace_callback(
'/%[0-9A-Fa-f]{2}/',
1757 array( __CLASS__,
'replaceUnusualEscapesCallback' ), $url );
1768 private static function replaceUnusualEscapesCallback(
$matches ) {
1770 $ord = ord( $char );
1771 # Is it an unsafe or HTTP reserved character according to RFC 1738?
1772 if ( $ord > 32 && $ord < 127 && strpos(
'<>"#{}|\^~[]`;/?', $char ) ===
false ) {
1773 # No, shouldn't be escaped
1776 # Yes, leave it escaped
1790 function maybeMakeExternalImage( $url ) {
1791 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
1792 $imagesexception = !empty( $imagesfrom );
1794 # $imagesfrom could be either a single string or an array of strings, parse out the latter
1795 if ( $imagesexception && is_array( $imagesfrom ) ) {
1796 $imagematch =
false;
1797 foreach ( $imagesfrom
as $match ) {
1798 if ( strpos( $url, $match ) === 0 ) {
1803 } elseif ( $imagesexception ) {
1804 $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
1806 $imagematch =
false;
1808 if ( $this->mOptions->getAllowExternalImages()
1809 || ( $imagesexception && $imagematch ) ) {
1810 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
1815 if ( !$text && $this->mOptions->getEnableImageWhitelist()
1816 && preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
1817 $whitelist = explode(
"\n",
wfMessage(
'external_image_whitelist' )->inContentLanguage()->
text() );
1818 foreach ( $whitelist
as $entry ) {
1819 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
1820 if ( strpos( $entry,
'#' ) === 0 || $entry ===
'' ) {
1823 if ( preg_match(
'/' . str_replace(
'/',
'\\/', $entry ) .
'/i', $url ) ) {
1824 # Image matches a whitelist entry
1842 function replaceInternalLinks(
$s ) {
1843 $this->mLinkHolders->merge( $this->replaceInternalLinks2(
$s ) );
1855 function replaceInternalLinks2( &
$s ) {
1859 static $tc =
false, $e1, $e1_img;
1860 # the % is needed to support urlencoded titles as well
1863 # Match a link having the form [[namespace:link|alternate]]trail
1864 $e1 =
"/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
1865 # Match cases where there is no "]]", which might still be images
1866 $e1_img =
"/^([{$tc}]+)\\|(.*)\$/sD";
1871 # split the entire text string on occurrences of [[
1873 # get the first element (all text up to first [[), and remove the space we added
1876 $line = $a->current(); # Workaround
for broken ArrayIterator::next()
that returns "
void"
1877 $s = substr(
$s, 1 );
1879 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
1881 if ( $useLinkPrefixExtension ) {
1882 # Match the end of a line for a word that's not followed by whitespace,
1883 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
1886 $e2 =
"/^((?>.*[^$charset]|))(.+)$/sDu";
1889 if ( is_null( $this->mTitle ) ) {
1892 throw new MWException( __METHOD__ .
": \$this->mTitle is null\n" );
1894 $nottalk = !$this->mTitle->isTalkPage();
1896 if ( $useLinkPrefixExtension ) {
1898 if ( preg_match( $e2,
$s, $m ) ) {
1899 $first_prefix = $m[2];
1901 $first_prefix =
false;
1907 $useSubpages = $this->areSubpagesAllowed();
1910 # Loop for each link
1911 for ( ;
$line !==
false &&
$line !==
null; $a->next(),
$line = $a->current() ) {
1912 # Check for excessive memory usage
1913 if ( $holders->isBig() ) {
1915 # Do the existence check, replace the link holders and clear the array
1916 $holders->replace(
$s );
1920 if ( $useLinkPrefixExtension ) {
1922 if ( preg_match( $e2,
$s, $m ) ) {
1929 if ( $first_prefix ) {
1930 $prefix = $first_prefix;
1931 $first_prefix =
false;
1936 $might_be_img =
false;
1941 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
1942 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
1943 # the real problem is with the $e1 regex
1946 # Still some problems for cases where the ] is meant to be outside punctuation,
1947 # and no image is in sight. See bug 2095.
1950 && substr( $m[3], 0, 1 ) ===
']'
1951 && strpos( $text,
'[' ) !==
false
1953 $text .=
']'; #
so that replaceExternalLinks($text) works
later
1954 $m[3] = substr( $m[3], 1 );
1956 # fix up urlencoded title texts
1957 if ( strpos( $m[1],
'%' ) !==
false ) {
1958 # Should anchors '#' also be rejected?
1959 $m[1] = str_replace(
array(
'<',
'>' ),
array(
'<',
'>' ), rawurldecode( $m[1] ) );
1962 } elseif ( preg_match( $e1_img,
$line, $m ) ) { # Invalid, but might be an image with a link
in its caption
1963 $might_be_img =
true;
1965 if ( strpos( $m[1],
'%' ) !==
false ) {
1966 $m[1] = str_replace(
array(
'<',
'>' ),
array(
'<',
'>' ), rawurldecode( $m[1] ) );
1977 # Don't allow internal links to pages containing
1978 # PROTO: where PROTO is a valid URL protocol; these
1979 # should be external links.
1980 if ( preg_match(
'/^(?i:' . $this->mUrlProtocols .
')/', $m[1] ) ) {
1986 # Make subpage if necessary
1987 if ( $useSubpages ) {
1988 $link = $this->maybeDoSubpageLink( $m[1], $text );
1993 $noforce = ( substr( $m[1], 0, 1 ) !==
':' );
1995 # Strip off leading ':'
2002 if ( $nt ===
null ) {
2008 $ns = $nt->getNamespace();
2009 $iw = $nt->getInterwiki();
2012 if ( $might_be_img ) { #
if this is actually an invalid link
2014 if ( $ns ==
NS_FILE && $noforce ) { # but might be an image
2017 # look at the next 'line' to see if we can close it there
2019 $next_line = $a->current();
2020 if ( $next_line ===
false || $next_line ===
null ) {
2023 $m = explode(
']]', $next_line, 3 );
2024 if ( count( $m ) == 3 ) {
2025 # the first ]] closes the inner link, the second the image
2027 $text .=
"[[{$m[0]}]]{$m[1]}";
2030 } elseif ( count( $m ) == 2 ) {
2031 # if there's exactly one ]] that's fine, we'll keep looking
2032 $text .=
"[[{$m[0]}]]{$m[1]}";
2034 # if $next_line is invalid too, we need look no further
2035 $text .=
'[[' . $next_line;
2040 # we couldn't find the end of this imageLink, so output it raw
2041 # but don't ignore what might be perfectly normal links in the text we've examined
2042 $holders->merge( $this->replaceInternalLinks2( $text ) );
2043 $s .=
"{$prefix}[[$link|$text";
2044 # note: no $trail, because without an end, there *is* no trail
2048 }
else { #
it's not an image, so output it raw
2049 $s .= "{$prefix}[[$link|$text";
2050 # note: no $trail, because without an end, there *is* no trail
2051 wfProfileOut( __METHOD__ . "-might_be_img" );
2054 wfProfileOut( __METHOD__ . "-might_be_img" );
2057 $wasblank = ( $text == '' );
2061 # Bug 4598 madness. Handle the quotes only if they come from the alternate part
2062 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2063 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2064 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2065 $text = $this->doQuotes( $text );
2068 # Link not escaped by : , create the various objects
2071 wfProfileIn( __METHOD__ . "-interwiki" );
2072 if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw
' ) ) {
2073 // XXX: the above check prevents links to sites with identifiers that are not language codes
2075 # Bug 24502: filter duplicates
2076 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2077 $this->mLangLinkLanguages[$iw] = true;
2078 $this->mOutput->addLanguageLink( $nt->getFullText() );
2081 $s = rtrim( $s . $prefix );
2082 $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
2083 wfProfileOut( __METHOD__ . "-interwiki" );
2086 wfProfileOut( __METHOD__ . "-interwiki" );
2088 if ( $ns == NS_FILE ) {
2089 wfProfileIn( __METHOD__ . "-image" );
2090 if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2092 # if no parameters were passed, $text
2093 # becomes something like "File:Foo.png",
2094 # which we don't want to pass
on to the
2098 # recursively parse links inside the image caption
2099 # actually, this will parse them in any other parameters, too,
2100 # but it might be hard to fix that, and it doesn't matter ATM
2101 $text = $this->replaceExternalLinks( $text );
2102 $holders->merge( $this->replaceInternalLinks2( $text ) );
2104 # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2105 $s .= $prefix . $this->armorLinks(
2106 $this->makeImage( $nt, $text, $holders ) ) . $trail;
2108 $s .= $prefix . $trail;
2116 $s = rtrim(
$s .
"\n" ); # bug 87
2119 $sortkey = $this->getDefaultSort();
2124 $sortkey = str_replace(
"\n",
'', $sortkey );
2125 $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2126 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2131 $s .= trim( $prefix . $trail,
"\n" ) ==
'' ?
'' : $prefix . $trail;
2138 # Self-link checking. For some languages, variants of the title are checked in
2139 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2140 # for linking to a different variant.
2141 if ( $ns !=
NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2146 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2147 # @todo FIXME: Should do batch file existence checks, see comment below
2150 # Give extensions a chance to select the file revision for us
2155 # Fetch and register the file (file title may be different via hooks)
2157 # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2158 $s .= $prefix . $this->armorLinks(
2165 # Some titles, such as valid special pages or files in foreign repos, should
2166 # be shown as bluelinks even though they're not included in the page table
2168 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2169 # batch file existence checks for NS_FILE and NS_MEDIA
2170 if ( $iw ==
'' && $nt->isAlwaysKnown() ) {
2171 $this->mOutput->addLink( $nt );
2172 $s .= $this->makeKnownLinkHolder( $nt, $text,
array(), $trail, $prefix );
2174 # Links will be added to the output link list after checking
2175 $s .= $holders->makeHolder( $nt, $text,
array(), $trail, $prefix );
2197 function makeKnownLinkHolder( $nt, $text =
'',
$query =
array(), $trail =
'', $prefix =
'' ) {
2200 if ( is_string(
$query ) ) {
2203 if ( $text ==
'' ) {
2204 $text = htmlspecialchars( $nt->getPrefixedText() );
2209 return $this->armorLinks(
$link ) . $trail;
2222 function armorLinks( $text ) {
2223 return preg_replace(
'/\b((?i)' . $this->mUrlProtocols .
')/',
2224 "{$this->mUniqPrefix}NOPARSE$1", $text );
2231 function areSubpagesAllowed() {
2232 # Some namespaces don't allow subpages
2244 function maybeDoSubpageLink( $target, &$text ) {
2254 function closeParagraph() {
2256 if ( $this->mLastSection !=
'' ) {
2257 $result =
'</' . $this->mLastSection .
">\n";
2259 $this->mInPre =
false;
2260 $this->mLastSection =
'';
2274 function getCommon( $st1, $st2 ) {
2275 $fl = strlen( $st1 );
2276 $shorter = strlen( $st2 );
2277 if ( $fl < $shorter ) {
2281 for ( $i = 0; $i < $shorter; ++$i ) {
2282 if ( $st1[$i] != $st2[$i] ) {
2298 function openList( $char ) {
2299 $result = $this->closeParagraph();
2301 if (
'*' === $char ) {
2303 } elseif (
'#' === $char ) {
2305 } elseif (
':' === $char ) {
2307 } elseif (
';' === $char ) {
2309 $this->mDTopen =
true;
2324 function nextItem( $char ) {
2325 if (
'*' === $char ||
'#' === $char ) {
2326 return "</li>\n<li>";
2327 } elseif (
':' === $char ||
';' === $char ) {
2329 if ( $this->mDTopen ) {
2332 if (
';' === $char ) {
2333 $this->mDTopen =
true;
2334 return $close .
'<dt>';
2336 $this->mDTopen =
false;
2337 return $close .
'<dd>';
2340 return '<!-- ERR 2 -->';
2350 function closeList( $char ) {
2351 if (
'*' === $char ) {
2352 $text =
"</li>\n</ul>";
2353 } elseif (
'#' === $char ) {
2354 $text =
"</li>\n</ol>";
2355 } elseif (
':' === $char ) {
2356 if ( $this->mDTopen ) {
2357 $this->mDTopen =
false;
2358 $text =
"</dt>\n</dl>";
2360 $text =
"</dd>\n</dl>";
2363 return '<!-- ERR 3 -->';
2365 return $text .
"\n";
2377 function doBlockLevels( $text, $linestart ) {
2380 # Parsing through the text line by line. The main thing
2381 # happening here is handling of block-level elements p, pre,
2382 # and making lists from lines starting with * # : etc.
2387 $this->mDTopen = $inBlockElem =
false;
2389 $paragraphStack =
false;
2390 $inBlockquote =
false;
2392 foreach ( $textLines
as $oLine ) {
2394 if ( !$linestart ) {
2404 $lastPrefixLength = strlen( $lastPrefix );
2405 $preCloseMatch = preg_match(
'/<\\/pre/i', $oLine );
2406 $preOpenMatch = preg_match(
'/<pre/i', $oLine );
2407 # If not in a <pre> element, scan for and figure out what prefixes are there.
2408 if ( !$this->mInPre ) {
2409 # Multiple prefixes may abut each other for nested lists.
2410 $prefixLength = strspn( $oLine,
'*#:;' );
2411 $prefix = substr( $oLine, 0, $prefixLength );
2414 # ; and : are both from definition-lists, so they're equivalent
2415 # for the purposes of determining whether or not we need to open/close
2417 $prefix2 = str_replace(
';',
':', $prefix );
2418 $t = substr( $oLine, $prefixLength );
2419 $this->mInPre = (bool)$preOpenMatch;
2421 # Don't interpret any other prefixes in preformatted text
2423 $prefix = $prefix2 =
'';
2428 if ( $prefixLength && $lastPrefix === $prefix2 ) {
2429 # Same as the last item, so no need to deal with nesting or opening stuff
2430 $output .= $this->nextItem( substr( $prefix, -1 ) );
2431 $paragraphStack =
false;
2433 if ( substr( $prefix, -1 ) ===
';' ) {
2434 # The one nasty exception: definition lists work like this:
2435 # ; title : definition text
2436 # So we check for : in the remainder text to split up the
2437 # title and definition, without b0rking links.
2439 if ( $this->findColonNoLinks(
$t,
$term, $t2 ) !==
false ) {
2444 } elseif ( $prefixLength || $lastPrefixLength ) {
2445 # We need to open or close prefixes, or both.
2447 # Either open or close a level...
2448 $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
2449 $paragraphStack =
false;
2451 # Close all the prefixes which aren't shared.
2452 while ( $commonPrefixLength < $lastPrefixLength ) {
2453 $output .= $this->closeList( $lastPrefix[$lastPrefixLength - 1] );
2454 --$lastPrefixLength;
2457 # Continue the current prefix if appropriate.
2458 if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
2459 $output .= $this->nextItem( $prefix[$commonPrefixLength - 1] );
2462 # Open prefixes where appropriate.
2463 while ( $prefixLength > $commonPrefixLength ) {
2464 $char = substr( $prefix, $commonPrefixLength, 1 );
2465 $output .= $this->openList( $char );
2467 if (
';' === $char ) {
2468 # @todo FIXME: This is dupe of code above
2469 if ( $this->findColonNoLinks(
$t,
$term, $t2 ) !==
false ) {
2474 ++$commonPrefixLength;
2476 $lastPrefix = $prefix2;
2479 # If we have no prefixes, go to paragraph mode.
2480 if ( 0 == $prefixLength ) {
2482 # No prefix (not in list)--go to paragraph mode
2483 # XXX: use a stack for nestable elements like span, table and div
2484 $openmatch = preg_match(
'/(?:<table|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<dl|<li|<\\/tr|<\\/td|<\\/th)/iS',
$t );
2485 $closematch = preg_match(
2486 '/(?:<\\/table|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|' .
2487 '<td|<th|<\\/?blockquote|<\\/?div|<hr|<\\/pre|<\\/p|<\\/mw:|' . $this->mUniqPrefix .
'-pre|<\\/li|<\\/ul|<\\/ol|<\\/dl|<\\/?center)/iS',
$t );
2488 if ( $openmatch or $closematch ) {
2489 $paragraphStack =
false;
2490 # TODO bug 5718: paragraph closed
2491 $output .= $this->closeParagraph();
2492 if ( $preOpenMatch and !$preCloseMatch ) {
2493 $this->mInPre =
true;
2496 while ( preg_match(
'/<(\\/?)blockquote[\s>]/i',
$t, $bqMatch, PREG_OFFSET_CAPTURE, $bqOffset ) ) {
2497 $inBlockquote = !$bqMatch[1][0];
2498 $bqOffset = $bqMatch[0][1] + strlen( $bqMatch[0][0] );
2500 $inBlockElem = !$closematch;
2501 } elseif ( !$inBlockElem && !$this->mInPre ) {
2502 if (
' ' == substr(
$t, 0, 1 ) and ( $this->mLastSection ===
'pre' || trim(
$t ) !=
'' ) and !$inBlockquote ) {
2504 if ( $this->mLastSection !==
'pre' ) {
2505 $paragraphStack =
false;
2506 $output .= $this->closeParagraph() .
'<pre>';
2507 $this->mLastSection =
'pre';
2509 $t = substr(
$t, 1 );
2512 if ( trim(
$t ) ===
'' ) {
2513 if ( $paragraphStack ) {
2514 $output .= $paragraphStack .
'<br />';
2515 $paragraphStack =
false;
2516 $this->mLastSection =
'p';
2518 if ( $this->mLastSection !==
'p' ) {
2519 $output .= $this->closeParagraph();
2520 $this->mLastSection =
'';
2521 $paragraphStack =
'<p>';
2523 $paragraphStack =
'</p><p>';
2527 if ( $paragraphStack ) {
2529 $paragraphStack =
false;
2530 $this->mLastSection =
'p';
2531 } elseif ( $this->mLastSection !==
'p' ) {
2532 $output .= $this->closeParagraph() .
'<p>';
2533 $this->mLastSection =
'p';
2540 # somewhere above we forget to get out of pre block (bug 785)
2541 if ( $preCloseMatch && $this->mInPre ) {
2542 $this->mInPre =
false;
2544 if ( $paragraphStack ===
false ) {
2548 while ( $prefixLength ) {
2549 $output .= $this->closeList( $prefix2[$prefixLength - 1] );
2552 if ( $this->mLastSection !=
'' ) {
2553 $output .=
'</' . $this->mLastSection .
'>';
2554 $this->mLastSection =
'';
2571 function findColonNoLinks( $str, &$before, &$after ) {
2574 $pos = strpos( $str,
':' );
2575 if ( $pos ===
false ) {
2581 $lt = strpos( $str,
'<' );
2582 if ( $lt ===
false || $lt > $pos ) {
2583 # Easy; no tag nesting to worry about
2584 $before = substr( $str, 0, $pos );
2585 $after = substr( $str, $pos + 1 );
2590 # Ugly state machine to walk through avoiding tags.
2591 $state = self::COLON_STATE_TEXT;
2593 $len = strlen( $str );
2594 for ( $i = 0; $i < $len; $i++ ) {
2598 # (Using the number is a performance hack for common cases)
2599 case 0: # self::COLON_STATE_TEXT:
2602 # Could be either a <start> tag or an </end> tag
2603 $state = self::COLON_STATE_TAGSTART;
2606 if ( $stack == 0 ) {
2608 $before = substr( $str, 0, $i );
2609 $after = substr( $str, $i + 1 );
2613 # Embedded in a tag; don't break it.
2616 # Skip ahead looking for something interesting
2617 $colon = strpos( $str,
':', $i );
2618 if ( $colon ===
false ) {
2619 # Nothing else interesting
2623 $lt = strpos( $str,
'<', $i );
2624 if ( $stack === 0 ) {
2625 if ( $lt ===
false || $colon < $lt ) {
2627 $before = substr( $str, 0, $colon );
2628 $after = substr( $str, $colon + 1 );
2633 if ( $lt ===
false ) {
2634 # Nothing else interesting to find; abort!
2635 # We're nested, but there's no close tags left. Abort!
2638 # Skip ahead to next tag start
2640 $state = self::COLON_STATE_TAGSTART;
2643 case 1: # self::COLON_STATE_TAG:
2648 $state = self::COLON_STATE_TEXT;
2651 # Slash may be followed by >?
2652 $state = self::COLON_STATE_TAGSLASH;
2658 case 2: # self::COLON_STATE_TAGSTART:
2661 $state = self::COLON_STATE_CLOSETAG;
2664 $state = self::COLON_STATE_COMMENT;
2667 # Illegal early close? This shouldn't happen D:
2668 $state = self::COLON_STATE_TEXT;
2671 $state = self::COLON_STATE_TAG;
2674 case 3: # self::COLON_STATE_CLOSETAG:
2679 wfDebug( __METHOD__ .
": Invalid input; too many close tags\n" );
2683 $state = self::COLON_STATE_TEXT;
2686 case self::COLON_STATE_TAGSLASH:
2688 # Yes, a self-closed tag <blah/>
2689 $state = self::COLON_STATE_TEXT;
2691 # Probably we're jumping the gun, and this is an attribute
2692 $state = self::COLON_STATE_TAG;
2695 case 5: # self::COLON_STATE_COMMENT:
2697 $state = self::COLON_STATE_COMMENTDASH;
2700 case self::COLON_STATE_COMMENTDASH:
2702 $state = self::COLON_STATE_COMMENTDASHDASH;
2704 $state = self::COLON_STATE_COMMENT;
2707 case self::COLON_STATE_COMMENTDASHDASH:
2709 $state = self::COLON_STATE_TEXT;
2711 $state = self::COLON_STATE_COMMENT;
2716 throw new MWException(
"State machine error in " . __METHOD__ );
2720 wfDebug( __METHOD__ .
": Invalid input; not enough close tags (stack $stack, state $state)\n" );
2739 function getVariableValue( $index, $frame =
false ) {
2743 if ( is_null( $this->mTitle ) ) {
2748 throw new MWException( __METHOD__ .
' Should only be '
2749 .
' called while parsing (no title set)' );
2756 if (
wfRunHooks(
'ParserGetVariableValueVarCache',
array( &$this, &$this->mVarCache ) ) ) {
2757 if ( isset( $this->mVarCache[$index] ) ) {
2758 return $this->mVarCache[$index];
2765 $pageLang = $this->getFunctionLang();
2768 case 'currentmonth':
2771 case 'currentmonth1':
2774 case 'currentmonthname':
2777 case 'currentmonthnamegen':
2780 case 'currentmonthabbrev':
2795 case 'localmonthname':
2798 case 'localmonthnamegen':
2801 case 'localmonthabbrev':
2816 case 'fullpagename':
2819 case 'fullpagenamee':
2825 case 'subpagenamee':
2828 case 'rootpagename':
2831 case 'rootpagenamee':
2834 case 'basepagename':
2837 case 'basepagenamee':
2840 case 'talkpagename':
2841 if ( $this->mTitle->canTalk() ) {
2842 $talkPage = $this->mTitle->getTalkPage();
2848 case 'talkpagenamee':
2849 if ( $this->mTitle->canTalk() ) {
2850 $talkPage = $this->mTitle->getTalkPage();
2856 case 'subjectpagename':
2857 $subjPage = $this->mTitle->getSubjectPage();
2860 case 'subjectpagenamee':
2861 $subjPage = $this->mTitle->getSubjectPage();
2865 $pageid = $this->getTitle()->getArticleID();
2866 if ( $pageid == 0 ) {
2867 # 0 means the page doesn't exist in the database,
2868 # which means the user is previewing a new page.
2869 # The vary-revision flag must be set, because the magic word
2870 # will have a different value once the page is saved.
2871 $this->mOutput->setFlag(
'vary-revision' );
2872 wfDebug( __METHOD__ .
": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2874 $value = $pageid ? $pageid :
null;
2877 # Let the edit saving system know we should parse the page
2878 # *after* a revision ID has been assigned.
2879 $this->mOutput->setFlag(
'vary-revision' );
2880 wfDebug( __METHOD__ .
": {{REVISIONID}} used, setting vary-revision...\n" );
2881 $value = $this->mRevisionId;
2884 # Let the edit saving system know we should parse the page
2885 # *after* a revision ID has been assigned. This is for null edits.
2886 $this->mOutput->setFlag(
'vary-revision' );
2887 wfDebug( __METHOD__ .
": {{REVISIONDAY}} used, setting vary-revision...\n" );
2888 $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
2890 case 'revisionday2':
2891 # Let the edit saving system know we should parse the page
2892 # *after* a revision ID has been assigned. This is for null edits.
2893 $this->mOutput->setFlag(
'vary-revision' );
2894 wfDebug( __METHOD__ .
": {{REVISIONDAY2}} used, setting vary-revision...\n" );
2895 $value = substr( $this->getRevisionTimestamp(), 6, 2 );
2897 case 'revisionmonth':
2898 # Let the edit saving system know we should parse the page
2899 # *after* a revision ID has been assigned. This is for null edits.
2900 $this->mOutput->setFlag(
'vary-revision' );
2901 wfDebug( __METHOD__ .
": {{REVISIONMONTH}} used, setting vary-revision...\n" );
2902 $value = substr( $this->getRevisionTimestamp(), 4, 2 );
2904 case 'revisionmonth1':
2905 # Let the edit saving system know we should parse the page
2906 # *after* a revision ID has been assigned. This is for null edits.
2907 $this->mOutput->setFlag(
'vary-revision' );
2908 wfDebug( __METHOD__ .
": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
2909 $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
2911 case 'revisionyear':
2912 # Let the edit saving system know we should parse the page
2913 # *after* a revision ID has been assigned. This is for null edits.
2914 $this->mOutput->setFlag(
'vary-revision' );
2915 wfDebug( __METHOD__ .
": {{REVISIONYEAR}} used, setting vary-revision...\n" );
2916 $value = substr( $this->getRevisionTimestamp(), 0, 4 );
2918 case 'revisiontimestamp':
2919 # Let the edit saving system know we should parse the page
2920 # *after* a revision ID has been assigned. This is for null edits.
2921 $this->mOutput->setFlag(
'vary-revision' );
2922 wfDebug( __METHOD__ .
": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2923 $value = $this->getRevisionTimestamp();
2925 case 'revisionuser':
2926 # Let the edit saving system know we should parse the page
2927 # *after* a revision ID has been assigned. This is for null edits.
2928 $this->mOutput->setFlag(
'vary-revision' );
2929 wfDebug( __METHOD__ .
": {{REVISIONUSER}} used, setting vary-revision...\n" );
2930 $value = $this->getRevisionUser();
2932 case 'revisionsize':
2933 # Let the edit saving system know we should parse the page
2934 # *after* a revision ID has been assigned. This is for null edits.
2935 $this->mOutput->setFlag(
'vary-revision' );
2936 wfDebug( __METHOD__ .
": {{REVISIONSIZE}} used, setting vary-revision...\n" );
2937 $value = $this->getRevisionSize();
2940 $value = str_replace(
'_',
' ',
$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2945 case 'namespacenumber':
2946 $value = $this->mTitle->getNamespace();
2949 $value = $this->mTitle->canTalk() ? str_replace(
'_',
' ', $this->mTitle->getTalkNsText() ) :
'';
2952 $value = $this->mTitle->canTalk() ?
wfUrlencode( $this->mTitle->getTalkNsText() ) :
'';
2954 case 'subjectspace':
2955 $value = str_replace(
'_',
' ', $this->mTitle->getSubjectNsText() );
2957 case 'subjectspacee':
2960 case 'currentdayname':
2973 # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2974 # int to remove the padding
2980 case 'localdayname':
2993 # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2994 # int to remove the padding
3000 case 'numberofarticles':
3003 case 'numberoffiles':
3006 case 'numberofusers':
3009 case 'numberofactiveusers':
3012 case 'numberofpages':
3015 case 'numberofadmins':
3018 case 'numberofedits':
3021 case 'numberofviews':
3022 global $wgDisableCounters;
3025 case 'currenttimestamp':
3028 case 'localtimestamp':
3031 case 'currentversion':
3042 return $serverParts && isset( $serverParts[
'host'] ) ? $serverParts[
'host'] : $wgServer;
3044 return $wgScriptPath;
3046 return $wgStylePath;
3047 case 'directionmark':
3048 return $pageLang->getDirMark();
3049 case 'contentlanguage':
3051 return $wgLanguageCode;
3052 case 'cascadingsources':
3057 wfRunHooks(
'ParserGetVariableValueSwitch',
array( &$this, &$this->mVarCache, &$index, &
$ret, &$frame ) );
3062 $this->mVarCache[$index] =
$value;
3073 function initialiseVariables() {
3105 function preprocessToDom( $text,
$flags = 0 ) {
3106 $dom = $this->getPreprocessor()->preprocessToObj( $text,
$flags );
3117 public static function splitWhitespace(
$s ) {
3118 $ltrimmed = ltrim(
$s );
3119 $w1 = substr(
$s, 0, strlen(
$s ) - strlen( $ltrimmed ) );
3120 $trimmed = rtrim( $ltrimmed );
3121 $diff = strlen( $ltrimmed ) - strlen( $trimmed );
3123 $w2 = substr( $ltrimmed, -$diff );
3127 return array( $w1, $trimmed, $w2 );
3149 function replaceVariables( $text, $frame =
false, $argsOnly =
false ) {
3150 # Is there any text? Also, Prevent too big inclusions!
3151 if ( strlen( $text ) < 1 || strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
3156 if ( $frame ===
false ) {
3157 $frame = $this->getPreprocessor()->newFrame();
3158 } elseif ( !( $frame instanceof
PPFrame ) ) {
3159 wfDebug( __METHOD__ .
" called using plain parameters instead of a PPFrame instance. Creating custom frame.\n" );
3160 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
3163 $dom = $this->preprocessToDom( $text );
3165 $text = $frame->expand( $dom,
$flags );
3178 static function createAssocArgs(
$args ) {
3179 $assocArgs =
array();
3182 $eqpos = strpos( $arg,
'=' );
3183 if ( $eqpos ===
false ) {
3184 $assocArgs[$index++] = $arg;
3186 $name = trim( substr( $arg, 0, $eqpos ) );
3187 $value = trim( substr( $arg, $eqpos + 1 ) );
3188 if (
$value ===
false ) {
3191 if (
$name !==
false ) {
3224 function limitationWarn( $limitationType, $current =
'', $max =
'' ) {
3225 # does no harm if $current and $max are present but are unnecessary for the message
3226 $warning =
wfMessage(
"$limitationType-warning" )->numParams( $current, $max )
3227 ->inLanguage( $this->mOptions->getUserLangObj() )->
text();
3228 $this->mOutput->addWarning( $warning );
3229 $this->addTrackingCategory(
"$limitationType-category" );
3245 function braceSubstitution( $piece, $frame ) {
3250 $found =
false; # $text has been filled
3251 $nowiki =
false; #
wiki markup
in $text should be escaped
3252 $isHTML =
false; # $text
is HTML, armour
it against wikitext transformation
3253 $forceRawInterwiki =
false; # Force interwiki transclusion to be done
in raw mode not rendered
3254 $isChildObj =
false; # $text
is a DOM node needing expansion
in a child frame
3255 $isLocalObj =
false; # $text
is a DOM node needing expansion
in the current frame
3257 # Title object, where $text came from
3260 # $part1 is the bit before the first |, and must contain only title characters.
3261 # Various prefixes will be stripped from it later.
3262 $titleWithSpaces = $frame->expand( $piece[
'title'] );
3263 $part1 = trim( $titleWithSpaces );
3266 # Original title text preserved for various purposes
3267 $originalTitle = $part1;
3269 # $args is a list of argument nodes, starting from index 0, not including $part1
3270 # @todo FIXME: If piece['parts'] is null then the call to getLength() below won't work b/c this $args isn't an object
3271 $args = (
null == $piece[
'parts'] ) ?
array() : $piece[
'parts'];
3274 $titleProfileIn =
null;
3280 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3282 # Possibilities for substMatch: "subst", "safesubst" or FALSE
3283 # Decide whether to expand template or keep wikitext as-is.
3284 if ( $this->ot[
'wiki'] ) {
3285 if ( $substMatch ===
false ) {
3286 $literal =
true; # literal when
in PST with no prefix
3288 $literal =
false; # expand when
in PST with subst: or safesubst:
3291 if ( $substMatch ==
'subst' ) {
3292 $literal =
true; # literal when not
in PST with plain subst:
3294 $literal =
false; # expand when not
in PST with safesubst: or no prefix
3298 $text = $frame->virtualBracketedImplode(
'{{',
'|',
'}}', $titleWithSpaces,
$args );
3305 if ( !$found &&
$args->getLength() == 0 ) {
3306 $id = $this->mVariables->matchStartToEnd( $part1 );
3307 if ( $id !==
false ) {
3308 $text = $this->getVariableValue( $id, $frame );
3316 # MSG, MSGNW and RAW
3320 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3323 # Remove obsolete MSG:
3325 $mwMsg->matchStartAndRemove( $part1 );
3330 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3331 $forceRawInterwiki =
true;
3340 $colonPos = strpos( $part1,
':' );
3341 if ( $colonPos !==
false ) {
3342 $func = substr( $part1, 0, $colonPos );
3343 $funcArgs =
array( trim( substr( $part1, $colonPos + 1 ) ) );
3344 for ( $i = 0; $i <
$args->getLength(); $i++ ) {
3345 $funcArgs[] =
$args->item( $i );
3348 $result = $this->callParserFunction( $frame, $func, $funcArgs );
3349 }
catch ( Exception $ex ) {
3355 # The interface for parser functions allows for extracting
3356 # flags into the local scope. Extract any forwarded flags
3363 # Finish mangling title and then check for loops.
3364 # Set $title to a Title object and $titleText to the PDBK
3367 # Split the title into page and subpage
3369 $relative = $this->maybeDoSubpageLink( $part1, $subpage );
3370 if ( $part1 !== $relative ) {
3372 $ns = $this->mTitle->getNamespace();
3376 $titleText =
$title->getPrefixedText();
3377 # Check for language variants if the template is not found
3378 if ( $this->getConverterLanguage()->hasVariants() &&
$title->getArticleID() == 0 ) {
3379 $this->getConverterLanguage()->findVariantLink( $part1,
$title,
true );
3381 # Do recursion depth check
3382 $limit = $this->mOptions->getMaxTemplateDepth();
3383 if ( $frame->depth >=
$limit ) {
3385 $text =
'<span class="error">'
3386 .
wfMessage(
'parser-template-recursion-depth-warning' )
3387 ->numParams(
$limit )->inContentLanguage()->text()
3393 # Load from database
3394 if ( !$found &&
$title ) {
3396 # Too many unique items can kill profiling DBs/collectors
3397 $titleProfileIn = __METHOD__ .
"-title-" .
$title->getPrefixedDBkey();
3401 if ( !
$title->isExternal() ) {
3402 if (
$title->isSpecialPage()
3403 && $this->mOptions->getAllowSpecialInclusion()
3404 && $this->ot[
'html']
3409 $pageArgs =
array();
3410 for ( $i = 0; $i <
$args->getLength(); $i++ ) {
3411 $bits =
$args->item( $i )->splitArg();
3412 if ( strval( $bits[
'index'] ) ===
'' ) {
3414 $value = trim( $frame->expand( $bits[
'value'] ) );
3422 $context->setRequest(
new FauxRequest( $pageArgs ) );
3423 $context->setUser( $this->getUser() );
3424 $context->setLanguage( $this->mOptions->getUserLangObj() );
3427 $text = $context->getOutput()->getHTML();
3428 $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3431 $this->disableCache();
3434 $found =
false; # access denied
3435 wfDebug( __METHOD__ .
": template inclusion denied for " .
3436 $title->getPrefixedDBkey() .
"\n" );
3439 if ( $text !==
false ) {
3445 # If the title is valid but undisplayable, make a link to it
3446 if ( !$found && ( $this->ot[
'html'] || $this->ot[
'pre'] ) ) {
3447 $text =
"[[:$titleText]]";
3450 } elseif (
$title->isTrans() ) {
3451 # Interwiki transclusion
3452 if ( $this->ot[
'html'] && !$forceRawInterwiki ) {
3453 $text = $this->interwikiTransclude(
$title,
'render' );
3456 $text = $this->interwikiTransclude(
$title,
'raw' );
3457 # Preprocess it like a template
3458 $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3464 # Do infinite loop check
3465 # This has to be done after redirect resolution to avoid infinite loops via redirects
3466 if ( !$frame->loopCheck(
$title ) ) {
3468 $text =
'<span class="error">'
3469 .
wfMessage(
'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3471 wfDebug( __METHOD__ .
": template loop broken at '$titleText'\n" );
3476 # If we haven't found text to substitute by now, we're done
3477 # Recover the source wikitext and return it
3479 $text = $frame->virtualBracketedImplode(
'{{',
'|',
'}}', $titleWithSpaces,
$args );
3480 if ( $titleProfileIn ) {
3484 return array(
'object' => $text );
3487 # Expand DOM-style return values in a child frame
3488 if ( $isChildObj ) {
3489 # Clean up argument array
3494 } elseif ( $titleText !==
false && $newFrame->isEmpty() ) {
3495 # Expansion is eligible for the empty-frame cache
3496 if ( isset( $this->mTplExpandCache[$titleText] ) ) {
3497 $text = $this->mTplExpandCache[$titleText];
3499 $text = $newFrame->expand( $text );
3500 $this->mTplExpandCache[$titleText] = $text;
3503 # Uncached expansion
3504 $text = $newFrame->expand( $text );
3507 if ( $isLocalObj && $nowiki ) {
3509 $isLocalObj =
false;
3512 if ( $titleProfileIn ) {
3516 # Replace raw HTML by a placeholder
3518 $text = $this->insertStripItem( $text );
3519 } elseif ( $nowiki && ( $this->ot[
'html'] || $this->ot[
'pre'] ) ) {
3520 # Escape nowiki-style return values
3522 } elseif ( is_string( $text )
3523 && !$piece[
'lineStart']
3524 && preg_match(
'/^(?:{\\||:|;|#|\*)/', $text )
3526 # Bug 529: if the template begins with a table or block-level
3527 # element, it should be treated as beginning a new line.
3528 # This behavior is somewhat controversial.
3529 $text =
"\n" . $text;
3532 if ( is_string( $text ) && !$this->incrementIncludeSize(
'post-expand', strlen( $text ) ) ) {
3533 # Error, oversize inclusion
3534 if ( $titleText !==
false ) {
3535 # Make a working, properly escaped link if possible (bug 23588)
3536 $text =
"[[:$titleText]]";
3538 # This will probably not be a working link, but at least it may
3539 # provide some hint of where the problem is
3540 preg_replace(
'/^:/',
'', $originalTitle );
3541 $text =
"[[:$originalTitle]]";
3543 $text .= $this->insertStripItem(
'<!-- WARNING: template omitted, post-expand include size too large -->' );
3544 $this->limitationWarn(
'post-expand-template-inclusion' );
3547 if ( $isLocalObj ) {
3575 public function callParserFunction( $frame, $function,
array $args =
array() ) {
3580 # Case sensitive functions
3581 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3582 $function = $this->mFunctionSynonyms[1][$function];
3584 # Case insensitive functions
3586 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3587 $function = $this->mFunctionSynonyms[0][$function];
3590 return array(
'found' =>
false );
3594 wfProfileIn( __METHOD__ .
'-pfunc-' . $function );
3595 list( $callback,
$flags ) = $this->mFunctionHooks[$function];
3597 # Workaround for PHP bug 35229 and similar
3598 if ( !is_callable( $callback ) ) {
3601 throw new MWException(
"Tag hook for $function is not callable\n" );
3604 $allArgs =
array( &$this );
3606 # Convert arguments to PPNodes and collect for appending to $allArgs
3607 $funcArgs =
array();
3608 foreach (
$args as $k => $v ) {
3609 if ( $v instanceof
PPNode || $k === 0 ) {
3612 $funcArgs[] = $this->mPreprocessor->newPartNodeArray(
array( $k => $v ) )->item( 0 );
3616 # Add a frame parameter, and pass the arguments as an array
3617 $allArgs[] = $frame;
3618 $allArgs[] = $funcArgs;
3620 # Convert arguments to plain text and append to $allArgs
3621 foreach (
$args as $k => $v ) {
3622 if ( $v instanceof
PPNode ) {
3623 $allArgs[] = trim( $frame->expand( $v ) );
3624 } elseif ( is_int( $k ) && $k >= 0 ) {
3625 $allArgs[] = trim( $v );
3627 $allArgs[] = trim(
"$k=$v" );
3632 $result = call_user_func_array( $callback, $allArgs );
3634 # The interface for function hooks allows them to return a wikitext
3635 # string or an array containing the string and any flags. This mungs
3636 # things around to match what this method should return.
3653 $preprocessFlags = 0;
3654 if ( isset(
$result[
'noparse'] ) ) {
3655 $noparse =
$result[
'noparse'];
3657 if ( isset(
$result[
'preprocessFlags'] ) ) {
3658 $preprocessFlags =
$result[
'preprocessFlags'];
3662 $result[
'text'] = $this->preprocessToDom(
$result[
'text'], $preprocessFlags );
3679 function getTemplateDom(
$title ) {
3681 $titleText =
$title->getPrefixedDBkey();
3683 if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3684 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3686 $titleText =
$title->getPrefixedDBkey();
3688 if ( isset( $this->mTplDomCache[$titleText] ) ) {
3689 return array( $this->mTplDomCache[$titleText],
$title );
3692 # Cache miss, go to the database
3695 if ( $text ===
false ) {
3696 $this->mTplDomCache[$titleText] =
false;
3700 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3701 $this->mTplDomCache[$titleText] = $dom;
3703 if ( !
$title->equals( $cacheTitle ) ) {
3704 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3716 function fetchTemplateAndTitle(
$title ) {
3717 $templateCb = $this->mOptions->getTemplateCallback(); # Defaults to Parser::statelessFetchTemplate()
3718 $stuff = call_user_func( $templateCb,
$title, $this );
3719 $text = $stuff['
text'];
3720 $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] :
$title;
3721 if ( isset( $stuff['deps'] ) ) {
3722 foreach ( $stuff[
'deps']
as $dep ) {
3723 $this->mOutput->addTemplate( $dep[
'title'], $dep[
'page_id'], $dep[
'rev_id'] );
3724 if ( $dep[
'title']->equals( $this->getTitle() ) ) {
3727 $this->mOutput->setFlag(
'vary-revision' );
3731 return array( $text, $finalTitle );
3739 function fetchTemplate(
$title ) {
3740 $rv = $this->fetchTemplateAndTitle(
$title );
3753 static function statelessFetchTemplate(
$title,
$parser =
false ) {
3754 $text = $skip =
false;
3758 # Loop to fetch the article, with up to 1 redirect
3759 for ( $i = 0; $i < 2 && is_object(
$title ); $i++ ) {
3760 # Give extensions a chance to select the revision instead
3761 $id =
false; # Assume current
3762 wfRunHooks(
'BeforeParserFetchTemplateAndtitle',
3769 'page_id' =>
$title->getArticleID(),
3778 $rev_id =
$rev ?
$rev->getId() : 0;
3779 # If there is no current revision, there is no page
3780 if ( $id ===
false && !
$rev ) {
3782 $linkCache->addBadLinkObj(
$title );
3787 'page_id' =>
$title->getArticleID(),
3788 'rev_id' => $rev_id );
3790 # We fetched a rev from a different title; register it too...
3792 'title' =>
$rev->getTitle(),
3793 'page_id' =>
$rev->getPage(),
3794 'rev_id' => $rev_id );
3798 $content =
$rev->getContent();
3799 $text = $content ? $content->getWikitextForTransclusion() :
null;
3801 if ( $text ===
false || $text ===
null ) {
3808 if ( !$message->exists() ) {
3812 $content = $message->content();
3813 $text = $message->plain();
3822 $title = $content->getRedirectTarget();
3826 'finalTitle' => $finalTitle,
3854 # Register the file as a dependency...
3855 $this->mOutput->addImage(
$title->getDBkey(),
$time, $sha1 );
3857 # Update fetched file title
3859 $this->mOutput->addImage(
$title->getDBkey(),
$time, $sha1 );
3875 if ( isset(
$options[
'broken'] ) ) {
3877 } elseif ( isset(
$options[
'sha1'] ) ) {
3893 function interwikiTransclude(
$title, $action ) {
3894 global $wgEnableScaryTranscluding;
3896 if ( !$wgEnableScaryTranscluding ) {
3897 return wfMessage(
'scarytranscludedisabled' )->inContentLanguage()->text();
3900 $url =
$title->getFullURL(
array(
'action' => $action ) );
3902 if ( strlen( $url ) > 255 ) {
3903 return wfMessage(
'scarytranscludetoolong' )->inContentLanguage()->text();
3905 return $this->fetchScaryTemplateMaybeFromCache( $url );
3912 function fetchScaryTemplateMaybeFromCache( $url ) {
3913 global $wgTranscludeCacheExpiry;
3915 $tsCond =
$dbr->timestamp( time() - $wgTranscludeCacheExpiry );
3916 $obj =
$dbr->selectRow(
'transcache',
array(
'tc_time',
'tc_contents' ),
3917 array(
'tc_url' => $url,
"tc_time >= " .
$dbr->addQuotes( $tsCond ) ) );
3919 return $obj->tc_contents;
3923 $status = $req->execute();
3924 if ( $status->isOK() ) {
3925 $text = $req->getContent();
3926 } elseif ( $req->getStatus() != 200 ) {
3927 return wfMessage(
'scarytranscludefailed-httpstatus', $url, $req->getStatus() )->inContentLanguage()->text();
3929 return wfMessage(
'scarytranscludefailed', $url )->inContentLanguage()->text();
3933 $dbw->replace(
'transcache',
array(
'tc_url' ),
array(
3935 'tc_time' => $dbw->timestamp( time() ),
3936 'tc_contents' => $text
3950 function argSubstitution( $piece, $frame ) {
3954 $parts = $piece[
'parts'];
3955 $nameWithSpaces = $frame->expand( $piece[
'title'] );
3956 $argName = trim( $nameWithSpaces );
3958 $text = $frame->getArgument( $argName );
3959 if ( $text ===
false && $parts->getLength() > 0
3960 && ( $this->ot[
'html']
3962 || ( $this->ot[
'wiki'] && $frame->isTemplate() )
3965 # No match in frame, use the supplied default
3966 $object = $parts->item( 0 )->getChildren();
3968 if ( !$this->incrementIncludeSize(
'arg', strlen( $text ) ) ) {
3969 $error =
'<!-- WARNING: argument omitted, expansion size too large -->';
3970 $this->limitationWarn(
'post-expand-template-argument' );
3973 if ( $text ===
false && $object ===
false ) {
3975 $object = $frame->virtualBracketedImplode(
'{{{',
'|',
'}}}', $nameWithSpaces, $parts );
3977 if (
$error !==
false ) {
3980 if ( $object !==
false ) {
4005 function extensionSubstitution(
$params, $frame ) {
4007 $attrText = !isset(
$params[
'attr'] ) ? null : $frame->expand(
$params[
'attr'] );
4008 $content = !isset(
$params[
'inner'] ) ? null : $frame->expand(
$params[
'inner'] );
4009 $marker =
"{$this->mUniqPrefix}-$name-" . sprintf(
'%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
4011 $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower(
$name )] ) &&
4012 ( $this->ot[
'html'] || $this->ot[
'pre'] );
4013 if ( $isFunctionTag ) {
4014 $markerType =
'none';
4016 $markerType =
'general';
4018 if ( $this->ot[
'html'] || $isFunctionTag ) {
4021 if ( isset(
$params[
'attributes'] ) ) {
4022 $attributes = $attributes +
$params[
'attributes'];
4025 if ( isset( $this->mTagHooks[
$name] ) ) {
4026 # Workaround for PHP bug 35229 and similar
4027 if ( !is_callable( $this->mTagHooks[
$name] ) ) {
4028 throw new MWException(
"Tag hook for $name is not callable\n" );
4030 $output = call_user_func_array( $this->mTagHooks[
$name],
4031 array( $content, $attributes, $this, $frame ) );
4032 } elseif ( isset( $this->mFunctionTagHooks[
$name] ) ) {
4033 list( $callback, ) = $this->mFunctionTagHooks[
$name];
4034 if ( !is_callable( $callback ) ) {
4035 throw new MWException(
"Tag hook for $name is not callable\n" );
4038 $output = call_user_func_array( $callback,
array( &$this, $frame, $content, $attributes ) );
4040 $output =
'<span class="error">Invalid tag extension name: ' .
4041 htmlspecialchars(
$name ) .
'</span>';
4045 # Extract flags to local scope (to override $markerType)
4052 if ( is_null( $attrText ) ) {
4055 if ( isset(
$params[
'attributes'] ) ) {
4056 foreach (
$params[
'attributes']
as $attrName => $attrValue ) {
4057 $attrText .=
' ' . htmlspecialchars( $attrName ) .
'="' .
4058 htmlspecialchars( $attrValue ) .
'"';
4061 if ( $content ===
null ) {
4062 $output =
"<$name$attrText/>";
4064 $close = is_null(
$params[
'close'] ) ?
'' : $frame->expand(
$params[
'close'] );
4065 $output =
"<$name$attrText>$content$close";
4069 if ( $markerType ===
'none' ) {
4071 } elseif ( $markerType ===
'nowiki' ) {
4072 $this->mStripState->addNoWiki( $marker,
$output );
4073 } elseif ( $markerType ===
'general' ) {
4074 $this->mStripState->addGeneral( $marker,
$output );
4076 throw new MWException( __METHOD__ .
': invalid marker type' );
4088 function incrementIncludeSize(
$type,
$size ) {
4089 if ( $this->mIncludeSizes[
$type] +
$size > $this->mOptions->getMaxIncludeSize() ) {
4102 function incrementExpensiveFunctionCount() {
4103 $this->mExpensiveFunctionCount++;
4104 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4115 function doDoubleUnderscore( $text ) {
4118 # The position of __TOC__ needs to be recorded
4120 if ( $mw->match( $text ) ) {
4121 $this->mShowToc =
true;
4122 $this->mForceTocPosition =
true;
4124 # Set a placeholder. At the end we'll fill it in with the TOC.
4125 $text = $mw->replace(
'<!--MWTOC-->', $text, 1 );
4127 # Only keep the first one.
4128 $text = $mw->replace(
'', $text );
4131 # Now match and remove the rest of them
4133 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4135 if ( isset( $this->mDoubleUnderscores[
'nogallery'] ) ) {
4136 $this->mOutput->mNoGallery =
true;
4138 if ( isset( $this->mDoubleUnderscores[
'notoc'] ) && !$this->mForceTocPosition ) {
4139 $this->mShowToc =
false;
4141 if ( isset( $this->mDoubleUnderscores[
'hiddencat'] ) && $this->mTitle->getNamespace() ==
NS_CATEGORY ) {
4142 $this->addTrackingCategory(
'hidden-category-category' );
4144 # (bug 8068) Allow control over whether robots index a page.
4146 # @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here! This
4147 # is not desirable, the last one on the page should win.
4148 if ( isset( $this->mDoubleUnderscores[
'noindex'] ) && $this->mTitle->canUseNoindex() ) {
4149 $this->mOutput->setIndexPolicy(
'noindex' );
4150 $this->addTrackingCategory(
'noindex-category' );
4152 if ( isset( $this->mDoubleUnderscores[
'index'] ) && $this->mTitle->canUseNoindex() ) {
4153 $this->mOutput->setIndexPolicy(
'index' );
4154 $this->addTrackingCategory(
'index-category' );
4157 # Cache all double underscores in the database
4158 foreach ( $this->mDoubleUnderscores
as $key => $val ) {
4159 $this->mOutput->setProperty( $key,
'' );
4177 public function addTrackingCategory( $msg ) {
4178 if ( $this->mTitle->getNamespace() ===
NS_SPECIAL ) {
4179 wfDebug( __METHOD__ .
": Not adding tracking category $msg to special page!\n" );
4184 ->title( $this->getTitle() )
4185 ->inContentLanguage()
4188 # Allow tracking categories to be disabled by setting them to "-"
4189 if ( $cat ===
'-' ) {
4194 if ( $containerCategory ) {
4195 $this->mOutput->addCategory( $containerCategory->getDBkey(), $this->getDefaultSort() );
4198 wfDebug( __METHOD__ .
": [[MediaWiki:$msg]] is not a valid title!\n" );
4219 function formatHeadings( $text, $origText, $isMain =
true ) {
4220 global $wgMaxTocLevel, $wgExperimentalHtmlIds;
4222 # Inhibit editsection links if requested in the page
4223 if ( isset( $this->mDoubleUnderscores[
'noeditsection'] ) ) {
4224 $maybeShowEditLink = $showEditLink =
false;
4226 $maybeShowEditLink =
true;
4227 $showEditLink = $this->mOptions->getEditSection();
4229 if ( $showEditLink ) {
4230 $this->mOutput->setEditSectionTokens(
true );
4233 # Get all headlines for numbering them and adding funky stuff like [edit]
4234 # links - this is for later, but we need the number of headlines right now
4236 $numMatches = preg_match_all(
'/<H(?P<level>[1-6])(?P<attrib>.*?' .
'>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i', $text,
$matches );
4238 # if there are fewer than 4 headlines in the article, do not show TOC
4239 # unless it's been explicitly enabled.
4240 $enoughToc = $this->mShowToc &&
4241 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4243 # Allow user to stipulate that a page should have a "new section"
4244 # link added via __NEWSECTIONLINK__
4245 if ( isset( $this->mDoubleUnderscores[
'newsectionlink'] ) ) {
4246 $this->mOutput->setNewSection(
true );
4249 # Allow user to remove the "new section"
4250 # link via __NONEWSECTIONLINK__
4251 if ( isset( $this->mDoubleUnderscores[
'nonewsectionlink'] ) ) {
4252 $this->mOutput->hideNewSection(
true );
4255 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4256 # override above conditions and always show TOC above first header
4257 if ( isset( $this->mDoubleUnderscores[
'forcetoc'] ) ) {
4258 $this->mShowToc =
true;
4266 # Ugh .. the TOC should have neat indentation levels which can be
4267 # passed to the skin functions. These are determined here
4271 $sublevelCount =
array();
4272 $levelCount =
array();
4277 $markerRegex =
"{$this->mUniqPrefix}-h-(\d+)-" . self::MARKER_SUFFIX;
4278 $baseTitleText = $this->mTitle->getPrefixedDBkey();
4279 $oldType = $this->mOutputType;
4281 $frame = $this->getPreprocessor()->newFrame();
4282 $root = $this->preprocessToDom( $origText );
4283 $node = $root->getFirstChild();
4289 $isTemplate =
false;
4291 $sectionIndex =
false;
4293 $markerMatches =
array();
4294 if ( preg_match(
"/^$markerRegex/", $headline, $markerMatches ) ) {
4295 $serial = $markerMatches[1];
4296 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4297 $isTemplate = ( $titleText != $baseTitleText );
4298 $headline = preg_replace(
"/^$markerRegex\\s*/",
"", $headline );
4302 $prevlevel = $level;
4304 $level =
$matches[1][$headlineCount];
4306 if ( $level > $prevlevel ) {
4307 # Increase TOC level
4309 $sublevelCount[$toclevel] = 0;
4310 if ( $toclevel < $wgMaxTocLevel ) {
4311 $prevtoclevel = $toclevel;
4315 } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4316 # Decrease TOC level, find level to jump to
4318 for ( $i = $toclevel; $i > 0; $i-- ) {
4319 if ( $levelCount[$i] == $level ) {
4320 # Found last matching level
4323 } elseif ( $levelCount[$i] < $level ) {
4324 # Found first matching level below current level
4332 if ( $toclevel < $wgMaxTocLevel ) {
4333 if ( $prevtoclevel < $wgMaxTocLevel ) {
4334 # Unindent only if the previous toc level was shown :p
4336 $prevtoclevel = $toclevel;
4342 # No change in level, end TOC line
4343 if ( $toclevel < $wgMaxTocLevel ) {
4348 $levelCount[$toclevel] = $level;
4350 # count number of headlines for each level
4351 $sublevelCount[$toclevel]++;
4353 for ( $i = 1; $i <= $toclevel; $i++ ) {
4354 if ( !empty( $sublevelCount[$i] ) ) {
4358 $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4363 # The safe header is a version of the header text safe to use for links
4365 # Remove link placeholders by the link text.
4366 # <!--LINK number-->
4368 # link text with suffix
4369 # Do this before unstrip since link text can contain strip markers
4370 $safeHeadline = $this->replaceLinkHoldersText( $headline );
4372 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4373 $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4375 # Strip out HTML (first regex removes any tag not allowed)
4377 # * <sup> and <sub> (bug 8393)
4380 # * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
4382 # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4383 # to allow setting directionality in toc items.
4384 $tocline = preg_replace(
4385 array(
'#<(?!/?(span|sup|sub|i|b)(?: [^>]*)?>).*?' .
'>#',
'#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|i|b))(?: .*?)?' .
'>#' ),
4386 array(
'',
'<$1>' ),
4389 $tocline = trim( $tocline );
4391 # For the anchor, strip out HTML-y stuff period
4392 $safeHeadline = preg_replace(
'/<.*?' .
'>/',
'', $safeHeadline );
4395 # Save headline for section edit hint before it's escaped
4396 $headlineHint = $safeHeadline;
4398 if ( $wgExperimentalHtmlIds ) {
4399 # For reverse compatibility, provide an id that's
4400 # HTML4-compatible, like we used to.
4402 # It may be worth noting, academically, that it's possible for
4403 # the legacy anchor to conflict with a non-legacy headline
4404 # anchor on the page. In this case likely the "correct" thing
4405 # would be to either drop the legacy anchors or make sure
4406 # they're numbered first. However, this would require people
4407 # to type in section names like "abc_.D7.93.D7.90.D7.A4"
4408 # manually, so let's not bother worrying about it.
4410 array(
'noninitial',
'legacy' ) );
4413 if ( $legacyHeadline == $safeHeadline ) {
4414 # No reason to have both (in fact, we can't)
4415 $legacyHeadline =
false;
4418 $legacyHeadline =
false;
4423 # HTML names must be case-insensitively unique (bug 10721).
4424 # This does not apply to Unicode characters per
4425 # http://dev.w3.org/html5/spec/infrastructure.html#case-sensitivity-and-string-comparison
4426 # @todo FIXME: We may be changing them depending on the current locale.
4427 $arrayKey = strtolower( $safeHeadline );
4428 if ( $legacyHeadline ===
false ) {
4429 $legacyArrayKey =
false;
4431 $legacyArrayKey = strtolower( $legacyHeadline );
4434 # count how many in assoc. array so we can track dupes in anchors
4435 if ( isset( $refers[$arrayKey] ) ) {
4436 $refers[$arrayKey]++;
4438 $refers[$arrayKey] = 1;
4440 if ( isset( $refers[$legacyArrayKey] ) ) {
4441 $refers[$legacyArrayKey]++;
4443 $refers[$legacyArrayKey] = 1;
4446 # Don't number the heading if it is the only one (looks silly)
4447 if ( count(
$matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4448 # the two are different if the line contains a link
4449 $headline =
Html::element(
'span',
array(
'class' =>
'mw-headline-number' ), $numbering ) .
' ' . $headline;
4452 # Create the anchor for linking from the TOC to the section
4453 $anchor = $safeHeadline;
4454 $legacyAnchor = $legacyHeadline;
4455 if ( $refers[$arrayKey] > 1 ) {
4456 $anchor .=
'_' . $refers[$arrayKey];
4458 if ( $legacyHeadline !==
false && $refers[$legacyArrayKey] > 1 ) {
4459 $legacyAnchor .=
'_' . $refers[$legacyArrayKey];
4461 if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4463 $numbering, $toclevel, ( $isTemplate ?
false : $sectionIndex ) );
4466 # Add the section to the section tree
4467 # Find the DOM node for this header
4468 $noOffset = ( $isTemplate || $sectionIndex ===
false );
4469 while ( $node && !$noOffset ) {
4470 if ( $node->getName() ===
'h' ) {
4471 $bits = $node->splitHeading();
4472 if ( $bits[
'i'] == $sectionIndex ) {
4476 $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4478 $node = $node->getNextSibling();
4481 'toclevel' => $toclevel,
4484 'number' => $numbering,
4485 'index' => ( $isTemplate ?
'T-' :
'' ) . $sectionIndex,
4486 'fromtitle' => $titleText,
4487 'byteoffset' => ( $noOffset ?
null : $byteOffset ),
4488 'anchor' => $anchor,
4491 # give headline the correct <h#> tag
4492 if ( $maybeShowEditLink && $sectionIndex !==
false ) {
4494 if ( $isTemplate ) {
4495 # Put a T flag in the section identifier, to indicate to extractSections()
4496 # that sections inside <includeonly> should be counted.
4497 $editlinkArgs =
array( $titleText,
"T-$sectionIndex" );
4499 $editlinkArgs =
array( $this->mTitle->getPrefixedText(), $sectionIndex, $headlineHint );
4508 $editlink =
'<mw:editsection page="' . htmlspecialchars( $editlinkArgs[0] );
4509 $editlink .=
'" section="' . htmlspecialchars( $editlinkArgs[1] ) .
'"';
4510 if ( isset( $editlinkArgs[2] ) ) {
4511 $editlink .=
'>' . $editlinkArgs[2] .
'</mw:editsection>';
4519 $matches[
'attrib'][$headlineCount], $anchor, $headline,
4520 $editlink, $legacyAnchor );
4525 $this->setOutputType( $oldType );
4527 # Never ever show TOC if no headers
4528 if ( $numVisible < 1 ) {
4533 if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4537 $this->mOutput->setTOCHTML( $toc );
4538 $toc = self::TOC_START . $toc . self::TOC_END;
4542 $this->mOutput->setSections( $tocraw );
4545 # split up and insert constructed headlines
4546 $blocks = preg_split(
'/<H[1-6].*?' .
'>[\s\S]*?<\/H[1-6]>/i', $text );
4550 $sections =
array();
4551 foreach ( $blocks
as $block ) {
4553 if ( empty( $head[$i - 1] ) ) {
4554 $sections[$i] = $block;
4556 $sections[$i] = $head[$i - 1] . $block;
4569 wfRunHooks(
'ParserSectionCreate',
array( $this, $i, &$sections[$i], $showEditLink ) );
4574 if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4577 $sections[0] = $sections[0] . $toc .
"\n";
4580 $full .= join(
'', $sections );
4582 if ( $this->mForceTocPosition ) {
4583 return str_replace(
'<!--MWTOC-->', $toc, $full );
4602 $this->setUser(
$user );
4607 $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
4608 if (
$options->getPreSaveTransform() ) {
4609 $text = $this->pstPass2( $text,
$user );
4611 $text = $this->mStripState->unstripBoth( $text );
4613 $this->setUser(
null ); #Reset
4626 private function pstPass2( $text,
$user ) {
4629 # Note: This is the timestamp saved as hardcoded wikitext to
4630 # the database, we use $wgContLang here in order to give
4631 # everyone the same signature and use the default one rather
4632 # than the one selected in each user's preferences.
4633 # (see also bug 12815)
4634 $ts = $this->mOptions->getTimestamp();
4637 $tzMsg =
$timestamp->format(
'T' ); # might vary
on DST changeover!
4639 # Allow translation of timezones through wiki. format() can return
4640 # whatever crap the system uses, localised or not, so we cannot
4641 # ship premade translations.
4642 $key =
'timezone-' . strtolower( trim( $tzMsg ) );
4643 $msg =
wfMessage( $key )->inContentLanguage();
4644 if ( $msg->exists() ) {
4645 $tzMsg = $msg->text();
4648 $d =
$wgContLang->timeanddate( $ts,
false,
false ) .
" ($tzMsg)";
4650 # Variable replacement
4651 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4652 $text = $this->replaceVariables( $text );
4654 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4655 # which may corrupt this parser instance via its wfMessage()->text() call-
4658 $sigText = $this->getUserSig(
$user );
4659 $text = strtr( $text,
array(
4661 '~~~~' =>
"$sigText $d",
4665 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4667 $nc =
'[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4669 $p1 =
"/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/"; # [[ns:
page (context)|]]
4670 $p4 =
"/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/"; # [[ns:page(context)|]] (
double-width brackets, added
in r40257)
4671 $p3 =
"/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/"; # [[ns:
page (context), context|]] (
using either single or
double-width comma)
4674 #
try $p1 first, to turn
"[[A, B (C)|]]" into
"[[A, B (C)|A, B]]"
4675 $text = preg_replace( $p1,
'[[\\1\\2\\3|\\2]]', $text );
4676 $text = preg_replace( $p4,
'[[\\1\\2\\3|\\2]]', $text );
4677 $text = preg_replace( $p3,
'[[\\1\\2\\3\\4|\\2]]', $text );
4679 $t = $this->mTitle->getText();
4681 if ( preg_match(
"/^($nc+:|)$tc+?( \\($tc+\\))$/",
$t, $m ) ) {
4682 $text = preg_replace( $p2,
"[[$m[1]\\1$m[2]|\\1]]", $text );
4683 } elseif ( preg_match(
"/^($nc+:|)$tc+?(, $tc+|)$/",
$t, $m ) &&
"$m[1]$m[2]" !=
'' ) {
4684 $text = preg_replace( $p2,
"[[$m[1]\\1$m[2]|\\1]]", $text );
4686 # if there's no context, don't bother duplicating the title
4687 $text = preg_replace( $p2,
'[[\\1]]', $text );
4690 # Trim trailing whitespace
4691 $text = rtrim( $text );
4710 function getUserSig( &
$user, $nickname =
false, $fancySig =
null ) {
4713 $username =
$user->getName();
4715 # If not given, retrieve from the user object.
4716 if ( $nickname ===
false ) {
4717 $nickname =
$user->getOption(
'nickname' );
4720 if ( is_null( $fancySig ) ) {
4721 $fancySig =
$user->getBoolOption(
'fancysig' );
4724 $nickname = $nickname ==
null ? $username : $nickname;
4726 if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4727 $nickname = $username;
4728 wfDebug( __METHOD__ .
": $username has overlong signature.\n" );
4729 } elseif ( $fancySig !==
false ) {
4730 # Sig. might contain markup; validate this
4731 if ( $this->validateSig( $nickname ) !==
false ) {
4732 # Validated; clean up (if needed) and return it
4733 return $this->cleanSig( $nickname,
true );
4735 # Failed to validate; fall back to the default
4736 $nickname = $username;
4737 wfDebug( __METHOD__ .
": $username has bad XML tags in signature.\n" );
4741 # Make sure nickname doesnt get a sig in a sig
4742 $nickname = self::cleanSigInSig( $nickname );
4744 # If we're still here, make it a link to the user page
4747 $msgName =
$user->isAnon() ?
'signature-anon' :
'signature';
4749 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()->title( $this->getTitle() )->text();
4758 function validateSig( $text ) {
4772 public function cleanSig( $text, $parsing =
false ) {
4778 # Option to disable this feature
4779 if ( !$this->mOptions->getCleanSignatures() ) {
4783 # @todo FIXME: Regex doesn't respect extension tags or nowiki
4784 # => Move this logic to braceSubstitution()
4786 $substRegex =
'/\{\{(?!(?:' . $substWord->getBaseRegex() .
'))/x' . $substWord->getRegexCase();
4787 $substText =
'{{' . $substWord->getSynonym( 0 );
4789 $text = preg_replace( $substRegex, $substText, $text );
4790 $text = self::cleanSigInSig( $text );
4791 $dom = $this->preprocessToDom( $text );
4792 $frame = $this->getPreprocessor()->newFrame();
4793 $text = $frame->expand( $dom );
4796 $text = $this->mStripState->unstripBoth( $text );
4808 public static function cleanSigInSig( $text ) {
4809 $text = preg_replace(
'/~{3,5}/',
'', $text );
4833 $this->setTitle(
$title );
4835 $this->setOutputType( $outputType );
4836 if ( $clearState ) {
4837 $this->clearState();
4850 static $executing =
false;
4852 # Guard against infinite recursion
4895 public function setHook( $tag, $callback ) {
4896 $tag = strtolower( $tag );
4897 if ( preg_match(
'/[<>\r\n]/', $tag, $m ) ) {
4898 throw new MWException(
"Invalid character {$m[0]} in setHook('$tag', ...) call" );
4900 $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] :
null;
4901 $this->mTagHooks[$tag] = $callback;
4902 if ( !in_array( $tag, $this->mStripList ) ) {
4903 $this->mStripList[] = $tag;
4926 function setTransparentTagHook( $tag, $callback ) {
4927 $tag = strtolower( $tag );
4928 if ( preg_match(
'/[<>\r\n]/', $tag, $m ) ) {
4929 throw new MWException(
"Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4931 $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] :
null;
4932 $this->mTransparentTagHooks[$tag] = $callback;
4940 function clearTagHooks() {
4941 $this->mTagHooks =
array();
4942 $this->mFunctionTagHooks =
array();
4943 $this->mStripList = $this->mDefaultStripList;
4989 public function setFunctionHook( $id, $callback,
$flags = 0 ) {
4992 $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] :
null;
4993 $this->mFunctionHooks[$id] =
array( $callback,
$flags );
4995 # Add to function cache
4998 throw new MWException( __METHOD__ .
'() expecting a magic word identifier.' );
5001 $synonyms = $mw->getSynonyms();
5002 $sensitive = intval( $mw->isCaseSensitive() );
5004 foreach ( $synonyms
as $syn ) {
5006 if ( !$sensitive ) {
5013 # Remove trailing colon
5014 if ( substr( $syn, -1, 1 ) ===
':' ) {
5015 $syn = substr( $syn, 0, -1 );
5017 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5027 function getFunctionHooks() {
5028 return array_keys( $this->mFunctionHooks );
5041 function setFunctionTagHook( $tag, $callback,
$flags ) {
5042 $tag = strtolower( $tag );
5043 if ( preg_match(
'/[<>\r\n]/', $tag, $m ) ) {
5044 throw new MWException(
"Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
5046 $old = isset( $this->mFunctionTagHooks[$tag] ) ?
5047 $this->mFunctionTagHooks[$tag] :
null;
5048 $this->mFunctionTagHooks[$tag] =
array( $callback,
$flags );
5050 if ( !in_array( $tag, $this->mStripList ) ) {
5051 $this->mStripList[] = $tag;
5067 function replaceLinkHolders( &$text,
$options = 0 ) {
5068 return $this->mLinkHolders->replace( $text );
5078 function replaceLinkHoldersText( $text ) {
5079 return $this->mLinkHolders->replaceText( $text );
5095 function renderImageGallery( $text,
$params ) {
5099 if ( isset(
$params[
'mode'] ) ) {
5110 $ig->setContextTitle( $this->mTitle );
5111 $ig->setShowBytes(
false );
5112 $ig->setShowFilename(
false );
5113 $ig->setParser( $this );
5114 $ig->setHideBadImages();
5117 if ( isset(
$params[
'showfilename'] ) ) {
5118 $ig->setShowFilename(
true );
5120 $ig->setShowFilename(
false );
5122 if ( isset(
$params[
'caption'] ) ) {
5123 $caption =
$params[
'caption'];
5124 $caption = htmlspecialchars( $caption );
5125 $caption = $this->replaceInternalLinks( $caption );
5126 $ig->setCaptionHtml( $caption );
5128 if ( isset(
$params[
'perrow'] ) ) {
5129 $ig->setPerRow(
$params[
'perrow'] );
5131 if ( isset(
$params[
'widths'] ) ) {
5132 $ig->setWidths(
$params[
'widths'] );
5134 if ( isset(
$params[
'heights'] ) ) {
5135 $ig->setHeights(
$params[
'heights'] );
5137 $ig->setAdditionalOptions(
$params );
5139 wfRunHooks(
'BeforeParserrenderImageGallery',
array( &$this, &$ig ) );
5143 # match lines like these:
5144 # Image:someimage.jpg|This is some image
5152 if ( strpos(
$matches[0],
'%' ) !==
false ) {
5156 if ( is_null(
$title ) ) {
5157 # Bogus title. Ignore these so we don't bomb out later.
5161 # We need to get what handler the file uses, to figure out parameters.
5162 # Note, a hook can overide the file name, and chose an entirely different
5163 # file (which potentially could be of a different type and have different handler).
5168 # Don't register it now, as ImageGallery does that later.
5170 $handler =
$file ?
$file->getHandler() :
false;
5174 'img_alt' =>
'gallery-internal-alt',
5175 'img_link' =>
'gallery-internal-link',
5178 $paramMap = $paramMap + $handler->getParamMap();
5181 unset( $paramMap[
'img_width'] );
5190 $handlerOptions =
array();
5201 foreach ( $parameterMatches
as $parameterMatch ) {
5202 list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5204 $paramName = $paramMap[$magicName];
5206 switch ( $paramName ) {
5207 case 'gallery-internal-alt':
5208 $alt = $this->stripAltText( $match,
false );
5210 case 'gallery-internal-link':
5211 $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
5212 $chars = self::EXT_LINK_URL_CLASS;
5213 $prots = $this->mUrlProtocols;
5215 if ( preg_match(
"/^($prots)$chars+$/u", $linkValue ) ) {
5219 if ( $localLinkTitle !==
null ) {
5220 $link = $localLinkTitle->getLocalURL();
5226 if ( $handler->validateParam( $paramName, $match ) ) {
5227 $handlerOptions[$paramName] = $match;
5230 wfDebug(
"$parameterMatch failed parameter validation\n" );
5231 $label .=
'|' . $parameterMatch;
5237 $label .=
'|' . $parameterMatch;
5241 $label = substr( $label, 1 );
5244 $ig->add(
$title, $label, $alt,
$link, $handlerOptions );
5246 $html = $ig->toHTML();
5255 function getImageParams( $handler ) {
5257 $handlerClass = get_class( $handler );
5261 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5262 # Initialise static lists
5263 static $internalParamNames =
array(
5264 'horizAlign' =>
array(
'left',
'right',
'center',
'none' ),
5265 'vertAlign' =>
array(
'baseline',
'sub',
'super',
'top',
'text-top',
'middle',
5266 'bottom',
'text-bottom' ),
5267 'frame' =>
array(
'thumbnail',
'manualthumb',
'framed',
'frameless',
5268 'upright',
'border',
'link',
'alt',
'class' ),
5270 static $internalParamMap;
5271 if ( !$internalParamMap ) {
5272 $internalParamMap =
array();
5273 foreach ( $internalParamNames
as $type => $names ) {
5275 $magicName = str_replace(
'-',
'_',
"img_$name" );
5281 # Add handler params
5282 $paramMap = $internalParamMap;
5284 $handlerParamMap = $handler->getParamMap();
5285 foreach ( $handlerParamMap
as $magic => $paramName ) {
5286 $paramMap[$magic] =
array(
'handler', $paramName );
5289 $this->mImageParams[$handlerClass] = $paramMap;
5290 $this->mImageParamsMagicArray[$handlerClass] =
new MagicWordArray( array_keys( $paramMap ) );
5292 return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
5304 # Check if the options text is of the form "options|alt text"
5306 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5307 # * left no resizing, just left align. label is used for alt= only
5308 # * right same, but right aligned
5309 # * none same, but not aligned
5310 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5311 # * center center the image
5312 # * frame Keep original image size, no magnify-button.
5313 # * framed Same as "frame"
5314 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5315 # * upright reduce width for upright images, rounded to full __0 px
5316 # * border draw a 1px border around the image
5317 # * alt Text for HTML alt attribute (defaults to empty)
5318 # * class Set a class for img node
5319 # * link Set the target of the image link. Can be external, interwiki, or local
5320 # vertical-align values (no % or length right now):
5332 # Give extensions a chance to select the file revision for us
5337 # Fetch and register the file (file title may be different via hooks)
5341 $handler =
$file ?
$file->getHandler() :
false;
5343 list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5346 $this->addTrackingCategory(
'broken-file-category' );
5349 # Process the input parameters
5352 'horizAlign' =>
array(),
'vertAlign' =>
array() );
5353 foreach ( $parts
as $part ) {
5354 $part = trim( $part );
5355 list( $magicName,
$value ) = $mwArray->matchVariableStartToEnd( $part );
5357 if ( isset( $paramMap[$magicName] ) ) {
5358 list(
$type, $paramName ) = $paramMap[$magicName];
5360 # Special case; width and height come in one variable together
5361 if (
$type ===
'handler' && $paramName ===
'width' ) {
5362 $parsedWidthParam = $this->parseWidthParam(
$value );
5363 if ( isset( $parsedWidthParam[
'width'] ) ) {
5364 $width = $parsedWidthParam[
'width'];
5365 if ( $handler->validateParam(
'width', $width ) ) {
5370 if ( isset( $parsedWidthParam[
'height'] ) ) {
5371 $height = $parsedWidthParam[
'height'];
5372 if ( $handler->validateParam(
'height', $height ) ) {
5377 # else no validation -- bug 13436
5379 if (
$type ===
'handler' ) {
5380 # Validate handler parameter
5381 $validated = $handler->validateParam( $paramName,
$value );
5383 # Validate internal parameters
5384 switch ( $paramName ) {
5388 # @todo FIXME: Possibly check validity here for
5389 # manualthumb? downstream behavior seems odd with
5390 # missing manual thumbs.
5395 $chars = self::EXT_LINK_URL_CLASS;
5396 $prots = $this->mUrlProtocols;
5398 $paramName =
'no-link';
5401 } elseif ( preg_match(
"/^(?i)$prots/",
$value ) ) {
5402 if ( preg_match(
"/^((?i)$prots)$chars+$/u",
$value, $m ) ) {
5403 $paramName =
'link-url';
5404 $this->mOutput->addExternalLink(
$value );
5405 if ( $this->mOptions->getExternalLinkTarget() ) {
5406 $params[
$type][
'link-target'] = $this->mOptions->getExternalLinkTarget();
5413 $paramName =
'link-title';
5415 $this->mOutput->addLink( $linkTitle );
5421 # Most other things appear to be empty or numeric...
5422 $validated = (
$value ===
false || is_numeric( trim(
$value ) ) );
5431 if ( !$validated ) {
5436 # Process alignment parameters
5437 if (
$params[
'horizAlign'] ) {
5444 $params[
'frame'][
'caption'] = $caption;
5446 # Will the image be presented in a frame, with the caption below?
5447 $imageIsFramed = isset(
$params[
'frame'][
'frame'] )
5448 || isset(
$params[
'frame'][
'framed'] )
5449 || isset(
$params[
'frame'][
'thumbnail'] )
5450 || isset(
$params[
'frame'][
'manualthumb'] );
5452 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5453 # came to also set the caption, ordinary text after the image -- which
5454 # makes no sense, because that just repeats the text multiple times in
5455 # screen readers. It *also* came to set the title attribute.
5457 # Now that we have an alt attribute, we should not set the alt text to
5458 # equal the caption: that's worse than useless, it just repeats the
5459 # text. This is the framed/thumbnail case. If there's no caption, we
5460 # use the unnamed parameter for alt text as well, just for the time be-
5461 # ing, if the unnamed param is set and the alt param is not.
5463 # For the future, we need to figure out if we want to tweak this more,
5464 # e.g., introducing a title= parameter for the title; ignoring the un-
5465 # named parameter entirely for images without a caption; adding an ex-
5466 # plicit caption= parameter and preserving the old magic unnamed para-
5468 if ( $imageIsFramed ) { # Framed image
5469 if ( $caption ===
'' && !isset(
$params[
'frame'][
'alt'] ) ) {
5470 # No caption or alt text, add the filename as the alt text so
5471 # that screen readers at least get some description of the image
5474 # Do not set $params['frame']['title'] because tooltips don't make sense
5476 }
else { # Inline image
5477 if ( !isset(
$params[
'frame'][
'alt'] ) ) {
5478 # No alt text, use the "caption" for the alt text
5479 if ( $caption !==
'' ) {
5480 $params[
'frame'][
'alt'] = $this->stripAltText( $caption, $holders );
5482 # No caption, fall back to using the filename for the
5487 # Use the "caption" for the tooltip text
5488 $params[
'frame'][
'title'] = $this->stripAltText( $caption, $holders );
5493 # Linker does the rest
5496 $time, $descQuery, $this->mOptions->getThumbSize() );
5498 # Give the handler a chance to modify the parser object
5500 $handler->parserTransformHook( $this,
$file );
5511 protected function stripAltText( $caption, $holders ) {
5512 # Strip bad stuff out of the title (tooltip). We can't just use
5513 # replaceLinkHoldersText() here, because if this function is called
5514 # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5516 $tooltip = $holders->replaceText( $caption );
5518 $tooltip = $this->replaceLinkHoldersText( $caption );
5521 # make sure there are no placeholders in thumbnail attributes
5522 # that are later expanded to html- so expand them now and
5524 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5534 function disableCache() {
5535 wfDebug(
"Parser output marked as uncacheable.\n" );
5536 if ( !$this->mOutput ) {
5538 " can only be called when actually parsing something" );
5540 $this->mOutput->setCacheTime( -1 );
5541 $this->mOutput->updateCacheExpiry( 0 );
5552 function attributeStripCallback( &$text, $frame =
false ) {
5553 $text = $this->replaceVariables( $text, $frame );
5554 $text = $this->mStripState->unstripBoth( $text );
5563 function getTags() {
5564 return array_merge( array_keys( $this->mTransparentTagHooks ), array_keys( $this->mTagHooks ), array_keys( $this->mFunctionTagHooks ) );
5577 function replaceTransparentTags( $text ) {
5579 $elements = array_keys( $this->mTransparentTagHooks );
5580 $text = self::extractTagsAndParams( $elements, $text,
$matches, $this->mUniqPrefix );
5581 $replacements =
array();
5584 list( $element, $content,
$params, $tag ) = $data;
5585 $tagName = strtolower( $element );
5586 if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5587 $output = call_user_func_array( $this->mTransparentTagHooks[$tagName],
array( $content,
$params, $this ) );
5591 $replacements[$marker] =
$output;
5593 return strtr( $text, $replacements );
5625 private function extractSections( $text,
$section, $mode, $newText =
'' ) {
5629 $frame = $this->getPreprocessor()->newFrame();
5631 # Process section extraction flags
5633 $sectionParts = explode(
'-',
$section );
5634 $sectionIndex = array_pop( $sectionParts );
5635 foreach ( $sectionParts
as $part ) {
5636 if ( $part ===
'T' ) {
5637 $flags |= self::PTD_FOR_INCLUSION;
5641 # Check for empty input
5642 if ( strval( $text ) ===
'' ) {
5643 # Only sections 0 and T-0 exist in an empty document
5644 if ( $sectionIndex == 0 ) {
5645 if ( $mode ===
'get' ) {
5651 if ( $mode ===
'get' ) {
5659 # Preprocess the text
5660 $root = $this->preprocessToDom( $text,
$flags );
5662 # <h> nodes indicate section breaks
5663 # They can only occur at the top level, so we can find them by iterating the root's children
5664 $node = $root->getFirstChild();
5666 # Find the target section
5667 if ( $sectionIndex == 0 ) {
5668 # Section zero doesn't nest, level=big
5669 $targetLevel = 1000;
5672 if ( $node->getName() ===
'h' ) {
5673 $bits = $node->splitHeading();
5674 if ( $bits[
'i'] == $sectionIndex ) {
5675 $targetLevel = $bits[
'level'];
5679 if ( $mode ===
'replace' ) {
5682 $node = $node->getNextSibling();
5688 if ( $mode ===
'get' ) {
5695 # Find the end of the section, including nested sections
5697 if ( $node->getName() ===
'h' ) {
5698 $bits = $node->splitHeading();
5699 $curLevel = $bits[
'level'];
5700 if ( $bits[
'i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5704 if ( $mode ===
'get' ) {
5707 $node = $node->getNextSibling();
5710 # Write out the remainder (in replace mode only)
5711 if ( $mode ===
'replace' ) {
5712 # Output the replacement text
5713 # Add two newlines on -- trailing whitespace in $newText is conventionally
5714 # stripped by the editor, so we need both newlines to restore the paragraph gap
5715 # Only add trailing whitespace if there is newText
5716 if ( $newText !=
"" ) {
5717 $outText .= $newText .
"\n\n";
5722 $node = $node->getNextSibling();
5726 if ( is_string( $outText ) ) {
5727 # Re-insert stripped tags
5728 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5746 public function getSection( $text,
$section, $deftext =
'' ) {
5747 return $this->extractSections( $text,
$section,
"get", $deftext );
5760 public function replaceSection( $oldtext,
$section, $text ) {
5761 return $this->extractSections( $oldtext,
$section,
"replace", $text );
5769 function getRevisionId() {
5770 return $this->mRevisionId;
5779 public function getRevisionObject() {
5780 if ( !is_null( $this->mRevisionObject ) ) {
5781 return $this->mRevisionObject;
5783 if ( is_null( $this->mRevisionId ) ) {
5788 return $this->mRevisionObject;
5795 function getRevisionTimestamp() {
5796 if ( is_null( $this->mRevisionTimestamp ) ) {
5801 $revObject = $this->getRevisionObject();
5804 # The cryptic '' timezone parameter tells to use the site-default
5805 # timezone offset instead of the user settings.
5807 # Since this value will be saved into the parser cache, served
5808 # to other users, and potentially even used inside links and such,
5809 # it needs to be consistent for all visitors.
5814 return $this->mRevisionTimestamp;
5822 function getRevisionUser() {
5823 if ( is_null( $this->mRevisionUser ) ) {
5824 $revObject = $this->getRevisionObject();
5826 # if this template is subst: the revision id will be blank,
5827 # so just use the current user's name
5829 $this->mRevisionUser = $revObject->getUserText();
5830 } elseif ( $this->ot[
'wiki'] || $this->mOptions->getIsPreview() ) {
5831 $this->mRevisionUser = $this->getUser()->getName();
5834 return $this->mRevisionUser;
5842 function getRevisionSize() {
5843 if ( is_null( $this->mRevisionSize ) ) {
5844 $revObject = $this->getRevisionObject();
5846 # if this variable is subst: the revision id will be blank,
5847 # so just use the parser input size, because the own substituation
5848 # will change the size.
5850 $this->mRevisionSize = $revObject->getSize();
5851 } elseif ( $this->ot[
'wiki'] || $this->mOptions->getIsPreview() ) {
5852 $this->mRevisionSize = $this->mInputSize;
5855 return $this->mRevisionSize;
5863 public function setDefaultSort(
$sort ) {
5864 $this->mDefaultSort =
$sort;
5865 $this->mOutput->setProperty(
'defaultsort',
$sort );
5878 public function getDefaultSort() {
5879 if ( $this->mDefaultSort !==
false ) {
5880 return $this->mDefaultSort;
5892 public function getCustomDefaultSort() {
5893 return $this->mDefaultSort;
5905 public function guessSectionNameFromWikiText( $text ) {
5906 # Strip out wikitext links(they break the anchor)
5907 $text = $this->stripSectionName( $text );
5920 public function guessLegacySectionNameFromWikiText( $text ) {
5921 # Strip out wikitext links(they break the anchor)
5922 $text = $this->stripSectionName( $text );
5941 public function stripSectionName( $text ) {
5942 # Strip internal link markup
5943 $text = preg_replace(
'/\[\[:?([^[|]+)\|([^[]+)\]\]/',
'$2', $text );
5944 $text = preg_replace(
'/\[\[:?([^[]+)\|?\]\]/',
'$1', $text );
5946 # Strip external link markup
5947 # @todo FIXME: Not tolerant to blank link text
5948 # I.E. [https://www.mediawiki.org] will render as [1] or something depending
5949 # on how many empty links there are on the page - need to figure that out.
5950 $text = preg_replace(
'/\[(?i:' . $this->mUrlProtocols .
')([^ ]+?) ([^[]+)\]/',
'$2', $text );
5952 # Parse wikitext quotes (italics & bold)
5953 $text = $this->doQuotes( $text );
5973 $text = $this->replaceVariables( $text );
5974 $text = $this->mStripState->unstripBoth( $text );
6015 function markerSkipCallback(
$s, $callback ) {
6018 while ( $i < strlen(
$s ) ) {
6019 $markerStart = strpos(
$s, $this->mUniqPrefix, $i );
6020 if ( $markerStart ===
false ) {
6021 $out .= call_user_func( $callback, substr(
$s, $i ) );
6024 $out .= call_user_func( $callback, substr(
$s, $i, $markerStart - $i ) );
6025 $markerEnd = strpos(
$s, self::MARKER_SUFFIX, $markerStart );
6026 if ( $markerEnd ===
false ) {
6027 $out .= substr(
$s, $markerStart );
6030 $markerEnd += strlen( self::MARKER_SUFFIX );
6031 $out .= substr(
$s, $markerStart, $markerEnd - $markerStart );
6045 function killMarkers( $text ) {
6046 return $this->mStripState->killMarkers( $text );
6065 function serializeHalfParsedText( $text ) {
6069 'version' => self::HALF_PARSED_VERSION,
6070 'stripState' => $this->mStripState->getSubState( $text ),
6071 'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6092 function unserializeHalfParsedText( $data ) {
6093 if ( !isset( $data[
'version'] ) || $data[
'version'] != self::HALF_PARSED_VERSION ) {
6094 throw new MWException( __METHOD__ .
': invalid version' );
6097 # First, extract the strip state.
6098 $texts =
array( $data[
'text'] );
6099 $texts = $this->mStripState->merge( $data[
'stripState'], $texts );
6101 # Now renumber links
6102 $texts = $this->mLinkHolders->mergeForeign( $data[
'linkHolders'], $texts );
6104 # Should be good to go.
6117 function isValidHalfParsedText( $data ) {
6118 return isset( $data[
'version'] ) && $data[
'version'] == self::HALF_PARSED_VERSION;
6129 public function parseWidthParam(
$value ) {
6130 $parsedWidthParam =
array();
6132 return $parsedWidthParam;
6135 # (bug 13500) In both cases (width/height and width only),
6136 # permit trailing "px" for backward compatibility.
6137 if ( preg_match(
'/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/',
$value, $m ) ) {
6138 $width = intval( $m[1] );
6139 $height = intval( $m[2] );
6140 $parsedWidthParam[
'width'] = $width;
6141 $parsedWidthParam[
'height'] = $height;
6142 } elseif ( preg_match(
'/^[0-9]*\s*(?:px)?\s*$/',
$value ) ) {
6143 $width = intval(
$value );
6144 $parsedWidthParam[
'width'] = $width;
6146 return $parsedWidthParam;