MediaWiki  master
Parser.php
Go to the documentation of this file.
1 <?php
51 use Psr\Log\LoggerInterface;
52 use Wikimedia\IPUtils;
53 use Wikimedia\ScopedCallback;
54 
95 class Parser {
97 
98  # Flags for Parser::setFunctionHook
99  public const SFH_NO_HASH = 1;
100  public const SFH_OBJECT_ARGS = 2;
101 
102  # Constants needed for external link processing
103  # Everything except bracket, space, or control characters
104  # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
105  # as well as U+3000 is IDEOGRAPHIC SPACE for T21052
106  # \x{FFFD} is the Unicode replacement character, which the HTML5 spec
107  # uses to replace invalid HTML characters.
108  public const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
109  # Simplified expression to match an IPv4 or IPv6 address, or
110  # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
111  // phpcs:ignore Generic.Files.LineLength
112  private const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
113  # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
114  // phpcs:ignore Generic.Files.LineLength
115  private const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
116  \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
117 
118  # Regular expression for a non-newline space
119  private const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
120 
125  public const PTD_FOR_INCLUSION = Preprocessor::DOM_FOR_INCLUSION;
126 
127  # Allowed values for $this->mOutputType
128  # Parameter to startExternalParse().
129  public const OT_HTML = 1; # like parse()
130  public const OT_WIKI = 2; # like preSaveTransform()
131  public const OT_PREPROCESS = 3; # like preprocess()
132  public const OT_MSG = 3;
133  # like extractSections() - portions of the original are returned unchanged.
134  public const OT_PLAIN = 4;
135 
153  public const MARKER_SUFFIX = "-QINU`\"'\x7f";
154  public const MARKER_PREFIX = "\x7f'\"`UNIQ-";
155 
168  public const TOC_START = '<mw:toc>';
169 
175  public const TOC_END = '</mw:toc>';
176 
194  public const TOC_PLACEHOLDER = '<mw:tocplace></mw:tocplace>';
195 
196  # Persistent:
197  private $mTagHooks = [];
198  private $mFunctionHooks = [];
199  private $mFunctionSynonyms = [ 0 => [], 1 => [] ];
200  private $mStripList = [];
201  private $mVarCache = [];
202  private $mImageParams = [];
205  public $mMarkerIndex = 0;
206 
207  # Initialised by initializeVariables()
208 
212  private $mVariables;
213 
217  private $mSubstWords;
218 
219  # Initialised in constructor
221 
227  private $urlUtils;
228 
229  # Initialized in constructor
233  private $mPreprocessor;
234 
235  # Cleared with clearState():
239  private $mOutput;
240  private $mAutonumber;
241 
245  private $mStripState;
246 
250  private $mLinkHolders;
251 
255  private $mLinkID;
256  private $mIncludeSizes;
263  public $mHeadings;
269  public $mExpensiveFunctionCount; # number of expensive parser function calls
270  private $mShowToc;
273  private $mTplDomCache;
274 
278  private $mUser;
279 
280  # Temporary
281  # These are variables reset at least once per parse regardless of $clearState
282 
287  public $mOptions;
288 
296  public $mTitle; # Title context, used for self-link rendering and similar things
297  private $mOutputType; # Output type, one of the OT_xxx constants
299  public $ot; # Shortcut alias, see setOutputType()
300  private $mRevisionId; # ID to display in {{REVISIONID}} tags
301  private $mRevisionTimestamp; # The timestamp of the specified revision ID
302  private $mRevisionUser; # User to display in {{REVISIONUSER}} tag
303  private $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
304  private $mInputSize = false; # For {{PAGESIZE}} on current page.
305 
308 
315 
323 
328  private $mInParse = false;
329 
331  private $mProfiler;
332 
336  private $mLinkRenderer;
337 
340 
342  private $contLang;
343 
346 
348  private $factory;
349 
352 
355 
363  private $svcOptions;
364 
367 
369  private $nsInfo;
370 
372  private $logger;
373 
375  private $badFileLookup;
376 
378  private $hookContainer;
379 
381  private $hookRunner;
382 
384  private $tidy;
385 
388 
390  private $userFactory;
391 
394 
397 
400 
402  private $userNameUtils;
403 
407  public const CONSTRUCTOR_OPTIONS = [
408  // See documentation for the corresponding config options
409  // Many of these are only used in (eg) CoreMagicVariables
410  MainConfigNames::AllowDisplayTitle,
411  MainConfigNames::AllowSlowParserFunctions,
412  MainConfigNames::ArticlePath,
413  MainConfigNames::EnableScaryTranscluding,
414  MainConfigNames::ExtraInterlanguageLinkPrefixes,
415  MainConfigNames::FragmentMode,
416  MainConfigNames::Localtimezone,
417  MainConfigNames::MaxSigChars,
418  MainConfigNames::MaxTocLevel,
419  MainConfigNames::MiserMode,
420  MainConfigNames::RawHtml,
421  MainConfigNames::ScriptPath,
422  MainConfigNames::Server,
423  MainConfigNames::ServerName,
424  MainConfigNames::ShowHostnames,
425  MainConfigNames::SignatureValidation,
426  MainConfigNames::Sitename,
427  MainConfigNames::StylePath,
428  MainConfigNames::TranscludeCacheExpiry,
429  MainConfigNames::PreprocessorCacheThreshold,
430  ];
431 
458  public function __construct(
464  SpecialPageFactory $spFactory,
467  LoggerInterface $logger,
472  WANObjectCache $wanCache,
480  ) {
481  if ( ParserFactory::$inParserFactory === 0 ) {
482  // Direct construction of Parser was deprecated in 1.34 and
483  // removed in 1.36; use a ParserFactory instead.
484  throw new MWException( 'Direct construction of Parser not allowed' );
485  }
486  $this->deprecatePublicProperty( 'mLinkID', '1.35', __CLASS__ );
487  $this->deprecatePublicProperty( 'mIncludeSizes', '1.35', __CLASS__ );
488  $this->deprecatePublicProperty( 'mDoubleUnderscores', '1.35', __CLASS__ );
489  $this->deprecatePublicProperty( 'mShowToc', '1.35', __CLASS__ );
490  $this->deprecatePublicProperty( 'mRevisionId', '1.35', __CLASS__ );
491  $this->deprecatePublicProperty( 'mRevisionTimestamp', '1.35', __CLASS__ );
492  $this->deprecatePublicProperty( 'mRevisionUser', '1.35', __CLASS__ );
493  $this->deprecatePublicProperty( 'mRevisionSize', '1.35', __CLASS__ );
494  $this->deprecatePublicProperty( 'mInputSize', '1.35', __CLASS__ );
495  $this->deprecatePublicProperty( 'mInParse', '1.35', __CLASS__ );
496  $this->deprecatePublicPropertyFallback( 'mFirstCall', '1.35', static function () {
497  return false;
498  }, static function ( $value ) { /* ignore */
499  } );
500  $this->deprecatePublicPropertyFallback( 'mGeneratedPPNodeCount', '1.35', static function () {
501  return 0;
502  }, static function ( $value ) { /* ignore */
503  } );
504  $svcOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
505  $this->svcOptions = $svcOptions;
506 
507  $this->urlUtils = $urlUtils;
508  $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->urlUtils->validProtocols() . ')' .
509  self::EXT_LINK_ADDR .
510  self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
511 
512  $this->magicWordFactory = $magicWordFactory;
513 
514  $this->contLang = $contLang;
515 
516  $this->factory = $factory;
517  $this->specialPageFactory = $spFactory;
518  $this->linkRendererFactory = $linkRendererFactory;
519  $this->nsInfo = $nsInfo;
520  $this->logger = $logger;
521  $this->badFileLookup = $badFileLookup;
522 
523  $this->languageConverterFactory = $languageConverterFactory;
524 
525  $this->hookContainer = $hookContainer;
526  $this->hookRunner = new HookRunner( $hookContainer );
527 
528  $this->tidy = $tidy;
529 
530  $this->mPreprocessor = new Preprocessor_Hash(
531  $this,
532  $wanCache,
533  [
534  'cacheThreshold' => $svcOptions->get( MainConfigNames::PreprocessorCacheThreshold ),
535  'disableLangConversion' => $languageConverterFactory->isConversionDisabled(),
536  ]
537  );
538 
539  $this->userOptionsLookup = $userOptionsLookup;
540  $this->userFactory = $userFactory;
541  $this->titleFormatter = $titleFormatter;
542  $this->httpRequestFactory = $httpRequestFactory;
543  $this->trackingCategories = $trackingCategories;
544  $this->signatureValidatorFactory = $signatureValidatorFactory;
545  $this->userNameUtils = $userNameUtils;
546 
547  // These steps used to be done in "::firstCallInit()"
548  // (if you're chasing a reference from some old code)
550  $this,
552  );
554  $this,
556  );
557  $this->initializeVariables();
558 
559  $this->hookRunner->onParserFirstCallInit( $this );
560  }
561 
565  public function __destruct() {
566  if ( isset( $this->mLinkHolders ) ) {
567  // @phan-suppress-next-line PhanTypeObjectUnsetDeclaredProperty
568  unset( $this->mLinkHolders );
569  }
570  // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
571  foreach ( $this as $name => $value ) {
572  unset( $this->$name );
573  }
574  }
575 
579  public function __clone() {
580  $this->mInParse = false;
581 
582  // T58226: When you create a reference "to" an object field, that
583  // makes the object field itself be a reference too (until the other
584  // reference goes out of scope). When cloning, any field that's a
585  // reference is copied as a reference in the new object. Both of these
586  // are defined PHP5 behaviors, as inconvenient as it is for us when old
587  // hooks from PHP4 days are passing fields by reference.
588  foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
589  // Make a non-reference copy of the field, then rebind the field to
590  // reference the new copy.
591  $tmp = $this->$k;
592  $this->$k =& $tmp;
593  unset( $tmp );
594  }
595 
596  $this->mPreprocessor = clone $this->mPreprocessor;
597  $this->mPreprocessor->resetParser( $this );
598 
599  $this->hookRunner->onParserCloned( $this );
600  }
601 
609  public function firstCallInit() {
610  /*
611  * This method should be hard-deprecated once remaining calls are
612  * removed; it no longer does anything.
613  */
614  }
615 
621  public function clearState() {
622  $this->resetOutput();
623  $this->mAutonumber = 0;
624  $this->mLinkHolders = new LinkHolderArray(
625  $this,
627  $this->getHookContainer()
628  );
629  $this->mLinkID = 0;
630  $this->mRevisionTimestamp = null;
631  $this->mRevisionId = null;
632  $this->mRevisionUser = null;
633  $this->mRevisionSize = null;
634  $this->mRevisionRecordObject = null;
635  $this->mVarCache = [];
636  $this->mUser = null;
637  $this->mLangLinkLanguages = [];
638  $this->currentRevisionCache = null;
639 
640  $this->mStripState = new StripState( $this );
641 
642  # Clear these on every parse, T6549
643  $this->mTplRedirCache = [];
644  $this->mTplDomCache = [];
645 
646  $this->mShowToc = true;
647  $this->mForceTocPosition = false;
648  $this->mIncludeSizes = [
649  'post-expand' => 0,
650  'arg' => 0,
651  ];
652  $this->mPPNodeCount = 0;
653  $this->mHighestExpansionDepth = 0;
654  $this->mHeadings = [];
655  $this->mDoubleUnderscores = [];
656  $this->mExpensiveFunctionCount = 0;
657 
658  $this->mProfiler = new SectionProfiler();
659 
660  $this->hookRunner->onParserClearState( $this );
661  }
662 
667  public function resetOutput() {
668  $this->mOutput = new ParserOutput;
669  $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
670  }
671 
690  public function parse(
691  $text, PageReference $page, ParserOptions $options,
692  $linestart = true, $clearState = true, $revid = null
693  ) {
694  if ( $clearState ) {
695  // We use U+007F DELETE to construct strip markers, so we have to make
696  // sure that this character does not occur in the input text.
697  $text = strtr( $text, "\x7f", "?" );
698  $magicScopeVariable = $this->lock();
699  }
700  // Strip U+0000 NULL (T159174)
701  $text = str_replace( "\000", '', $text );
702 
703  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
704  $this->startParse( $page, $options, self::OT_HTML, $clearState );
705 
706  $this->currentRevisionCache = null;
707  $this->mInputSize = strlen( $text );
708  $this->mOutput->resetParseStartTime();
709 
710  $oldRevisionId = $this->mRevisionId;
711  $oldRevisionRecordObject = $this->mRevisionRecordObject;
712  $oldRevisionTimestamp = $this->mRevisionTimestamp;
713  $oldRevisionUser = $this->mRevisionUser;
714  $oldRevisionSize = $this->mRevisionSize;
715  if ( $revid !== null ) {
716  $this->mRevisionId = $revid;
717  $this->mRevisionRecordObject = null;
718  $this->mRevisionTimestamp = null;
719  $this->mRevisionUser = null;
720  $this->mRevisionSize = null;
721  }
722 
723  $text = $this->internalParse( $text );
724  $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
725 
726  $text = $this->internalParseHalfParsed( $text, true, $linestart );
727 
735  if ( !$options->getDisableTitleConversion()
736  && !isset( $this->mDoubleUnderscores['nocontentconvert'] )
737  && !isset( $this->mDoubleUnderscores['notitleconvert'] )
738  && $this->mOutput->getDisplayTitle() === false
739  ) {
740  $titleText = $this->getTargetLanguageConverter()->getConvRuleTitle();
741  if ( $titleText !== false ) {
742  $titleText = Sanitizer::removeSomeTags( $titleText );
743  } else {
744  $titleText = $this->getTargetLanguageConverter()->convertTitle( $page );
745  $titleText = htmlspecialchars( $titleText, ENT_NOQUOTES );
746  }
747  $this->mOutput->setTitleText( $titleText );
748  }
749 
750  # Compute runtime adaptive expiry if set
751  $this->mOutput->finalizeAdaptiveCacheExpiry();
752 
753  # Warn if too many heavyweight parser functions were used
754  if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
755  $this->limitationWarn( 'expensive-parserfunction',
756  $this->mExpensiveFunctionCount,
757  $this->mOptions->getExpensiveParserFunctionLimit()
758  );
759  }
760 
761  # Information on limits, for the benefit of users who try to skirt them
762  if ( MediaWikiServices::getInstance()->getMainConfig()->get(
763  MainConfigNames::EnableParserLimitReporting ) ) {
764  $this->makeLimitReport();
765  }
766 
767  # Wrap non-interface parser output in a <div> so it can be targeted
768  # with CSS (T37247)
769  $class = $this->mOptions->getWrapOutputClass();
770  if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
771  $this->mOutput->addWrapperDivClass( $class );
772  }
773 
774  $this->mOutput->setText( $text );
775 
776  $this->mRevisionId = $oldRevisionId;
777  $this->mRevisionRecordObject = $oldRevisionRecordObject;
778  $this->mRevisionTimestamp = $oldRevisionTimestamp;
779  $this->mRevisionUser = $oldRevisionUser;
780  $this->mRevisionSize = $oldRevisionSize;
781  $this->mInputSize = false;
782  $this->currentRevisionCache = null;
783 
784  return $this->mOutput;
785  }
786 
790  protected function makeLimitReport() {
791  $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
792 
793  $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
794  if ( $cpuTime !== null ) {
795  $this->mOutput->setLimitReportData( 'limitreport-cputime',
796  sprintf( "%.3f", $cpuTime )
797  );
798  }
799 
800  $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
801  $this->mOutput->setLimitReportData( 'limitreport-walltime',
802  sprintf( "%.3f", $wallTime )
803  );
804 
805  $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
806  [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
807  );
808  $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
809  [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
810  );
811  $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
812  [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
813  );
814  $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
815  [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
816  );
817  $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
818  [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
819  );
820 
821  foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
822  $this->mOutput->setLimitReportData( $key, $value );
823  }
824 
825  $this->hookRunner->onParserLimitReportPrepare( $this, $this->mOutput );
826 
827  // Add on template profiling data in human/machine readable way
828  $dataByFunc = $this->mProfiler->getFunctionStats();
829  uasort( $dataByFunc, static function ( $a, $b ) {
830  return $b['real'] <=> $a['real']; // descending order
831  } );
832  $profileReport = [];
833  foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
834  $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
835  $item['%real'], $item['real'], $item['calls'],
836  htmlspecialchars( $item['name'] ) );
837  }
838 
839  $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
840 
841  // Add other cache related metadata
842  if ( $this->svcOptions->get( MainConfigNames::ShowHostnames ) ) {
843  $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
844  }
845  $this->mOutput->setLimitReportData( 'cachereport-timestamp',
846  $this->mOutput->getCacheTime() );
847  $this->mOutput->setLimitReportData( 'cachereport-ttl',
848  $this->mOutput->getCacheExpiry() );
849  $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
850  $this->mOutput->hasReducedExpiry() );
851  }
852 
878  public function recursiveTagParse( $text, $frame = false ) {
879  $text = $this->internalParse( $text, false, $frame );
880  return $text;
881  }
882 
902  public function recursiveTagParseFully( $text, $frame = false ) {
903  $text = $this->recursiveTagParse( $text, $frame );
904  $text = $this->internalParseHalfParsed( $text, false );
905  return $text;
906  }
907 
927  public function parseExtensionTagAsTopLevelDoc( $text ) {
928  $text = $this->recursiveTagParse( $text );
929  $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
930  $text = $this->internalParseHalfParsed( $text, true );
931  return $text;
932  }
933 
946  public function preprocess(
947  $text,
948  ?PageReference $page,
949  ParserOptions $options,
950  $revid = null,
951  $frame = false
952  ) {
953  $magicScopeVariable = $this->lock();
954  $this->startParse( $page, $options, self::OT_PREPROCESS, true );
955  if ( $revid !== null ) {
956  $this->mRevisionId = $revid;
957  }
958  $this->hookRunner->onParserBeforePreprocess( $this, $text, $this->mStripState );
959  $text = $this->replaceVariables( $text, $frame );
960  $text = $this->mStripState->unstripBoth( $text );
961  return $text;
962  }
963 
973  public function recursivePreprocess( $text, $frame = false ) {
974  $text = $this->replaceVariables( $text, $frame );
975  $text = $this->mStripState->unstripBoth( $text );
976  return $text;
977  }
978 
993  public function getPreloadText( $text, PageReference $page, ParserOptions $options, $params = [] ) {
994  $msg = new RawMessage( $text );
995  $text = $msg->params( $params )->plain();
996 
997  # Parser (re)initialisation
998  $magicScopeVariable = $this->lock();
999  $this->startParse( $page, $options, self::OT_PLAIN, true );
1000 
1002  $dom = $this->preprocessToDom( $text, Preprocessor::DOM_FOR_INCLUSION );
1003  $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
1004  $text = $this->mStripState->unstripBoth( $text );
1005  return $text;
1006  }
1007 
1015  public function setUser( ?UserIdentity $user ) {
1016  $this->mUser = $user;
1017  }
1018 
1026  public function setTitle( Title $t = null ) {
1027  $this->setPage( $t );
1028  }
1029 
1035  public function getTitle(): Title {
1036  if ( !$this->mTitle ) {
1037  $this->mTitle = Title::makeTitle( NS_SPECIAL, 'Badtitle/Parser' );
1038  }
1039  return $this->mTitle;
1040  }
1041 
1048  public function setPage( ?PageReference $t = null ) {
1049  if ( !$t ) {
1050  $t = Title::makeTitle( NS_SPECIAL, 'Badtitle/Parser' );
1051  } else {
1052  // For now (early 1.37 alpha), always convert to Title, so we don't have to do it over
1053  // and over again in other methods. Eventually, we will no longer need to have a Title
1054  // instance internally.
1056  }
1057 
1058  if ( $t->hasFragment() ) {
1059  # Strip the fragment to avoid various odd effects
1060  $this->mTitle = $t->createFragmentTarget( '' );
1061  } else {
1062  $this->mTitle = $t;
1063  }
1064  }
1065 
1071  public function getPage(): ?PageReference {
1072  return $this->mTitle;
1073  }
1074 
1080  public function getOutputType(): int {
1081  return $this->mOutputType;
1082  }
1083 
1089  public function setOutputType( $ot ): void {
1090  $this->mOutputType = $ot;
1091  # Shortcut alias
1092  $this->ot = [
1093  'html' => $ot == self::OT_HTML,
1094  'wiki' => $ot == self::OT_WIKI,
1095  'pre' => $ot == self::OT_PREPROCESS,
1096  'plain' => $ot == self::OT_PLAIN,
1097  ];
1098  }
1099 
1107  public function OutputType( $x = null ) {
1108  wfDeprecated( __METHOD__, '1.35' );
1109  return wfSetVar( $this->mOutputType, $x );
1110  }
1111 
1116  public function getOutput() {
1117  return $this->mOutput;
1118  }
1119 
1124  public function getOptions() {
1125  return $this->mOptions;
1126  }
1127 
1133  public function setOptions( ParserOptions $options ): void {
1134  $this->mOptions = $options;
1135  }
1136 
1144  public function Options( $x = null ) {
1145  wfDeprecated( __METHOD__, '1.35' );
1146  return wfSetVar( $this->mOptions, $x );
1147  }
1148 
1153  public function nextLinkID() {
1154  return $this->mLinkID++;
1155  }
1156 
1161  public function setLinkID( $id ) {
1162  $this->mLinkID = $id;
1163  }
1164 
1170  public function getFunctionLang() {
1171  return $this->getTargetLanguage();
1172  }
1173 
1182  public function getTargetLanguage() {
1183  $target = $this->mOptions->getTargetLanguage();
1184 
1185  if ( $target !== null ) {
1186  return $target;
1187  } elseif ( $this->mOptions->getInterfaceMessage() ) {
1188  return $this->mOptions->getUserLangObj();
1189  }
1190 
1191  return $this->getTitle()->getPageLanguage();
1192  }
1193 
1201  public function getUserIdentity(): UserIdentity {
1202  return $this->mUser ?? $this->getOptions()->getUserIdentity();
1203  }
1204 
1211  public function getPreprocessor() {
1212  return $this->mPreprocessor;
1213  }
1214 
1221  public function getLinkRenderer() {
1222  // XXX We make the LinkRenderer with current options and then cache it forever
1223  if ( !$this->mLinkRenderer ) {
1224  $this->mLinkRenderer = $this->linkRendererFactory->create();
1225  }
1226 
1227  return $this->mLinkRenderer;
1228  }
1229 
1236  public function getMagicWordFactory() {
1237  return $this->magicWordFactory;
1238  }
1239 
1246  public function getContentLanguage() {
1247  return $this->contLang;
1248  }
1249 
1256  public function getBadFileLookup() {
1257  return $this->badFileLookup;
1258  }
1259 
1279  public static function extractTagsAndParams( array $elements, $text, &$matches ) {
1280  static $n = 1;
1281  $stripped = '';
1282  $matches = [];
1283 
1284  $taglist = implode( '|', $elements );
1285  $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1286 
1287  while ( $text != '' ) {
1288  $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1289  $stripped .= $p[0];
1290  if ( count( $p ) < 5 ) {
1291  break;
1292  }
1293  if ( count( $p ) > 5 ) {
1294  # comment
1295  $element = $p[4];
1296  $attributes = '';
1297  $close = '';
1298  $inside = $p[5];
1299  } else {
1300  # tag
1301  list( , $element, $attributes, $close, $inside ) = $p;
1302  }
1303 
1304  $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1305  $stripped .= $marker;
1306 
1307  if ( $close === '/>' ) {
1308  # Empty element tag, <tag />
1309  $content = null;
1310  $text = $inside;
1311  $tail = null;
1312  } else {
1313  if ( $element === '!--' ) {
1314  $end = '/(-->)/';
1315  } else {
1316  $end = "/(<\\/$element\\s*>)/i";
1317  }
1318  $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1319  $content = $q[0];
1320  if ( count( $q ) < 3 ) {
1321  # No end tag -- let it run out to the end of the text.
1322  $tail = '';
1323  $text = '';
1324  } else {
1325  list( , $tail, $text ) = $q;
1326  }
1327  }
1328 
1329  $matches[$marker] = [ $element,
1330  $content,
1331  Sanitizer::decodeTagAttributes( $attributes ),
1332  "<$element$attributes$close$content$tail" ];
1333  }
1334  return $stripped;
1335  }
1336 
1342  public function getStripList() {
1343  return $this->mStripList;
1344  }
1345 
1350  public function getStripState() {
1351  return $this->mStripState;
1352  }
1353 
1363  public function insertStripItem( $text ) {
1364  $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1365  $this->mMarkerIndex++;
1366  $this->mStripState->addGeneral( $marker, $text );
1367  return $marker;
1368  }
1369 
1376  private function handleTables( $text ) {
1377  $lines = StringUtils::explode( "\n", $text );
1378  $out = '';
1379  $td_history = []; # Is currently a td tag open?
1380  $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1381  $tr_history = []; # Is currently a tr tag open?
1382  $tr_attributes = []; # history of tr attributes
1383  $has_opened_tr = []; # Did this table open a <tr> element?
1384  $indent_level = 0; # indent level of the table
1385 
1386  foreach ( $lines as $outLine ) {
1387  $line = trim( $outLine );
1388 
1389  if ( $line === '' ) { # empty line, go to next line
1390  $out .= $outLine . "\n";
1391  continue;
1392  }
1393 
1394  $first_character = $line[0];
1395  $first_two = substr( $line, 0, 2 );
1396  $matches = [];
1397 
1398  if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1399  # First check if we are starting a new table
1400  $indent_level = strlen( $matches[1] );
1401 
1402  $attributes = $this->mStripState->unstripBoth( $matches[2] );
1403  $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1404 
1405  $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1406  array_push( $td_history, false );
1407  array_push( $last_tag_history, '' );
1408  array_push( $tr_history, false );
1409  array_push( $tr_attributes, '' );
1410  array_push( $has_opened_tr, false );
1411  } elseif ( count( $td_history ) == 0 ) {
1412  # Don't do any of the following
1413  $out .= $outLine . "\n";
1414  continue;
1415  } elseif ( $first_two === '|}' ) {
1416  # We are ending a table
1417  $line = '</table>' . substr( $line, 2 );
1418  $last_tag = array_pop( $last_tag_history );
1419 
1420  if ( !array_pop( $has_opened_tr ) ) {
1421  $line = "<tr><td></td></tr>{$line}";
1422  }
1423 
1424  if ( array_pop( $tr_history ) ) {
1425  $line = "</tr>{$line}";
1426  }
1427 
1428  if ( array_pop( $td_history ) ) {
1429  $line = "</{$last_tag}>{$line}";
1430  }
1431  array_pop( $tr_attributes );
1432  if ( $indent_level > 0 ) {
1433  $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1434  } else {
1435  $outLine = $line;
1436  }
1437  } elseif ( $first_two === '|-' ) {
1438  # Now we have a table row
1439  $line = preg_replace( '#^\|-+#', '', $line );
1440 
1441  # Whats after the tag is now only attributes
1442  $attributes = $this->mStripState->unstripBoth( $line );
1443  $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1444  array_pop( $tr_attributes );
1445  array_push( $tr_attributes, $attributes );
1446 
1447  $line = '';
1448  $last_tag = array_pop( $last_tag_history );
1449  array_pop( $has_opened_tr );
1450  array_push( $has_opened_tr, true );
1451 
1452  if ( array_pop( $tr_history ) ) {
1453  $line = '</tr>';
1454  }
1455 
1456  if ( array_pop( $td_history ) ) {
1457  $line = "</{$last_tag}>{$line}";
1458  }
1459 
1460  $outLine = $line;
1461  array_push( $tr_history, false );
1462  array_push( $td_history, false );
1463  array_push( $last_tag_history, '' );
1464  } elseif ( $first_character === '|'
1465  || $first_character === '!'
1466  || $first_two === '|+'
1467  ) {
1468  # This might be cell elements, td, th or captions
1469  if ( $first_two === '|+' ) {
1470  $first_character = '+';
1471  $line = substr( $line, 2 );
1472  } else {
1473  $line = substr( $line, 1 );
1474  }
1475 
1476  // Implies both are valid for table headings.
1477  if ( $first_character === '!' ) {
1478  $line = StringUtils::replaceMarkup( '!!', '||', $line );
1479  }
1480 
1481  # Split up multiple cells on the same line.
1482  # FIXME : This can result in improper nesting of tags processed
1483  # by earlier parser steps.
1484  $cells = explode( '||', $line );
1485 
1486  $outLine = '';
1487 
1488  # Loop through each table cell
1489  foreach ( $cells as $cell ) {
1490  $previous = '';
1491  if ( $first_character !== '+' ) {
1492  $tr_after = array_pop( $tr_attributes );
1493  if ( !array_pop( $tr_history ) ) {
1494  $previous = "<tr{$tr_after}>\n";
1495  }
1496  array_push( $tr_history, true );
1497  array_push( $tr_attributes, '' );
1498  array_pop( $has_opened_tr );
1499  array_push( $has_opened_tr, true );
1500  }
1501 
1502  $last_tag = array_pop( $last_tag_history );
1503 
1504  if ( array_pop( $td_history ) ) {
1505  $previous = "</{$last_tag}>\n{$previous}";
1506  }
1507 
1508  if ( $first_character === '|' ) {
1509  $last_tag = 'td';
1510  } elseif ( $first_character === '!' ) {
1511  $last_tag = 'th';
1512  } elseif ( $first_character === '+' ) {
1513  $last_tag = 'caption';
1514  } else {
1515  $last_tag = '';
1516  }
1517 
1518  array_push( $last_tag_history, $last_tag );
1519 
1520  # A cell could contain both parameters and data
1521  $cell_data = explode( '|', $cell, 2 );
1522 
1523  # T2553: Note that a '|' inside an invalid link should not
1524  # be mistaken as delimiting cell parameters
1525  # Bug T153140: Neither should language converter markup.
1526  if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1527  $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1528  } elseif ( count( $cell_data ) == 1 ) {
1529  // Whitespace in cells is trimmed
1530  $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1531  } else {
1532  $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1533  $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1534  // Whitespace in cells is trimmed
1535  $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1536  }
1537 
1538  $outLine .= $cell;
1539  array_push( $td_history, true );
1540  }
1541  }
1542  $out .= $outLine . "\n";
1543  }
1544 
1545  # Closing open td, tr && table
1546  while ( count( $td_history ) > 0 ) {
1547  if ( array_pop( $td_history ) ) {
1548  $out .= "</td>\n";
1549  }
1550  if ( array_pop( $tr_history ) ) {
1551  $out .= "</tr>\n";
1552  }
1553  if ( !array_pop( $has_opened_tr ) ) {
1554  $out .= "<tr><td></td></tr>\n";
1555  }
1556 
1557  $out .= "</table>\n";
1558  }
1559 
1560  # Remove trailing line-ending (b/c)
1561  if ( substr( $out, -1 ) === "\n" ) {
1562  $out = substr( $out, 0, -1 );
1563  }
1564 
1565  # special case: don't return empty table
1566  if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1567  $out = '';
1568  }
1569 
1570  return $out;
1571  }
1572 
1586  public function internalParse( $text, $isMain = true, $frame = false ) {
1587  $origText = $text;
1588 
1589  # Hook to suspend the parser in this state
1590  if ( !$this->hookRunner->onParserBeforeInternalParse( $this, $text, $this->mStripState ) ) {
1591  return $text;
1592  }
1593 
1594  # if $frame is provided, then use $frame for replacing any variables
1595  if ( $frame ) {
1596  # use frame depth to infer how include/noinclude tags should be handled
1597  # depth=0 means this is the top-level document; otherwise it's an included document
1598  if ( !$frame->depth ) {
1599  $flag = 0;
1600  } else {
1602  }
1603  $dom = $this->preprocessToDom( $text, $flag );
1604  $text = $frame->expand( $dom );
1605  } else {
1606  # if $frame is not provided, then use old-style replaceVariables
1607  $text = $this->replaceVariables( $text );
1608  }
1609 
1610  $this->hookRunner->onInternalParseBeforeSanitize( $this, $text, $this->mStripState );
1612  $text,
1613  // Callback from the Sanitizer for expanding items found in
1614  // HTML attribute values, so they can be safely tested and escaped.
1615  function ( &$text, $frame = false ) {
1616  $text = $this->replaceVariables( $text, $frame );
1617  $text = $this->mStripState->unstripBoth( $text );
1618  },
1619  false,
1620  [],
1621  []
1622  );
1623  $this->hookRunner->onInternalParseBeforeLinks( $this, $text, $this->mStripState );
1624 
1625  # Tables need to come after variable replacement for things to work
1626  # properly; putting them before other transformations should keep
1627  # exciting things like link expansions from showing up in surprising
1628  # places.
1629  $text = $this->handleTables( $text );
1630 
1631  $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1632 
1633  $text = $this->handleDoubleUnderscore( $text );
1634 
1635  $text = $this->handleHeadings( $text );
1636  $text = $this->handleInternalLinks( $text );
1637  $text = $this->handleAllQuotes( $text );
1638  $text = $this->handleExternalLinks( $text );
1639 
1640  # handleInternalLinks may sometimes leave behind
1641  # absolute URLs, which have to be masked to hide them from handleExternalLinks
1642  $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1643 
1644  $text = $this->handleMagicLinks( $text );
1645  $text = $this->finalizeHeadings( $text, $origText, $isMain );
1646 
1647  return $text;
1648  }
1649 
1657  return $this->languageConverterFactory->getLanguageConverter(
1658  $this->getTargetLanguage()
1659  );
1660  }
1661 
1668  return $this->languageConverterFactory->getLanguageConverter(
1669  $this->getContentLanguage()
1670  );
1671  }
1672 
1680  protected function getHookContainer() {
1681  return $this->hookContainer;
1682  }
1683 
1692  protected function getHookRunner() {
1693  return $this->hookRunner;
1694  }
1695 
1705  private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1706  $text = $this->mStripState->unstripGeneral( $text );
1707 
1708  $text = BlockLevelPass::doBlockLevels( $text, $linestart );
1709 
1710  $this->replaceLinkHoldersPrivate( $text );
1711 
1719  if ( !( $this->mOptions->getDisableContentConversion()
1720  || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1721  && !$this->mOptions->getInterfaceMessage()
1722  ) {
1723  # The position of the convert() call should not be changed. it
1724  # assumes that the links are all replaced and the only thing left
1725  # is the <nowiki> mark.
1726  $text = $this->getTargetLanguageConverter()->convert( $text );
1727  // Record information necessary for language conversion of TOC.
1728  $this->mOutput->setExtensionData(
1729  // T303329: this should migrate out of extension data
1730  'core:target-lang',
1731  $this->getTargetLanguage()->getCode()
1732  );
1733  $this->mOutput->setExtensionData(
1734  // T303329: this should migrate out of extension data
1735  'core:target-lang-variant',
1736  $this->getTargetLanguageConverter()->getPreferredVariant()
1737  );
1738  } else {
1739  $this->mOutput->setOutputFlag( ParserOutputFlags::NO_TOC_CONVERSION );
1740  }
1741 
1742  $text = $this->mStripState->unstripNoWiki( $text );
1743 
1744  $text = $this->mStripState->unstripGeneral( $text );
1745 
1746  $text = $this->tidy->tidy( $text, [ Sanitizer::class, 'armorFrenchSpaces' ] );
1747 
1748  if ( $isMain ) {
1749  $this->hookRunner->onParserAfterTidy( $this, $text );
1750  }
1751 
1752  return $text;
1753  }
1754 
1765  private function handleMagicLinks( $text ) {
1766  $prots = $this->urlUtils->validAbsoluteProtocols();
1767  $urlChar = self::EXT_LINK_URL_CLASS;
1768  $addr = self::EXT_LINK_ADDR;
1769  $space = self::SPACE_NOT_NL; # non-newline space
1770  $spdash = "(?:-|$space)"; # a dash or a non-newline space
1771  $spaces = "$space++"; # possessive match of 1 or more spaces
1772  $text = preg_replace_callback(
1773  '!(?: # Start cases
1774  (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1775  (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1776  (\b # m[3]: Free external links
1777  (?i:$prots)
1778  ($addr$urlChar*) # m[4]: Post-protocol path
1779  ) |
1780  \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1781  ([0-9]+)\b |
1782  \bISBN $spaces ( # m[6]: ISBN, capture number
1783  (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1784  (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1785  [0-9Xx] # check digit
1786  )\b
1787  )!xu",
1788  [ $this, 'magicLinkCallback' ],
1789  $text
1790  );
1791  return $text;
1792  }
1793 
1799  private function magicLinkCallback( array $m ) {
1800  if ( isset( $m[1] ) && $m[1] !== '' ) {
1801  # Skip anchor
1802  return $m[0];
1803  } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1804  # Skip HTML element
1805  return $m[0];
1806  } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1807  # Free external link
1808  return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1809  } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1810  # RFC or PMID
1811  if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1812  if ( !$this->mOptions->getMagicRFCLinks() ) {
1813  return $m[0];
1814  }
1815  $keyword = 'RFC';
1816  $urlmsg = 'rfcurl';
1817  $cssClass = 'mw-magiclink-rfc';
1818  $trackingCat = 'magiclink-tracking-rfc';
1819  $id = $m[5];
1820  } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1821  if ( !$this->mOptions->getMagicPMIDLinks() ) {
1822  return $m[0];
1823  }
1824  $keyword = 'PMID';
1825  $urlmsg = 'pubmedurl';
1826  $cssClass = 'mw-magiclink-pmid';
1827  $trackingCat = 'magiclink-tracking-pmid';
1828  $id = $m[5];
1829  } else {
1830  // Should never happen
1831  throw new MWException( __METHOD__ . ': unrecognised match type "' .
1832  substr( $m[0], 0, 20 ) . '"' );
1833  }
1834  $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1835  $this->addTrackingCategory( $trackingCat );
1836  return Linker::makeExternalLink(
1837  $url,
1838  "{$keyword} {$id}",
1839  true,
1840  $cssClass,
1841  [],
1842  $this->getTitle()
1843  );
1844  } elseif ( isset( $m[6] ) && $m[6] !== ''
1845  && $this->mOptions->getMagicISBNLinks()
1846  ) {
1847  # ISBN
1848  $isbn = $m[6];
1849  $space = self::SPACE_NOT_NL; # non-newline space
1850  $isbn = preg_replace( "/$space/", ' ', $isbn );
1851  $num = strtr( $isbn, [
1852  '-' => '',
1853  ' ' => '',
1854  'x' => 'X',
1855  ] );
1856  $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1857  return $this->getLinkRenderer()->makeKnownLink(
1858  SpecialPage::getTitleFor( 'Booksources', $num ),
1859  "ISBN $isbn",
1860  [
1861  'class' => 'internal mw-magiclink-isbn',
1862  'title' => false // suppress title attribute
1863  ]
1864  );
1865  } else {
1866  return $m[0];
1867  }
1868  }
1869 
1879  private function makeFreeExternalLink( $url, $numPostProto ) {
1880  $trail = '';
1881 
1882  # The characters '<' and '>' (which were escaped by
1883  # internalRemoveHtmlTags()) should not be included in
1884  # URLs, per RFC 2396.
1885  # Make &nbsp; terminate a URL as well (bug T84937)
1886  $m2 = [];
1887  if ( preg_match(
1888  '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1889  $url,
1890  $m2,
1891  PREG_OFFSET_CAPTURE
1892  ) ) {
1893  $trail = substr( $url, $m2[0][1] ) . $trail;
1894  $url = substr( $url, 0, $m2[0][1] );
1895  }
1896 
1897  # Move trailing punctuation to $trail
1898  $sep = ',;\.:!?';
1899  # If there is no left bracket, then consider right brackets fair game too
1900  if ( strpos( $url, '(' ) === false ) {
1901  $sep .= ')';
1902  }
1903 
1904  $urlRev = strrev( $url );
1905  $numSepChars = strspn( $urlRev, $sep );
1906  # Don't break a trailing HTML entity by moving the ; into $trail
1907  # This is in hot code, so use substr_compare to avoid having to
1908  # create a new string object for the comparison
1909  if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1910  # more optimization: instead of running preg_match with a $
1911  # anchor, which can be slow, do the match on the reversed
1912  # string starting at the desired offset.
1913  # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1914  if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1915  $numSepChars--;
1916  }
1917  }
1918  if ( $numSepChars ) {
1919  $trail = substr( $url, -$numSepChars ) . $trail;
1920  $url = substr( $url, 0, -$numSepChars );
1921  }
1922 
1923  # Verify that we still have a real URL after trail removal, and
1924  # not just lone protocol
1925  if ( strlen( $trail ) >= $numPostProto ) {
1926  return $url . $trail;
1927  }
1928 
1929  $url = Sanitizer::cleanUrl( $url );
1930 
1931  # Is this an external image?
1932  $text = $this->maybeMakeExternalImage( $url );
1933  if ( $text === false ) {
1934  # Not an image, make a link
1935  $text = Linker::makeExternalLink(
1936  $url,
1937  $this->getTargetLanguageConverter()->markNoConversion( $url ),
1938  true,
1939  'free',
1940  $this->getExternalLinkAttribs( $url ),
1941  $this->getTitle()
1942  );
1943  # Register it in the output object...
1944  $this->mOutput->addExternalLink( $url );
1945  }
1946  return $text . $trail;
1947  }
1948 
1955  private function handleHeadings( $text ) {
1956  for ( $i = 6; $i >= 1; --$i ) {
1957  $h = str_repeat( '=', $i );
1958  // Trim non-newline whitespace from headings
1959  // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1960  $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1961  }
1962  return $text;
1963  }
1964 
1972  private function handleAllQuotes( $text ) {
1973  $outtext = '';
1974  $lines = StringUtils::explode( "\n", $text );
1975  foreach ( $lines as $line ) {
1976  $outtext .= $this->doQuotes( $line ) . "\n";
1977  }
1978  $outtext = substr( $outtext, 0, -1 );
1979  return $outtext;
1980  }
1981 
1990  public function doQuotes( $text ) {
1991  $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1992  $countarr = count( $arr );
1993  if ( $countarr == 1 ) {
1994  return $text;
1995  }
1996 
1997  // First, do some preliminary work. This may shift some apostrophes from
1998  // being mark-up to being text. It also counts the number of occurrences
1999  // of bold and italics mark-ups.
2000  $numbold = 0;
2001  $numitalics = 0;
2002  for ( $i = 1; $i < $countarr; $i += 2 ) {
2003  $thislen = strlen( $arr[$i] );
2004  // If there are ever four apostrophes, assume the first is supposed to
2005  // be text, and the remaining three constitute mark-up for bold text.
2006  // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
2007  if ( $thislen == 4 ) {
2008  $arr[$i - 1] .= "'";
2009  $arr[$i] = "'''";
2010  $thislen = 3;
2011  } elseif ( $thislen > 5 ) {
2012  // If there are more than 5 apostrophes in a row, assume they're all
2013  // text except for the last 5.
2014  // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
2015  $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
2016  $arr[$i] = "'''''";
2017  $thislen = 5;
2018  }
2019  // Count the number of occurrences of bold and italics mark-ups.
2020  if ( $thislen == 2 ) {
2021  $numitalics++;
2022  } elseif ( $thislen == 3 ) {
2023  $numbold++;
2024  } elseif ( $thislen == 5 ) {
2025  $numitalics++;
2026  $numbold++;
2027  }
2028  }
2029 
2030  // If there is an odd number of both bold and italics, it is likely
2031  // that one of the bold ones was meant to be an apostrophe followed
2032  // by italics. Which one we cannot know for certain, but it is more
2033  // likely to be one that has a single-letter word before it.
2034  if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
2035  $firstsingleletterword = -1;
2036  $firstmultiletterword = -1;
2037  $firstspace = -1;
2038  for ( $i = 1; $i < $countarr; $i += 2 ) {
2039  if ( strlen( $arr[$i] ) == 3 ) {
2040  $x1 = substr( $arr[$i - 1], -1 );
2041  $x2 = substr( $arr[$i - 1], -2, 1 );
2042  if ( $x1 === ' ' ) {
2043  if ( $firstspace == -1 ) {
2044  $firstspace = $i;
2045  }
2046  } elseif ( $x2 === ' ' ) {
2047  $firstsingleletterword = $i;
2048  // if $firstsingleletterword is set, we don't
2049  // look at the other options, so we can bail early.
2050  break;
2051  } elseif ( $firstmultiletterword == -1 ) {
2052  $firstmultiletterword = $i;
2053  }
2054  }
2055  }
2056 
2057  // If there is a single-letter word, use it!
2058  if ( $firstsingleletterword > -1 ) {
2059  $arr[$firstsingleletterword] = "''";
2060  $arr[$firstsingleletterword - 1] .= "'";
2061  } elseif ( $firstmultiletterword > -1 ) {
2062  // If not, but there's a multi-letter word, use that one.
2063  $arr[$firstmultiletterword] = "''";
2064  $arr[$firstmultiletterword - 1] .= "'";
2065  } elseif ( $firstspace > -1 ) {
2066  // ... otherwise use the first one that has neither.
2067  // (notice that it is possible for all three to be -1 if, for example,
2068  // there is only one pentuple-apostrophe in the line)
2069  $arr[$firstspace] = "''";
2070  $arr[$firstspace - 1] .= "'";
2071  }
2072  }
2073 
2074  // Now let's actually convert our apostrophic mush to HTML!
2075  $output = '';
2076  $buffer = '';
2077  $state = '';
2078  $i = 0;
2079  foreach ( $arr as $r ) {
2080  if ( ( $i % 2 ) == 0 ) {
2081  if ( $state === 'both' ) {
2082  $buffer .= $r;
2083  } else {
2084  $output .= $r;
2085  }
2086  } else {
2087  $thislen = strlen( $r );
2088  if ( $thislen == 2 ) {
2089  // two quotes - open or close italics
2090  if ( $state === 'i' ) {
2091  $output .= '</i>';
2092  $state = '';
2093  } elseif ( $state === 'bi' ) {
2094  $output .= '</i>';
2095  $state = 'b';
2096  } elseif ( $state === 'ib' ) {
2097  $output .= '</b></i><b>';
2098  $state = 'b';
2099  } elseif ( $state === 'both' ) {
2100  $output .= '<b><i>' . $buffer . '</i>';
2101  $state = 'b';
2102  } else { // $state can be 'b' or ''
2103  $output .= '<i>';
2104  $state .= 'i';
2105  }
2106  } elseif ( $thislen == 3 ) {
2107  // three quotes - open or close bold
2108  if ( $state === 'b' ) {
2109  $output .= '</b>';
2110  $state = '';
2111  } elseif ( $state === 'bi' ) {
2112  $output .= '</i></b><i>';
2113  $state = 'i';
2114  } elseif ( $state === 'ib' ) {
2115  $output .= '</b>';
2116  $state = 'i';
2117  } elseif ( $state === 'both' ) {
2118  $output .= '<i><b>' . $buffer . '</b>';
2119  $state = 'i';
2120  } else { // $state can be 'i' or ''
2121  $output .= '<b>';
2122  $state .= 'b';
2123  }
2124  } elseif ( $thislen == 5 ) {
2125  // five quotes - open or close both separately
2126  if ( $state === 'b' ) {
2127  $output .= '</b><i>';
2128  $state = 'i';
2129  } elseif ( $state === 'i' ) {
2130  $output .= '</i><b>';
2131  $state = 'b';
2132  } elseif ( $state === 'bi' ) {
2133  $output .= '</i></b>';
2134  $state = '';
2135  } elseif ( $state === 'ib' ) {
2136  $output .= '</b></i>';
2137  $state = '';
2138  } elseif ( $state === 'both' ) {
2139  $output .= '<i><b>' . $buffer . '</b></i>';
2140  $state = '';
2141  } else { // ($state == '')
2142  $buffer = '';
2143  $state = 'both';
2144  }
2145  }
2146  }
2147  $i++;
2148  }
2149  // Now close all remaining tags. Notice that the order is important.
2150  if ( $state === 'b' || $state === 'ib' ) {
2151  $output .= '</b>';
2152  }
2153  if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
2154  $output .= '</i>';
2155  }
2156  if ( $state === 'bi' ) {
2157  $output .= '</b>';
2158  }
2159  // There might be lonely ''''', so make sure we have a buffer
2160  if ( $state === 'both' && $buffer ) {
2161  $output .= '<b><i>' . $buffer . '</i></b>';
2162  }
2163  return $output;
2164  }
2165 
2176  private function handleExternalLinks( $text ) {
2177  $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
2178  // @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3161
2179  if ( $bits === false ) {
2180  throw new MWException( "PCRE needs to be compiled with "
2181  . "--enable-unicode-properties in order for MediaWiki to function" );
2182  }
2183  $s = array_shift( $bits );
2184 
2185  $i = 0;
2186  while ( $i < count( $bits ) ) {
2187  $url = $bits[$i++];
2188  $i++; // protocol
2189  $text = $bits[$i++];
2190  $trail = $bits[$i++];
2191 
2192  # The characters '<' and '>' (which were escaped by
2193  # internalRemoveHtmlTags()) should not be included in
2194  # URLs, per RFC 2396.
2195  $m2 = [];
2196  if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
2197  $text = substr( $url, $m2[0][1] ) . ' ' . $text;
2198  $url = substr( $url, 0, $m2[0][1] );
2199  }
2200 
2201  # If the link text is an image URL, replace it with an <img> tag
2202  # This happened by accident in the original parser, but some people used it extensively
2203  $img = $this->maybeMakeExternalImage( $text );
2204  if ( $img !== false ) {
2205  $text = $img;
2206  }
2207 
2208  $dtrail = '';
2209 
2210  # Set linktype for CSS
2211  $linktype = 'text';
2212 
2213  # No link text, e.g. [http://domain.tld/some.link]
2214  if ( $text == '' ) {
2215  # Autonumber
2216  $langObj = $this->getTargetLanguage();
2217  $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
2218  $linktype = 'autonumber';
2219  } else {
2220  # Have link text, e.g. [http://domain.tld/some.link text]s
2221  # Check for trail
2222  list( $dtrail, $trail ) = Linker::splitTrail( $trail );
2223  }
2224 
2225  // Excluding protocol-relative URLs may avoid many false positives.
2226  if ( preg_match( '/^(?:' . $this->urlUtils->validAbsoluteProtocols() . ')/', $text ) ) {
2227  $text = $this->getTargetLanguageConverter()->markNoConversion( $text );
2228  }
2229 
2230  $url = Sanitizer::cleanUrl( $url );
2231 
2232  # Use the encoded URL
2233  # This means that users can paste URLs directly into the text
2234  # Funny characters like ö aren't valid in URLs anyway
2235  # This was changed in August 2004
2236  // @phan-suppress-next-line SecurityCheck-XSS,SecurityCheck-DoubleEscaped using false for escape is valid
2237  $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
2238  $this->getExternalLinkAttribs( $url ), $this->getTitle() ) . $dtrail . $trail;
2239 
2240  # Register link in the output object.
2241  $this->mOutput->addExternalLink( $url );
2242  }
2243 
2244  // @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive from array_shift
2245  return $s;
2246  }
2247 
2258  public static function getExternalLinkRel( $url = false, LinkTarget $title = null ) {
2259  $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
2260  $noFollowLinks = $mainConfig->get( MainConfigNames::NoFollowLinks );
2261  $noFollowNsExceptions = $mainConfig->get( MainConfigNames::NoFollowNsExceptions );
2262  $noFollowDomainExceptions = $mainConfig->get( MainConfigNames::NoFollowDomainExceptions );
2263  $ns = $title ? $title->getNamespace() : false;
2264  if ( $noFollowLinks && !in_array( $ns, $noFollowNsExceptions )
2265  && !wfMatchesDomainList( $url, $noFollowDomainExceptions )
2266  ) {
2267  return 'nofollow';
2268  }
2269  return null;
2270  }
2271 
2283  public function getExternalLinkAttribs( $url ) {
2284  $attribs = [];
2285  $rel = self::getExternalLinkRel( $url, $this->getTitle() );
2286 
2287  $target = $this->mOptions->getExternalLinkTarget();
2288  if ( $target ) {
2289  $attribs['target'] = $target;
2290  if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
2291  // T133507. New windows can navigate parent cross-origin.
2292  // Including noreferrer due to lacking browser
2293  // support of noopener. Eventually noreferrer should be removed.
2294  if ( $rel !== '' ) {
2295  $rel .= ' ';
2296  }
2297  $rel .= 'noreferrer noopener';
2298  }
2299  }
2300  $attribs['rel'] = $rel;
2301  return $attribs;
2302  }
2303 
2314  public static function normalizeLinkUrl( $url ) {
2315  # Test for RFC 3986 IPv6 syntax
2316  $scheme = '[a-z][a-z0-9+.-]*:';
2317  $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2318  $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2319  if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2320  IPUtils::isValid( rawurldecode( $m[1] ) )
2321  ) {
2322  $isIPv6 = rawurldecode( $m[1] );
2323  } else {
2324  $isIPv6 = false;
2325  }
2326 
2327  # Make sure unsafe characters are encoded
2328  $url = preg_replace_callback(
2329  '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
2330  static function ( $m ) {
2331  return rawurlencode( $m[0] );
2332  },
2333  $url
2334  );
2335 
2336  $ret = '';
2337  $end = strlen( $url );
2338 
2339  # Fragment part - 'fragment'
2340  $start = strpos( $url, '#' );
2341  if ( $start !== false && $start < $end ) {
2342  $ret = self::normalizeUrlComponent(
2343  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2344  $end = $start;
2345  }
2346 
2347  # Query part - 'query' minus &=+;
2348  $start = strpos( $url, '?' );
2349  if ( $start !== false && $start < $end ) {
2350  $ret = self::normalizeUrlComponent(
2351  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2352  $end = $start;
2353  }
2354 
2355  # Scheme and path part - 'pchar'
2356  # (we assume no userinfo or encoded colons in the host)
2357  $ret = self::normalizeUrlComponent(
2358  substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2359 
2360  # Fix IPv6 syntax
2361  if ( $isIPv6 !== false ) {
2362  $ipv6Host = "%5B({$isIPv6})%5D";
2363  $ret = preg_replace(
2364  "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2365  "$1[$2]",
2366  $ret
2367  );
2368  }
2369 
2370  return $ret;
2371  }
2372 
2373  private static function normalizeUrlComponent( $component, $unsafe ) {
2374  $callback = static function ( $matches ) use ( $unsafe ) {
2375  $char = urldecode( $matches[0] );
2376  $ord = ord( $char );
2377  if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2378  # Unescape it
2379  return $char;
2380  } else {
2381  # Leave it escaped, but use uppercase for a-f
2382  return strtoupper( $matches[0] );
2383  }
2384  };
2385  return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2386  }
2387 
2396  private function maybeMakeExternalImage( $url ) {
2397  $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2398  $imagesexception = !empty( $imagesfrom );
2399  $text = false;
2400  # $imagesfrom could be either a single string or an array of strings, parse out the latter
2401  if ( $imagesexception && is_array( $imagesfrom ) ) {
2402  $imagematch = false;
2403  foreach ( $imagesfrom as $match ) {
2404  if ( strpos( $url, $match ) === 0 ) {
2405  $imagematch = true;
2406  break;
2407  }
2408  }
2409  } elseif ( $imagesexception ) {
2410  $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2411  } else {
2412  $imagematch = false;
2413  }
2414 
2415  if ( $this->mOptions->getAllowExternalImages()
2416  || ( $imagesexception && $imagematch )
2417  ) {
2418  if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2419  # Image found
2420  $text = Linker::makeExternalImage( $url );
2421  }
2422  }
2423  if ( !$text && $this->mOptions->getEnableImageWhitelist()
2424  && preg_match( self::EXT_IMAGE_REGEX, $url )
2425  ) {
2426  $whitelist = explode(
2427  "\n",
2428  wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2429  );
2430 
2431  foreach ( $whitelist as $entry ) {
2432  # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2433  if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2434  continue;
2435  }
2436  // @phan-suppress-next-line SecurityCheck-ReDoS preg_quote is not wanted here
2437  if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2438  # Image matches a whitelist entry
2439  $text = Linker::makeExternalImage( $url );
2440  break;
2441  }
2442  }
2443  }
2444  return $text;
2445  }
2446 
2454  private function handleInternalLinks( $text ) {
2455  $this->mLinkHolders->merge( $this->handleInternalLinks2( $text ) );
2456  return $text;
2457  }
2458 
2464  private function handleInternalLinks2( &$s ) {
2465  static $tc = false, $e1, $e1_img;
2466  # the % is needed to support urlencoded titles as well
2467  if ( !$tc ) {
2468  $tc = Title::legalChars() . '#%';
2469  # Match a link having the form [[namespace:link|alternate]]trail
2470  $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2471  # Match cases where there is no "]]", which might still be images
2472  $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2473  }
2474 
2475  $holders = new LinkHolderArray(
2476  $this,
2477  $this->getContentLanguageConverter(),
2478  $this->getHookContainer() );
2479 
2480  # split the entire text string on occurrences of [[
2481  $a = StringUtils::explode( '[[', ' ' . $s );
2482  # get the first element (all text up to first [[), and remove the space we added
2483  $s = $a->current();
2484  $a->next();
2485  $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2486  $s = substr( $s, 1 );
2487 
2488  $nottalk = !$this->getTitle()->isTalkPage();
2489 
2490  $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2491  $e2 = null;
2492  if ( $useLinkPrefixExtension ) {
2493  # Match the end of a line for a word that's not followed by whitespace,
2494  # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2495  $charset = $this->contLang->linkPrefixCharset();
2496  $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2497  $m = [];
2498  if ( preg_match( $e2, $s, $m ) ) {
2499  $first_prefix = $m[2];
2500  } else {
2501  $first_prefix = false;
2502  }
2503  $prefix = false;
2504  } else {
2505  $first_prefix = false;
2506  $prefix = '';
2507  }
2508 
2509  # Some namespaces don't allow subpages
2510  $useSubpages = $this->nsInfo->hasSubpages(
2511  $this->getTitle()->getNamespace()
2512  );
2513 
2514  # Loop for each link
2515  for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2516  # Check for excessive memory usage
2517  if ( $holders->isBig() ) {
2518  # Too big
2519  # Do the existence check, replace the link holders and clear the array
2520  $holders->replace( $s );
2521  $holders->clear();
2522  }
2523 
2524  if ( $useLinkPrefixExtension ) {
2525  // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal $e2 is set under this condition
2526  if ( preg_match( $e2, $s, $m ) ) {
2527  list( , $s, $prefix ) = $m;
2528  } else {
2529  $prefix = '';
2530  }
2531  # first link
2532  if ( $first_prefix ) {
2533  $prefix = $first_prefix;
2534  $first_prefix = false;
2535  }
2536  }
2537 
2538  $might_be_img = false;
2539 
2540  if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2541  $text = $m[2];
2542  # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2543  # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2544  # the real problem is with the $e1 regex
2545  # See T1500.
2546  # Still some problems for cases where the ] is meant to be outside punctuation,
2547  # and no image is in sight. See T4095.
2548  if ( $text !== ''
2549  && substr( $m[3], 0, 1 ) === ']'
2550  && strpos( $text, '[' ) !== false
2551  ) {
2552  $text .= ']'; # so that handleExternalLinks($text) works later
2553  $m[3] = substr( $m[3], 1 );
2554  }
2555  # fix up urlencoded title texts
2556  if ( strpos( $m[1], '%' ) !== false ) {
2557  # Should anchors '#' also be rejected?
2558  $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2559  }
2560  $trail = $m[3];
2561  } elseif ( preg_match( $e1_img, $line, $m ) ) {
2562  # Invalid, but might be an image with a link in its caption
2563  $might_be_img = true;
2564  $text = $m[2];
2565  if ( strpos( $m[1], '%' ) !== false ) {
2566  $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2567  }
2568  $trail = "";
2569  } else { # Invalid form; output directly
2570  $s .= $prefix . '[[' . $line;
2571  continue;
2572  }
2573 
2574  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset preg_match success when reached here
2575  $origLink = ltrim( $m[1], ' ' );
2576 
2577  # Don't allow internal links to pages containing
2578  # PROTO: where PROTO is a valid URL protocol; these
2579  # should be external links.
2580  if ( preg_match( '/^(?i:' . $this->urlUtils->validProtocols() . ')/', $origLink ) ) {
2581  $s .= $prefix . '[[' . $line;
2582  continue;
2583  }
2584 
2585  # Make subpage if necessary
2586  if ( $useSubpages ) {
2588  $this->getTitle(), $origLink, $text
2589  );
2590  } else {
2591  $link = $origLink;
2592  }
2593 
2594  // \x7f isn't a default legal title char, so most likely strip
2595  // markers will force us into the "invalid form" path above. But,
2596  // just in case, let's assert that xmlish tags aren't valid in
2597  // the title position.
2598  $unstrip = $this->mStripState->killMarkers( $link );
2599  $noMarkers = ( $unstrip === $link );
2600 
2601  $nt = $noMarkers ? Title::newFromText( $link ) : null;
2602  if ( $nt === null ) {
2603  $s .= $prefix . '[[' . $line;
2604  continue;
2605  }
2606 
2607  $ns = $nt->getNamespace();
2608  $iw = $nt->getInterwiki();
2609 
2610  $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2611 
2612  if ( $might_be_img ) { # if this is actually an invalid link
2613  if ( $ns === NS_FILE && $noforce ) { # but might be an image
2614  $found = false;
2615  while ( true ) {
2616  # look at the next 'line' to see if we can close it there
2617  $a->next();
2618  $next_line = $a->current();
2619  if ( $next_line === false || $next_line === null ) {
2620  break;
2621  }
2622  $m = explode( ']]', $next_line, 3 );
2623  if ( count( $m ) == 3 ) {
2624  # the first ]] closes the inner link, the second the image
2625  $found = true;
2626  $text .= "[[{$m[0]}]]{$m[1]}";
2627  $trail = $m[2];
2628  break;
2629  } elseif ( count( $m ) == 2 ) {
2630  # if there's exactly one ]] that's fine, we'll keep looking
2631  $text .= "[[{$m[0]}]]{$m[1]}";
2632  } else {
2633  # if $next_line is invalid too, we need look no further
2634  $text .= '[[' . $next_line;
2635  break;
2636  }
2637  }
2638  if ( !$found ) {
2639  # we couldn't find the end of this imageLink, so output it raw
2640  # but don't ignore what might be perfectly normal links in the text we've examined
2641  $holders->merge( $this->handleInternalLinks2( $text ) );
2642  $s .= "{$prefix}[[$link|$text";
2643  # note: no $trail, because without an end, there *is* no trail
2644  continue;
2645  }
2646  } else { # it's not an image, so output it raw
2647  $s .= "{$prefix}[[$link|$text";
2648  # note: no $trail, because without an end, there *is* no trail
2649  continue;
2650  }
2651  }
2652 
2653  $wasblank = ( $text == '' );
2654  if ( $wasblank ) {
2655  $text = $link;
2656  if ( !$noforce ) {
2657  # Strip off leading ':'
2658  $text = substr( $text, 1 );
2659  }
2660  } else {
2661  # T6598 madness. Handle the quotes only if they come from the alternate part
2662  # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2663  # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2664  # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2665  $text = $this->doQuotes( $text );
2666  }
2667 
2668  # Link not escaped by : , create the various objects
2669  if ( $noforce && !$nt->wasLocalInterwiki() ) {
2670  # Interwikis
2671  if (
2672  $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2673  MediaWikiServices::getInstance()->getLanguageNameUtils()
2674  ->getLanguageName(
2675  $iw,
2676  LanguageNameUtils::AUTONYMS,
2677  LanguageNameUtils::DEFINED
2678  )
2679  || in_array( $iw, $this->svcOptions->get( MainConfigNames::ExtraInterlanguageLinkPrefixes ) )
2680  )
2681  ) {
2682  # T26502: filter duplicates
2683  if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2684  $this->mLangLinkLanguages[$iw] = true;
2685  $this->mOutput->addLanguageLink( $nt->getFullText() );
2686  }
2687 
2691  $s = rtrim( $s . $prefix ) . $trail; # T175416
2692  continue;
2693  }
2694 
2695  if ( $ns === NS_FILE ) {
2696  if ( !$this->badFileLookup->isBadFile( $nt->getDBkey(), $this->getTitle() ) ) {
2697  if ( $wasblank ) {
2698  # if no parameters were passed, $text
2699  # becomes something like "File:Foo.png",
2700  # which we don't want to pass on to the
2701  # image generator
2702  $text = '';
2703  } else {
2704  # recursively parse links inside the image caption
2705  # actually, this will parse them in any other parameters, too,
2706  # but it might be hard to fix that, and it doesn't matter ATM
2707  $text = $this->handleExternalLinks( $text );
2708  $holders->merge( $this->handleInternalLinks2( $text ) );
2709  }
2710  # cloak any absolute URLs inside the image markup, so handleExternalLinks() won't touch them
2711  $s .= $prefix . $this->armorLinks(
2712  $this->makeImage( $nt, $text, $holders ) ) . $trail;
2713  continue;
2714  }
2715  } elseif ( $ns === NS_CATEGORY ) {
2719  $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2720 
2721  if ( $wasblank ) {
2722  $sortkey = $this->mOutput->getPageProperty( 'defaultsort' ) ?? '';
2723  } else {
2724  $sortkey = $text;
2725  }
2726  $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2727  $sortkey = str_replace( "\n", '', $sortkey );
2728  $sortkey = $this->getTargetLanguageConverter()->convertCategoryKey( $sortkey );
2729  $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2730 
2731  continue;
2732  }
2733  }
2734 
2735  # Self-link checking. For some languages, variants of the title are checked in
2736  # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2737  # for linking to a different variant.
2738  if ( $ns !== NS_SPECIAL && $nt->equals( $this->getTitle() ) && !$nt->hasFragment() ) {
2739  $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2740  continue;
2741  }
2742 
2743  # NS_MEDIA is a pseudo-namespace for linking directly to a file
2744  # @todo FIXME: Should do batch file existence checks, see comment below
2745  if ( $ns === NS_MEDIA ) {
2746  # Give extensions a chance to select the file revision for us
2747  $options = [];
2748  $descQuery = false;
2749  $this->hookRunner->onBeforeParserFetchFileAndTitle(
2750  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
2751  $this, $nt, $options, $descQuery
2752  );
2753  # Fetch and register the file (file title may be different via hooks)
2754  list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2755  # Cloak with NOPARSE to avoid replacement in handleExternalLinks
2756  $s .= $prefix . $this->armorLinks(
2757  Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2758  continue;
2759  }
2760 
2761  # Some titles, such as valid special pages or files in foreign repos, should
2762  # be shown as bluelinks even though they're not included in the page table
2763  # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2764  # batch file existence checks for NS_FILE and NS_MEDIA
2765  if ( $iw == '' && $nt->isAlwaysKnown() ) {
2766  $this->mOutput->addLink( $nt );
2767  $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2768  } else {
2769  # Links will be added to the output link list after checking
2770  $s .= $holders->makeHolder( $nt, $text, $trail, $prefix );
2771  }
2772  }
2773  return $holders;
2774  }
2775 
2789  private function makeKnownLinkHolder( LinkTarget $nt, $text = '', $trail = '', $prefix = '' ) {
2790  list( $inside, $trail ) = Linker::splitTrail( $trail );
2791 
2792  if ( $text == '' ) {
2793  $text = htmlspecialchars( $this->titleFormatter->getPrefixedText( $nt ) );
2794  }
2795 
2796  $link = $this->getLinkRenderer()->makeKnownLink(
2797  $nt, new HtmlArmor( "$prefix$text$inside" )
2798  );
2799 
2800  return $this->armorLinks( $link ) . $trail;
2801  }
2802 
2813  private function armorLinks( $text ) {
2814  return preg_replace( '/\b((?i)' . $this->urlUtils->validProtocols() . ')/',
2815  self::MARKER_PREFIX . "NOPARSE$1", $text );
2816  }
2817 
2827  public function doBlockLevels( $text, $linestart ) {
2828  wfDeprecated( __METHOD__, '1.35' );
2829  return BlockLevelPass::doBlockLevels( $text, $linestart );
2830  }
2831 
2840  private function expandMagicVariable( $index, $frame = false ) {
2845  if (
2846  $this->hookRunner->onParserGetVariableValueVarCache( $this, $this->mVarCache ) &&
2847  isset( $this->mVarCache[$index] )
2848  ) {
2849  return $this->mVarCache[$index];
2850  }
2851 
2852  $ts = new MWTimestamp( $this->mOptions->getTimestamp() /* TS_MW */ );
2853  if ( $this->hookContainer->isRegistered( 'ParserGetVariableValueTs' ) ) {
2854  $s = $ts->getTimestamp( TS_UNIX );
2855  $this->hookRunner->onParserGetVariableValueTs( $this, $s );
2856  $ts = new MWTimestamp( $s );
2857  }
2858 
2859  $value = CoreMagicVariables::expand(
2860  $this, $index, $ts, $this->nsInfo, $this->svcOptions, $this->logger
2861  );
2862 
2863  if ( $value === null ) {
2864  // Not a defined core magic word
2865  $ret = null;
2866  $originalIndex = $index;
2867  $this->hookRunner->onParserGetVariableValueSwitch(
2868  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
2869  $this, $this->mVarCache, $index, $ret, $frame
2870  );
2871  if ( $index !== $originalIndex ) {
2873  'A ParserGetVariableValueSwitch hook handler modified $index, ' .
2874  'this is deprecated since MediaWiki 1.35',
2875  '1.35', false, false
2876  );
2877  }
2878  if ( !isset( $this->mVarCache[$originalIndex] ) ||
2879  $this->mVarCache[$originalIndex] !== $ret
2880  ) {
2882  'A ParserGetVariableValueSwitch hook handler bypassed the cache, ' .
2883  'this is deprecated since MediaWiki 1.35', '1.35', false, false
2884  );
2885  }
2886  // FIXME: in the future, don't give this hook unrestricted
2887  // access to mVarCache; we can cache it ourselves by falling
2888  // through here.
2889  return $ret;
2890  }
2891 
2892  $this->mVarCache[$index] = $value;
2893 
2894  return $value;
2895  }
2896 
2901  private function initializeVariables() {
2902  $variableIDs = $this->magicWordFactory->getVariableIDs();
2903  $substIDs = $this->magicWordFactory->getSubstIDs();
2904 
2905  $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
2906  $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
2907  }
2908 
2927  public function preprocessToDom( $text, $flags = 0 ) {
2928  return $this->getPreprocessor()->preprocessToObj( $text, $flags );
2929  }
2930 
2952  public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2953  # Is there any text? Also, Prevent too big inclusions!
2954  $textSize = strlen( $text );
2955  if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2956  return $text;
2957  }
2958 
2959  if ( $frame === false ) {
2960  $frame = $this->getPreprocessor()->newFrame();
2961  } elseif ( !( $frame instanceof PPFrame ) ) {
2962  $this->logger->debug(
2963  __METHOD__ . " called using plain parameters instead of " .
2964  "a PPFrame instance. Creating custom frame."
2965  );
2966  $frame = $this->getPreprocessor()->newCustomFrame( $frame );
2967  }
2968 
2969  $dom = $this->preprocessToDom( $text );
2970  $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2971  $text = $frame->expand( $dom, $flags );
2972 
2973  return $text;
2974  }
2975 
3003  public function limitationWarn( $limitationType, $current = '', $max = '' ) {
3004  # does no harm if $current and $max are present but are unnecessary for the message
3005  # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
3006  # only during preview, and that would split the parser cache unnecessarily.
3007  $this->mOutput->addWarningMsg(
3008  "$limitationType-warning",
3009  Message::numParam( $current ),
3010  Message::numParam( $max )
3011  );
3012  $this->addTrackingCategory( "$limitationType-category" );
3013  }
3014 
3028  public function braceSubstitution( array $piece, PPFrame $frame ) {
3029  // Flags
3030 
3031  // $text has been filled
3032  $found = false;
3033  $text = '';
3034  // wiki markup in $text should be escaped
3035  $nowiki = false;
3036  // $text is HTML, armour it against wikitext transformation
3037  $isHTML = false;
3038  // Force interwiki transclusion to be done in raw mode not rendered
3039  $forceRawInterwiki = false;
3040  // $text is a DOM node needing expansion in a child frame
3041  $isChildObj = false;
3042  // $text is a DOM node needing expansion in the current frame
3043  $isLocalObj = false;
3044 
3045  # Title object, where $text came from
3046  $title = false;
3047 
3048  # $part1 is the bit before the first |, and must contain only title characters.
3049  # Various prefixes will be stripped from it later.
3050  $titleWithSpaces = $frame->expand( $piece['title'] );
3051  $part1 = trim( $titleWithSpaces );
3052  $titleText = false;
3053 
3054  # Original title text preserved for various purposes
3055  $originalTitle = $part1;
3056 
3057  # $args is a list of argument nodes, starting from index 0, not including $part1
3058  # @todo FIXME: If piece['parts'] is null then the call to getLength()
3059  # below won't work b/c this $args isn't an object
3060  $args = ( $piece['parts'] == null ) ? [] : $piece['parts'];
3061 
3062  $profileSection = null; // profile templates
3063 
3064  $sawDeprecatedTemplateEquals = false; // T91154
3065 
3066  # SUBST
3067  // @phan-suppress-next-line PhanImpossibleCondition
3068  if ( !$found ) {
3069  $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3070 
3071  # Possibilities for substMatch: "subst", "safesubst" or FALSE
3072  # Decide whether to expand template or keep wikitext as-is.
3073  if ( $this->ot['wiki'] ) {
3074  if ( $substMatch === false ) {
3075  $literal = true; # literal when in PST with no prefix
3076  } else {
3077  $literal = false; # expand when in PST with subst: or safesubst:
3078  }
3079  } else {
3080  if ( $substMatch == 'subst' ) {
3081  $literal = true; # literal when not in PST with plain subst:
3082  } else {
3083  $literal = false; # expand when not in PST with safesubst: or no prefix
3084  }
3085  }
3086  if ( $literal ) {
3087  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3088  $isLocalObj = true;
3089  $found = true;
3090  }
3091  }
3092 
3093  # Variables
3094  if ( !$found && $args->getLength() == 0 ) {
3095  $id = $this->mVariables->matchStartToEnd( $part1 );
3096  if ( $id !== false ) {
3097  $text = $this->expandMagicVariable( $id, $frame );
3098  if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
3099  $this->mOutput->updateCacheExpiry(
3100  $this->magicWordFactory->getCacheTTL( $id ) );
3101  }
3102  $found = true;
3103  }
3104  }
3105 
3106  # MSG, MSGNW and RAW
3107  if ( !$found ) {
3108  # Check for MSGNW:
3109  $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
3110  if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3111  $nowiki = true;
3112  } else {
3113  # Remove obsolete MSG:
3114  $mwMsg = $this->magicWordFactory->get( 'msg' );
3115  $mwMsg->matchStartAndRemove( $part1 );
3116  }
3117 
3118  # Check for RAW:
3119  $mwRaw = $this->magicWordFactory->get( 'raw' );
3120  if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3121  $forceRawInterwiki = true;
3122  }
3123  }
3124 
3125  # Parser functions
3126  if ( !$found ) {
3127  $colonPos = strpos( $part1, ':' );
3128  if ( $colonPos !== false ) {
3129  $func = substr( $part1, 0, $colonPos );
3130  $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3131  $argsLength = $args->getLength();
3132  for ( $i = 0; $i < $argsLength; $i++ ) {
3133  $funcArgs[] = $args->item( $i );
3134  }
3135 
3136  $result = $this->callParserFunction( $frame, $func, $funcArgs );
3137 
3138  // Extract any forwarded flags
3139  if ( isset( $result['title'] ) ) {
3140  $title = $result['title'];
3141  }
3142  if ( isset( $result['found'] ) ) {
3143  $found = $result['found'];
3144  }
3145  if ( array_key_exists( 'text', $result ) ) {
3146  // a string or null
3147  $text = $result['text'];
3148  }
3149  if ( isset( $result['nowiki'] ) ) {
3150  $nowiki = $result['nowiki'];
3151  }
3152  if ( isset( $result['isHTML'] ) ) {
3153  $isHTML = $result['isHTML'];
3154  }
3155  if ( isset( $result['forceRawInterwiki'] ) ) {
3156  $forceRawInterwiki = $result['forceRawInterwiki'];
3157  }
3158  if ( isset( $result['isChildObj'] ) ) {
3159  $isChildObj = $result['isChildObj'];
3160  }
3161  if ( isset( $result['isLocalObj'] ) ) {
3162  $isLocalObj = $result['isLocalObj'];
3163  }
3164  }
3165  }
3166 
3167  # Finish mangling title and then check for loops.
3168  # Set $title to a Title object and $titleText to the PDBK
3169  if ( !$found ) {
3170  $ns = NS_TEMPLATE;
3171  # Split the title into page and subpage
3172  $subpage = '';
3173  $relative = Linker::normalizeSubpageLink(
3174  $this->getTitle(), $part1, $subpage
3175  );
3176  if ( $part1 !== $relative ) {
3177  $part1 = $relative;
3178  $ns = $this->getTitle()->getNamespace();
3179  }
3180  $title = Title::newFromText( $part1, $ns );
3181  if ( $title ) {
3182  $titleText = $title->getPrefixedText();
3183  # Check for language variants if the template is not found
3184  if ( $this->getTargetLanguageConverter()->hasVariants() && $title->getArticleID() == 0 ) {
3185  $this->getTargetLanguageConverter()->findVariantLink( $part1, $title, true );
3186  }
3187  # Do recursion depth check
3188  $limit = $this->mOptions->getMaxTemplateDepth();
3189  if ( $frame->depth >= $limit ) {
3190  $found = true;
3191  $text = '<span class="error">'
3192  . wfMessage( 'parser-template-recursion-depth-warning' )
3193  ->numParams( $limit )->inContentLanguage()->text()
3194  . '</span>';
3195  }
3196  }
3197  }
3198 
3199  # Load from database
3200  if ( !$found && $title ) {
3201  $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3202  if ( !$title->isExternal() ) {
3203  if ( $title->isSpecialPage()
3204  && $this->mOptions->getAllowSpecialInclusion()
3205  && $this->ot['html']
3206  ) {
3207  $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
3208  // Pass the template arguments as URL parameters.
3209  // "uselang" will have no effect since the Language object
3210  // is forced to the one defined in ParserOptions.
3211  $pageArgs = [];
3212  $argsLength = $args->getLength();
3213  for ( $i = 0; $i < $argsLength; $i++ ) {
3214  $bits = $args->item( $i )->splitArg();
3215  if ( strval( $bits['index'] ) === '' ) {
3216  $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3217  $value = trim( $frame->expand( $bits['value'] ) );
3218  $pageArgs[$name] = $value;
3219  }
3220  }
3221 
3222  // Create a new context to execute the special page
3223  $context = new RequestContext;
3224  $context->setTitle( $title );
3225  $context->setRequest( new FauxRequest( $pageArgs ) );
3226  if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3227  $context->setUser( $this->userFactory->newFromUserIdentity( $this->getUserIdentity() ) );
3228  } else {
3229  // If this page is cached, then we better not be per user.
3230  $context->setUser( User::newFromName( '127.0.0.1', false ) );
3231  }
3232  $context->setLanguage( $this->mOptions->getUserLangObj() );
3233  $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
3234  if ( $ret ) {
3235  $text = $context->getOutput()->getHTML();
3236  $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3237  $found = true;
3238  $isHTML = true;
3239  if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3240  $this->mOutput->updateRuntimeAdaptiveExpiry(
3241  $specialPage->maxIncludeCacheTime()
3242  );
3243  }
3244  }
3245  } elseif ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
3246  $found = false; # access denied
3247  $this->logger->debug(
3248  __METHOD__ .
3249  ": template inclusion denied for " . $title->getPrefixedDBkey()
3250  );
3251  } else {
3252  list( $text, $title ) = $this->getTemplateDom( $title );
3253  if ( $text !== false ) {
3254  $found = true;
3255  $isChildObj = true;
3256  if (
3257  $title->getNamespace() === NS_TEMPLATE &&
3258  $title->getDBkey() === '=' &&
3259  $originalTitle === '='
3260  ) {
3261  // Note that we won't get here if `=` is evaluated
3262  // (in the future) as a parser function, nor if
3263  // the Template namespace is given explicitly,
3264  // ie `{{Template:=}}`. Only `{{=}}` triggers.
3265  $sawDeprecatedTemplateEquals = true; // T91154
3266  }
3267  }
3268  }
3269 
3270  # If the title is valid but undisplayable, make a link to it
3271  if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3272  $text = "[[:$titleText]]";
3273  $found = true;
3274  }
3275  } elseif ( $title->isTrans() ) {
3276  # Interwiki transclusion
3277  if ( $this->ot['html'] && !$forceRawInterwiki ) {
3278  $text = $this->interwikiTransclude( $title, 'render' );
3279  $isHTML = true;
3280  } else {
3281  $text = $this->interwikiTransclude( $title, 'raw' );
3282  # Preprocess it like a template
3283  $text = $this->preprocessToDom( $text, Preprocessor::DOM_FOR_INCLUSION );
3284  $isChildObj = true;
3285  }
3286  $found = true;
3287  }
3288 
3289  # Do infinite loop check
3290  # This has to be done after redirect resolution to avoid infinite loops via redirects
3291  if ( !$frame->loopCheck( $title ) ) {
3292  $found = true;
3293  $text = '<span class="error">'
3294  . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3295  . '</span>';
3296  $this->addTrackingCategory( 'template-loop-category' );
3297  $this->mOutput->addWarningMsg(
3298  'template-loop-warning',
3299  Message::plaintextParam( $titleText )
3300  );
3301  $this->logger->debug( __METHOD__ . ": template loop broken at '$titleText'" );
3302  }
3303  }
3304 
3305  # If we haven't found text to substitute by now, we're done
3306  # Recover the source wikitext and return it
3307  if ( !$found ) {
3308  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3309  if ( $profileSection ) {
3310  $this->mProfiler->scopedProfileOut( $profileSection );
3311  }
3312  return [ 'object' => $text ];
3313  }
3314 
3315  # Expand DOM-style return values in a child frame
3316  if ( $isChildObj ) {
3317  # Clean up argument array
3318  $newFrame = $frame->newChild( $args, $title );
3319 
3320  if ( $nowiki ) {
3321  $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3322  } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3323  # Expansion is eligible for the empty-frame cache
3324  $text = $newFrame->cachedExpand( $titleText, $text );
3325  } else {
3326  # Uncached expansion
3327  $text = $newFrame->expand( $text );
3328  }
3329  }
3330  if ( $isLocalObj && $nowiki ) {
3331  $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3332  $isLocalObj = false;
3333  }
3334 
3335  if ( $profileSection ) {
3336  $this->mProfiler->scopedProfileOut( $profileSection );
3337  }
3338  if (
3339  $sawDeprecatedTemplateEquals &&
3340  $this->mStripState->unstripBoth( $text ) !== '='
3341  ) {
3342  // T91154: {{=}} is deprecated when it doesn't expand to `=`;
3343  // use {{Template:=}} if you must.
3344  $this->addTrackingCategory( 'template-equals-category' );
3345  $this->mOutput->addWarningMsg( 'template-equals-warning' );
3346  }
3347 
3348  # Replace raw HTML by a placeholder
3349  if ( $isHTML ) {
3350  // @phan-suppress-next-line SecurityCheck-XSS Mixed mode, here html and safe
3351  $text = $this->insertStripItem( $text );
3352  } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3353  # Escape nowiki-style return values
3354  // @phan-suppress-next-line SecurityCheck-DoubleEscaped Mixed mode, here html and safe
3355  $text = wfEscapeWikiText( $text );
3356  } elseif ( is_string( $text )
3357  && !$piece['lineStart']
3358  && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3359  ) {
3360  # T2529: if the template begins with a table or block-level
3361  # element, it should be treated as beginning a new line.
3362  # This behavior is somewhat controversial.
3363  $text = "\n" . $text;
3364  }
3365 
3366  if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3367  # Error, oversize inclusion
3368  if ( $titleText !== false ) {
3369  # Make a working, properly escaped link if possible (T25588)
3370  $text = "[[:$titleText]]";
3371  } else {
3372  # This will probably not be a working link, but at least it may
3373  # provide some hint of where the problem is
3374  $originalTitle = preg_replace( '/^:/', '', $originalTitle );
3375  $text = "[[:$originalTitle]]";
3376  }
3377  $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3378  . 'post-expand include size too large -->' );
3379  $this->limitationWarn( 'post-expand-template-inclusion' );
3380  }
3381 
3382  if ( $isLocalObj ) {
3383  $ret = [ 'object' => $text ];
3384  } else {
3385  $ret = [ 'text' => $text ];
3386  }
3387 
3388  return $ret;
3389  }
3390 
3409  public function callParserFunction( PPFrame $frame, $function, array $args = [] ) {
3410  # Case sensitive functions
3411  if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3412  $function = $this->mFunctionSynonyms[1][$function];
3413  } else {
3414  # Case insensitive functions
3415  $function = $this->contLang->lc( $function );
3416  if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3417  $function = $this->mFunctionSynonyms[0][$function];
3418  } else {
3419  return [ 'found' => false ];
3420  }
3421  }
3422 
3423  list( $callback, $flags ) = $this->mFunctionHooks[$function];
3424 
3425  $allArgs = [ $this ];
3426  if ( $flags & self::SFH_OBJECT_ARGS ) {
3427  # Convert arguments to PPNodes and collect for appending to $allArgs
3428  $funcArgs = [];
3429  foreach ( $args as $k => $v ) {
3430  if ( $v instanceof PPNode || $k === 0 ) {
3431  $funcArgs[] = $v;
3432  } else {
3433  $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3434  }
3435  }
3436 
3437  # Add a frame parameter, and pass the arguments as an array
3438  $allArgs[] = $frame;
3439  $allArgs[] = $funcArgs;
3440  } else {
3441  # Convert arguments to plain text and append to $allArgs
3442  foreach ( $args as $k => $v ) {
3443  if ( $v instanceof PPNode ) {
3444  $allArgs[] = trim( $frame->expand( $v ) );
3445  } elseif ( is_int( $k ) && $k >= 0 ) {
3446  $allArgs[] = trim( $v );
3447  } else {
3448  $allArgs[] = trim( "$k=$v" );
3449  }
3450  }
3451  }
3452 
3453  $result = $callback( ...$allArgs );
3454 
3455  # The interface for function hooks allows them to return a wikitext
3456  # string or an array containing the string and any flags. This mungs
3457  # things around to match what this method should return.
3458  if ( !is_array( $result ) ) {
3459  $result = [
3460  'found' => true,
3461  'text' => $result,
3462  ];
3463  } else {
3464  if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3465  $result['text'] = $result[0];
3466  }
3467  unset( $result[0] );
3468  $result += [
3469  'found' => true,
3470  ];
3471  }
3472 
3473  $noparse = true;
3474  $preprocessFlags = 0;
3475  if ( isset( $result['noparse'] ) ) {
3476  $noparse = $result['noparse'];
3477  }
3478  if ( isset( $result['preprocessFlags'] ) ) {
3479  $preprocessFlags = $result['preprocessFlags'];
3480  }
3481 
3482  if ( !$noparse ) {
3483  $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3484  $result['isChildObj'] = true;
3485  }
3486 
3487  return $result;
3488  }
3489 
3499  public function getTemplateDom( LinkTarget $title ) {
3500  $cacheTitle = $title;
3501  $titleKey = CacheKeyHelper::getKeyForPage( $title );
3502 
3503  if ( isset( $this->mTplRedirCache[$titleKey] ) ) {
3504  list( $ns, $dbk ) = $this->mTplRedirCache[$titleKey];
3505  $title = Title::makeTitle( $ns, $dbk );
3506  $titleKey = CacheKeyHelper::getKeyForPage( $title );
3507  }
3508  if ( isset( $this->mTplDomCache[$titleKey] ) ) {
3509  return [ $this->mTplDomCache[$titleKey], $title ];
3510  }
3511 
3512  # Cache miss, go to the database
3513  list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3514 
3515  if ( $text === false ) {
3516  $this->mTplDomCache[$titleKey] = false;
3517  return [ false, $title ];
3518  }
3519 
3520  $dom = $this->preprocessToDom( $text, Preprocessor::DOM_FOR_INCLUSION );
3521  $this->mTplDomCache[$titleKey] = $dom;
3522 
3523  if ( !$title->isSamePageAs( $cacheTitle ) ) {
3524  $this->mTplRedirCache[ CacheKeyHelper::getKeyForPage( $cacheTitle ) ] =
3525  [ $title->getNamespace(), $title->getDBkey() ];
3526  }
3527 
3528  return [ $dom, $title ];
3529  }
3530 
3545  $cacheKey = CacheKeyHelper::getKeyForPage( $link );
3546  if ( !$this->currentRevisionCache ) {
3547  $this->currentRevisionCache = new MapCacheLRU( 100 );
3548  }
3549  if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3550  $title = Title::castFromLinkTarget( $link ); // hook signature compat
3551  $revisionRecord =
3552  // Defaults to Parser::statelessFetchRevisionRecord()
3553  call_user_func(
3554  $this->mOptions->getCurrentRevisionRecordCallback(),
3555  $title,
3556  $this
3557  );
3558  if ( !$revisionRecord ) {
3559  // Parser::statelessFetchRevisionRecord() can return false;
3560  // normalize it to null.
3561  $revisionRecord = null;
3562  }
3563  $this->currentRevisionCache->set( $cacheKey, $revisionRecord );
3564  }
3565  return $this->currentRevisionCache->get( $cacheKey );
3566  }
3567 
3574  public function isCurrentRevisionOfTitleCached( LinkTarget $link ) {
3575  $key = CacheKeyHelper::getKeyForPage( $link );
3576  return (
3577  $this->currentRevisionCache &&
3578  $this->currentRevisionCache->has( $key )
3579  );
3580  }
3581 
3590  public static function statelessFetchRevisionRecord( LinkTarget $link, $parser = null ) {
3591  if ( $link instanceof PageIdentity ) {
3592  // probably a Title, just use it.
3593  $page = $link;
3594  } else {
3595  // XXX: use RevisionStore::getPageForLink()!
3596  // ...but get the info for the current revision at the same time?
3597  // Should RevisionStore::getKnownCurrentRevision accept a LinkTarget?
3598  $page = Title::castFromLinkTarget( $link );
3599  }
3600 
3601  $revRecord = MediaWikiServices::getInstance()
3602  ->getRevisionLookup()
3603  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
3604  ->getKnownCurrentRevision( $page );
3605  return $revRecord;
3606  }
3607 
3614  public function fetchTemplateAndTitle( LinkTarget $link ) {
3615  // Use Title for compatibility with callbacks and return type
3616  $title = Title::castFromLinkTarget( $link );
3617 
3618  // Defaults to Parser::statelessFetchTemplate()
3619  $templateCb = $this->mOptions->getTemplateCallback();
3620  $stuff = call_user_func( $templateCb, $title, $this );
3621  if ( isset( $stuff['revision-record'] ) ) {
3622  $revRecord = $stuff['revision-record'];
3623  } else {
3624  $revRecord = null;
3625  }
3626 
3627  $text = $stuff['text'];
3628  if ( is_string( $stuff['text'] ) ) {
3629  // We use U+007F DELETE to distinguish strip markers from regular text
3630  $text = strtr( $text, "\x7f", "?" );
3631  }
3632  $finalTitle = $stuff['finalTitle'] ?? $title;
3633  foreach ( ( $stuff['deps'] ?? [] ) as $dep ) {
3634  $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3635  if ( $dep['title']->equals( $this->getTitle() ) && $revRecord instanceof RevisionRecord ) {
3636  // Self-transclusion; final result may change based on the new page version
3637  try {
3638  $sha1 = $revRecord->getSha1();
3639  } catch ( RevisionAccessException $e ) {
3640  $sha1 = null;
3641  }
3642  $this->setOutputFlag( ParserOutputFlags::VARY_REVISION_SHA1, 'Self transclusion' );
3643  $this->getOutput()->setRevisionUsedSha1Base36( $sha1 );
3644  }
3645  }
3646 
3647  return [ $text, $finalTitle ];
3648  }
3649 
3660  public static function statelessFetchTemplate( $page, $parser = false ) {
3661  $title = Title::castFromLinkTarget( $page ); // for compatibility with return type
3662  $text = $skip = false;
3663  $finalTitle = $title;
3664  $deps = [];
3665  $revRecord = null;
3666  $contextTitle = $parser ? $parser->getTitle() : null;
3667 
3668  # Loop to fetch the article, with up to 2 redirects
3669  $revLookup = MediaWikiServices::getInstance()->getRevisionLookup();
3670  for ( $i = 0; $i < 3 && is_object( $title ); $i++ ) {
3671  # Give extensions a chance to select the revision instead
3672  $revRecord = null; # Assume no hook
3673  $id = false; # Assume current
3674  $origTitle = $title;
3675  $titleChanged = false;
3676  Hooks::runner()->onBeforeParserFetchTemplateRevisionRecord(
3677  # The $title is a not a PageIdentity, as it may
3678  # contain fragments or even represent an attempt to transclude
3679  # a broken or otherwise-missing Title, which the hook may
3680  # fix up. Similarly, the $contextTitle may represent a special
3681  # page or other page which "exists" as a parsing context but
3682  # is not in the DB.
3683  $contextTitle, $title,
3684  $skip, $revRecord
3685  );
3686  if ( !$skip && !$revRecord ) {
3687  # Deprecated legacy hook
3688  Hooks::runner()->onBeforeParserFetchTemplateAndtitle(
3689  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
3690  $parser, $title, $skip, $id
3691  );
3692  }
3693 
3694  if ( $skip ) {
3695  $text = false;
3696  $deps[] = [
3697  'title' => $title,
3698  'page_id' => $title->getArticleID(),
3699  'rev_id' => null
3700  ];
3701  break;
3702  }
3703  # Get the revision
3704  if ( !$revRecord ) {
3705  if ( $id ) {
3706  # Handle $id returned by deprecated legacy hook
3707  $revRecord = $revLookup->getRevisionById( $id );
3708  } elseif ( $parser ) {
3709  $revRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
3710  } else {
3711  $revRecord = $revLookup->getRevisionByTitle( $title );
3712  }
3713  }
3714  if ( $revRecord ) {
3715  # Update title, as $revRecord may have been changed by hook
3717  $revRecord->getPageAsLinkTarget()
3718  );
3719  $deps[] = [
3720  'title' => $title,
3721  'page_id' => $revRecord->getPageId(),
3722  'rev_id' => $revRecord->getId(),
3723  ];
3724  } else {
3725  $deps[] = [
3726  'title' => $title,
3727  'page_id' => $title->getArticleID(),
3728  'rev_id' => null,
3729  ];
3730  }
3731  if ( !$title->equals( $origTitle ) ) {
3732  # If we fetched a rev from a different title, register
3733  # the original title too...
3734  $deps[] = [
3735  'title' => $origTitle,
3736  'page_id' => $origTitle->getArticleID(),
3737  'rev_id' => null,
3738  ];
3739  $titleChanged = true;
3740  }
3741  # If there is no current revision, there is no page
3742  if ( $revRecord === null || $revRecord->getId() === null ) {
3743  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3744  $linkCache->addBadLinkObj( $title );
3745  }
3746  if ( $revRecord ) {
3747  if ( $titleChanged && !$revRecord->hasSlot( SlotRecord::MAIN ) ) {
3748  // We've added this (missing) title to the dependencies;
3749  // give the hook another chance to redirect it to an
3750  // actual page.
3751  $text = false;
3752  $finalTitle = $title;
3753  continue;
3754  }
3755  if ( $revRecord->hasSlot( SlotRecord::MAIN ) ) { // T276476
3756  $content = $revRecord->getContent( SlotRecord::MAIN );
3757  $text = $content ? $content->getWikitextForTransclusion() : null;
3758  } else {
3759  $text = false;
3760  }
3761 
3762  if ( $text === false || $text === null ) {
3763  $text = false;
3764  break;
3765  }
3766  } elseif ( $title->getNamespace() === NS_MEDIAWIKI ) {
3767  $message = wfMessage( MediaWikiServices::getInstance()->getContentLanguage()->
3768  lcfirst( $title->getText() ) )->inContentLanguage();
3769  if ( !$message->exists() ) {
3770  $text = false;
3771  break;
3772  }
3773  $text = $message->plain();
3774  break;
3775  } else {
3776  break;
3777  }
3778  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Only reached when content is set
3779  if ( !$content ) {
3780  break;
3781  }
3782  # Redirect?
3783  $finalTitle = $title;
3784  $title = $content->getRedirectTarget();
3785  }
3786 
3787  $retValues = [
3788  // previously, when this also returned a Revision object, we set
3789  // 'revision-record' to false instead of null if it was unavailable,
3790  // so that callers to use isset and then rely on the revision-record
3791  // key instead of the revision key, even if there was no corresponding
3792  // object - we continue to set to false here for backwards compatability
3793  'revision-record' => $revRecord ?: false,
3794  'text' => $text,
3795  'finalTitle' => $finalTitle,
3796  'deps' => $deps
3797  ];
3798  return $retValues;
3799  }
3800 
3809  public function fetchFileAndTitle( LinkTarget $link, array $options = [] ) {
3810  $file = $this->fetchFileNoRegister( $link, $options );
3811 
3812  $time = $file ? $file->getTimestamp() : false;
3813  $sha1 = $file ? $file->getSha1() : false;
3814  # Register the file as a dependency...
3815  $this->mOutput->addImage( $link->getDBkey(), $time, $sha1 );
3816  if ( $file && !$link->isSameLinkAs( $file->getTitle() ) ) {
3817  # Update fetched file title
3818  $page = $file->getTitle();
3819  $this->mOutput->addImage( $page->getDBkey(), $time, $sha1 );
3820  }
3821 
3822  $title = Title::castFromLinkTarget( $link ); // for return type compat
3823  return [ $file, $title ];
3824  }
3825 
3836  protected function fetchFileNoRegister( LinkTarget $link, array $options = [] ) {
3837  if ( isset( $options['broken'] ) ) {
3838  $file = false; // broken thumbnail forced by hook
3839  } else {
3840  $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
3841  if ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3842  $file = $repoGroup->findFileFromKey( $options['sha1'], $options );
3843  } else { // get by (name,timestamp)
3844  $file = $repoGroup->findFile( $link, $options );
3845  }
3846  }
3847  return $file;
3848  }
3849 
3859  public function interwikiTransclude( LinkTarget $link, $action ) {
3860  if ( !$this->svcOptions->get( MainConfigNames::EnableScaryTranscluding ) ) {
3861  return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3862  }
3863 
3864  // TODO: extract relevant functionality from Title
3865  $title = Title::castFromLinkTarget( $link );
3866 
3867  $url = $title->getFullURL( [ 'action' => $action ] );
3868  if ( strlen( $url ) > 1024 ) {
3869  return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3870  }
3871 
3872  $wikiId = $title->getTransWikiID(); // remote wiki ID or false
3873 
3874  $fname = __METHOD__;
3875  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3876 
3877  $data = $cache->getWithSetCallback(
3878  $cache->makeGlobalKey(
3879  'interwiki-transclude',
3880  ( $wikiId !== false ) ? $wikiId : 'external',
3881  sha1( $url )
3882  ),
3883  $this->svcOptions->get( MainConfigNames::TranscludeCacheExpiry ),
3884  function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
3885  $req = $this->httpRequestFactory->create( $url, [], $fname );
3886 
3887  $status = $req->execute(); // Status object
3888  if ( !$status->isOK() ) {
3889  $ttl = $cache::TTL_UNCACHEABLE;
3890  } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
3891  $ttl = min( $cache::TTL_LAGGED, $ttl );
3892  }
3893 
3894  return [
3895  'text' => $status->isOK() ? $req->getContent() : null,
3896  'code' => $req->getStatus()
3897  ];
3898  },
3899  [
3900  'checkKeys' => ( $wikiId !== false )
3901  ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
3902  : [],
3903  'pcGroup' => 'interwiki-transclude:5',
3904  'pcTTL' => $cache::TTL_PROC_LONG
3905  ]
3906  );
3907 
3908  if ( is_string( $data['text'] ) ) {
3909  $text = $data['text'];
3910  } elseif ( $data['code'] != 200 ) {
3911  // Though we failed to fetch the content, this status is useless.
3912  $text = wfMessage( 'scarytranscludefailed-httpstatus' )
3913  ->params( $url, $data['code'] )->inContentLanguage()->text();
3914  } else {
3915  $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3916  }
3917 
3918  return $text;
3919  }
3920 
3931  public function argSubstitution( array $piece, PPFrame $frame ) {
3932  $error = false;
3933  $parts = $piece['parts'];
3934  $nameWithSpaces = $frame->expand( $piece['title'] );
3935  $argName = trim( $nameWithSpaces );
3936  $object = false;
3937  $text = $frame->getArgument( $argName );
3938  if ( $text === false && $parts->getLength() > 0
3939  && ( $this->ot['html']
3940  || $this->ot['pre']
3941  || ( $this->ot['wiki'] && $frame->isTemplate() )
3942  )
3943  ) {
3944  # No match in frame, use the supplied default
3945  $object = $parts->item( 0 )->getChildren();
3946  }
3947  if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3948  $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3949  $this->limitationWarn( 'post-expand-template-argument' );
3950  }
3951 
3952  if ( $text === false && $object === false ) {
3953  # No match anywhere
3954  $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3955  }
3956  if ( $error !== false ) {
3957  $text .= $error;
3958  }
3959  if ( $object !== false ) {
3960  $ret = [ 'object' => $object ];
3961  } else {
3962  $ret = [ 'text' => $text ];
3963  }
3964 
3965  return $ret;
3966  }
3967 
3985  public function extensionSubstitution( array $params, PPFrame $frame ) {
3986  static $errorStr = '<span class="error">';
3987  static $errorLen = 20;
3988 
3989  $name = $frame->expand( $params['name'] );
3990  if ( substr( $name, 0, $errorLen ) === $errorStr ) {
3991  // Probably expansion depth or node count exceeded. Just punt the
3992  // error up.
3993  return $name;
3994  }
3995 
3996  $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3997  if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
3998  // See above
3999  return $attrText;
4000  }
4001 
4002  // We can't safely check if the expansion for $content resulted in an
4003  // error, because the content could happen to be the error string
4004  // (T149622).
4005  $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
4006 
4007  $marker = self::MARKER_PREFIX . "-$name-"
4008  . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
4009 
4010  $markerType = 'general';
4011  if ( $this->ot['html'] ) {
4012  $name = strtolower( $name );
4013  $attributes = Sanitizer::decodeTagAttributes( $attrText );
4014  if ( isset( $params['attributes'] ) ) {
4015  $attributes += $params['attributes'];
4016  }
4017 
4018  if ( isset( $this->mTagHooks[$name] ) ) {
4019  // Note that $content may be null here, for example if the
4020  // tag is self-closed.
4021  $output = call_user_func_array( $this->mTagHooks[$name],
4022  [ $content, $attributes, $this, $frame ] );
4023  } else {
4024  $output = '<span class="error">Invalid tag extension name: ' .
4025  htmlspecialchars( $name ) . '</span>';
4026  }
4027 
4028  if ( is_array( $output ) ) {
4029  // Extract flags
4030  $flags = $output;
4031  $output = $flags[0];
4032  if ( isset( $flags['markerType'] ) ) {
4033  $markerType = $flags['markerType'];
4034  }
4035  }
4036  } else {
4037  if ( $attrText === null ) {
4038  $attrText = '';
4039  }
4040  if ( isset( $params['attributes'] ) ) {
4041  foreach ( $params['attributes'] as $attrName => $attrValue ) {
4042  $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
4043  htmlspecialchars( $attrValue, ENT_COMPAT ) . '"';
4044  }
4045  }
4046  if ( $content === null ) {
4047  $output = "<$name$attrText/>";
4048  } else {
4049  $close = $params['close'] === null ? '' : $frame->expand( $params['close'] );
4050  if ( substr( $close, 0, $errorLen ) === $errorStr ) {
4051  // See above
4052  return $close;
4053  }
4054  $output = "<$name$attrText>$content$close";
4055  }
4056  }
4057 
4058  if ( $markerType === 'none' ) {
4059  return $output;
4060  } elseif ( $markerType === 'nowiki' ) {
4061  $this->mStripState->addNoWiki( $marker, $output );
4062  } elseif ( $markerType === 'general' ) {
4063  $this->mStripState->addGeneral( $marker, $output );
4064  } else {
4065  throw new MWException( __METHOD__ . ': invalid marker type' );
4066  }
4067  return $marker;
4068  }
4069 
4077  private function incrementIncludeSize( $type, $size ) {
4078  if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4079  return false;
4080  } else {
4081  $this->mIncludeSizes[$type] += $size;
4082  return true;
4083  }
4084  }
4085 
4091  $this->mExpensiveFunctionCount++;
4092  return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4093  }
4094 
4102  private function handleDoubleUnderscore( $text ) {
4103  # The position of __TOC__ needs to be recorded
4104  $mw = $this->magicWordFactory->get( 'toc' );
4105  if ( $mw->match( $text ) ) {
4106  $this->mShowToc = true;
4107  $this->mForceTocPosition = true;
4108 
4109  # Set a placeholder. At the end we'll fill it in with the TOC.
4110  $text = $mw->replace( self::TOC_PLACEHOLDER, $text, 1 );
4111 
4112  # Only keep the first one.
4113  $text = $mw->replace( '', $text );
4114  }
4115 
4116  # Now match and remove the rest of them
4117  $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4118  $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4119 
4120  if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4121  $this->mOutput->setNoGallery( true );
4122  }
4123  if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4124  $this->mShowToc = false;
4125  }
4126  if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4127  && $this->getTitle()->getNamespace() === NS_CATEGORY
4128  ) {
4129  $this->addTrackingCategory( 'hidden-category-category' );
4130  }
4131  # (T10068) Allow control over whether robots index a page.
4132  # __INDEX__ always overrides __NOINDEX__, see T16899
4133  if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->getTitle()->canUseNoindex() ) {
4134  $this->mOutput->setIndexPolicy( 'noindex' );
4135  $this->addTrackingCategory( 'noindex-category' );
4136  }
4137  if ( isset( $this->mDoubleUnderscores['index'] ) && $this->getTitle()->canUseNoindex() ) {
4138  $this->mOutput->setIndexPolicy( 'index' );
4139  $this->addTrackingCategory( 'index-category' );
4140  }
4141 
4142  # Cache all double underscores in the database
4143  foreach ( $this->mDoubleUnderscores as $key => $val ) {
4144  $this->mOutput->setPageProperty( $key, '' );
4145  }
4146 
4147  // @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive
4148  return $text;
4149  }
4150 
4157  public function addTrackingCategory( $msg ) {
4158  return $this->trackingCategories->addTrackingCategory(
4159  $this->mOutput, $msg, $this->getPage()
4160  );
4161  }
4162 
4178  private function finalizeHeadings( $text, $origText, $isMain = true ) {
4179  # Inhibit editsection links if requested in the page
4180  if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4181  $maybeShowEditLink = false;
4182  } else {
4183  $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4184  }
4185 
4186  # Get all headlines for numbering them and adding funky stuff like [edit]
4187  # links - this is for later, but we need the number of headlines right now
4188  # NOTE: white space in headings have been trimmed in handleHeadings. They shouldn't
4189  # be trimmed here since whitespace in HTML headings is significant.
4190  $matches = [];
4191  $numMatches = preg_match_all(
4192  '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4193  $text,
4194  $matches
4195  );
4196 
4197  # if there are fewer than 4 headlines in the article, do not show TOC
4198  # unless it's been explicitly enabled.
4199  $enoughToc = $this->mShowToc &&
4200  ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4201 
4202  # Allow user to stipulate that a page should have a "new section"
4203  # link added via __NEWSECTIONLINK__
4204  if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4205  $this->mOutput->setNewSection( true );
4206  }
4207 
4208  # Allow user to remove the "new section"
4209  # link via __NONEWSECTIONLINK__
4210  if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4211  $this->mOutput->setHideNewSection( true );
4212  }
4213 
4214  # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4215  # override above conditions and always show TOC above first header
4216  if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4217  $this->mShowToc = true;
4218  $enoughToc = true;
4219  }
4220 
4221  # headline counter
4222  $headlineCount = 0;
4223  $numVisible = 0;
4224 
4225  # Ugh .. the TOC should have neat indentation levels which can be
4226  # passed to the skin functions. These are determined here
4227  $toc = '';
4228  $full = '';
4229  $head = [];
4230  $sublevelCount = [];
4231  $levelCount = [];
4232  $level = 0;
4233  $prevlevel = 0;
4234  $toclevel = 0;
4235  $prevtoclevel = 0;
4236  $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4237  $baseTitleText = $this->getTitle()->getPrefixedDBkey();
4238  $oldType = $this->mOutputType;
4239  $this->setOutputType( self::OT_WIKI );
4240  $frame = $this->getPreprocessor()->newFrame();
4241  $root = $this->preprocessToDom( $origText );
4242  $node = $root->getFirstChild();
4243  $byteOffset = 0;
4244  $tocraw = [];
4245  $refers = [];
4246 
4247  $headlines = $numMatches !== false ? $matches[3] : [];
4248 
4249  $maxTocLevel = $this->svcOptions->get( MainConfigNames::MaxTocLevel );
4250  foreach ( $headlines as $headline ) {
4251  $isTemplate = false;
4252  $titleText = false;
4253  $sectionIndex = false;
4254  $numbering = '';
4255  $markerMatches = [];
4256  if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4257  $serial = (int)$markerMatches[1];
4258  list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4259  $isTemplate = ( $titleText != $baseTitleText );
4260  $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4261  }
4262 
4263  if ( $toclevel ) {
4264  $prevlevel = $level;
4265  }
4266  $level = (int)$matches[1][$headlineCount];
4267 
4268  if ( $level > $prevlevel ) {
4269  # Increase TOC level
4270  $toclevel++;
4271  $sublevelCount[$toclevel] = 0;
4272  if ( $toclevel < $maxTocLevel ) {
4273  $prevtoclevel = $toclevel;
4274  $toc .= Linker::tocIndent();
4275  $numVisible++;
4276  }
4277  } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4278  # Decrease TOC level, find level to jump to
4279 
4280  for ( $i = $toclevel; $i > 0; $i-- ) {
4281  // @phan-suppress-next-line PhanTypeInvalidDimOffset
4282  if ( $levelCount[$i] == $level ) {
4283  # Found last matching level
4284  $toclevel = $i;
4285  break;
4286  } elseif ( $levelCount[$i] < $level ) {
4287  // @phan-suppress-previous-line PhanTypeInvalidDimOffset
4288  # Found first matching level below current level
4289  $toclevel = $i + 1;
4290  break;
4291  }
4292  }
4293  if ( $i == 0 ) {
4294  $toclevel = 1;
4295  }
4296  if ( $toclevel < $maxTocLevel ) {
4297  if ( $prevtoclevel < $maxTocLevel ) {
4298  # Unindent only if the previous toc level was shown :p
4299  $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4300  $prevtoclevel = $toclevel;
4301  } else {
4302  $toc .= Linker::tocLineEnd();
4303  }
4304  }
4305  } else {
4306  # No change in level, end TOC line
4307  if ( $toclevel < $maxTocLevel ) {
4308  $toc .= Linker::tocLineEnd();
4309  }
4310  }
4311 
4312  $levelCount[$toclevel] = $level;
4313 
4314  # count number of headlines for each level
4315  $sublevelCount[$toclevel]++;
4316  $dot = 0;
4317  for ( $i = 1; $i <= $toclevel; $i++ ) {
4318  if ( !empty( $sublevelCount[$i] ) ) {
4319  if ( $dot ) {
4320  $numbering .= '.';
4321  }
4322  $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4323  $dot = 1;
4324  }
4325  }
4326 
4327  # The safe header is a version of the header text safe to use for links
4328 
4329  # Remove link placeholders by the link text.
4330  # <!--LINK number-->
4331  # turns into
4332  # link text with suffix
4333  # Do this before unstrip since link text can contain strip markers
4334  $safeHeadline = $this->replaceLinkHoldersText( $headline );
4335 
4336  # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4337  $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4338 
4339  # Remove any <style> or <script> tags (T198618)
4340  $safeHeadline = preg_replace(
4341  '#<(style|script)(?: [^>]*[^>/])?>.*?</\1>#is',
4342  '',
4343  $safeHeadline
4344  );
4345 
4346  # Strip out HTML (first regex removes any tag not allowed)
4347  # Allowed tags are:
4348  # * <sup> and <sub> (T10393)
4349  # * <i> (T28375)
4350  # * <b> (r105284)
4351  # * <bdi> (T74884)
4352  # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4353  # * <s> and <strike> (T35715)
4354  # * <q> (T251672)
4355  # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4356  # to allow setting directionality in toc items.
4357  $tocline = preg_replace(
4358  [
4359  '#<(?!/?(span|sup|sub|bdi|i|b|s|strike|q)(?: [^>]*)?>).*?>#',
4360  '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4361  ],
4362  [ '', '<$1>' ],
4363  $safeHeadline
4364  );
4365 
4366  # Strip '<span></span>', which is the result from the above if
4367  # <span id="foo"></span> is used to produce an additional anchor
4368  # for a section.
4369  $tocline = str_replace( '<span></span>', '', $tocline );
4370 
4371  $tocline = trim( $tocline );
4372 
4373  # For the anchor, strip out HTML-y stuff period
4374  $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4375  $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4376 
4377  # Save headline for section edit hint before it's escaped
4378  $headlineHint = $safeHeadline;
4379 
4380  # Decode HTML entities
4381  $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4382 
4383  $safeHeadline = self::normalizeSectionName( $safeHeadline );
4384 
4385  $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4386  $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4387  $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4388  if ( $fallbackHeadline === $safeHeadline ) {
4389  # No reason to have both (in fact, we can't)
4390  $fallbackHeadline = false;
4391  }
4392 
4393  # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4394  $arrayKey = strtolower( $safeHeadline );
4395  if ( $fallbackHeadline === false ) {
4396  $fallbackArrayKey = false;
4397  } else {
4398  $fallbackArrayKey = strtolower( $fallbackHeadline );
4399  }
4400 
4401  # Create the anchor for linking from the TOC to the section
4402  $anchor = $safeHeadline;
4403  $fallbackAnchor = $fallbackHeadline;
4404  if ( isset( $refers[$arrayKey] ) ) {
4405  for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4406  $anchor .= "_$i";
4407  $linkAnchor .= "_$i";
4408  $refers["${arrayKey}_$i"] = true;
4409  } else {
4410  $refers[$arrayKey] = true;
4411  }
4412  if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
4413  for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
4414  $fallbackAnchor .= "_$i";
4415  $refers["${fallbackArrayKey}_$i"] = true;
4416  } else {
4417  $refers[$fallbackArrayKey] = true;
4418  }
4419 
4420  if ( $enoughToc && ( !isset( $maxTocLevel ) || $toclevel < $maxTocLevel ) ) {
4421  $toc .= Linker::tocLine(
4422  $linkAnchor,
4423  $tocline,
4424  $numbering,
4425  $toclevel,
4426  ( $isTemplate ? false : $sectionIndex )
4427  );
4428  }
4429 
4430  # Add the section to the section tree
4431  # Find the DOM node for this header
4432  $noOffset = ( $isTemplate || $sectionIndex === false );
4433  while ( $node && !$noOffset ) {
4434  if ( $node->getName() === 'h' ) {
4435  $bits = $node->splitHeading();
4436  if ( $bits['i'] == $sectionIndex ) {
4437  break;
4438  }
4439  }
4440  $byteOffset += mb_strlen(
4441  $this->mStripState->unstripBoth(
4442  $frame->expand( $node, PPFrame::RECOVER_ORIG )
4443  )
4444  );
4445  $node = $node->getNextSibling();
4446  }
4447  $tocraw[] = [
4448  'toclevel' => $toclevel,
4449  // cast $level to string in order to keep b/c for the parse api
4450  'level' => (string)$level,
4451  'line' => $tocline,
4452  'number' => $numbering,
4453  'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4454  'fromtitle' => $titleText,
4455  'byteoffset' => ( $noOffset ? null : $byteOffset ),
4456  'anchor' => $anchor,
4457  ];
4458 
4459  # give headline the correct <h#> tag
4460  if ( $maybeShowEditLink && $sectionIndex !== false ) {
4461  // Output edit section links as markers with styles that can be customized by skins
4462  if ( $isTemplate ) {
4463  # Put a T flag in the section identifier, to indicate to extractSections()
4464  # that sections inside <includeonly> should be counted.
4465  $editsectionPage = $titleText;
4466  $editsectionSection = "T-$sectionIndex";
4467  } else {
4468  $editsectionPage = $this->getTitle()->getPrefixedText();
4469  $editsectionSection = $sectionIndex;
4470  }
4471  $editsectionContent = $headlineHint;
4472  // We use a bit of pesudo-xml for editsection markers. The
4473  // language converter is run later on. Using a UNIQ style marker
4474  // leads to the converter screwing up the tokens when it
4475  // converts stuff. And trying to insert strip tags fails too. At
4476  // this point all real inputted tags have already been escaped,
4477  // so we don't have to worry about a user trying to input one of
4478  // these markers directly. We use a page and section attribute
4479  // to stop the language converter from converting these
4480  // important bits of data, but put the headline hint inside a
4481  // content block because the language converter is supposed to
4482  // be able to convert that piece of data.
4483  // Gets replaced with html in ParserOutput::getText
4484  $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage, ENT_COMPAT );
4485  // @phan-suppress-next-line SecurityCheck-DoubleEscaped
4486  $editlink .= '" section="' . htmlspecialchars( $editsectionSection, ENT_COMPAT ) . '"';
4487  $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4488  } else {
4489  $editlink = '';
4490  }
4491  $head[$headlineCount] = Linker::makeHeadline(
4492  $level,
4493  $matches['attrib'][$headlineCount],
4494  $anchor,
4495  $headline,
4496  $editlink,
4497  $fallbackAnchor
4498  );
4499 
4500  $headlineCount++;
4501  }
4502 
4503  $this->setOutputType( $oldType );
4504 
4505  # Never ever show TOC if no headers
4506  if ( $numVisible < 1 ) {
4507  $enoughToc = false;
4508  }
4509 
4510  if ( $enoughToc ) {
4511  if ( $prevtoclevel > 0 && $prevtoclevel < $maxTocLevel ) {
4512  $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4513  }
4514  $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4515  $this->mOutput->setTOCHTML( $toc );
4516  }
4517 
4518  if ( $isMain ) {
4519  $this->mOutput->setSections( $tocraw );
4520  }
4521 
4522  # split up and insert constructed headlines
4523  $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4524  $i = 0;
4525 
4526  // build an array of document sections
4527  $sections = [];
4528  foreach ( $blocks as $block ) {
4529  // $head is zero-based, sections aren't.
4530  if ( empty( $head[$i - 1] ) ) {
4531  $sections[$i] = $block;
4532  } else {
4533  $sections[$i] = $head[$i - 1] . $block;
4534  }
4535 
4546  $this->hookRunner->onParserSectionCreate( $this, $i, $sections[$i], $maybeShowEditLink );
4547 
4548  $i++;
4549  }
4550 
4551  if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4552  // append the TOC at the beginning
4553  // Top anchor now in skin
4554  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset At least one element when enoughToc is true
4555  $sections[0] .= self::TOC_PLACEHOLDER . "\n";
4556  }
4557 
4558  $full .= implode( '', $sections );
4559 
4560  return $full;
4561  }
4562 
4575  public function preSaveTransform(
4576  $text,
4577  PageReference $page,
4578  UserIdentity $user,
4579  ParserOptions $options,
4580  $clearState = true
4581  ) {
4582  if ( $clearState ) {
4583  $magicScopeVariable = $this->lock();
4584  }
4585  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
4586  $this->startParse( $page, $options, self::OT_WIKI, $clearState );
4587  $this->setUser( $user );
4588 
4589  // Strip U+0000 NULL (T159174)
4590  $text = str_replace( "\000", '', $text );
4591 
4592  // We still normalize line endings (including trimming trailing whitespace) for
4593  // backwards-compatibility with other code that just calls PST, but this should already
4594  // be handled in TextContent subclasses
4595  $text = TextContent::normalizeLineEndings( $text );
4596 
4597  if ( $options->getPreSaveTransform() ) {
4598  $text = $this->pstPass2( $text, $user );
4599  }
4600  $text = $this->mStripState->unstripBoth( $text );
4601 
4602  // Trim trailing whitespace again, because the previous steps can introduce it.
4603  $text = rtrim( $text );
4604 
4605  $this->hookRunner->onParserPreSaveTransformComplete( $this, $text );
4606 
4607  $this->setUser( null ); # Reset
4608 
4609  return $text;
4610  }
4611 
4620  private function pstPass2( $text, UserIdentity $user ) {
4621  # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4622  # $this->contLang here in order to give everyone the same signature and use the default one
4623  # rather than the one selected in each user's preferences. (see also T14815)
4624  $ts = $this->mOptions->getTimestamp();
4625  $timestamp = MWTimestamp::getLocalInstance( $ts );
4626  $ts = $timestamp->format( 'YmdHis' );
4627  $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4628 
4629  $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4630 
4631  # Variable replacement
4632  # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4633  $text = $this->replaceVariables( $text );
4634 
4635  # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4636  # which may corrupt this parser instance via its wfMessage()->text() call-
4637 
4638  # Signatures
4639  if ( strpos( $text, '~~~' ) !== false ) {
4640  $sigText = $this->getUserSig( $user );
4641  $text = strtr( $text, [
4642  '~~~~~' => $d,
4643  '~~~~' => "$sigText $d",
4644  '~~~' => $sigText
4645  ] );
4646  # The main two signature forms used above are time-sensitive
4647  $this->setOutputFlag( ParserOutputFlags::USER_SIGNATURE, 'User signature detected' );
4648  }
4649 
4650  # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4651  $tc = '[' . Title::legalChars() . ']';
4652  $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4653 
4654  // [[ns:page (context)|]]
4655  $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4656  // [[ns:page(context)|]] (double-width brackets, added in r40257)
4657  $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4658  // [[ns:page (context), context|]] (using single, double-width or Arabic comma)
4659  $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,|، )$tc+|)\\|]]/";
4660  // [[|page]] (reverse pipe trick: add context from page title)
4661  $p2 = "/\[\[\\|($tc+)]]/";
4662 
4663  # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4664  $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4665  $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4666  $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4667 
4668  $t = $this->getTitle()->getText();
4669  $m = [];
4670  if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4671  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4672  } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4673  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4674  } else {
4675  # if there's no context, don't bother duplicating the title
4676  $text = preg_replace( $p2, '[[\\1]]', $text );
4677  }
4678 
4679  return $text;
4680  }
4681 
4697  public function getUserSig( UserIdentity $user, $nickname = false, $fancySig = null ) {
4698  $username = $user->getName();
4699 
4700  # If not given, retrieve from the user object.
4701  if ( $nickname === false ) {
4702  $nickname = $this->userOptionsLookup->getOption( $user, 'nickname' );
4703  }
4704 
4705  if ( $fancySig === null ) {
4706  $fancySig = $this->userOptionsLookup->getBoolOption( $user, 'fancysig' );
4707  }
4708 
4709  if ( $nickname === null || $nickname === '' ) {
4710  // Empty value results in the default signature (even when fancysig is enabled)
4711  $nickname = $username;
4712  } elseif ( mb_strlen( $nickname ) > $this->svcOptions->get( MainConfigNames::MaxSigChars ) ) {
4713  $nickname = $username;
4714  $this->logger->debug( __METHOD__ . ": $username has overlong signature." );
4715  } elseif ( $fancySig !== false ) {
4716  # Sig. might contain markup; validate this
4717  $isValid = $this->validateSig( $nickname ) !== false;
4718 
4719  # New validator
4720  $sigValidation = $this->svcOptions->get( MainConfigNames::SignatureValidation );
4721  if ( $isValid && $sigValidation === 'disallow' ) {
4722  $parserOpts = new ParserOptions(
4723  $this->mOptions->getUserIdentity(),
4724  $this->contLang
4725  );
4726  $validator = $this->signatureValidatorFactory
4727  ->newSignatureValidator( $user, null, $parserOpts );
4728  $isValid = !$validator->validateSignature( $nickname );
4729  }
4730 
4731  if ( $isValid ) {
4732  # Validated; clean up (if needed) and return it
4733  return $this->cleanSig( $nickname, true );
4734  } else {
4735  # Failed to validate; fall back to the default
4736  $nickname = $username;
4737  $this->logger->debug( __METHOD__ . ": $username has invalid signature." );
4738  }
4739  }
4740 
4741  # Make sure nickname doesnt get a sig in a sig
4742  $nickname = self::cleanSigInSig( $nickname );
4743 
4744  # If we're still here, make it a link to the user page
4745  $userText = wfEscapeWikiText( $username );
4746  $nickText = wfEscapeWikiText( $nickname );
4747  if ( $this->userNameUtils->isTemp( $username ) ) {
4748  $msgName = 'signature-temp';
4749  } elseif ( $user->isRegistered() ) {
4750  $msgName = 'signature';
4751  } else {
4752  $msgName = 'signature-anon';
4753  }
4754 
4755  return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4756  ->page( $this->getPage() )->text();
4757  }
4758 
4766  public function validateSig( $text ) {
4767  return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4768  }
4769 
4781  public function cleanSig( $text, $parsing = false ) {
4782  if ( !$parsing ) {
4783  global $wgTitle;
4784  $magicScopeVariable = $this->lock();
4785  $this->startParse(
4786  $wgTitle,
4789  true
4790  );
4791  }
4792 
4793  # Option to disable this feature
4794  if ( !$this->mOptions->getCleanSignatures() ) {
4795  return $text;
4796  }
4797 
4798  # @todo FIXME: Regex doesn't respect extension tags or nowiki
4799  # => Move this logic to braceSubstitution()
4800  $substWord = $this->magicWordFactory->get( 'subst' );
4801  $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4802  $substText = '{{' . $substWord->getSynonym( 0 );
4803 
4804  $text = preg_replace( $substRegex, $substText, $text );
4805  $text = self::cleanSigInSig( $text );
4806  $dom = $this->preprocessToDom( $text );
4807  $frame = $this->getPreprocessor()->newFrame();
4808  $text = $frame->expand( $dom );
4809 
4810  if ( !$parsing ) {
4811  $text = $this->mStripState->unstripBoth( $text );
4812  }
4813 
4814  return $text;
4815  }
4816 
4824  public static function cleanSigInSig( $text ) {
4825  $text = preg_replace( '/~{3,5}/', '', $text );
4826  return $text;
4827  }
4828 
4845  public static function replaceTableOfContentsMarker( $text, $toc ) {
4846  return str_replace(
4847  self::TOC_PLACEHOLDER,
4848  $toc,
4849  $text
4850  );
4851  }
4852 
4864  public function startExternalParse( ?PageReference $page, ParserOptions $options,
4865  $outputType, $clearState = true, $revId = null
4866  ) {
4867  $this->startParse( $page, $options, $outputType, $clearState );
4868  if ( $revId !== null ) {
4869  $this->mRevisionId = $revId;
4870  }
4871  }
4872 
4879  private function startParse( ?PageReference $page, ParserOptions $options,
4880  $outputType, $clearState = true
4881  ) {
4882  $this->setPage( $page );
4883  $this->mOptions = $options;
4884  $this->setOutputType( $outputType );
4885  if ( $clearState ) {
4886  $this->clearState();
4887  }
4888  }
4889 
4899  public function transformMsg( $text, ParserOptions $options, ?PageReference $page = null ) {
4900  static $executing = false;
4901 
4902  # Guard against infinite recursion
4903  if ( $executing ) {
4904  return $text;
4905  }
4906  $executing = true;
4907 
4908  if ( !$page ) {
4909  global $wgTitle;
4910  $page = $wgTitle;
4911  }
4912 
4913  $text = $this->preprocess( $text, $page, $options );
4914 
4915  $executing = false;
4916  return $text;
4917  }
4918 
4942  public function setHook( $tag, callable $callback ) {
4943  $tag = strtolower( $tag );
4944  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4945  throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4946  }
4947  $oldVal = $this->mTagHooks[$tag] ?? null;
4948  $this->mTagHooks[$tag] = $callback;
4949  if ( !in_array( $tag, $this->mStripList ) ) {
4950  $this->mStripList[] = $tag;
4951  }
4952 
4953  return $oldVal;
4954  }
4955 
4960  public function clearTagHooks() {
4961  $this->mTagHooks = [];
4962  $this->mStripList = [];
4963  }
4964 
5009  public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
5010  $oldVal = $this->mFunctionHooks[$id][0] ?? null;
5011  $this->mFunctionHooks[$id] = [ $callback, $flags ];
5012 
5013  # Add to function cache
5014  $mw = $this->magicWordFactory->get( $id );
5015  if ( !$mw ) {
5016  throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
5017  }
5018 
5019  $synonyms = $mw->getSynonyms();
5020  $sensitive = intval( $mw->isCaseSensitive() );
5021 
5022  foreach ( $synonyms as $syn ) {
5023  # Case
5024  if ( !$sensitive ) {
5025  $syn = $this->contLang->lc( $syn );
5026  }
5027  # Add leading hash
5028  if ( !( $flags & self::SFH_NO_HASH ) ) {
5029  $syn = '#' . $syn;
5030  }
5031  # Remove trailing colon
5032  if ( substr( $syn, -1, 1 ) === ':' ) {
5033  $syn = substr( $syn, 0, -1 );
5034  }
5035  $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5036  }
5037  return $oldVal;
5038  }
5039 
5046  public function getFunctionHooks() {
5047  return array_keys( $this->mFunctionHooks );
5048  }
5049 
5058  public function replaceLinkHolders( &$text, $options = 0 ) {
5059  $this->replaceLinkHoldersPrivate( $text, $options );
5060  }
5061 
5069  private function replaceLinkHoldersPrivate( &$text, $options = 0 ) {
5070  $this->mLinkHolders->replace( $text );
5071  }
5072 
5080  private function replaceLinkHoldersText( $text ) {
5081  return $this->mLinkHolders->replaceText( $text );
5082  }
5083 
5098  public function renderImageGallery( $text, array $params ) {
5099  $mode = false;
5100  if ( isset( $params['mode'] ) ) {
5101  $mode = $params['mode'];
5102  }
5103 
5104  try {
5105  $ig = ImageGalleryBase::factory( $mode );
5106  } catch ( ImageGalleryClassNotFoundException $e ) {
5107  // If invalid type set, fallback to default.
5108  $ig = ImageGalleryBase::factory( false );
5109  }
5110 
5111  $ig->setContextTitle( $this->getTitle() );
5112  $ig->setShowBytes( false );
5113  $ig->setShowDimensions( false );
5114  $ig->setShowFilename( false );
5115  $ig->setParser( $this );
5116  $ig->setHideBadImages();
5117  $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
5118 
5119  if ( isset( $params['showfilename'] ) ) {
5120  $ig->setShowFilename( true );
5121  } else {
5122  $ig->setShowFilename( false );
5123  }
5124  if ( isset( $params['caption'] ) ) {
5125  // NOTE: We aren't passing a frame here or below. Frame info
5126  // is currently opaque to Parsoid, which acts on OT_PREPROCESS.
5127  // See T107332#4030581
5128  $caption = $this->recursiveTagParse( $params['caption'] );
5129  $ig->setCaptionHtml( $caption );
5130  }
5131  if ( isset( $params['perrow'] ) ) {
5132  $ig->setPerRow( $params['perrow'] );
5133  }
5134  if ( isset( $params['widths'] ) ) {
5135  $ig->setWidths( $params['widths'] );
5136  }
5137  if ( isset( $params['heights'] ) ) {
5138  $ig->setHeights( $params['heights'] );
5139  }
5140  $ig->setAdditionalOptions( $params );
5141 
5142  $this->hookRunner->onBeforeParserrenderImageGallery( $this, $ig );
5143 
5144  $lines = StringUtils::explode( "\n", $text );
5145  foreach ( $lines as $line ) {
5146  # match lines like these:
5147  # Image:someimage.jpg|This is some image
5148  $matches = [];
5149  preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5150  # Skip empty lines
5151  if ( count( $matches ) == 0 ) {
5152  continue;
5153  }
5154 
5155  if ( strpos( $matches[0], '%' ) !== false ) {
5156  $matches[1] = rawurldecode( $matches[1] );
5157  }
5159  if ( $title === null ) {
5160  # Bogus title. Ignore these so we don't bomb out later.
5161  continue;
5162  }
5163 
5164  # We need to get what handler the file uses, to figure out parameters.
5165  # Note, a hook can override the file name, and chose an entirely different
5166  # file (which potentially could be of a different type and have different handler).
5167  $options = [];
5168  $descQuery = false;
5169  $this->hookRunner->onBeforeParserFetchFileAndTitle(
5170  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
5171  $this, $title, $options, $descQuery
5172  );
5173  # Don't register it now, as TraditionalImageGallery does that later.
5174  $file = $this->fetchFileNoRegister( $title, $options );
5175  $handler = $file ? $file->getHandler() : false;
5176 
5177  $paramMap = [
5178  'img_alt' => 'gallery-internal-alt',
5179  'img_link' => 'gallery-internal-link',
5180  ];
5181  if ( $handler ) {
5182  $paramMap += $handler->getParamMap();
5183  // We don't want people to specify per-image widths.
5184  // Additionally the width parameter would need special casing anyhow.
5185  unset( $paramMap['img_width'] );
5186  }
5187 
5188  $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5189 
5190  $label = '';
5191  $alt = '';
5192  $handlerOptions = [];
5193  $imageOptions = [];
5194  $hasAlt = false;
5195 
5196  if ( isset( $matches[3] ) ) {
5197  // look for an |alt= definition while trying not to break existing
5198  // captions with multiple pipes (|) in it, until a more sensible grammar
5199  // is defined for images in galleries
5200 
5201  // FIXME: Doing recursiveTagParse at this stage, and the trim before
5202  // splitting on '|' is a bit odd, and different from makeImage.
5203  $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5204  // Protect LanguageConverter markup
5205  $parameterMatches = StringUtils::delimiterExplode(
5206  '-{', '}-',
5207  '|',
5208  $matches[3],
5209  true /* nested */
5210  );
5211 
5212  foreach ( $parameterMatches as $parameterMatch ) {
5213  list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5214  if ( !$magicName ) {
5215  // Last pipe wins.
5216  $label = $parameterMatch;
5217  continue;
5218  }
5219 
5220  $paramName = $paramMap[$magicName];
5221  switch ( $paramName ) {
5222  case 'gallery-internal-alt':
5223  $hasAlt = true;
5224  $alt = $this->stripAltText( $match, false );
5225  break;
5226  case 'gallery-internal-link':
5227  $linkValue = $this->stripAltText( $match, false );
5228  if ( preg_match( '/^-{R\|(.*)}-$/', $linkValue ) ) {
5229  // Result of LanguageConverter::markNoConversion
5230  // invoked on an external link.
5231  $linkValue = substr( $linkValue, 4, -2 );
5232  }
5233  list( $type, $target ) = $this->parseLinkParameter( $linkValue );
5234  if ( $type ) {
5235  if ( $type === 'no-link' ) {
5236  $target = true;
5237  }
5238  $imageOptions[$type] = $target;
5239  }
5240  break;
5241  default:
5242  // Must be a handler specific parameter.
5243  if ( $handler->validateParam( $paramName, $match ) ) {
5244  $handlerOptions[$paramName] = $match;
5245  } else {
5246  // Guess not, consider it as caption.
5247  $this->logger->debug(
5248  "$parameterMatch failed parameter validation" );
5249  $label = $parameterMatch;
5250  }
5251  }
5252  }
5253  }
5254 
5255  // Match makeImage when !$hasVisibleCaption
5256  if ( !$hasAlt ) {
5257  if ( $label !== '' ) {
5258  $alt = $this->stripAltText( $label, false );
5259  } else {
5260  $alt = $title->getText();
5261  }
5262  }
5263  $imageOptions['title'] = $this->stripAltText( $label, false );
5264 
5265  $ig->add(
5266  $title, $label, $alt, '', $handlerOptions,
5267  ImageGalleryBase::LOADING_DEFAULT, $imageOptions
5268  );
5269  }
5270  $html = $ig->toHTML();
5271  $this->hookRunner->onAfterParserFetchFileAndTitle( $this, $ig, $html );
5272  return $html;
5273  }
5274 
5279  private function getImageParams( $handler ) {
5280  if ( $handler ) {
5281  $handlerClass = get_class( $handler );
5282  } else {
5283  $handlerClass = '';
5284  }
5285  if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5286  # Initialise static lists
5287  static $internalParamNames = [
5288  'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5289  'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5290  'bottom', 'text-bottom' ],
5291  'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5292  'upright', 'border', 'link', 'alt', 'class' ],
5293  ];
5294  static $internalParamMap;
5295  if ( !$internalParamMap ) {
5296  $internalParamMap = [];
5297  foreach ( $internalParamNames as $type => $names ) {
5298  foreach ( $names as $name ) {
5299  // For grep: img_left, img_right, img_center, img_none,
5300  // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5301  // img_bottom, img_text_bottom,
5302  // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5303  // img_border, img_link, img_alt, img_class
5304  $magicName = str_replace( '-', '_', "img_$name" );
5305  $internalParamMap[$magicName] = [ $type, $name ];
5306  }
5307  }
5308  }
5309 
5310  # Add handler params
5311  $paramMap = $internalParamMap;
5312  if ( $handler ) {
5313  $handlerParamMap = $handler->getParamMap();
5314  foreach ( $handlerParamMap as $magic => $paramName ) {
5315  $paramMap[$magic] = [ 'handler', $paramName ];
5316  }
5317  } else {
5318  // Parse the size for non-existent files. See T273013
5319  $paramMap[ 'img_width' ] = [ 'handler', 'width' ];
5320  }
5321  $this->mImageParams[$handlerClass] = $paramMap;
5322  $this->mImageParamsMagicArray[$handlerClass] =
5323  $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5324  }
5325  return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5326  }
5327 
5337  public function makeImage( LinkTarget $link, $options, $holders = false ) {
5338  # Check if the options text is of the form "options|alt text"
5339  # Options are:
5340  # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5341  # * left no resizing, just left align. label is used for alt= only
5342  # * right same, but right aligned
5343  # * none same, but not aligned
5344  # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5345  # * center center the image
5346  # * framed Keep original image size, no magnify-button.
5347  # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5348  # * upright reduce width for upright images, rounded to full __0 px
5349  # * border draw a 1px border around the image
5350  # * alt Text for HTML alt attribute (defaults to empty)
5351  # * class Set a class for img node
5352  # * link Set the target of the image link. Can be external, interwiki, or local
5353  # vertical-align values (no % or length right now):
5354  # * baseline
5355  # * sub
5356  # * super
5357  # * top
5358  # * text-top
5359  # * middle
5360  # * bottom
5361  # * text-bottom
5362 
5363  # Protect LanguageConverter markup when splitting into parts
5365  '-{', '}-', '|', $options, true /* allow nesting */
5366  );
5367 
5368  # Give extensions a chance to select the file revision for us
5369  $options = [];
5370  $descQuery = false;
5371  $title = Title::castFromLinkTarget( $link ); // hook signature compat
5372  $this->hookRunner->onBeforeParserFetchFileAndTitle(
5373  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
5374  $this, $title, $options, $descQuery
5375  );
5376  # Fetch and register the file (file title may be different via hooks)
5377  list( $file, $link ) = $this->fetchFileAndTitle( $link, $options );
5378 
5379  # Get parameter map
5380  $handler = $file ? $file->getHandler() : false;
5381 
5382  list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5383 
5384  if ( !$file ) {
5385  $this->addTrackingCategory( 'broken-file-category' );
5386  }
5387 
5388  # Process the input parameters
5389  $caption = '';
5390  $params = [ 'frame' => [], 'handler' => [],
5391  'horizAlign' => [], 'vertAlign' => [] ];
5392  $seenformat = false;
5393  foreach ( $parts as $part ) {
5394  $part = trim( $part );
5395  list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5396  $validated = false;
5397  if ( isset( $paramMap[$magicName] ) ) {
5398  list( $type, $paramName ) = $paramMap[$magicName];
5399 
5400  # Special case; width and height come in one variable together
5401  if ( $type === 'handler' && $paramName === 'width' ) {
5402  $parsedWidthParam = self::parseWidthParam( $value );
5403  // Parsoid applies data-(width|height) attributes to broken
5404  // media spans, for client use. See T273013
5405  $validateFunc = static function ( $name, $value ) use ( $handler ) {
5406  return $handler
5407  ? $handler->validateParam( $name, $value )
5408  : $value > 0;
5409  };
5410  if ( isset( $parsedWidthParam['width'] ) ) {
5411  $width = $parsedWidthParam['width'];
5412  if ( $validateFunc( 'width', $width ) ) {
5413  $params[$type]['width'] = $width;
5414  $validated = true;
5415  }
5416  }
5417  if ( isset( $parsedWidthParam['height'] ) ) {
5418  $height = $parsedWidthParam['height'];
5419  if ( $validateFunc( 'height', $height ) ) {
5420  $params[$type]['height'] = $height;
5421  $validated = true;
5422  }
5423  }
5424  # else no validation -- T15436
5425  } else {
5426  if ( $type === 'handler' ) {
5427  # Validate handler parameter
5428  $validated = $handler->validateParam( $paramName, $value );
5429  } else {
5430  # Validate internal parameters
5431  switch ( $paramName ) {
5432  case 'alt':
5433  case 'class':
5434  $validated = true;
5435  $value = $this->stripAltText( $value, $holders );
5436  break;
5437  case 'link':
5438  list( $paramName, $value ) =
5439  $this->parseLinkParameter(
5440  $this->stripAltText( $value, $holders )
5441  );
5442  if ( $paramName ) {
5443  $validated = true;
5444  if ( $paramName === 'no-link' ) {
5445  $value = true;
5446  }
5447  }
5448  break;
5449  case 'manualthumb':
5450  # @todo FIXME: Possibly check validity here for
5451  # manualthumb? downstream behavior seems odd with
5452  # missing manual thumbs.
5453  $value = $this->stripAltText( $value, $holders );
5454  // fall through
5455  case 'frameless':
5456  case 'framed':
5457  case 'thumbnail':
5458  // use first appearing option, discard others.
5459  $validated = !$seenformat;
5460  $seenformat = true;
5461  break;
5462  default:
5463  # Most other things appear to be empty or numeric...
5464  $validated = ( $value === false || is_numeric( trim( $value ) ) );
5465  }
5466  }
5467 
5468  if ( $validated ) {
5469  $params[$type][$paramName] = $value;
5470  }
5471  }
5472  }
5473  if ( !$validated ) {
5474  $caption = $part;
5475  }
5476  }
5477 
5478  # Process alignment parameters
5479  if ( $params['horizAlign'] !== [] ) {
5480  $params['frame']['align'] = key( $params['horizAlign'] );
5481  }
5482  if ( $params['vertAlign'] !== [] ) {
5483  $params['frame']['valign'] = key( $params['vertAlign'] );
5484  }
5485 
5486  $params['frame']['caption'] = $caption;
5487 
5488  # Will the image be presented in a frame, with the caption below?
5489  // @phan-suppress-next-line PhanImpossibleCondition
5490  $hasVisibleCaption = isset( $params['frame']['framed'] )
5491  // @phan-suppress-next-line PhanImpossibleCondition
5492  || isset( $params['frame']['thumbnail'] )
5493  // @phan-suppress-next-line PhanImpossibleCondition
5494  || isset( $params['frame']['manualthumb'] );
5495 
5496  # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5497  # came to also set the caption, ordinary text after the image -- which
5498  # makes no sense, because that just repeats the text multiple times in
5499  # screen readers. It *also* came to set the title attribute.
5500  # Now that we have an alt attribute, we should not set the alt text to
5501  # equal the caption: that's worse than useless, it just repeats the
5502  # text. This is the framed/thumbnail case. If there's no caption, we
5503  # use the unnamed parameter for alt text as well, just for the time be-
5504  # ing, if the unnamed param is set and the alt param is not.
5505  # For the future, we need to figure out if we want to tweak this more,
5506  # e.g., introducing a title= parameter for the title; ignoring the un-
5507  # named parameter entirely for images without a caption; adding an ex-
5508  # plicit caption= parameter and preserving the old magic unnamed para-
5509  # meter for BC; ...
5510  if ( $hasVisibleCaption ) {
5511  // @phan-suppress-next-line PhanImpossibleCondition
5512  if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5513  # No caption or alt text, add the filename as the alt text so
5514  # that screen readers at least get some description of the image
5515  $params['frame']['alt'] = $link->getText();
5516  }
5517  # Do not set $params['frame']['title'] because tooltips are unnecessary
5518  # for framed images, the caption is visible
5519  } else {
5520  // @phan-suppress-next-line PhanImpossibleCondition
5521  if ( !isset( $params['frame']['alt'] ) ) {
5522  # No alt text, use the "caption" for the alt text
5523  if ( $caption !== '' ) {
5524  $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5525  } else {
5526  # No caption, fall back to using the filename for the
5527  # alt text
5528  $params['frame']['alt'] = $link->getText();
5529  }
5530  }
5531  # Use the "caption" for the tooltip text
5532  $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5533  }
5534  $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
5535 
5536  // hook signature compat again, $link may have changed
5537  $title = Title::castFromLinkTarget( $link );
5538  $this->hookRunner->onParserMakeImageParams( $title, $file, $params, $this );
5539 
5540  # Linker does the rest
5541  $time = $options['time'] ?? false;
5542  $ret = Linker::makeImageLink( $this, $link, $file, $params['frame'], $params['handler'],
5543  $time, $descQuery, $this->mOptions->getThumbSize() );
5544 
5545  # Give the handler a chance to modify the parser object
5546  if ( $handler ) {
5547  $handler->parserTransformHook( $this, $file );
5548  }
5549  if ( $file ) {
5550  $this->modifyImageHtml( $file, $params, $ret );
5551  }
5552 
5553  return $ret;
5554  }
5555 
5574  private function parseLinkParameter( $value ) {
5575  $chars = self::EXT_LINK_URL_CLASS;
5576  $addr = self::EXT_LINK_ADDR;
5577  $prots = $this->urlUtils->validProtocols();
5578  $type = null;
5579  $target = false;
5580  if ( $value === '' ) {
5581  $type = 'no-link';
5582  } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5583  if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5584  $this->mOutput->addExternalLink( $value );
5585  $type = 'link-url';
5586  $target = $value;
5587  }
5588  } else {
5589  // Percent-decode link arguments for consistency with wikilink
5590  // handling (T216003#7836261).
5591  //
5592  // There's slight concern here though. The |link= option supports
5593  // two formats, link=Test%22test vs link=[[Test%22test]], both of
5594  // which are about to be decoded.
5595  //
5596  // In the former case, the decoding here is straightforward and
5597  // desirable.
5598  //
5599  // In the latter case, there's a potential for double decoding,
5600  // because the wikilink syntax has a higher precedence and has
5601  // already been parsed as a link before we get here. $value
5602  // has had stripAltText() called on it, which in turn calls
5603  // replaceLinkHoldersText() on the link. So, the text we're
5604  // getting at this point has already been percent decoded.
5605  //
5606  // The problematic case is if %25 is in the title, since that
5607  // decodes to %, which could combine with trailing characters.
5608  // However, % is not a valid link title character, so it would
5609  // not parse as a link and the string we received here would
5610  // still contain the encoded %25.
5611  //
5612  // Hence, double decoded is not an issue. See the test,
5613  // "Should not double decode the link option"
5614  if ( strpos( $value, '%' ) !== false ) {
5615  $value = rawurldecode( $value );
5616  }
5617  $linkTitle = Title::newFromText( $value );
5618  if ( $linkTitle ) {
5619  $this->mOutput->addLink( $linkTitle );
5620  $type = 'link-title';
5621  $target = $linkTitle;
5622  }
5623  }
5624  return [ $type, $target ];
5625  }
5626 
5634  public function modifyImageHtml( File $file, array $params, string &$html ) {
5635  $this->hookRunner->onParserModifyImageHTML( $this, $file, $params, $html );
5636  }
5637 
5643  private function stripAltText( $caption, $holders ) {
5644  # Strip bad stuff out of the title (tooltip). We can't just use
5645  # replaceLinkHoldersText() here, because if this function is called
5646  # from handleInternalLinks2(), mLinkHolders won't be up-to-date.
5647  if ( $holders ) {
5648  $tooltip = $holders->replaceText( $caption );
5649  } else {
5650  $tooltip = $this->replaceLinkHoldersText( $caption );
5651  }
5652 
5653  # make sure there are no placeholders in thumbnail attributes
5654  # that are later expanded to html- so expand them now and
5655  # remove the tags
5656  $tooltip = $this->mStripState->unstripBoth( $tooltip );
5657  # Compatibility hack! In HTML certain entity references not terminated
5658  # by a semicolon are decoded (but not if we're in an attribute; that's
5659  # how link URLs get away without properly escaping & in queries).
5660  # But wikitext has always required semicolon-termination of entities,
5661  # so encode & where needed to avoid decode of semicolon-less entities.
5662  # See T209236 and
5663  # https://www.w3.org/TR/html5/syntax.html#named-character-references
5664  # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
5665  $tooltip = preg_replace( "/
5666  & # 1. entity prefix
5667  (?= # 2. followed by:
5668  (?: # a. one of the legacy semicolon-less named entities
5669  A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
5670  C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
5671  GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
5672  O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
5673  U(?:acute|circ|grave|uml)|Yacute|
5674  a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
5675  c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
5676  divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
5677  frac(?:1(?:2|4)|34)|
5678  gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
5679  i(?:acute|circ|excl|grave|quest|uml)|laquo|
5680  lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
5681  m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
5682  not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
5683  o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
5684  p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
5685  s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
5686  u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
5687  )
5688  (?:[^;]|$)) # b. and not followed by a semicolon
5689  # S = study, for efficiency
5690  /Sx", '&amp;', $tooltip );
5691  $tooltip = Sanitizer::stripAllTags( $tooltip );
5692 
5693  return $tooltip;
5694  }
5695 
5705  public function attributeStripCallback( &$text, $frame = false ) {
5706  wfDeprecated( __METHOD__, '1.35' );
5707  $text = $this->replaceVariables( $text, $frame );
5708  $text = $this->mStripState->unstripBoth( $text );
5709  return $text;
5710  }
5711 
5718  public function getTags() {
5719  return array_keys( $this->mTagHooks );
5720  }
5721 
5726  public function getFunctionSynonyms() {
5727  return $this->mFunctionSynonyms;
5728  }
5729 
5734  public function getUrlProtocols() {
5735  return $this->urlUtils->validProtocols();
5736  }
5737 
5767  private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5768  global $wgTitle; # not generally used but removes an ugly failure mode
5769 
5770  $magicScopeVariable = $this->lock();
5771  $this->startParse(
5772  $wgTitle,
5775  true
5776  );
5777  $outText = '';
5778  $frame = $this->getPreprocessor()->newFrame();
5779 
5780  # Process section extraction flags
5781  $flags = 0;
5782  $sectionParts = explode( '-', $sectionId );
5783  $sectionIndex = array_pop( $sectionParts );
5784  foreach ( $sectionParts as $part ) {
5785  if ( $part === 'T' ) {
5787  }
5788  }
5789 
5790  # Check for empty input
5791  if ( strval( $text ) === '' ) {
5792  # Only sections 0 and T-0 exist in an empty document
5793  if ( $sectionIndex == 0 ) {
5794  if ( $mode === 'get' ) {
5795  return '';
5796  }
5797 
5798  return $newText;
5799  } else {
5800  if ( $mode === 'get' ) {
5801  return $newText;
5802  }
5803 
5804  return $text;
5805  }
5806  }
5807 
5808  # Preprocess the text
5809  $root = $this->preprocessToDom( $text, $flags );
5810 
5811  # <h> nodes indicate section breaks
5812  # They can only occur at the top level, so we can find them by iterating the root's children
5813  $node = $root->getFirstChild();
5814 
5815  # Find the target section
5816  if ( $sectionIndex == 0 ) {
5817  # Section zero doesn't nest, level=big
5818  $targetLevel = 1000;
5819  } else {
5820  while ( $node ) {
5821  if ( $node->getName() === 'h' ) {
5822  $bits = $node->splitHeading();
5823  if ( $bits['i'] == $sectionIndex ) {
5824  $targetLevel = $bits['level'];
5825  break;
5826  }
5827  }
5828  if ( $mode === 'replace' ) {
5829  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5830  }
5831  $node = $node->getNextSibling();
5832  }
5833  }
5834 
5835  if ( !$node ) {
5836  # Not found
5837  if ( $mode === 'get' ) {
5838  return $newText;
5839  } else {
5840  return $text;
5841  }
5842  }
5843 
5844  # Find the end of the section, including nested sections
5845  do {
5846  if ( $node->getName() === 'h' ) {
5847  $bits = $node->splitHeading();
5848  $curLevel = $bits['level'];
5849  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
5850  if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5851  break;
5852  }
5853  }
5854  if ( $mode === 'get' ) {
5855  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5856  }
5857  $node = $node->getNextSibling();
5858  } while ( $node );
5859 
5860  # Write out the remainder (in replace mode only)
5861  if ( $mode === 'replace' ) {
5862  # Output the replacement text
5863  # Add two newlines on -- trailing whitespace in $newText is conventionally
5864  # stripped by the editor, so we need both newlines to restore the paragraph gap
5865  # Only add trailing whitespace if there is newText
5866  if ( $newText != "" ) {
5867  $outText .= $newText . "\n\n";
5868  }
5869 
5870  while ( $node ) {
5871  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5872  $node = $node->getNextSibling();
5873  }
5874  }
5875 
5876  # Re-insert stripped tags
5877  $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5878 
5879  return $outText;
5880  }
5881 
5897  public function getSection( $text, $sectionId, $defaultText = '' ) {
5898  return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5899  }
5900 
5914  public function replaceSection( $oldText, $sectionId, $newText ) {
5915  return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5916  }
5917 
5947  public function getFlatSectionInfo( $text ) {
5948  $magicScopeVariable = $this->lock();
5949  $this->startParse(
5950  null,
5953  true
5954  );
5955  $frame = $this->getPreprocessor()->newFrame();
5956  $root = $this->preprocessToDom( $text, 0 );
5957  $node = $root->getFirstChild();
5958  $offset = 0;
5959  $currentSection = [
5960  'index' => 0,
5961  'level' => 0,
5962  'offset' => 0,
5963  'heading' => '',
5964  'text' => ''
5965  ];
5966  $sections = [];
5967 
5968  while ( $node ) {
5969  $nodeText = $frame->expand( $node, PPFrame::RECOVER_ORIG );
5970  if ( $node->getName() === 'h' ) {
5971  $bits = $node->splitHeading();
5972  $sections[] = $currentSection;
5973  $currentSection = [
5974  'index' => $bits['i'],
5975  'level' => $bits['level'],
5976  'offset' => $offset,
5977  'heading' => $nodeText,
5978  'text' => $nodeText
5979  ];
5980  } else {
5981  $currentSection['text'] .= $nodeText;
5982  }
5983  $offset += strlen( $nodeText );
5984  $node = $node->getNextSibling();
5985  }
5986  $sections[] = $currentSection;
5987  return $sections;
5988  }
5989 
6001  public function getRevisionId() {
6002  return $this->mRevisionId;
6003  }
6004 
6011  public function getRevisionRecordObject() {
6012  if ( $this->mRevisionRecordObject ) {
6013  return $this->mRevisionRecordObject;
6014  }
6015 
6016  // NOTE: try to get the RevisionRecord object even if mRevisionId is null.
6017  // This is useful when parsing a revision that has not yet been saved.
6018  // However, if we get back a saved revision even though we are in
6019  // preview mode, we'll have to ignore it, see below.
6020  // NOTE: This callback may be used to inject an OLD revision that was
6021  // already loaded, so "current" is a bit of a misnomer. We can't just
6022  // skip it if mRevisionId is set.
6023  $rev = call_user_func(
6024  $this->mOptions->getCurrentRevisionRecordCallback(),
6025  $this->getTitle(),
6026  $this
6027  );
6028 
6029  if ( $rev === false ) {
6030  // The revision record callback returns `false` (not null) to
6031  // indicate that the revision is missing. (See for example
6032  // Parser::statelessFetchRevisionRecord(), the default callback.)
6033  // This API expects `null` instead. (T251952)
6034  $rev = null;
6035  }
6036 
6037  if ( $this->mRevisionId === null && $rev && $rev->getId() ) {
6038  // We are in preview mode (mRevisionId is null), and the current revision callback
6039  // returned an existing revision. Ignore it and return null, it's probably the page's
6040  // current revision, which is not what we want here. Note that we do want to call the
6041  // callback to allow the unsaved revision to be injected here, e.g. for
6042  // self-transclusion previews.
6043  return null;
6044  }
6045 
6046  // If the parse is for a new revision, then the callback should have
6047  // already been set to force the object and should match mRevisionId.
6048  // If not, try to fetch by mRevisionId instead.
6049  if ( $this->mRevisionId && $rev && $rev->getId() != $this->mRevisionId ) {
6050  $rev = MediaWikiServices::getInstance()
6051  ->getRevisionLookup()
6052  ->getRevisionById( $this->mRevisionId );
6053  }
6054 
6055  $this->mRevisionRecordObject = $rev;
6056 
6057  return $this->mRevisionRecordObject;
6058  }
6059 
6066  public function getRevisionTimestamp() {
6067  if ( $this->mRevisionTimestamp !== null ) {
6068  return $this->mRevisionTimestamp;
6069  }
6070 
6071  # Use specified revision timestamp, falling back to the current timestamp
6072  $revObject = $this->getRevisionRecordObject();
6073  $timestamp = $revObject ? $revObject->getTimestamp() : $this->mOptions->getTimestamp();
6074  $this->mOutput->setRevisionTimestampUsed( $timestamp ); // unadjusted time zone
6075 
6076  # The cryptic '' timezone parameter tells to use the site-default
6077  # timezone offset instead of the user settings.
6078  # Since this value will be saved into the parser cache, served
6079  # to other users, and potentially even used inside links and such,
6080  # it needs to be consistent for all visitors.
6081  $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
6082 
6083  return $this->mRevisionTimestamp;
6084  }
6085 
6092  public function getRevisionUser(): ?string {
6093  if ( $this->mRevisionUser === null ) {
6094  $revObject = $this->getRevisionRecordObject();
6095 
6096  # if this template is subst: the revision id will be blank,
6097  # so just use the current user's name
6098  if ( $revObject && $revObject->getUser() ) {
6099  $this->mRevisionUser = $revObject->getUser()->getName();
6100  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6101  $this->mRevisionUser = $this->getUserIdentity()->getName();
6102  } else {
6103  # Note that we fall through here with
6104  # $this->mRevisionUser still null
6105  }
6106  }
6107  return $this->mRevisionUser;
6108  }
6109 
6116  public function getRevisionSize() {
6117  if ( $this->mRevisionSize === null ) {
6118  $revObject = $this->getRevisionRecordObject();
6119 
6120  # if this variable is subst: the revision id will be blank,
6121  # so just use the parser input size, because the own substitution
6122  # will change the size.
6123  if ( $revObject ) {
6124  $this->mRevisionSize = $revObject->getSize();
6125  } else {
6126  $this->mRevisionSize = $this->mInputSize;
6127  }
6128  }
6129  return $this->mRevisionSize;
6130  }
6131 
6140  public function setDefaultSort( $sort ) {
6141  wfDeprecated( __METHOD__, '1.38' );
6142  $this->mOutput->setPageProperty( 'defaultsort', $sort );
6143  }
6144 
6158  public function getDefaultSort() {
6159  wfDeprecated( __METHOD__, '1.38' );
6160  return $this->mOutput->getPageProperty( 'defaultsort' ) ?? '';
6161  }
6162 
6172  public function getCustomDefaultSort() {
6173  wfDeprecated( __METHOD__, '1.38' );
6174  return $this->mOutput->getPageProperty( 'defaultsort' ) ?? false;
6175  }
6176 
6177  private static function getSectionNameFromStrippedText( $text ) {
6179  $text = Sanitizer::decodeCharReferences( $text );
6180  $text = self::normalizeSectionName( $text );
6181  return $text;
6182  }
6183 
6184  private static function makeAnchor( $sectionName ) {
6185  return '#' . Sanitizer::escapeIdForLink( $sectionName );
6186  }
6187 
6188  private function makeLegacyAnchor( $sectionName ) {
6189  $fragmentMode = $this->svcOptions->get( MainConfigNames::FragmentMode );
6190  if ( isset( $fragmentMode[1] ) && $fragmentMode[1] === 'legacy' ) {
6191  // ForAttribute() and ForLink() are the same for legacy encoding
6193  } else {
6194  $id = Sanitizer::escapeIdForLink( $sectionName );
6195  }
6196 
6197  return "#$id";
6198  }
6199 
6209  public function guessSectionNameFromWikiText( $text ) {
6210  # Strip out wikitext links(they break the anchor)
6211  $text = $this->stripSectionName( $text );
6212  $sectionName = self::getSectionNameFromStrippedText( $text );
6213  return self::makeAnchor( $sectionName );
6214  }
6215 
6226  public function guessLegacySectionNameFromWikiText( $text ) {
6227  # Strip out wikitext links(they break the anchor)
6228  $text = $this->stripSectionName( $text );
6229  $sectionName = self::getSectionNameFromStrippedText( $text );
6230  return $this->makeLegacyAnchor( $sectionName );
6231  }
6232 
6239  public static function guessSectionNameFromStrippedText( $text ) {
6240  $sectionName = self::getSectionNameFromStrippedText( $text );
6241  return self::makeAnchor( $sectionName );
6242  }
6243 
6250  private static function normalizeSectionName( $text ) {
6251  # T90902: ensure the same normalization is applied for IDs as to links
6253  $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6254  '@phan-var MediaWikiTitleCodec $titleParser';
6255  try {
6256 
6257  $parts = $titleParser->splitTitleString( "#$text" );
6258  } catch ( MalformedTitleException $ex ) {
6259  return $text;
6260  }
6261  return $parts['fragment'];
6262  }
6263 
6279  public function stripSectionName( $text ) {
6280  # Strip internal link markup
6281  $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6282  $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6283 
6284  # Strip external link markup
6285  # @todo FIXME: Not tolerant to blank link text
6286  # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6287  # on how many empty links there are on the page - need to figure that out.
6288  $text = preg_replace(
6289  '/\[(?i:' . $this->urlUtils->validProtocols() . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6290 
6291  # Parse wikitext quotes (italics & bold)
6292  $text = $this->doQuotes( $text );
6293 
6294  # Strip HTML tags
6295  $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6296  return $text;
6297  }
6298 
6312  private function fuzzTestSrvus( $text, PageReference $page, ParserOptions $options,
6313  $outputType = self::OT_HTML
6314  ) {
6315  $magicScopeVariable = $this->lock();
6316  $this->startParse( $page, $options, $outputType, true );
6317 
6318  $text = $this->replaceVariables( $text );
6319  $text = $this->mStripState->unstripBoth( $text );
6320  $text = Sanitizer::internalRemoveHtmlTags( $text );
6321  return $text;
6322  }
6323 
6335  private function fuzzTestPst( $text, PageReference $page, ParserOptions $options ) {
6336  return $this->preSaveTransform( $text, $page, $options->getUserIdentity(), $options );
6337  }
6338 
6350  private function fuzzTestPreprocess( $text, PageReference $page, ParserOptions $options ) {
6351  return $this->fuzzTestSrvus( $text, $page, $options, self::OT_PREPROCESS );
6352  }
6353 
6372  public function markerSkipCallback( $s, callable $callback ) {
6373  $i = 0;
6374  $out = '';
6375  while ( $i < strlen( $s ) ) {
6376  $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6377  if ( $markerStart === false ) {
6378  $out .= call_user_func( $callback, substr( $s, $i ) );
6379  break;
6380  } else {
6381  $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6382  $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6383  if ( $markerEnd === false ) {
6384  $out .= substr( $s, $markerStart );
6385  break;
6386  } else {
6387  $markerEnd += strlen( self::MARKER_SUFFIX );
6388  $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6389  $i = $markerEnd;
6390  }
6391  }
6392  }
6393  return $out;
6394  }
6395 
6403  public function killMarkers( $text ) {
6404  return $this->mStripState->killMarkers( $text );
6405  }
6406 
6417  public static function parseWidthParam( $value, $parseHeight = true ) {
6418  $parsedWidthParam = [];
6419  if ( $value === '' ) {
6420  return $parsedWidthParam;
6421  }
6422  $m = [];
6423  # (T15500) In both cases (width/height and width only),
6424  # permit trailing "px" for backward compatibility.
6425  if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6426  $width = intval( $m[1] );
6427  $height = intval( $m[2] );
6428  $parsedWidthParam['width'] = $width;
6429  $parsedWidthParam['height'] = $height;
6430  } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6431  $width = intval( $value );
6432  $parsedWidthParam['width'] = $width;
6433  }
6434  return $parsedWidthParam;
6435  }
6436 
6446  protected function lock() {
6447  if ( $this->mInParse ) {
6448  throw new MWException( "Parser state cleared while parsing. "
6449  . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6450  }
6451 
6452  // Save the backtrace when locking, so that if some code tries locking again,
6453  // we can print the lock owner's backtrace for easier debugging
6454  $e = new Exception;
6455  $this->mInParse = $e->getTraceAsString();
6456 
6457  $recursiveCheck = new ScopedCallback( function () {
6458  $this->mInParse = false;
6459  } );
6460 
6461  return $recursiveCheck;
6462  }
6463 
6474  public static function stripOuterParagraph( $html ) {
6475  $m = [];
6476  if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && strpos( $m[1], '</p>' ) === false ) {
6477  $html = $m[1];
6478  }
6479 
6480  return $html;
6481  }
6482 
6493  public function getFreshParser() {
6494  if ( $this->mInParse ) {
6495  return $this->factory->create();
6496  } else {
6497  return $this;
6498  }
6499  }
6500 
6508  public function enableOOUI() {
6509  wfDeprecated( __METHOD__, '1.35' );
6511  $this->mOutput->setEnableOOUI( true );
6512  }
6513 
6520  private function setOutputFlag( string $flag, string $reason ): void {
6521  $this->mOutput->setOutputFlag( $flag );
6522  $name = $this->getTitle()->getPrefixedText();
6523  $this->logger->debug( __METHOD__ . ": set $flag flag on '$name'; $reason" );
6524  }
6525 }
getUser()
const OT_WIKI
Definition: Defines.php:158
const SFH_NO_HASH
Definition: Defines.php:170
const SFH_OBJECT_ARGS
Definition: Defines.php:171
const NS_FILE
Definition: Defines.php:70
const NS_MEDIAWIKI
Definition: Defines.php:72
const NS_TEMPLATE
Definition: Defines.php:74
const NS_SPECIAL
Definition: Defines.php:53
const OT_PLAIN
Definition: Defines.php:161
const OT_PREPROCESS
Definition: Defines.php:159
const OT_HTML
Definition: Defines.php:157
const NS_MEDIA
Definition: Defines.php:52
const NS_CATEGORY
Definition: Defines.php:78
deprecatePublicPropertyFallback(string $property, string $version, $getter, $setter=null, $class=null, $component=null)
Mark a removed public property as deprecated and provide fallback getter and setter callables.
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated or implementati...
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfHostname()
Get host name of the current machine, for use in error reporting.
wfSetVar(&$dest, $source, $force=false)
Sets dest to source and returns the original value of dest If source is NULL, it just returns the val...
wfMatchesDomainList( $url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
$matches
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgTitle
Definition: Setup.php:486
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition: WebStart.php:82
static doBlockLevels( $text, $lineStart)
Make lists from lines starting with ':', '*', '#', etc.
static expand(Parser $parser, string $id, ConvertibleTimestamp $ts, NamespaceInfo $nsInfo, ServiceOptions $svcOptions, LoggerInterface $logger)
Expand the magic variable given by $index.
static register(Parser $parser, ServiceOptions $options)
static register(Parser $parser, ServiceOptions $options)
const REGISTER_OPTIONS
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:37
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:68
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
static factory( $mode=false, IContextSource $context=null)
Get a new image gallery.
Class for exceptions thrown by ImageGalleryBase::factory().
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:45
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition: Linker.php:948
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition: Linker.php:1617
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition: Linker.php:242
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition: Linker.php:1441
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition: Linker.php:163
static tocIndent()
Add another level to the Table of Contents.
Definition: Linker.php:1591
static makeImageLink(Parser $parser, LinkTarget $title, $file, $frameParams=[], $handlerParams=[], $time=false, $query="", $widthOption=null)
Given parameters derived from [[Image:Foo|options...]], generate the HTML that that syntax inserts in...
Definition: Linker.php:299
static splitTrail( $trail)
Split a link trail, return the "inside" portion and the remainder of the trail as a two-element array...
Definition: Linker.php:1749
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:1016
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition: Linker.php:1728
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
Definition: Linker.php:1602
static tocList( $toc, Language $lang=null)
Wraps the TOC in a div with ARIA navigation role and provides the hide/collapse JavaScript.
Definition: Linker.php:1653
static tocLineEnd()
End a Table Of Contents line.
Definition: Linker.php:1641
MediaWiki exception.
Definition: MWException.php:29
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:39
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
A factory that stores information about MagicWords, and creates them on demand with caching.
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:36
Helper class for mapping value objects representing basic entities to cache keys.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:562
Factory creating MWHttpRequest objects.
An interface for creating language converters.
isConversionDisabled()
Whether to disable language variant conversion.
A service that provides utilities to do with language names and codes.
Factory to create LinkRender objects.
Class that generates HTML anchor link elements for pages.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Exception representing a failure to look up a revision.
Page revision base class.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
Factory for handling the special page list and generating SpecialPage objects.
Base class for HTML cleanup utilities.
Creates User objects.
Definition: UserFactory.php:38
UserNameUtils service.
Provides access to user options.
A service to expand, parse, and otherwise manipulate URLs.
Definition: UrlUtils.php:17
validProtocols()
Returns a regular expression of recognized URL protocols.
Definition: UrlUtils.php:345
static plaintextParam( $plaintext)
Definition: Message.php:1287
static numParam( $num)
Definition: Message.php:1166
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
static setupOOUI( $skinName='default', $dir='ltr')
Helper function to setup the PHP implementation of OOUI to use in this request.
static int $inParserFactory
Track calls to Parser constructor to aid in deprecation of direct Parser invocation.
Set options of the Parser.
getUserIdentity()
Get the identity of the user for whom the parse is made.
getPreSaveTransform()
Transform wiki markup when saving the page?
static newFromUser( $user)
Get a ParserOptions object from a given user.
getDisableTitleConversion()
Whether title conversion should be disabled.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition: Parser.php:95
addTrackingCategory( $msg)
Definition: Parser.php:4157
static extractTagsAndParams(array $elements, $text, &$matches)
Replaces all occurrences of HTML-style comments and the given tags in the text with a random marker a...
Definition: Parser.php:1279
getTargetLanguage()
Get the target language for the content being parsed.
Definition: Parser.php:1182
handleDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores,...
Definition: Parser.php:4102
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition: Parser.php:6066
pstPass2( $text, UserIdentity $user)
Pre-save transform helper function.
Definition: Parser.php:4620
static normalizeUrlComponent( $component, $unsafe)
Definition: Parser.php:2373
extensionSubstitution(array $params, PPFrame $frame)
Return the text to be used for a given extension tag.
Definition: Parser.php:3985
array $mTplDomCache
Definition: Parser.php:273
bool string $mInParse
Recursive call protection.
Definition: Parser.php:328
array< string, string > $mDoubleUnderscores
Definition: Parser.php:267
handleInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
Definition: Parser.php:2464
setDefaultSort( $sort)
Mutator for the 'defaultsort' page property.
Definition: Parser.php:6140
static stripOuterParagraph( $html)
Strip outer.
Definition: Parser.php:6474
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition: Parser.php:2314
getContentLanguageConverter()
Shorthand for getting a Language Converter for Content language.
Definition: Parser.php:1667
MagicWordFactory $magicWordFactory
Definition: Parser.php:339
__clone()
Allow extensions to clean up when the parser is cloned.
Definition: Parser.php:579
ParserOutput $mOutput
Definition: Parser.php:239
maybeMakeExternalImage( $url)
make an image if it's allowed, either through the global option, through the exception,...
Definition: Parser.php:2396
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition: Parser.php:4824
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition: Parser.php:1236
LinkRenderer $mLinkRenderer
Definition: Parser.php:336
$mHighestExpansionDepth
Definition: Parser.php:260
magicLinkCallback(array $m)
Definition: Parser.php:1799
parse( $text, PageReference $page, ParserOptions $options, $linestart=true, $clearState=true, $revid=null)
Convert wikitext to HTML Do not call this function recursively.
Definition: Parser.php:690
HookRunner $hookRunner
Definition: Parser.php:381
getCustomDefaultSort()
Accessor for the 'defaultsort' page property.
Definition: Parser.php:6172
static getSectionNameFromStrippedText( $text)
Definition: Parser.php:6177
ParserOptions null $mOptions
Definition: Parser.php:287
stripAltText( $caption, $holders)
Definition: Parser.php:5643
getOptions()
Definition: Parser.php:1124
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition: Parser.php:4781
getStripState()
Definition: Parser.php:1350
getRevisionUser()
Get the name of the user that edited the last revision.
Definition: Parser.php:6092
fuzzTestPreprocess( $text, PageReference $page, ParserOptions $options)
Strip/replaceVariables/unstrip for preprocessor regression testing.
Definition: Parser.php:6350
isCurrentRevisionOfTitleCached(LinkTarget $link)
Definition: Parser.php:3574
replaceVariables( $text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition: Parser.php:2952
getFunctionSynonyms()
Definition: Parser.php:5726
interwikiTransclude(LinkTarget $link, $action)
Transclude an interwiki link.
Definition: Parser.php:3859
const OT_HTML
Definition: Parser.php:129
armorLinks( $text)
Insert a NOPARSE hacky thing into any inline links in a chunk that's going to go through further pars...
Definition: Parser.php:2813
Language $contLang
Definition: Parser.php:342
$mHeadings
Definition: Parser.php:263
UserFactory $userFactory
Definition: Parser.php:390
getPreloadText( $text, PageReference $page, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition: Parser.php:993
setTitle(Title $t=null)
Set the context title.
Definition: Parser.php:1026
static guessSectionNameFromStrippedText( $text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
Definition: Parser.php:6239
UserOptionsLookup $userOptionsLookup
Definition: Parser.php:387
const OT_WIKI
Definition: Parser.php:130
getFlatSectionInfo( $text)
Get an array of preprocessor section information.
Definition: Parser.php:5947
limitationWarn( $limitationType, $current='', $max='')
Warn the user when a parser limitation is reached Will warn at most once the user per limitation type...
Definition: Parser.php:3003
LinkHolderArray $mLinkHolders
Definition: Parser.php:250
$mRevisionId
Definition: Parser.php:300
modifyImageHtml(File $file, array $params, string &$html)
Give hooks a chance to modify image thumbnail HTML.
Definition: Parser.php:5634
fetchTemplateAndTitle(LinkTarget $link)
Fetch the unparsed text of a template and register a reference to it.
Definition: Parser.php:3614
Title null $mTitle
Since 1.34, leaving mTitle uninitialized or setting mTitle to null is deprecated.
Definition: Parser.php:296
setUser(?UserIdentity $user)
Set the current user.
Definition: Parser.php:1015
getRevisionSize()
Get the size of the revision.
Definition: Parser.php:6116
extractSections( $text, $sectionId, $mode, $newText='')
Break wikitext input into sections, and either pull or replace some particular section's text.
Definition: Parser.php:5767
BadFileLookup $badFileLookup
Definition: Parser.php:375
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition: Parser.php:6279
$mTagHooks
Definition: Parser.php:197
getOutputType()
Accessor for the output type.
Definition: Parser.php:1080
UrlUtils $urlUtils
Initialized in constructor.
Definition: Parser.php:227
static normalizeSectionName( $text)
Apply the same normalization as code making links to this section would.
Definition: Parser.php:6250
makeFreeExternalLink( $url, $numPostProto)
Make a free external link, given a user-supplied URL.
Definition: Parser.php:1879
fetchCurrentRevisionRecordOfTitle(LinkTarget $link)
Fetch the current revision of a given title as a RevisionRecord.
Definition: Parser.php:3544
fetchFileAndTitle(LinkTarget $link, array $options=[])
Fetch a file and its title and register a reference to it.
Definition: Parser.php:3809
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition: Parser.php:973
$mFunctionHooks
Definition: Parser.php:198
$mShowToc
Definition: Parser.php:270
getImageParams( $handler)
Definition: Parser.php:5279
fetchFileNoRegister(LinkTarget $link, array $options=[])
Helper function for fetchFileAndTitle.
Definition: Parser.php:3836
internalParseHalfParsed( $text, $isMain=true, $linestart=true)
Helper function for parse() that transforms half-parsed HTML into fully parsed HTML.
Definition: Parser.php:1705
getUrlProtocols()
Definition: Parser.php:5734
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition: Parser.php:5009
$mRevisionSize
Definition: Parser.php:303
getHookRunner()
Get a HookRunner for calling core hooks.
Definition: Parser.php:1692
UserNameUtils $userNameUtils
Definition: Parser.php:402
getTitle()
Definition: Parser.php:1035
handleHeadings( $text)
Parse headers and return html.
Definition: Parser.php:1955
braceSubstitution(array $piece, PPFrame $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
Definition: Parser.php:3028
MagicWordArray $mSubstWords
Definition: Parser.php:217
MapCacheLRU null $currentRevisionCache
Definition: Parser.php:322
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition: Parser.php:1221
preprocess( $text, ?PageReference $page, ParserOptions $options, $revid=null, $frame=false)
Expand templates and variables in the text, producing valid, static wikitext.
Definition: Parser.php:946
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
Definition: Parser.php:2283
setOutputType( $ot)
Mutator for the output type.
Definition: Parser.php:1089
makeLegacyAnchor( $sectionName)
Definition: Parser.php:6188
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
Definition: Parser.php:1170
StripState $mStripState
Definition: Parser.php:245
callParserFunction(PPFrame $frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
Definition: Parser.php:3409
makeLimitReport()
Set the limit report data in the current ParserOutput.
Definition: Parser.php:790
ServiceOptions $svcOptions
This is called $svcOptions instead of $options like elsewhere to avoid confusion with $mOptions,...
Definition: Parser.php:363
parseExtensionTagAsTopLevelDoc( $text)
Needed by Parsoid/PHP to ensure all the hooks for extensions are run in the right order.
Definition: Parser.php:927
transformMsg( $text, ParserOptions $options, ?PageReference $page=null)
Wrapper for preprocess()
Definition: Parser.php:4899
$mInputSize
Definition: Parser.php:304
startExternalParse(?PageReference $page, ParserOptions $options, $outputType, $clearState=true, $revId=null)
Set up some variables which are usually set up in parse() so that an external function can call some ...
Definition: Parser.php:4864
MagicWordArray $mVariables
Definition: Parser.php:212
getStripList()
Get a list of strippable XML-like elements.
Definition: Parser.php:1342
$mOutputType
Definition: Parser.php:297
SignatureValidatorFactory $signatureValidatorFactory
Definition: Parser.php:399
startParse(?PageReference $page, ParserOptions $options, $outputType, $clearState=true)
Definition: Parser.php:4879
$mImageParamsMagicArray
Definition: Parser.php:203
$mAutonumber
Definition: Parser.php:240
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition: Parser.php:4942
expandMagicVariable( $index, $frame=false)
Return value of a magic variable (like PAGENAME)
Definition: Parser.php:2840
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
Definition: Parser.php:1256
getTags()
Accessor.
Definition: Parser.php:5718
HookContainer $hookContainer
Definition: Parser.php:378
array $mLangLinkLanguages
Array with the language name of each language link (i.e.
Definition: Parser.php:314
static statelessFetchRevisionRecord(LinkTarget $link, $parser=null)
Wrapper around RevisionLookup::getKnownCurrentRevision.
Definition: Parser.php:3590
LinkRendererFactory $linkRendererFactory
Definition: Parser.php:366
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition: Parser.php:878
$mRevisionUser
Definition: Parser.php:302
handleInternalLinks( $text)
Process [[ ]] wikilinks.
Definition: Parser.php:2454
getTargetLanguageConverter()
Shorthand for getting a Language Converter for Target language.
Definition: Parser.php:1656
TrackingCategories $trackingCategories
Definition: Parser.php:396
static replaceTableOfContentsMarker( $text, $toc)
Replace table of contents marker in parsed HTML.
Definition: Parser.php:4845
$mTplRedirCache
Definition: Parser.php:261
fuzzTestSrvus( $text, PageReference $page, ParserOptions $options, $outputType=self::OT_HTML)
Strip/replaceVariables/unstrip for preprocessor regression testing.
Definition: Parser.php:6312
$mFunctionSynonyms
Definition: Parser.php:199
const OT_PLAIN
Definition: Parser.php:134
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
Definition: Parser.php:5897
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition: Parser.php:1586
$mRevisionTimestamp
Definition: Parser.php:301
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition: Parser.php:5914
setPage(?PageReference $t=null)
Set the page used as context for parsing, e.g.
Definition: Parser.php:1048
replaceLinkHoldersPrivate(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition: Parser.php:5069
getUserIdentity()
Get a user either from the user set on Parser if it's set, or from the ParserOptions object otherwise...
Definition: Parser.php:1201
LanguageConverterFactory $languageConverterFactory
Definition: Parser.php:345
incrementIncludeSize( $type, $size)
Increment an include size counter.
Definition: Parser.php:4077
handleExternalLinks( $text)
Replace external links (REL)
Definition: Parser.php:2176
RevisionRecord null $mRevisionRecordObject
Definition: Parser.php:307
fuzzTestPst( $text, PageReference $page, ParserOptions $options)
Strip/replaceVariables/unstrip for preprocessor regression testing.
Definition: Parser.php:6335
doBlockLevels( $text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
Definition: Parser.php:2827
$mPPNodeCount
Definition: Parser.php:258
getDefaultSort()
Accessor for the 'defaultsort' page property.
Definition: Parser.php:6158
const MARKER_PREFIX
Definition: Parser.php:154
replaceLinkHoldersText( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
Definition: Parser.php:5080
setOutputFlag(string $flag, string $reason)
Sets the flag on the parser output but also does some debug logging.
Definition: Parser.php:6520
getHookContainer()
Get a HookContainer capable of returning metadata about hooks or running extension hooks.
Definition: Parser.php:1680
getFunctionHooks()
Get all registered function hook identifiers.
Definition: Parser.php:5046
UserIdentity null $mUser
Definition: Parser.php:278
TidyDriverBase $tidy
Definition: Parser.php:384
$mMarkerIndex
Definition: Parser.php:205
const EXT_LINK_ADDR
Definition: Parser.php:112
getPage()
Returns the page used as context for parsing, e.g.
Definition: Parser.php:1071
handleTables( $text)
Parse the wiki syntax used to render tables.
Definition: Parser.php:1376
static getExternalLinkRel( $url=false, LinkTarget $title=null)
Get the rel attribute for a particular external link.
Definition: Parser.php:2258
$mExpensiveFunctionCount
Definition: Parser.php:269
getContentLanguage()
Get the content language that this Parser is using.
Definition: Parser.php:1246
Preprocessor $mPreprocessor
Definition: Parser.php:233
getOutput()
Definition: Parser.php:1116
$mVarCache
Definition: Parser.php:201
Options( $x=null)
Accessor/mutator for the ParserOptions object.
Definition: Parser.php:1144
$mImageParams
Definition: Parser.php:202
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition: Parser.php:902
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition: Parser.php:6209
clearTagHooks()
Remove all tag hooks.
Definition: Parser.php:4960
getRevisionId()
Get the ID of the revision we are parsing.
Definition: Parser.php:6001
setOptions(ParserOptions $options)
Mutator for the ParserOptions object.
Definition: Parser.php:1133
static parseWidthParam( $value, $parseHeight=true)
Parsed a width param of imagelink like 300px or 200x300px.
Definition: Parser.php:6417
validateSig( $text)
Check that the user's signature contains no bad XML.
Definition: Parser.php:4766
getPreprocessor()
Get a preprocessor object.
Definition: Parser.php:1211
parseLinkParameter( $value)
Parse the value of 'link' parameter in image syntax ([[File:Foo.jpg|link=<value>]]).
Definition: Parser.php:5574
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
Definition: Parser.php:6226
const EXT_LINK_URL_CLASS
Definition: Parser.php:108
OutputType( $x=null)
Accessor/mutator for the output type.
Definition: Parser.php:1107
LoggerInterface $logger
Definition: Parser.php:372
initializeVariables()
Initialize the magic variables (like CURRENTMONTHNAME) and substitution modifiers.
Definition: Parser.php:2901
$mIncludeSizes
Definition: Parser.php:256
const SFH_OBJECT_ARGS
Definition: Parser.php:100
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition: Parser.php:565
resetOutput()
Reset the ParserOutput.
Definition: Parser.php:667
setLinkID( $id)
Definition: Parser.php:1161
doQuotes( $text)
Helper function for handleAllQuotes()
Definition: Parser.php:1990
getTemplateDom(LinkTarget $title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
Definition: Parser.php:3499
$mExtLinkBracketedRegex
Definition: Parser.php:220
finalizeHeadings( $text, $origText, $isMain=true)
This function accomplishes several tasks: 1) Auto-number headings if that option is enabled 2) Add an...
Definition: Parser.php:4178
const EXT_IMAGE_REGEX
Definition: Parser.php:115
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition: Parser.php:5058
insertStripItem( $text)
Add an item to the strip state Returns the unique tag which must be inserted into the stripped text T...
Definition: Parser.php:1363
handleMagicLinks( $text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
Definition: Parser.php:1765
const CONSTRUCTOR_OPTIONS
Definition: Parser.php:407
NamespaceInfo $nsInfo
Definition: Parser.php:369
const OT_MSG
Definition: Parser.php:132
incrementExpensiveFunctionCount()
Definition: Parser.php:4090
makeImage(LinkTarget $link, $options, $holders=false)
Parse image options text and use it to make an image.
Definition: Parser.php:5337
static makeAnchor( $sectionName)
Definition: Parser.php:6184
const SPACE_NOT_NL
Definition: Parser.php:119
const OT_PREPROCESS
Definition: Parser.php:131
int $mLinkID
Definition: Parser.php:255
handleAllQuotes( $text)
Replace single quotes with HTML markup.
Definition: Parser.php:1972
SpecialPageFactory $specialPageFactory
Definition: Parser.php:351
getUserSig(UserIdentity $user, $nickname=false, $fancySig=null)
Fetch the user's signature text, if any, and normalize to validated, ready-to-insert wikitext.
Definition: Parser.php:4697
makeKnownLinkHolder(LinkTarget $nt, $text='', $trail='', $prefix='')
Render a forced-blue link inline; protect against double expansion of URLs if we're in a mode that pr...
Definition: Parser.php:2789
__construct(ServiceOptions $svcOptions, MagicWordFactory $magicWordFactory, Language $contLang, ParserFactory $factory, UrlUtils $urlUtils, SpecialPageFactory $spFactory, LinkRendererFactory $linkRendererFactory, NamespaceInfo $nsInfo, LoggerInterface $logger, BadFileLookup $badFileLookup, LanguageConverterFactory $languageConverterFactory, HookContainer $hookContainer, TidyDriverBase $tidy, WANObjectCache $wanCache, UserOptionsLookup $userOptionsLookup, UserFactory $userFactory, TitleFormatter $titleFormatter, HttpRequestFactory $httpRequestFactory, TrackingCategories $trackingCategories, SignatureValidatorFactory $signatureValidatorFactory, UserNameUtils $userNameUtils)
Constructing parsers directly is not allowed! Use a ParserFactory.
Definition: Parser.php:458
HttpRequestFactory $httpRequestFactory
Definition: Parser.php:393
firstCallInit()
Used to do various kinds of initialisation on the first call of the parser.
Definition: Parser.php:609
preprocessToDom( $text, $flags=0)
Get the document object model for the given wikitext.
Definition: Parser.php:2927
$mForceTocPosition
Definition: Parser.php:271
markerSkipCallback( $s, callable $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition: Parser.php:6372
clearState()
Clear Parser state.
Definition: Parser.php:621
killMarkers( $text)
Remove any strip markers found in the given text.
Definition: Parser.php:6403
ParserFactory $factory
Definition: Parser.php:348
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
Definition: Parser.php:6508
nextLinkID()
Definition: Parser.php:1153
getRevisionRecordObject()
Get the revision record object for $this->mRevisionId.
Definition: Parser.php:6011
attributeStripCallback(&$text, $frame=false)
Callback from the Sanitizer for expanding items found in HTML attribute values, so they can be safely...
Definition: Parser.php:5705
argSubstitution(array $piece, PPFrame $frame)
Triple brace replacement – used for template arguments.
Definition: Parser.php:3931
const SFH_NO_HASH
Definition: Parser.php:99
renderImageGallery( $text, array $params)
Renders an image gallery from a text with one line per image.
Definition: Parser.php:5098
lock()
Lock the current instance of the parser.
Definition: Parser.php:6446
TitleFormatter $titleFormatter
Definition: Parser.php:354
static statelessFetchTemplate( $page, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
Definition: Parser.php:3660
$mStripList
Definition: Parser.php:200
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
Definition: Parser.php:6493
SectionProfiler $mProfiler
Definition: Parser.php:331
preSaveTransform( $text, PageReference $page, UserIdentity $user, ParserOptions $options, $clearState=true)
Transform wiki markup when saving a page by doing "\\r\\n" -> "\\n" conversion, substituting signatur...
Definition: Parser.php:4575
Differences from DOM schema:
resetParser(?Parser $parser)
Allows resetting the internal Parser reference after Preprocessor is cloned.
const DOM_FOR_INCLUSION
Transclusion mode flag for Preprocessor::preprocessToObj()
Variant of the Message class.
Definition: RawMessage.php:35
Group all the pieces relevant to the context of a request into one instance.
setTitle(Title $title=null)
static getMain()
Get the RequestContext object associated with the main request.
static fixTagAttributes( $text, $element, $sorted=false)
Take a tag soup fragment listing an HTML element's attributes and normalize it to well-formed XML,...
Definition: Sanitizer.php:838
static cleanUrl( $url)
Definition: Sanitizer.php:1762
static escapeIdForLink( $id)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid URL...
Definition: Sanitizer.php:973
static validateTagAttributes( $attribs, $element)
Take an array of attribute names and values and normalize or discard illegal values for the given ele...
Definition: Sanitizer.php:526
static removeSomeTags(string $text, array $options=[])
Cleans up HTML, removes dangerous tags and attributes, and removes HTML comments; the result will alw...
Definition: Sanitizer.php:392
static internalRemoveHtmlTags( $text, $processCallback=null, $args=[], $extratags=[], $removetags=[])
Cleans up HTML, removes dangerous tags and attributes, and removes HTML comments; BEWARE there may be...
Definition: Sanitizer.php:317
static normalizeSectionNameWhitespace( $section)
Normalizes whitespace in a section name, such as might be returned by Parser::stripSectionName(),...
Definition: Sanitizer.php:1239
const ID_FALLBACK
Tells escapeUrlForHtml() to encode the ID using the fallback encoding, or return false if no fallback...
Definition: Sanitizer.php:85
static decodeCharReferences( $text)
Decode any character references, numeric or named entities, in the text and return a UTF-8 string.
Definition: Sanitizer.php:1363
static escapeIdForAttribute( $id, $mode=self::ID_PRIMARY)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid HTM...
Definition: Sanitizer.php:946
static stripAllTags( $html)
Take a fragment of (potentially invalid) HTML and return a version with any tags removed,...
Definition: Sanitizer.php:1711
static decodeTagAttributes( $text)
Return an associative array of attribute names and values from a partial tag string.
Definition: Sanitizer.php:1139
const ID_PRIMARY
Tells escapeUrlForHtml() to encode the ID using the wiki's primary encoding.
Definition: Sanitizer.php:77
Arbitrary section name based PHP profiling.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
static delimiterReplace( $startDelim, $endDelim, $replace, $subject, $flags='')
Perform an operation equivalent to preg_replace() with flags.
static replaceMarkup( $search, $replace, $text)
More or less "markup-safe" str_replace() Ignores any instances of the separator inside <....
static explode( $separator, $subject)
Workalike for explode() with limited memory usage.
static delimiterExplode( $startDelim, $endDelim, $separator, $subject, $nested=false)
Explode a string, but ignore any instances of the separator inside the given start and end delimiters...
Definition: StringUtils.php:59
static normalizeLineEndings( $text)
Do a "\\r\\n" -> "\\n" and "\\r" -> "\\n" transformation as well as trim trailing whitespace.
Represents a title within MediaWiki.
Definition: Title.php:49
static castFromPageReference(?PageReference $pageReference)
Return a Title for a given Reference.
Definition: Title.php:332
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:282
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition: Title.php:734
static castFromLinkTarget( $linkTarget)
Same as newFromLinkTarget, but if passed null, returns null.
Definition: Title.php:306
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:370
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:638
This class performs some operations related to tracking categories, such as creating a list of all su...
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:68
static newFromName( $name, $validate='valid')
Definition: User.php:596
Multi-datacenter aware caching interface.
static isWellFormedXmlFragment( $text)
Check if a string is a well-formed XML fragment.
Definition: Xml.php:749
return[0=> 'ـ', 1=> ' ', 2=> '`', 3=> '´', 4=> '˜', 5=> '^', 6=> '¯', 7=> '‾', 8=> '˘', 9=> '˙', 10=> '¨', 11=> '˚', 12=> '˝', 13=> '᾽', 14=> '῝', 15=> '¸', 16=> '˛', 17=> '_', 18=> '‗', 19=> '῀', 20=> '﮲', 21=> '﮳', 22=> '﮴', 23=> '﮵', 24=> '﮶', 25=> '﮷', 26=> '﮸', 27=> '﮹', 28=> '﮺', 29=> '﮻', 30=> '﮼', 31=> '﮽', 32=> '﮾', 33=> '﮿', 34=> '﯀', 35=> '﯁', 36=> '゛', 37=> '゜', 38=> '-', 39=> '֊', 40=> '᐀', 41=> '᭠', 42=> '᠆', 43=> '᠇', 44=> '‐', 45=> '‒', 46=> '–', 47=> '—', 48=> '―', 49=> '⁓', 50=> '⸗', 51=> '゠', 52=> '・', 53=> ',', 54=> '՝', 55=> '،', 56=> '؍', 57=> '٫', 58=> '٬', 59=> '߸', 60=> '᠂', 61=> '᠈', 62=> '꓾', 63=> '꘍', 64=> '꛵', 65=> '︑', 66=> ';', 67=> '؛', 68=> '⁏', 69=> '꛶', 70=> ':', 71=> '։', 72=> '؞', 73=> '܃', 74=> '܄', 75=> '܅', 76=> '܆', 77=> '܇', 78=> '܈', 79=> '࠰', 80=> '࠱', 81=> '࠲', 82=> '࠳', 83=> '࠴', 84=> '࠵', 85=> '࠶', 86=> '࠷', 87=> '࠸', 88=> '࠹', 89=> '࠺', 90=> '࠻', 91=> '࠼', 92=> '࠽', 93=> '࠾', 94=> '፡', 95=> '፣', 96=> '፤', 97=> '፥', 98=> '፦', 99=> '᠄', 100=> '᠅', 101=> '༔', 102=> '៖', 103=> '᭝', 104=> '꧇', 105=> '᛫', 106=> '᛬', 107=> '᛭', 108=> '꛴', 109=> '!', 110=> '¡', 111=> '՜', 112=> '߹', 113=> '᥄', 114=> '?', 115=> '¿', 116=> '⸮', 117=> '՞', 118=> '؟', 119=> '܉', 120=> '፧', 121=> '᥅', 122=> '⳺', 123=> '⳻', 124=> '꘏', 125=> '꛷', 126=> '‽', 127=> '⸘', 128=> '.', 129=> '᠁', 130=> '۔', 131=> '܁', 132=> '܂', 133=> '።', 134=> '᠃', 135=> '᠉', 136=> '᙮', 137=> '᭜', 138=> '⳹', 139=> '⳾', 140=> '⸰', 141=> '꓿', 142=> '꘎', 143=> '꛳', 144=> '︒', 145=> '·', 146=> '⸱', 147=> '।', 148=> '॥', 149=> '꣎', 150=> '꣏', 151=> '᰻', 152=> '᰼', 153=> '꡶', 154=> '꡷', 155=> '᜵', 156=> '᜶', 157=> '꤯', 158=> '၊', 159=> '။', 160=> '។', 161=> '៕', 162=> '᪨', 163=> '᪩', 164=> '᪪', 165=> '᪫', 166=> '᭞', 167=> '᭟', 168=> '꧈', 169=> '꧉', 170=> '꩝', 171=> '꩞', 172=> '꩟', 173=> '꯫', 174=> '𐩖', 175=> '𐩗', 176=> '𑁇', 177=> '𑁈', 178=> '𑃀', 179=> '𑃁', 180=> '᱾', 181=> '᱿', 182=> '܀', 183=> '߷', 184=> '჻', 185=> '፠', 186=> '፨', 187=> '᨞', 188=> '᨟', 189=> '᭚', 190=> '᭛', 191=> '꧁', 192=> '꧂', 193=> '꧃', 194=> '꧄', 195=> '꧅', 196=> '꧆', 197=> '꧊', 198=> '꧋', 199=> '꧌', 200=> '꧍', 201=> '꛲', 202=> '꥟', 203=> '𐡗', 204=> '𐬺', 205=> '𐬻', 206=> '𐬼', 207=> '𐬽', 208=> '𐬾', 209=> '𐬿', 210=> '𑂾', 211=> '𑂿', 212=> '⁕', 213=> '⁖', 214=> '⁘', 215=> '⁙', 216=> '⁚', 217=> '⁛', 218=> '⁜', 219=> '⁝', 220=> '⁞', 221=> '⸪', 222=> '⸫', 223=> '⸬', 224=> '⸭', 225=> '⳼', 226=> '⳿', 227=> '⸙', 228=> '𐤿', 229=> '𐄀', 230=> '𐄁', 231=> '𐄂', 232=> '𐎟', 233=> '𐏐', 234=> '𐤟', 235=> '𒑰', 236=> '𒑱', 237=> '𒑲', 238=> '𒑳', 239=> '\'', 240=> '‘', 241=> '’', 242=> '‚', 243=> '‛', 244=> '‹', 245=> '›', 246=> '"', 247 => '“', 248 => '”', 249 => '„', 250 => '‟', 251 => '«', 252 => '»', 253 => '(', 254 => ')', 255 => '[', 256 => ']', 257 => '{', 258 => '}', 259 => '༺', 260 => '༻', 261 => '༼', 262 => '༽', 263 => '᚛', 264 => '᚜', 265 => '⁅', 266 => '⁆', 267 => '⧼', 268 => '⧽', 269 => '⦃', 270 => '⦄', 271 => '⦅', 272 => '⦆', 273 => '⦇', 274 => '⦈', 275 => '⦉', 276 => '⦊', 277 => '⦋', 278 => '⦌', 279 => '⦍', 280 => '⦎', 281 => '⦏', 282 => '⦐', 283 => '⦑', 284 => '⦒', 285 => '⦓', 286 => '⦔', 287 => '⦕', 288 => '⦖', 289 => '⦗', 290 => '⦘', 291 => '⟬', 292 => '⟭', 293 => '⟮', 294 => '⟯', 295 => '⸂', 296 => '⸃', 297 => '⸄', 298 => '⸅', 299 => '⸉', 300 => '⸊', 301 => '⸌', 302 => '⸍', 303 => '⸜', 304 => '⸝', 305 => '⸠', 306 => '⸡', 307 => '⸢', 308 => '⸣', 309 => '⸤', 310 => '⸥', 311 => '⸦', 312 => '⸧', 313 => '⸨', 314 => '⸩', 315 => '〈', 316 => '〉', 317 => '「', 318 => '」', 319 => '﹝', 320 => '﹞', 321 => '︗', 322 => '︘', 323 => '﴾', 324 => '﴿', 325 => '§', 326 => '¶', 327 => '⁋', 328 => '©', 329 => '®', 330 => '@', 331 => '*', 332 => '⁎', 333 => '⁑', 334 => '٭', 335 => '꙳', 336 => '/', 337 => '⁄', 338 => '\\', 339 => '&', 340 => '⅋', 341 => '⁊', 342 => '#', 343 => '%', 344 => '٪', 345 => '‰', 346 => '؉', 347 => '‱', 348 => '؊', 349 => '⁒', 350 => '†', 351 => '‡', 352 => '•', 353 => '‣', 354 => '‧', 355 => '⁃', 356 => '⁌', 357 => '⁍', 358 => '′', 359 => '‵', 360 => '‸', 361 => '※', 362 => '‿', 363 => '⁔', 364 => '⁀', 365 => '⁐', 366 => '⁁', 367 => '⁂', 368 => '⸀', 369 => '⸁', 370 => '⸆', 371 => '⸇', 372 => '⸈', 373 => '⸋', 374 => '⸎', 375 => '⸏', 376 => '⸐', 377 => '⸑', 378 => '⸒', 379 => '⸓', 380 => '⸔', 381 => '⸕', 382 => '⸖', 383 => '⸚', 384 => '⸛', 385 => '⸞', 386 => '⸟', 387 => '꙾', 388 => '՚', 389 => '՛', 390 => '՟', 391 => '־', 392 => '׀', 393 => '׃', 394 => '׆', 395 => '׳', 396 => '״', 397 => '܊', 398 => '܋', 399 => '܌', 400 => '܍', 401 => '࡞', 402 => '᠀', 403 => '॰', 404 => '꣸', 405 => '꣹', 406 => '꣺', 407 => '෴', 408 => '๚', 409 => '๛', 410 => '꫞', 411 => '꫟', 412 => '༄', 413 => '༅', 414 => '༆', 415 => '༇', 416 => '༈', 417 => '༉', 418 => '༊', 419 => '࿐', 420 => '࿑', 421 => '་', 422 => '།', 423 => '༎', 424 => '༏', 425 => '༐', 426 => '༑', 427 => '༒', 428 => '྅', 429 => '࿒', 430 => '࿓', 431 => '࿔', 432 => '࿙', 433 => '࿚', 434 => '᰽', 435 => '᰾', 436 => '᰿', 437 => '᥀', 438 => '၌', 439 => '၍', 440 => '၎', 441 => '၏', 442 => '႞', 443 => '႟', 444 => '꩷', 445 => '꩸', 446 => '꩹', 447 => 'ៗ', 448 => '៘', 449 => '៙', 450 => '៚', 451 => '᪠', 452 => '᪡', 453 => '᪢', 454 => '᪣', 455 => '᪤', 456 => '᪥', 457 => '᪦', 458 => '᪬', 459 => '᪭', 460 => '᙭', 461 => '⵰', 462 => '꡴', 463 => '꡵', 464 => '᯼', 465 => '᯽', 466 => '᯾', 467 => '᯿', 468 => '꤮', 469 => '꧞', 470 => '꧟', 471 => '꩜', 472 => '𑁉', 473 => '𑁊', 474 => '𑁋', 475 => '𑁌', 476 => '𑁍', 477 => '𐩐', 478 => '𐩑', 479 => '𐩒', 480 => '𐩓', 481 => '𐩔', 482 => '𐩕', 483 => '𐩘', 484 => '𐬹', 485 => '𑂻', 486 => '𑂼', 487 => 'ʹ', 488 => '͵', 489 => 'ʺ', 490 => '˂', 491 => '˃', 492 => '˄', 493 => '˅', 494 => 'ˆ', 495 => 'ˇ', 496 => 'ˈ', 497 => 'ˉ', 498 => 'ˊ', 499 => 'ˋ', 500 => 'ˌ', 501 => 'ˍ', 502 => 'ˎ', 503 => 'ˏ', 504 => '˒', 505 => '˓', 506 => '˔', 507 => '˕', 508 => '˖', 509 => '˗', 510 => '˞', 511 => '˟', 512 => '˥', 513 => '˦', 514 => '˧', 515 => '˨', 516 => '˩', 517 => '˪', 518 => '˫', 519 => 'ˬ', 520 => '˭', 521 => '˯', 522 => '˰', 523 => '˱', 524 => '˲', 525 => '˳', 526 => '˴', 527 => '˵', 528 => '˶', 529 => '˷', 530 => '˸', 531 => '˹', 532 => '˺', 533 => '˻', 534 => '˼', 535 => '˽', 536 => '˾', 537 => '˿', 538 => '᎐', 539 => '᎑', 540 => '᎒', 541 => '᎓', 542 => '᎔', 543 => '᎕', 544 => '᎖', 545 => '᎗', 546 => '᎘', 547 => '᎙', 548 => '꜀', 549 => '꜁', 550 => '꜂', 551 => '꜃', 552 => '꜄', 553 => '꜅', 554 => '꜆', 555 => '꜇', 556 => '꜈', 557 => '꜉', 558 => '꜊', 559 => '꜋', 560 => '꜌', 561 => '꜍', 562 => '꜎', 563 => '꜏', 564 => '꜐', 565 => '꜑', 566 => '꜒', 567 => '꜓', 568 => '꜔', 569 => '꜕', 570 => '꜖', 571 => 'ꜗ', 572 => 'ꜘ', 573 => 'ꜙ', 574 => 'ꜚ', 575 => 'ꜛ', 576 => 'ꜜ', 577 => 'ꜝ', 578 => 'ꜞ', 579 => 'ꜟ', 580 => '꜠', 581 => '꜡', 582 => 'ꞈ', 583 => '꞉', 584 => '꞊', 585 => '°', 586 => '҂', 587 => '؈', 588 => '؎', 589 => '؏', 590 => '۞', 591 => '۩', 592 => '﷽', 593 => '߶', 594 => '৺', 595 => '୰', 596 => '௳', 597 => '௴', 598 => '௵', 599 => '௶', 600 => '௷', 601 => '௸', 602 => '௺', 603 => '౿', 604 => '൹', 605 => '꠨', 606 => '꠩', 607 => '꠪', 608 => '꠫', 609 => '꠶', 610 => '꠷', 611 => '꠹', 612 => '๏', 613 => '༁', 614 => '༂', 615 => '༃', 616 => '༓', 617 => '༕', 618 => '༖', 619 => '༗', 620 => '༚', 621 => '༛', 622 => '༜', 623 => '༝', 624 => '༞', 625 => '༟', 626 => '༴', 627 => '༶', 628 => '༸', 629 => '྾', 630 => '྿', 631 => '࿀', 632 => '࿁', 633 => '࿂', 634 => '࿃', 635 => '࿄', 636 => '࿅', 637 => '࿇', 638 => '࿈', 639 => '࿉', 640 => '࿊', 641 => '࿋', 642 => '࿌', 643 => '࿎', 644 => '࿏', 645 => '࿕', 646 => '࿖', 647 => '࿗', 648 => '࿘', 649 => '᧠', 650 => '᧡', 651 => '᧢', 652 => '᧣', 653 => '᧤', 654 => '᧥', 655 => '᧦', 656 => '᧧', 657 => '᧨', 658 => '᧩', 659 => '᧪', 660 => '᧫', 661 => '᧬', 662 => '᧭', 663 => '᧮', 664 => '᧯', 665 => '᧰', 666 => '᧱', 667 => '᧲', 668 => '᧳', 669 => '᧴', 670 => '᧵', 671 => '᧶', 672 => '᧷', 673 => '᧸', 674 => '᧹', 675 => '᧺', 676 => '᧻', 677 => '᧼', 678 => '᧽', 679 => '᧾', 680 => '᧿', 681 => '᭡', 682 => '᭢', 683 => '᭣', 684 => '᭤', 685 => '᭥', 686 => '᭦', 687 => '᭧', 688 => '᭨', 689 => '᭩', 690 => '᭪', 691 => '᭴', 692 => '᭵', 693 => '᭶', 694 => '᭷', 695 => '᭸', 696 => '᭹', 697 => '᭺', 698 => '᭻', 699 => '᭼', 700 => '℄', 701 => '℈', 702 => '℔', 703 => '℗', 704 => '℘', 705 => '℞', 706 => '℟', 707 => '℣', 708 => '℥', 709 => '℧', 710 => '℩', 711 => '℮', 712 => '℺', 713 => '⅁', 714 => '⅂', 715 => '⅃', 716 => '⅄', 717 => '⅊', 718 => '⅌', 719 => '⅍', 720 => '⅏', 721 => '←', 722 => '→', 723 => '↑', 724 => '↓', 725 => '↔', 726 => '↕', 727 => '↖', 728 => '↗', 729 => '↘', 730 => '↙', 731 => '↜', 732 => '↝', 733 => '↞', 734 => '↟', 735 => '↠', 736 => '↡', 737 => '↢', 738 => '↣', 739 => '↤', 740 => '↥', 741 => '↦', 742 => '↧', 743 => '↨', 744 => '↩', 745 => '↪', 746 => '↫', 747 => '↬', 748 => '↭', 749 => '↯', 750 => '↰', 751 => '↱', 752 => '↲', 753 => '↳', 754 => '↴', 755 => '↵', 756 => '↶', 757 => '↷', 758 => '↸', 759 => '↹', 760 => '↺', 761 => '↻', 762 => '↼', 763 => '↽', 764 => '↾', 765 => '↿', 766 => '⇀', 767 => '⇁', 768 => '⇂', 769 => '⇃', 770 => '⇄', 771 => '⇅', 772 => '⇆', 773 => '⇇', 774 => '⇈', 775 => '⇉', 776 => '⇊', 777 => '⇋', 778 => '⇌', 779 => '⇐', 780 => '⇑', 781 => '⇒', 782 => '⇓', 783 => '⇔', 784 => '⇕', 785 => '⇖', 786 => '⇗', 787 => '⇘', 788 => '⇙', 789 => '⇚', 790 => '⇛', 791 => '⇜', 792 => '⇝', 793 => '⇞', 794 => '⇟', 795 => '⇠', 796 => '⇡', 797 => '⇢', 798 => '⇣', 799 => '⇤', 800 => '⇥', 801 => '⇦', 802 => '⇧', 803 => '⇨', 804 => '⇩', 805 => '⇪', 806 => '⇫', 807 => '⇬', 808 => '⇭', 809 => '⇮', 810 => '⇯', 811 => '⇰', 812 => '⇱', 813 => '⇲', 814 => '⇳', 815 => '⇴', 816 => '⇵', 817 => '⇶', 818 => '⇷', 819 => '⇸', 820 => '⇹', 821 => '⇺', 822 => '⇻', 823 => '⇼', 824 => '⇽', 825 => '⇾', 826 => '⇿', 827 => '∀', 828 => '∁', 829 => '∂', 830 => '∃', 831 => '∅', 832 => '∆', 833 => '∇', 834 => '∈', 835 => '∊', 836 => '∋', 837 => '∍', 838 => '϶', 839 => '∎', 840 => '∏', 841 => '∐', 842 => '∑', 843 => '+', 844 => '±', 845 => '÷', 846 => '×', 847 => '<', 848 => '=', 849 => '>', 850 => '¬', 851 => '|', 852 => '¦', 853 => '‖', 854 => '~', 855 => '−', 856 => '∓', 857 => '∔', 858 => '∕', 859 => '∖', 860 => '∗', 861 => '∘', 862 => '∙', 863 => '√', 864 => '∛', 865 => '؆', 866 => '∜', 867 => '؇', 868 => '∝', 869 => '∞', 870 => '∟', 871 => '∠', 872 => '∡', 873 => '∢', 874 => '∣', 875 => '∥', 876 => '∧', 877 => '∨', 878 => '∩', 879 => '∪', 880 => '∫', 881 => '∮', 882 => '∱', 883 => '∲', 884 => '∳', 885 => '∴', 886 => '∵', 887 => '∶', 888 => '∷', 889 => '∸', 890 => '∹', 891 => '∺', 892 => '∻', 893 => '∼', 894 => '∽', 895 => '∾', 896 => '∿', 897 => '≀', 898 => '≂', 899 => '≃', 900 => '≅', 901 => '≆', 902 => '≈', 903 => '≊', 904 => '≋', 905 => '≌', 906 => '≍', 907 => '≎', 908 => '≏', 909 => '≐', 910 => '≑', 911 => '≒', 912 => '≓', 913 => '≔', 914 => '≕', 915 => '≖', 916 => '≗', 917 => '≘', 918 => '≙', 919 => '≚', 920 => '≛', 921 => '≜', 922 => '≝', 923 => '≞', 924 => '≟', 925 => '≡', 926 => '≣', 927 => '≤', 928 => '≥', 929 => '≦', 930 => '≧', 931 => '≨', 932 => '≩', 933 => '≪', 934 => '≫', 935 => '≬', 936 => '≲', 937 => '≳', 938 => '≶', 939 => '≷', 940 => '≺', 941 => '≻', 942 => '≼', 943 => '≽', 944 => '≾', 945 => '≿', 946 => '⊂', 947 => '⊃', 948 => '⊆', 949 => '⊇', 950 => '⊊', 951 => '⊋', 952 => '⊌', 953 => '⊍', 954 => '⊎', 955 => '⊏', 956 => '⊐', 957 => '⊑', 958 => '⊒', 959 => '⊓', 960 => '⊔', 961 => '⊕', 962 => '⊖', 963 => '⊗', 964 => '⊘', 965 => '⊙', 966 => '⊚', 967 => '⊛', 968 => '⊜', 969 => '⊝', 970 => '⊞', 971 => '⊟', 972 => '⊠', 973 => '⊡', 974 => '⊢', 975 => '⊣', 976 => '⊤', 977 => '⊥', 978 => '⊦', 979 => '⊧', 980 => '⊨', 981 => '⊩', 982 => '⊪', 983 => '⊫', 984 => '⊰', 985 => '⊱', 986 => '⊲', 987 => '⊳', 988 => '⊴', 989 => '⊵', 990 => '⊶', 991 => '⊷', 992 => '⊸', 993 => '⊹', 994 => '⊺', 995 => '⊻', 996 => '⊼', 997 => '⊽', 998 => '⊾', 999 => '⊿', 1000 => '⋀', 1001 => '⋁', 1002 => '⋂', 1003 => '⋃', 1004 => '⋄', 1005 => '⋅', 1006 => '⋆', 1007 => '⋇', 1008 => '⋈', 1009 => '⋉', 1010 => '⋊', 1011 => '⋋', 1012 => '⋌', 1013 => '⋍', 1014 => '⋎', 1015 => '⋏', 1016 => '⋐', 1017 => '⋑', 1018 => '⋒', 1019 => '⋓', 1020 => '⋔', 1021 => '⋕', 1022 => '⋖', 1023 => '⋗', 1024 => '⋘', 1025 => '⋙', 1026 => '⋚', 1027 => '⋛', 1028 => '⋜', 1029 => '⋝', 1030 => '⋞', 1031 => '⋟', 1032 => '⋤', 1033 => '⋥', 1034 => '⋦', 1035 => '⋧', 1036 => '⋨', 1037 => '⋩', 1038 => '⋮', 1039 => '⋯', 1040 => '⋰', 1041 => '⋱', 1042 => '⋲', 1043 => '⋳', 1044 => '⋴', 1045 => '⋵', 1046 => '⋶', 1047 => '⋷', 1048 => '⋸', 1049 => '⋹', 1050 => '⋺', 1051 => '⋻', 1052 => '⋼', 1053 => '⋽', 1054 => '⋾', 1055 => '⋿', 1056 => '⌀', 1057 => '⌁', 1058 => '⌂', 1059 => '⌃', 1060 => '⌄', 1061 => '⌅', 1062 => '⌆', 1063 => '⌇', 1064 => '⌈', 1065 => '⌉', 1066 => '⌊', 1067 => '⌋', 1068 => '⌌', 1069 => '⌍', 1070 => '⌎', 1071 => '⌏', 1072 => '⌐', 1073 => '⌑', 1074 => '⌒', 1075 => '⌓', 1076 => '⌔', 1077 => '⌕', 1078 => '⌖', 1079 => '⌗', 1080 => '⌘', 1081 => '⌙', 1082 => '⌚', 1083 => '⌛', 1084 => '⌜', 1085 => '⌝', 1086 => '⌞', 1087 => '⌟', 1088 => '⌠', 1089 => '⌡', 1090 => '⌢', 1091 => '⌣', 1092 => '⌤', 1093 => '⌥', 1094 => '⌦', 1095 => '⌧', 1096 => '⌨', 1097 => '⌫', 1098 => '⌬', 1099 => '⌭', 1100 => '⌮', 1101 => '⌯', 1102 => '⌰', 1103 => '⌱', 1104 => '⌲', 1105 => '⌳', 1106 => '⌴', 1107 => '⌵', 1108 => '⌶', 1109 => '⌷', 1110 => '⌸', 1111 => '⌹', 1112 => '⌺', 1113 => '⌻', 1114 => '⌼', 1115 => '⌽', 1116 => '⌾', 1117 => '⌿', 1118 => '⍀', 1119 => '⍁', 1120 => '⍂', 1121 => '⍃', 1122 => '⍄', 1123 => '⍅', 1124 => '⍆', 1125 => '⍇', 1126 => '⍈', 1127 => '⍉', 1128 => '⍊', 1129 => '⍋', 1130 => '⍌', 1131 => '⍍', 1132 => '⍎', 1133 => '⍏', 1134 => '⍐', 1135 => '⍑', 1136 => '⍒', 1137 => '⍓', 1138 => '⍔', 1139 => '⍕', 1140 => '⍖', 1141 => '⍗', 1142 => '⍘', 1143 => '⍙', 1144 => '⍚', 1145 => '⍛', 1146 => '⍜', 1147 => '⍝', 1148 => '⍞', 1149 => '⍟', 1150 => '⍠', 1151 => '⍡', 1152 => '⍢', 1153 => '⍣', 1154 => '⍤', 1155 => '⍥', 1156 => '⍦', 1157 => '⍧', 1158 => '⍨', 1159 => '⍩', 1160 => '⍪', 1161 => '⍫', 1162 => '⍬', 1163 => '⍭', 1164 => '⍮', 1165 => '⍯', 1166 => '⍰', 1167 => '⍱', 1168 => '⍲', 1169 => '⍳', 1170 => '⍴', 1171 => '⍵', 1172 => '⍶', 1173 => '⍷', 1174 => '⍸', 1175 => '⍹', 1176 => '⍺', 1177 => '⍻', 1178 => '⍼', 1179 => '⍽', 1180 => '⍾', 1181 => '⍿', 1182 => '⎀', 1183 => '⎁', 1184 => '⎂', 1185 => '⎃', 1186 => '⎄', 1187 => '⎅', 1188 => '⎆', 1189 => '⎇', 1190 => '⎈', 1191 => '⎉', 1192 => '⎊', 1193 => '⎋', 1194 => '⎌', 1195 => '⎍', 1196 => '⎎', 1197 => '⎏', 1198 => '⎐', 1199 => '⎑', 1200 => '⎒', 1201 => '⎓', 1202 => '⎔', 1203 => '⎕', 1204 => '⎖', 1205 => '⎗', 1206 => '⎘', 1207 => '⎙', 1208 => '⎚', 1209 => '⎛', 1210 => '⎜', 1211 => '⎝', 1212 => '⎞', 1213 => '⎟', 1214 => '⎠', 1215 => '⎡', 1216 => '⎢', 1217 => '⎣', 1218 => '⎤', 1219 => '⎥', 1220 => '⎦', 1221 => '⎧', 1222 => '⎨', 1223 => '⎩', 1224 => '⎪', 1225 => '⎫', 1226 => '⎬', 1227 => '⎭', 1228 => '⎮', 1229 => '⎯', 1230 => '⎰', 1231 => '⎱', 1232 => '⎲', 1233 => '⎳', 1234 => '⎴', 1235 => '⎵', 1236 => '⎶', 1237 => '⎷', 1238 => '⎸', 1239 => '⎹', 1240 => '⎺', 1241 => '⎻', 1242 => '⎼', 1243 => '⎽', 1244 => '⎾', 1245 => '⎿', 1246 => '⏀', 1247 => '⏁', 1248 => '⏂', 1249 => '⏃', 1250 => '⏄', 1251 => '⏅', 1252 => '⏆', 1253 => '⏇', 1254 => '⏈', 1255 => '⏉', 1256 => '⏊', 1257 => '⏋', 1258 => '⏌', 1259 => '⏍', 1260 => '⏎', 1261 => '⏏', 1262 => '⏐', 1263 => '⏑', 1264 => '⏒', 1265 => '⏓', 1266 => '⏔', 1267 => '⏕', 1268 => '⏖', 1269 => '⏗', 1270 => '⏘', 1271 => '⏙', 1272 => '⏚', 1273 => '⏛', 1274 => '⏜', 1275 => '⏝', 1276 => '⏞', 1277 => '⏟', 1278 => '⏠', 1279 => '⏡', 1280 => '⏢', 1281 => '⏣', 1282 => '⏤', 1283 => '⏥', 1284 => '⏦', 1285 => '⏧', 1286 => '⏨', 1287 => '⏩', 1288 => '⏪', 1289 => '⏫', 1290 => '⏬', 1291 => '⏭', 1292 => '⏮', 1293 => '⏯', 1294 => '⏰', 1295 => '⏱', 1296 => '⏲', 1297 => '⏳', 1298 => '␀', 1299 => '␁', 1300 => '␂', 1301 => '␃', 1302 => '␄', 1303 => '␅', 1304 => '␆', 1305 => '␇', 1306 => '␈', 1307 => '␉', 1308 => '␊', 1309 => '␋', 1310 => '␌', 1311 => '␍', 1312 => '␎', 1313 => '␏', 1314 => '␐', 1315 => '␑', 1316 => '␒', 1317 => '␓', 1318 => '␔', 1319 => '␕', 1320 => '␖', 1321 => '␗', 1322 => '␘', 1323 => '␙', 1324 => '␚', 1325 => '␛', 1326 => '␜', 1327 => '␝', 1328 => '␞', 1329 => '␟', 1330 => '␠', 1331 => '␡', 1332 => '␢', 1333 => '␣', 1334 => '␤', 1335 => '␥', 1336 => '␦', 1337 => '⑀', 1338 => '⑁', 1339 => '⑂', 1340 => '⑃', 1341 => '⑄', 1342 => '⑅', 1343 => '⑆', 1344 => '⑇', 1345 => '⑈', 1346 => '⑉', 1347 => '⑊', 1348 => '─', 1349 => '━', 1350 => '│', 1351 => '┃', 1352 => '┄', 1353 => '┅', 1354 => '┆', 1355 => '┇', 1356 => '┈', 1357 => '┉', 1358 => '┊', 1359 => '┋', 1360 => '┌', 1361 => '┍', 1362 => '┎', 1363 => '┏', 1364 => '┐', 1365 => '┑', 1366 => '┒', 1367 => '┓', 1368 => '└', 1369 => '┕', 1370 => '┖', 1371 => '┗', 1372 => '┘', 1373 => '┙', 1374 => '┚', 1375 => '┛', 1376 => '├', 1377 => '┝', 1378 => '┞', 1379 => '┟', 1380 => '┠', 1381 => '┡', 1382 => '┢', 1383 => '┣', 1384 => '┤', 1385 => '┥', 1386 => '┦', 1387 => '┧', 1388 => '┨', 1389 => '┩', 1390 => '┪', 1391 => '┫', 1392 => '┬', 1393 => '┭', 1394 => '┮', 1395 => '┯', 1396 => '┰', 1397 => '┱', 1398 => '┲', 1399 => '┳', 1400 => '┴', 1401 => '┵', 1402 => '┶', 1403 => '┷', 1404 => '┸', 1405 => '┹', 1406 => '┺', 1407 => '┻', 1408 => '┼', 1409 => '┽', 1410 => '┾', 1411 => '┿', 1412 => '╀', 1413 => '╁', 1414 => '╂', 1415 => '╃', 1416 => '╄', 1417 => '╅', 1418 => '╆', 1419 => '╇', 1420 => '╈', 1421 => '╉', 1422 => '╊', 1423 => '╋', 1424 => '╌', 1425 => '╍', 1426 => '╎', 1427 => '╏', 1428 => '═', 1429 => '║', 1430 => '╒', 1431 => '╓', 1432 => '╔', 1433 => '╕', 1434 => '╖', 1435 => '╗', 1436 => '╘', 1437 => '╙', 1438 => '╚', 1439 => '╛', 1440 => '╜', 1441 => '╝', 1442 => '╞', 1443 => '╟', 1444 => '╠', 1445 => '╡', 1446 => '╢', 1447 => '╣', 1448 => '╤', 1449 => '╥', 1450 => '╦', 1451 => '╧', 1452 => '╨', 1453 => '╩', 1454 => '╪', 1455 => '╫', 1456 => '╬', 1457 => '╭', 1458 => '╮', 1459 => '╯', 1460 => '╰', 1461 => '╱', 1462 => '╲', 1463 => '╳', 1464 => '╴', 1465 => '╵', 1466 => '╶', 1467 => '╷', 1468 => '╸', 1469 => '╹', 1470 => '╺', 1471 => '╻', 1472 => '╼', 1473 => '╽', 1474 => '╾', 1475 => '╿', 1476 => '▀', 1477 => '▁', 1478 => '▂', 1479 => '▃', 1480 => '▄', 1481 => '▅', 1482 => '▆', 1483 => '▇', 1484 => '█', 1485 => '▉', 1486 => '▊', 1487 => '▋', 1488 => '▌', 1489 => '▍', 1490 => '▎', 1491 => '▏', 1492 => '▐', 1493 => '░', 1494 => '▒', 1495 => '▓', 1496 => '▔', 1497 => '▕', 1498 => '▖', 1499 => '▗', 1500 => '▘', 1501 => '▙', 1502 => '▚', 1503 => '▛', 1504 => '▜', 1505 => '▝', 1506 => '▞', 1507 => '▟', 1508 => '■', 1509 => '□', 1510 => '▢', 1511 => '▣', 1512 => '▤', 1513 => '▥', 1514 => '▦', 1515 => '▧', 1516 => '▨', 1517 => '▩', 1518 => '▪', 1519 => '▫', 1520 => '▬', 1521 => '▭', 1522 => '▮', 1523 => '▯', 1524 => '▰', 1525 => '▱', 1526 => '▲', 1527 => '△', 1528 => '▴', 1529 => '▵', 1530 => '▶', 1531 => '▷', 1532 => '▸', 1533 => '▹', 1534 => '►', 1535 => '▻', 1536 => '▼', 1537 => '▽', 1538 => '▾', 1539 => '▿', 1540 => '◀', 1541 => '◁', 1542 => '◂', 1543 => '◃', 1544 => '◄', 1545 => '◅', 1546 => '◆', 1547 => '◇', 1548 => '◈', 1549 => '◉', 1550 => '◊', 1551 => '○', 1552 => '◌', 1553 => '◍', 1554 => '◎', 1555 => '●', 1556 => '◐', 1557 => '◑', 1558 => '◒', 1559 => '◓', 1560 => '◔', 1561 => '◕', 1562 => '◖', 1563 => '◗', 1564 => '◘', 1565 => '◙', 1566 => '◚', 1567 => '◛', 1568 => '◜', 1569 => '◝', 1570 => '◞', 1571 => '◟', 1572 => '◠', 1573 => '◡', 1574 => '◢', 1575 => '◣', 1576 => '◤', 1577 => '◥', 1578 => '◦', 1579 => '◧', 1580 => '◨', 1581 => '◩', 1582 => '◪', 1583 => '◫', 1584 => '◬', 1585 => '◭', 1586 => '◮', 1587 => '◯', 1588 => '◰', 1589 => '◱', 1590 => '◲', 1591 => '◳', 1592 => '◴', 1593 => '◵', 1594 => '◶', 1595 => '◷', 1596 => '◸', 1597 => '◹', 1598 => '◺', 1599 => '◻', 1600 => '◼', 1601 => '◽', 1602 => '◾', 1603 => '◿', 1604 => '☀', 1605 => '☁', 1606 => '☂', 1607 => '☃', 1608 => '☄', 1609 => '★', 1610 => '☆', 1611 => '☇', 1612 => '☈', 1613 => '☉', 1614 => '☊', 1615 => '☋', 1616 => '☌', 1617 => '☍', 1618 => '☎', 1619 => '☏', 1620 => '☐', 1621 => '☑', 1622 => '☒', 1623 => '☓', 1624 => '☔', 1625 => '☕', 1626 => '☖', 1627 => '☗', 1628 => '☘', 1629 => '☙', 1630 => '☚', 1631 => '☛', 1632 => '☜', 1633 => '☝', 1634 => '☞', 1635 => '☟', 1636 => '☠', 1637 => '☡', 1638 => '☢', 1639 => '☣', 1640 => '☤', 1641 => '☥', 1642 => '☦', 1643 => '☧', 1644 => '☨', 1645 => '☩', 1646 => '☪', 1647 => '☫', 1648 => '☬', 1649 => '☭', 1650 => '☮', 1651 => '☯', 1652 => '☸', 1653 => '☹', 1654 => '☺', 1655 => '☻', 1656 => '☼', 1657 => '☽', 1658 => '☾', 1659 => '☿', 1660 => '♀', 1661 => '♁', 1662 => '♂', 1663 => '♃', 1664 => '♄', 1665 => '♅', 1666 => '♆', 1667 => '♇', 1668 => '♈', 1669 => '♉', 1670 => '♊', 1671 => '♋', 1672 => '♌', 1673 => '♍', 1674 => '♎', 1675 => '♏', 1676 => '♐', 1677 => '♑', 1678 => '♒', 1679 => '♓', 1680 => '♔', 1681 => '♕', 1682 => '♖', 1683 => '♗', 1684 => '♘', 1685 => '♙', 1686 => '♚', 1687 => '♛', 1688 => '♜', 1689 => '♝', 1690 => '♞', 1691 => '♟', 1692 => '♠', 1693 => '♡', 1694 => '♢', 1695 => '♣', 1696 => '♤', 1697 => '♥', 1698 => '♦', 1699 => '♧', 1700 => '♨', 1701 => '♩', 1702 => '♪', 1703 => '♫', 1704 => '♬', 1705 => '♰', 1706 => '♱', 1707 => '♲', 1708 => '♳', 1709 => '♴', 1710 => '♵', 1711 => '♶', 1712 => '♷', 1713 => '♸', 1714 => '♹', 1715 => '♺', 1716 => '♻', 1717 => '♼', 1718 => '♽', 1719 => '♾', 1720 => '♿', 1721 => '⚀', 1722 => '⚁', 1723 => '⚂', 1724 => '⚃', 1725 => '⚄', 1726 => '⚅', 1727 => '⚆', 1728 => '⚇', 1729 => '⚈', 1730 => '⚉', 1731 => '⚐', 1732 => '⚑', 1733 => '⚒', 1734 => '⚓', 1735 => '⚔', 1736 => '⚕', 1737 => '⚖', 1738 => '⚗', 1739 => '⚘', 1740 => '⚙', 1741 => '⚚', 1742 => '⚛', 1743 => '⚜', 1744 => '⚝', 1745 => '⚞', 1746 => '⚟', 1747 => '⚠', 1748 => '⚡', 1749 => '⚢', 1750 => '⚣', 1751 => '⚤', 1752 => '⚥', 1753 => '⚦', 1754 => '⚧', 1755 => '⚨', 1756 => '⚩', 1757 => '⚪', 1758 => '⚫', 1759 => '⚬', 1760 => '⚭', 1761 => '⚮', 1762 => '⚯', 1763 => '⚰', 1764 => '⚱', 1765 => '⚲', 1766 => '⚳', 1767 => '⚴', 1768 => '⚵', 1769 => '⚶', 1770 => '⚷', 1771 => '⚸', 1772 => '⚹', 1773 => '⚺', 1774 => '⚻', 1775 => '⚼', 1776 => '⚽', 1777 => '⚾', 1778 => '⚿', 1779 => '⛀', 1780 => '⛁', 1781 => '⛂', 1782 => '⛃', 1783 => '⛄', 1784 => '⛅', 1785 => '⛆', 1786 => '⛇', 1787 => '⛈', 1788 => '⛉', 1789 => '⛊', 1790 => '⛋', 1791 => '⛌', 1792 => '⛍', 1793 => '⛎', 1794 => '⛏', 1795 => '⛐', 1796 => '⛑', 1797 => '⛒', 1798 => '⛓', 1799 => '⛔', 1800 => '⛕', 1801 => '⛖', 1802 => '⛗', 1803 => '⛘', 1804 => '⛙', 1805 => '⛚', 1806 => '⛛', 1807 => '⛜', 1808 => '⛝', 1809 => '⛞', 1810 => '⛟', 1811 => '⛠', 1812 => '⛡', 1813 => '⛢', 1814 => '⛣', 1815 => '⛤', 1816 => '⛥', 1817 => '⛦', 1818 => '⛧', 1819 => '⛨', 1820 => '⛩', 1821 => '⛪', 1822 => '⛫', 1823 => '⛬', 1824 => '⛭', 1825 => '⛮', 1826 => '⛯', 1827 => '⛰', 1828 => '⛱', 1829 => '⛲', 1830 => '⛳', 1831 => '⛴', 1832 => '⛵', 1833 => '⛶', 1834 => '⛷', 1835 => '⛸', 1836 => '⛹', 1837 => '⛺', 1838 => '⛻', 1839 => '⛼', 1840 => '⛽', 1841 => '⛾', 1842 => '⛿', 1843 => '✁', 1844 => '✂', 1845 => '✃', 1846 => '✄', 1847 => '✅', 1848 => '✆', 1849 => '✇', 1850 => '✈', 1851 => '✉', 1852 => '✊', 1853 => '✋', 1854 => '✌', 1855 => '✍', 1856 => '✎', 1857 => '✏', 1858 => '✐', 1859 => '✑', 1860 => '✒', 1861 => '✓', 1862 => '✔', 1863 => '✕', 1864 => '✖', 1865 => '✗', 1866 => '✘', 1867 => '✙', 1868 => '✚', 1869 => '✛', 1870 => '✜', 1871 => '✝', 1872 => '✞', 1873 => '✟', 1874 => '✠', 1875 => '✡', 1876 => '✢', 1877 => '✣', 1878 => '✤', 1879 => '✥', 1880 => '✦', 1881 => '✧', 1882 => '✨', 1883 => '✩', 1884 => '✪', 1885 => '✫', 1886 => '✬', 1887 => '✭', 1888 => '✮', 1889 => '✯', 1890 => '✰', 1891 => '✱', 1892 => '✲', 1893 => '✳', 1894 => '✴', 1895 => '✵', 1896 => '✶', 1897 => '✷', 1898 => '✸', 1899 => '✹', 1900 => '✺', 1901 => '✻', 1902 => '✼', 1903 => '✽', 1904 => '✾', 1905 => '✿', 1906 => '❀', 1907 => '❁', 1908 => '❂', 1909 => '❃', 1910 => '❄', 1911 => '❅', 1912 => '❆', 1913 => '❇', 1914 => '❈', 1915 => '❉', 1916 => '❊', 1917 => '❋', 1918 => '❌', 1919 => '❍', 1920 => '❎', 1921 => '❏', 1922 => '❐', 1923 => '❑', 1924 => '❒', 1925 => '❓', 1926 => '❔', 1927 => '❕', 1928 => '❖', 1929 => '❗', 1930 => '❘', 1931 => '❙', 1932 => '❚', 1933 => '❛', 1934 => '❜', 1935 => '❝', 1936 => '❞', 1937 => '❟', 1938 => '❠', 1939 => '❡', 1940 => '❢', 1941 => '❣', 1942 => '❤', 1943 => '❥', 1944 => '❦', 1945 => '❧', 1946 => '❨', 1947 => '❩', 1948 => '❪', 1949 => '❫', 1950 => '❬', 1951 => '❭', 1952 => '❮', 1953 => '❯', 1954 => '❰', 1955 => '❱', 1956 => '❲', 1957 => '❳', 1958 => '❴', 1959 => '❵', 1960 => '➔', 1961 => '➕', 1962 => '➖', 1963 => '➗', 1964 => '➘', 1965 => '➙', 1966 => '➚', 1967 => '➛', 1968 => '➜', 1969 => '➝', 1970 => '➞', 1971 => '➟', 1972 => '➠', 1973 => '➡', 1974 => '➢', 1975 => '➣', 1976 => '➤', 1977 => '➥', 1978 => '➦', 1979 => '➧', 1980 => '➨', 1981 => '➩', 1982 => '➪', 1983 => '➫', 1984 => '➬', 1985 => '➭', 1986 => '➮', 1987 => '➯', 1988 => '➰', 1989 => '➱', 1990 => '➲', 1991 => '➳', 1992 => '➴', 1993 => '➵', 1994 => '➶', 1995 => '➷', 1996 => '➸', 1997 => '➹', 1998 => '➺', 1999 => '➻', 2000 => '➼', 2001 => '➽', 2002 => '➾', 2003 => '➿', 2004 => '⟀', 2005 => '⟁', 2006 => '⟂', 2007 => '⟃', 2008 => '⟄', 2009 => '⟅', 2010 => '⟆', 2011 => '⟇', 2012 => '⟈', 2013 => '⟉', 2014 => '⟊', 2015 => '⟌', 2016 => '⟎', 2017 => '⟏', 2018 => '⟐', 2019 => '⟑', 2020 => '⟒', 2021 => '⟓', 2022 => '⟔', 2023 => '⟕', 2024 => '⟖', 2025 => '⟗', 2026 => '⟘', 2027 => '⟙', 2028 => '⟚', 2029 => '⟛', 2030 => '⟜', 2031 => '⟝', 2032 => '⟞', 2033 => '⟟', 2034 => '⟠', 2035 => '⟡', 2036 => '⟢', 2037 => '⟣', 2038 => '⟤', 2039 => '⟥', 2040 => '⟦', 2041 => '⟧', 2042 => '⟨', 2043 => '⟩', 2044 => '⟪', 2045 => '⟫', 2046 => '⟰', 2047 => '⟱', 2048 => '⟲', 2049 => '⟳', 2050 => '⟴', 2051 => '⟵', 2052 => '⟶', 2053 => '⟷', 2054 => '⟸', 2055 => '⟹', 2056 => '⟺', 2057 => '⟻', 2058 => '⟼', 2059 => '⟽', 2060 => '⟾', 2061 => '⟿', 2062 => '⤀', 2063 => '⤁', 2064 => '⤂', 2065 => '⤃', 2066 => '⤄', 2067 => '⤅', 2068 => '⤆', 2069 => '⤇', 2070 => '⤈', 2071 => '⤉', 2072 => '⤊', 2073 => '⤋', 2074 => '⤌', 2075 => '⤍', 2076 => '⤎', 2077 => '⤏', 2078 => '⤐', 2079 => '⤑', 2080 => '⤒', 2081 => '⤓', 2082 => '⤔', 2083 => '⤕', 2084 => '⤖', 2085 => '⤗', 2086 => '⤘', 2087 => '⤙', 2088 => '⤚', 2089 => '⤛', 2090 => '⤜', 2091 => '⤝', 2092 => '⤞', 2093 => '⤟', 2094 => '⤠', 2095 => '⤡', 2096 => '⤢', 2097 => '⤣', 2098 => '⤤', 2099 => '⤥', 2100 => '⤦', 2101 => '⤧', 2102 => '⤨', 2103 => '⤩', 2104 => '⤪', 2105 => '⤫', 2106 => '⤬', 2107 => '⤭', 2108 => '⤮', 2109 => '⤯', 2110 => '⤰', 2111 => '⤱', 2112 => '⤲', 2113 => '⤳', 2114 => '⤴', 2115 => '⤵', 2116 => '⤶', 2117 => '⤷', 2118 => '⤸', 2119 => '⤹', 2120 => '⤺', 2121 => '⤻', 2122 => '⤼', 2123 => '⤽', 2124 => '⤾', 2125 => '⤿', 2126 => '⥀', 2127 => '⥁', 2128 => '⥂', 2129 => '⥃', 2130 => '⥄', 2131 => '⥅', 2132 => '⥆', 2133 => '⥇', 2134 => '⥈', 2135 => '⥉', 2136 => '⥊', 2137 => '⥋', 2138 => '⥌', 2139 => '⥍', 2140 => '⥎', 2141 => '⥏', 2142 => '⥐', 2143 => '⥑', 2144 => '⥒', 2145 => '⥓', 2146 => '⥔', 2147 => '⥕', 2148 => '⥖', 2149 => '⥗', 2150 => '⥘', 2151 => '⥙', 2152 => '⥚', 2153 => '⥛', 2154 => '⥜', 2155 => '⥝', 2156 => '⥞', 2157 => '⥟', 2158 => '⥠', 2159 => '⥡', 2160 => '⥢', 2161 => '⥣', 2162 => '⥤', 2163 => '⥥', 2164 => '⥦', 2165 => '⥧', 2166 => '⥨', 2167 => '⥩', 2168 => '⥪', 2169 => '⥫', 2170 => '⥬', 2171 => '⥭', 2172 => '⥮', 2173 => '⥯', 2174 => '⥰', 2175 => '⥱', 2176 => '⥲', 2177 => '⥳', 2178 => '⥴', 2179 => '⥵', 2180 => '⥶', 2181 => '⥷', 2182 => '⥸', 2183 => '⥹', 2184 => '⥺', 2185 => '⥻', 2186 => '⥼', 2187 => '⥽', 2188 => '⥾', 2189 => '⥿', 2190 => '⦀', 2191 => '⦁', 2192 => '⦂', 2193 => '⦙', 2194 => '⦚', 2195 => '⦛', 2196 => '⦜', 2197 => '⦝', 2198 => '⦞', 2199 => '⦟', 2200 => '⦠', 2201 => '⦡', 2202 => '⦢', 2203 => '⦣', 2204 => '⦤', 2205 => '⦥', 2206 => '⦦', 2207 => '⦧', 2208 => '⦨', 2209 => '⦩', 2210 => '⦪', 2211 => '⦫', 2212 => '⦬', 2213 => '⦭', 2214 => '⦮', 2215 => '⦯', 2216 => '⦰', 2217 => '⦱', 2218 => '⦲', 2219 => '⦳', 2220 => '⦴', 2221 => '⦵', 2222 => '⦶', 2223 => '⦷', 2224 => '⦸', 2225 => '⦹', 2226 => '⦺', 2227 => '⦻', 2228 => '⦼', 2229 => '⦽', 2230 => '⦾', 2231 => '⦿', 2232 => '⧀', 2233 => '⧁', 2234 => '⧂', 2235 => '⧃', 2236 => '⧄', 2237 => '⧅', 2238 => '⧆', 2239 => '⧇', 2240 => '⧈', 2241 => '⧉', 2242 => '⧊', 2243 => '⧋', 2244 => '⧌', 2245 => '⧍', 2246 => '⧎', 2247 => '⧏', 2248 => '⧐', 2249 => '⧑', 2250 => '⧒', 2251 => '⧓', 2252 => '⧔', 2253 => '⧕', 2254 => '⧖', 2255 => '⧗', 2256 => '⧘', 2257 => '⧙', 2258 => '⧚', 2259 => '⧛', 2260 => '⧜', 2261 => '⧝', 2262 => '⧞', 2263 => '⧟', 2264 => '⧠', 2265 => '⧡', 2266 => '⧢', 2267 => '⧣', 2268 => '⧤', 2269 => '⧥', 2270 => '⧦', 2271 => '⧧', 2272 => '⧨', 2273 => '⧩', 2274 => '⧪', 2275 => '⧫', 2276 => '⧬', 2277 => '⧭', 2278 => '⧮', 2279 => '⧯', 2280 => '⧰', 2281 => '⧱', 2282 => '⧲', 2283 => '⧳', 2284 => '⧴', 2285 => '⧵', 2286 => '⧶', 2287 => '⧷', 2288 => '⧸', 2289 => '⧹', 2290 => '⧺', 2291 => '⧻', 2292 => '⧾', 2293 => '⧿', 2294 => '⨀', 2295 => '⨁', 2296 => '⨂', 2297 => '⨃', 2298 => '⨄', 2299 => '⨅', 2300 => '⨆', 2301 => '⨇', 2302 => '⨈', 2303 => '⨉', 2304 => '⨊', 2305 => '⨋', 2306 => '⨍', 2307 => '⨎', 2308 => '⨏', 2309 => '⨐', 2310 => '⨑', 2311 => '⨒', 2312 => '⨓', 2313 => '⨔', 2314 => '⨕', 2315 => '⨖', 2316 => '⨗', 2317 => '⨘', 2318 => '⨙', 2319 => '⨚', 2320 => '⨛', 2321 => '⨜', 2322 => '⨝', 2323 => '⨞', 2324 => '⨟', 2325 => '⨠', 2326 => '⨡', 2327 => '⨢', 2328 => '⨣', 2329 => '⨤', 2330 => '⨥', 2331 => '⨦', 2332 => '⨧', 2333 => '⨨', 2334 => '⨩', 2335 => '⨪', 2336 => '⨫', 2337 => '⨬', 2338 => '⨭', 2339 => '⨮', 2340 => '⨯', 2341 => '⨰', 2342 => '⨱', 2343 => '⨲', 2344 => '⨳', 2345 => '⨴', 2346 => '⨵', 2347 => '⨶', 2348 => '⨷', 2349 => '⨸', 2350 => '⨹', 2351 => '⨺', 2352 => '⨻', 2353 => '⨼', 2354 => '⨽', 2355 => '⨾', 2356 => '⨿', 2357 => '⩀', 2358 => '⩁', 2359 => '⩂', 2360 => '⩃', 2361 => '⩄', 2362 => '⩅', 2363 => '⩆', 2364 => '⩇', 2365 => '⩈', 2366 => '⩉', 2367 => '⩊', 2368 => '⩋', 2369 => '⩌', 2370 => '⩍', 2371 => '⩎', 2372 => '⩏', 2373 => '⩐', 2374 => '⩑', 2375 => '⩒', 2376 => '⩓', 2377 => '⩔', 2378 => '⩕', 2379 => '⩖', 2380 => '⩗', 2381 => '⩘', 2382 => '⩙', 2383 => '⩚', 2384 => '⩛', 2385 => '⩜', 2386 => '⩝', 2387 => '⩞', 2388 => '⩟', 2389 => '⩠', 2390 => '⩡', 2391 => '⩢', 2392 => '⩣', 2393 => '⩤', 2394 => '⩥', 2395 => '⩦', 2396 => '⩧', 2397 => '⩨', 2398 => '⩩', 2399 => '⩪', 2400 => '⩫', 2401 => '⩬', 2402 => '⩭', 2403 => '⩮', 2404 => '⩯', 2405 => '⩰', 2406 => '⩱', 2407 => '⩲', 2408 => '⩳', 2409 => '⩷', 2410 => '⩸', 2411 => '⩹', 2412 => '⩺', 2413 => '⩻', 2414 => '⩼', 2415 => '⩽', 2416 => '⩾', 2417 => '⩿', 2418 => '⪀', 2419 => '⪁', 2420 => '⪂', 2421 => '⪃', 2422 => '⪄', 2423 => '⪅', 2424 => '⪆', 2425 => '⪇', 2426 => '⪈', 2427 => '⪉', 2428 => '⪊', 2429 => '⪋', 2430 => '⪌', 2431 => '⪍', 2432 => '⪎', 2433 => '⪏', 2434 => '⪐', 2435 => '⪑', 2436 => '⪒', 2437 => '⪓', 2438 => '⪔', 2439 => '⪕', 2440 => '⪖', 2441 => '⪗', 2442 => '⪘', 2443 => '⪙', 2444 => '⪚', 2445 => '⪛', 2446 => '⪜', 2447 => '⪝', 2448 => '⪞', 2449 => '⪟', 2450 => '⪠', 2451 => '⪡', 2452 => '⪢', 2453 => '⪣', 2454 => '⪤', 2455 => '⪥', 2456 => '⪦', 2457 => '⪧', 2458 => '⪨', 2459 => '⪩', 2460 => '⪪', 2461 => '⪫', 2462 => '⪬', 2463 => '⪭', 2464 => '⪮', 2465 => '⪯', 2466 => '⪰', 2467 => '⪱', 2468 => '⪲', 2469 => '⪳', 2470 => '⪴', 2471 => '⪵', 2472 => '⪶', 2473 => '⪷', 2474 => '⪸', 2475 => '⪹', 2476 => '⪺', 2477 => '⪻', 2478 => '⪼', 2479 => '⪽', 2480 => '⪾', 2481 => '⪿', 2482 => '⫀', 2483 => '⫁', 2484 => '⫂', 2485 => '⫃', 2486 => '⫄', 2487 => '⫅', 2488 => '⫆', 2489 => '⫇', 2490 => '⫈', 2491 => '⫉', 2492 => '⫊', 2493 => '⫋', 2494 => '⫌', 2495 => '⫍', 2496 => '⫎', 2497 => '⫏', 2498 => '⫐', 2499 => '⫑', 2500 => '⫒', 2501 => '⫓', 2502 => '⫔', 2503 => '⫕', 2504 => '⫖', 2505 => '⫗', 2506 => '⫘', 2507 => '⫙', 2508 => '⫚', 2509 => '⫛', 2510 => '⫝', 2511 => '⫞', 2512 => '⫟', 2513 => '⫠', 2514 => '⫡', 2515 => '⫢', 2516 => '⫣', 2517 => '⫤', 2518 => '⫥', 2519 => '⫦', 2520 => '⫧', 2521 => '⫨', 2522 => '⫩', 2523 => '⫪', 2524 => '⫫', 2525 => '⫬', 2526 => '⫭', 2527 => '⫮', 2528 => '⫯', 2529 => '⫰', 2530 => '⫱', 2531 => '⫲', 2532 => '⫳', 2533 => '⫴', 2534 => '⫵', 2535 => '⫶', 2536 => '⫷', 2537 => '⫸', 2538 => '⫹', 2539 => '⫺', 2540 => '⫻', 2541 => '⫼', 2542 => '⫽', 2543 => '⫾', 2544 => '⫿', 2545 => '⬀', 2546 => '⬁', 2547 => '⬂', 2548 => '⬃', 2549 => '⬄', 2550 => '⬅', 2551 => '⬆', 2552 => '⬇', 2553 => '⬈', 2554 => '⬉', 2555 => '⬊', 2556 => '⬋', 2557 => '⬌', 2558 => '⬍', 2559 => '⬎', 2560 => '⬏', 2561 => '⬐', 2562 => '⬑', 2563 => '⬒', 2564 => '⬓', 2565 => '⬔', 2566 => '⬕', 2567 => '⬖', 2568 => '⬗', 2569 => '⬘', 2570 => '⬙', 2571 => '⬚', 2572 => '⬛', 2573 => '⬜', 2574 => '⬝', 2575 => '⬞', 2576 => '⬟', 2577 => '⬠', 2578 => '⬡', 2579 => '⬢', 2580 => '⬣', 2581 => '⬤', 2582 => '⬥', 2583 => '⬦', 2584 => '⬧', 2585 => '⬨', 2586 => '⬩', 2587 => '⬪', 2588 => '⬫', 2589 => '⬬', 2590 => '⬭', 2591 => '⬮', 2592 => '⬯', 2593 => '⬰', 2594 => '⬱', 2595 => '⬲', 2596 => '⬳', 2597 => '⬴', 2598 => '⬵', 2599 => '⬶', 2600 => '⬷', 2601 => '⬸', 2602 => '⬹', 2603 => '⬺', 2604 => '⬻', 2605 => '⬼', 2606 => '⬽', 2607 => '⬾', 2608 => '⬿', 2609 => '⭀', 2610 => '⭁', 2611 => '⭂', 2612 => '⭃', 2613 => '⭄', 2614 => '⭅', 2615 => '⭆', 2616 => '⭇', 2617 => '⭈', 2618 => '⭉', 2619 => '⭊', 2620 => '⭋', 2621 => '⭌', 2622 => '⭐', 2623 => '⭑', 2624 => '⭒', 2625 => '⭓', 2626 => '⭔', 2627 => '⭕', 2628 => '⭖', 2629 => '⭗', 2630 => '⭘', 2631 => '⭙', 2632 => '⳥', 2633 => '⳦', 2634 => '⳧', 2635 => '⳨', 2636 => '⳩', 2637 => '⳪', 2638 => '⠀', 2639 => '⠁', 2640 => '⠂', 2641 => '⠃', 2642 => '⠄', 2643 => '⠅', 2644 => '⠆', 2645 => '⠇', 2646 => '⠈', 2647 => '⠉', 2648 => '⠊', 2649 => '⠋', 2650 => '⠌', 2651 => '⠍', 2652 => '⠎', 2653 => '⠏', 2654 => '⠐', 2655 => '⠑', 2656 => '⠒', 2657 => '⠓', 2658 => '⠔', 2659 => '⠕', 2660 => '⠖', 2661 => '⠗', 2662 => '⠘', 2663 => '⠙', 2664 => '⠚', 2665 => '⠛', 2666 => '⠜', 2667 => '⠝', 2668 => '⠞', 2669 => '⠟', 2670 => '⠠', 2671 => '⠡', 2672 => '⠢', 2673 => '⠣', 2674 => '⠤', 2675 => '⠥', 2676 => '⠦', 2677 => '⠧', 2678 => '⠨', 2679 => '⠩', 2680 => '⠪', 2681 => '⠫', 2682 => '⠬', 2683 => '⠭', 2684 => '⠮', 2685 => '⠯', 2686 => '⠰', 2687 => '⠱', 2688 => '⠲', 2689 => '⠳', 2690 => '⠴', 2691 => '⠵', 2692 => '⠶', 2693 => '⠷', 2694 => '⠸', 2695 => '⠹', 2696 => '⠺', 2697 => '⠻', 2698 => '⠼', 2699 => '⠽', 2700 => '⠾', 2701 => '⠿', 2702 => '⡀', 2703 => '⡁', 2704 => '⡂', 2705 => '⡃', 2706 => '⡄', 2707 => '⡅', 2708 => '⡆', 2709 => '⡇', 2710 => '⡈', 2711 => '⡉', 2712 => '⡊', 2713 => '⡋', 2714 => '⡌', 2715 => '⡍', 2716 => '⡎', 2717 => '⡏', 2718 => '⡐', 2719 => '⡑', 2720 => '⡒', 2721 => '⡓', 2722 => '⡔', 2723 => '⡕', 2724 => '⡖', 2725 => '⡗', 2726 => '⡘', 2727 => '⡙', 2728 => '⡚', 2729 => '⡛', 2730 => '⡜', 2731 => '⡝', 2732 => '⡞', 2733 => '⡟', 2734 => '⡠', 2735 => '⡡', 2736 => '⡢', 2737 => '⡣', 2738 => '⡤', 2739 => '⡥', 2740 => '⡦', 2741 => '⡧', 2742 => '⡨', 2743 => '⡩', 2744 => '⡪', 2745 => '⡫', 2746 => '⡬', 2747 => '⡭', 2748 => '⡮', 2749 => '⡯', 2750 => '⡰', 2751 => '⡱', 2752 => '⡲', 2753 => '⡳', 2754 => '⡴', 2755 => '⡵', 2756 => '⡶', 2757 => '⡷', 2758 => '⡸', 2759 => '⡹', 2760 => '⡺', 2761 => '⡻', 2762 => '⡼', 2763 => '⡽', 2764 => '⡾', 2765 => '⡿', 2766 => '⢀', 2767 => '⢁', 2768 => '⢂', 2769 => '⢃', 2770 => '⢄', 2771 => '⢅', 2772 => '⢆', 2773 => '⢇', 2774 => '⢈', 2775 => '⢉', 2776 => '⢊', 2777 => '⢋', 2778 => '⢌', 2779 => '⢍', 2780 => '⢎', 2781 => '⢏', 2782 => '⢐', 2783 => '⢑', 2784 => '⢒', 2785 => '⢓', 2786 => '⢔', 2787 => '⢕', 2788 => '⢖', 2789 => '⢗', 2790 => '⢘', 2791 => '⢙', 2792 => '⢚', 2793 => '⢛', 2794 => '⢜', 2795 => '⢝', 2796 => '⢞', 2797 => '⢟', 2798 => '⢠', 2799 => '⢡', 2800 => '⢢', 2801 => '⢣', 2802 => '⢤', 2803 => '⢥', 2804 => '⢦', 2805 => '⢧', 2806 => '⢨', 2807 => '⢩', 2808 => '⢪', 2809 => '⢫', 2810 => '⢬', 2811 => '⢭', 2812 => '⢮', 2813 => '⢯', 2814 => '⢰', 2815 => '⢱', 2816 => '⢲', 2817 => '⢳', 2818 => '⢴', 2819 => '⢵', 2820 => '⢶', 2821 => '⢷', 2822 => '⢸', 2823 => '⢹', 2824 => '⢺', 2825 => '⢻', 2826 => '⢼', 2827 => '⢽', 2828 => '⢾', 2829 => '⢿', 2830 => '⣀', 2831 => '⣁', 2832 => '⣂', 2833 => '⣃', 2834 => '⣄', 2835 => '⣅', 2836 => '⣆', 2837 => '⣇', 2838 => '⣈', 2839 => '⣉', 2840 => '⣊', 2841 => '⣋', 2842 => '⣌', 2843 => '⣍', 2844 => '⣎', 2845 => '⣏', 2846 => '⣐', 2847 => '⣑', 2848 => '⣒', 2849 => '⣓', 2850 => '⣔', 2851 => '⣕', 2852 => '⣖', 2853 => '⣗', 2854 => '⣘', 2855 => '⣙', 2856 => '⣚', 2857 => '⣛', 2858 => '⣜', 2859 => '⣝', 2860 => '⣞', 2861 => '⣟', 2862 => '⣠', 2863 => '⣡', 2864 => '⣢', 2865 => '⣣', 2866 => '⣤', 2867 => '⣥', 2868 => '⣦', 2869 => '⣧', 2870 => '⣨', 2871 => '⣩', 2872 => '⣪', 2873 => '⣫', 2874 => '⣬', 2875 => '⣭', 2876 => '⣮', 2877 => '⣯', 2878 => '⣰', 2879 => '⣱', 2880 => '⣲', 2881 => '⣳', 2882 => '⣴', 2883 => '⣵', 2884 => '⣶', 2885 => '⣷', 2886 => '⣸', 2887 => '⣹', 2888 => '⣺', 2889 => '⣻', 2890 => '⣼', 2891 => '⣽', 2892 => '⣾', 2893 => '⣿', 2894 => '⚊', 2895 => '⚋', 2896 => '⚌', 2897 => '⚍', 2898 => '⚎', 2899 => '⚏', 2900 => '☰', 2901 => '☱', 2902 => '☲', 2903 => '☳', 2904 => '☴', 2905 => '☵', 2906 => '☶', 2907 => '☷', 2908 => '䷀', 2909 => '䷁', 2910 => '䷂', 2911 => '䷃', 2912 => '䷄', 2913 => '䷅', 2914 => '䷆', 2915 => '䷇', 2916 => '䷈', 2917 => '䷉', 2918 => '䷊', 2919 => '䷋', 2920 => '䷌', 2921 => '䷍', 2922 => '䷎', 2923 => '䷏', 2924 => '䷐', 2925 => '䷑', 2926 => '䷒', 2927 => '䷓', 2928 => '䷔', 2929 => '䷕', 2930 => '䷖', 2931 => '䷗', 2932 => '䷘', 2933 => '䷙', 2934 => '䷚', 2935 => '䷛', 2936 => '䷜', 2937 => '䷝', 2938 => '䷞', 2939 => '䷟', 2940 => '䷠', 2941 => '䷡', 2942 => '䷢', 2943 => '䷣', 2944 => '䷤', 2945 => '䷥', 2946 => '䷦', 2947 => '䷧', 2948 => '䷨', 2949 => '䷩', 2950 => '䷪', 2951 => '䷫', 2952 => '䷬', 2953 => '䷭', 2954 => '䷮', 2955 => '䷯', 2956 => '䷰', 2957 => '䷱', 2958 => '䷲', 2959 => '䷳', 2960 => '䷴', 2961 => '䷵', 2962 => '䷶', 2963 => '䷷', 2964 => '䷸', 2965 => '䷹', 2966 => '䷺', 2967 => '䷻', 2968 => '䷼', 2969 => '䷽', 2970 => '䷾', 2971 => '䷿', 2972 => '𝌀', 2973 => '𝌁', 2974 => '𝌂', 2975 => '𝌃', 2976 => '𝌄', 2977 => '𝌅', 2978 => '𝌆', 2979 => '𝌇', 2980 => '𝌈', 2981 => '𝌉', 2982 => '𝌊', 2983 => '𝌋', 2984 => '𝌌', 2985 => '𝌍', 2986 => '𝌎', 2987 => '𝌏', 2988 => '𝌐', 2989 => '𝌑', 2990 => '𝌒', 2991 => '𝌓', 2992 => '𝌔', 2993 => '𝌕', 2994 => '𝌖', 2995 => '𝌗', 2996 => '𝌘', 2997 => '𝌙', 2998 => '𝌚', 2999 => '𝌛', 3000 => '𝌜', 3001 => '𝌝', 3002 => '𝌞', 3003 => '𝌟', 3004 => '𝌠', 3005 => '𝌡', 3006 => '𝌢', 3007 => '𝌣', 3008 => '𝌤', 3009 => '𝌥', 3010 => '𝌦', 3011 => '𝌧', 3012 => '𝌨', 3013 => '𝌩', 3014 => '𝌪', 3015 => '𝌫', 3016 => '𝌬', 3017 => '𝌭', 3018 => '𝌮', 3019 => '𝌯', 3020 => '𝌰', 3021 => '𝌱', 3022 => '𝌲', 3023 => '𝌳', 3024 => '𝌴', 3025 => '𝌵', 3026 => '𝌶', 3027 => '𝌷', 3028 => '𝌸', 3029 => '𝌹', 3030 => '𝌺', 3031 => '𝌻', 3032 => '𝌼', 3033 => '𝌽', 3034 => '𝌾', 3035 => '𝌿', 3036 => '𝍀', 3037 => '𝍁', 3038 => '𝍂', 3039 => '𝍃', 3040 => '𝍄', 3041 => '𝍅', 3042 => '𝍆', 3043 => '𝍇', 3044 => '𝍈', 3045 => '𝍉', 3046 => '𝍊', 3047 => '𝍋', 3048 => '𝍌', 3049 => '𝍍', 3050 => '𝍎', 3051 => '𝍏', 3052 => '𝍐', 3053 => '𝍑', 3054 => '𝍒', 3055 => '𝍓', 3056 => '𝍔', 3057 => '𝍕', 3058 => '𝍖', 3059 => '꒐', 3060 => '꒑', 3061 => '꒒', 3062 => '꒓', 3063 => '꒔', 3064 => '꒕', 3065 => '꒖', 3066 => '꒗', 3067 => '꒘', 3068 => '꒙', 3069 => '꒚', 3070 => '꒛', 3071 => '꒜', 3072 => '꒝', 3073 => '꒞', 3074 => '꒟', 3075 => '꒠', 3076 => '꒡', 3077 => '꒢', 3078 => '꒣', 3079 => '꒤', 3080 => '꒥', 3081 => '꒦', 3082 => '꒧', 3083 => '꒨', 3084 => '꒩', 3085 => '꒪', 3086 => '꒫', 3087 => '꒬', 3088 => '꒭', 3089 => '꒮', 3090 => '꒯', 3091 => '꒰', 3092 => '꒱', 3093 => '꒲', 3094 => '꒳', 3095 => '꒴', 3096 => '꒵', 3097 => '꒶', 3098 => '꒷', 3099 => '꒸', 3100 => '꒹', 3101 => '꒺', 3102 => '꒻', 3103 => '꒼', 3104 => '꒽', 3105 => '꒾', 3106 => '꒿', 3107 => '꓀', 3108 => '꓁', 3109 => '꓂', 3110 => '꓃', 3111 => '꓄', 3112 => '꓅', 3113 => '꓆', 3114 => '𐄷', 3115 => '𐄸', 3116 => '𐄹', 3117 => '𐄺', 3118 => '𐄻', 3119 => '𐄼', 3120 => '𐄽', 3121 => '𐄾', 3122 => '𐄿', 3123 => '𐅹', 3124 => '𐅺', 3125 => '𐅻', 3126 => '𐅼', 3127 => '𐅽', 3128 => '𐅾', 3129 => '𐅿', 3130 => '𐆀', 3131 => '𐆁', 3132 => '𐆂', 3133 => '𐆃', 3134 => '𐆄', 3135 => '𐆅', 3136 => '𐆆', 3137 => '𐆇', 3138 => '𐆈', 3139 => '𐆉', 3140 => '𐆐', 3141 => '𐆑', 3142 => '𐆒', 3143 => '𐆓', 3144 => '𐆔', 3145 => '𐆕', 3146 => '𐆖', 3147 => '𐆗', 3148 => '𐆘', 3149 => '𐆙', 3150 => '𐆚', 3151 => '𐆛', 3152 => '𐇐', 3153 => '𐇑', 3154 => '𐇒', 3155 => '𐇓', 3156 => '𐇔', 3157 => '𐇕', 3158 => '𐇖', 3159 => '𐇗', 3160 => '𐇘', 3161 => '𐇙', 3162 => '𐇚', 3163 => '𐇛', 3164 => '𐇜', 3165 => '𐇝', 3166 => '𐇞', 3167 => '𐇟', 3168 => '𐇠', 3169 => '𐇡', 3170 => '𐇢', 3171 => '𐇣', 3172 => '𐇤', 3173 => '𐇥', 3174 => '𐇦', 3175 => '𐇧', 3176 => '𐇨', 3177 => '𐇩', 3178 => '𐇪', 3179 => '𐇫', 3180 => '𐇬', 3181 => '𐇭', 3182 => '𐇮', 3183 => '𐇯', 3184 => '𐇰', 3185 => '𐇱', 3186 => '𐇲', 3187 => '𐇳', 3188 => '𐇴', 3189 => '𐇵', 3190 => '𐇶', 3191 => '𐇷', 3192 => '𐇸', 3193 => '𐇹', 3194 => '𐇺', 3195 => '𐇻', 3196 => '𐇼', 3197 => '𝀀', 3198 => '𝀁', 3199 => '𝀂', 3200 => '𝀃', 3201 => '𝀄', 3202 => '𝀅', 3203 => '𝀆', 3204 => '𝀇', 3205 => '𝀈', 3206 => '𝀉', 3207 => '𝀊', 3208 => '𝀋', 3209 => '𝀌', 3210 => '𝀍', 3211 => '𝀎', 3212 => '𝀏', 3213 => '𝀐', 3214 => '𝀑', 3215 => '𝀒', 3216 => '𝀓', 3217 => '𝀔', 3218 => '𝀕', 3219 => '𝀖', 3220 => '𝀗', 3221 => '𝀘', 3222 => '𝀙', 3223 => '𝀚', 3224 => '𝀛', 3225 => '𝀜', 3226 => '𝀝', 3227 => '𝀞', 3228 => '𝀟', 3229 => '𝀠', 3230 => '𝀡', 3231 => '𝀢', 3232 => '𝀣', 3233 => '𝀤', 3234 => '𝀥', 3235 => '𝀦', 3236 => '𝀧', 3237 => '𝀨', 3238 => '𝀩', 3239 => '𝀪', 3240 => '𝀫', 3241 => '𝀬', 3242 => '𝀭', 3243 => '𝀮', 3244 => '𝀯', 3245 => '𝀰', 3246 => '𝀱', 3247 => '𝀲', 3248 => '𝀳', 3249 => '𝀴', 3250 => '𝀵', 3251 => '𝀶', 3252 => '𝀷', 3253 => '𝀸', 3254 => '𝀹', 3255 => '𝀺', 3256 => '𝀻', 3257 => '𝀼', 3258 => '𝀽', 3259 => '𝀾', 3260 => '𝀿', 3261 => '𝁀', 3262 => '𝁁', 3263 => '𝁂', 3264 => '𝁃', 3265 => '𝁄', 3266 => '𝁅', 3267 => '𝁆', 3268 => '𝁇', 3269 => '𝁈', 3270 => '𝁉', 3271 => '𝁊', 3272 => '𝁋', 3273 => '𝁌', 3274 => '𝁍', 3275 => '𝁎', 3276 => '𝁏', 3277 => '𝁐', 3278 => '𝁑', 3279 => '𝁒', 3280 => '𝁓', 3281 => '𝁔', 3282 => '𝁕', 3283 => '𝁖', 3284 => '𝁗', 3285 => '𝁘', 3286 => '𝁙', 3287 => '𝁚', 3288 => '𝁛', 3289 => '𝁜', 3290 => '𝁝', 3291 => '𝁞', 3292 => '𝁟', 3293 => '𝁠', 3294 => '𝁡', 3295 => '𝁢', 3296 => '𝁣', 3297 => '𝁤', 3298 => '𝁥', 3299 => '𝁦', 3300 => '𝁧', 3301 => '𝁨', 3302 => '𝁩', 3303 => '𝁪', 3304 => '𝁫', 3305 => '𝁬', 3306 => '𝁭', 3307 => '𝁮', 3308 => '𝁯', 3309 => '𝁰', 3310 => '𝁱', 3311 => '𝁲', 3312 => '𝁳', 3313 => '𝁴', 3314 => '𝁵', 3315 => '𝁶', 3316 => '𝁷', 3317 => '𝁸', 3318 => '𝁹', 3319 => '𝁺', 3320 => '𝁻', 3321 => '𝁼', 3322 => '𝁽', 3323 => '𝁾', 3324 => '𝁿', 3325 => '𝂀', 3326 => '𝂁', 3327 => '𝂂', 3328 => '𝂃', 3329 => '𝂄', 3330 => '𝂅', 3331 => '𝂆', 3332 => '𝂇', 3333 => '𝂈', 3334 => '𝂉', 3335 => '𝂊', 3336 => '𝂋', 3337 => '𝂌', 3338 => '𝂍', 3339 => '𝂎', 3340 => '𝂏', 3341 => '𝂐', 3342 => '𝂑', 3343 => '𝂒', 3344 => '𝂓', 3345 => '𝂔', 3346 => '𝂕', 3347 => '𝂖', 3348 => '𝂗', 3349 => '𝂘', 3350 => '𝂙', 3351 => '𝂚', 3352 => '𝂛', 3353 => '𝂜', 3354 => '𝂝', 3355 => '𝂞', 3356 => '𝂟', 3357 => '𝂠', 3358 => '𝂡', 3359 => '𝂢', 3360 => '𝂣', 3361 => '𝂤', 3362 => '𝂥', 3363 => '𝂦', 3364 => '𝂧', 3365 => '𝂨', 3366 => '𝂩', 3367 => '𝂪', 3368 => '𝂫', 3369 => '𝂬', 3370 => '𝂭', 3371 => '𝂮', 3372 => '𝂯', 3373 => '𝂰', 3374 => '𝂱', 3375 => '𝂲', 3376 => '𝂳', 3377 => '𝂴', 3378 => '𝂵', 3379 => '𝂶', 3380 => '𝂷', 3381 => '𝂸', 3382 => '𝂹', 3383 => '𝂺', 3384 => '𝂻', 3385 => '𝂼', 3386 => '𝂽', 3387 => '𝂾', 3388 => '𝂿', 3389 => '𝃀', 3390 => '𝃁', 3391 => '𝃂', 3392 => '𝃃', 3393 => '𝃄', 3394 => '𝃅', 3395 => '𝃆', 3396 => '𝃇', 3397 => '𝃈', 3398 => '𝃉', 3399 => '𝃊', 3400 => '𝃋', 3401 => '𝃌', 3402 => '𝃍', 3403 => '𝃎', 3404 => '𝃏', 3405 => '𝃐', 3406 => '𝃑', 3407 => '𝃒', 3408 => '𝃓', 3409 => '𝃔', 3410 => '𝃕', 3411 => '𝃖', 3412 => '𝃗', 3413 => '𝃘', 3414 => '𝃙', 3415 => '𝃚', 3416 => '𝃛', 3417 => '𝃜', 3418 => '𝃝', 3419 => '𝃞', 3420 => '𝃟', 3421 => '𝃠', 3422 => '𝃡', 3423 => '𝃢', 3424 => '𝃣', 3425 => '𝃤', 3426 => '𝃥', 3427 => '𝃦', 3428 => '𝃧', 3429 => '𝃨', 3430 => '𝃩', 3431 => '𝃪', 3432 => '𝃫', 3433 => '𝃬', 3434 => '𝃭', 3435 => '𝃮', 3436 => '𝃯', 3437 => '𝃰', 3438 => '𝃱', 3439 => '𝃲', 3440 => '𝃳', 3441 => '𝃴', 3442 => '𝃵', 3443 => '𝄀', 3444 => '𝄁', 3445 => '𝄂', 3446 => '𝄃', 3447 => '𝄄', 3448 => '𝄅', 3449 => '𝄆', 3450 => '𝄇', 3451 => '𝄈', 3452 => '𝄉', 3453 => '𝄊', 3454 => '𝄋', 3455 => '𝄌', 3456 => '𝄍', 3457 => '𝄎', 3458 => '𝄏', 3459 => '𝄐', 3460 => '𝄑', 3461 => '𝄒', 3462 => '𝄓', 3463 => '𝄔', 3464 => '𝄕', 3465 => '𝄖', 3466 => '𝄗', 3467 => '𝄘', 3468 => '𝄙', 3469 => '𝄚', 3470 => '𝄛', 3471 => '𝄜', 3472 => '𝄝', 3473 => '𝄞', 3474 => '𝄟', 3475 => '𝄠', 3476 => '𝄡', 3477 => '𝄢', 3478 => '𝄣', 3479 => '𝄤', 3480 => '𝄥', 3481 => '𝄦', 3482 => '♭', 3483 => '♮', 3484 => '♯', 3485 => '𝄪', 3486 => '𝄫', 3487 => '𝄬', 3488 => '𝄭', 3489 => '𝄮', 3490 => '𝄯', 3491 => '𝄰', 3492 => '𝄱', 3493 => '𝄲', 3494 => '𝄳', 3495 => '𝄴', 3496 => '𝄵', 3497 => '𝄶', 3498 => '𝄷', 3499 => '𝄸', 3500 => '𝄹', 3501 => '𝄩', 3502 => '𝄺', 3503 => '𝄻', 3504 => '𝄼', 3505 => '𝄽', 3506 => '𝄾', 3507 => '𝄿', 3508 => '𝅀', 3509 => '𝅁', 3510 => '𝅂', 3511 => '𝅃', 3512 => '𝅄', 3513 => '𝅅', 3514 => '𝅆', 3515 => '𝅇', 3516 => '𝅈', 3517 => '𝅉', 3518 => '𝅊', 3519 => '𝅋', 3520 => '𝅌', 3521 => '𝅍', 3522 => '𝅎', 3523 => '𝅏', 3524 => '𝅐', 3525 => '𝅑', 3526 => '𝅒', 3527 => '𝅓', 3528 => '𝅔', 3529 => '𝅕', 3530 => '𝅖', 3531 => '𝅗', 3532 => '𝅘', 3533 => '𝅙', 3534 => '𝅚', 3535 => '𝅛', 3536 => '𝅜', 3537 => '𝅝', 3538 => '𝅪', 3539 => '𝅫', 3540 => '𝅬', 3541 => '𝆃', 3542 => '𝆄', 3543 => '𝆌', 3544 => '𝆍', 3545 => '𝆎', 3546 => '𝆏', 3547 => '𝆐', 3548 => '𝆑', 3549 => '𝆒', 3550 => '𝆓', 3551 => '𝆔', 3552 => '𝆕', 3553 => '𝆖', 3554 => '𝆗', 3555 => '𝆘', 3556 => '𝆙', 3557 => '𝆚', 3558 => '𝆛', 3559 => '𝆜', 3560 => '𝆝', 3561 => '𝆞', 3562 => '𝆟', 3563 => '𝆠', 3564 => '𝆡', 3565 => '𝆢', 3566 => '𝆣', 3567 => '𝆤', 3568 => '𝆥', 3569 => '𝆦', 3570 => '𝆧', 3571 => '𝆨', 3572 => '𝆩', 3573 => '𝆮', 3574 => '𝆯', 3575 => '𝆰', 3576 => '𝆱', 3577 => '𝆲', 3578 => '𝆳', 3579 => '𝆴', 3580 => '𝆵', 3581 => '𝆶', 3582 => '𝆷', 3583 => '𝆸', 3584 => '𝆹', 3585 => '𝆺', 3586 => '𝇁', 3587 => '𝇂', 3588 => '𝇃', 3589 => '𝇄', 3590 => '𝇅', 3591 => '𝇆', 3592 => '𝇇', 3593 => '𝇈', 3594 => '𝇉', 3595 => '𝇊', 3596 => '𝇋', 3597 => '𝇌', 3598 => '𝇍', 3599 => '𝇎', 3600 => '𝇏', 3601 => '𝇐', 3602 => '𝇑', 3603 => '𝇒', 3604 => '𝇓', 3605 => '𝇔', 3606 => '𝇕', 3607 => '𝇖', 3608 => '𝇗', 3609 => '𝇘', 3610 => '𝇙', 3611 => '𝇚', 3612 => '𝇛', 3613 => '𝇜', 3614 => '𝇝', 3615 => '𝈀', 3616 => '𝈁', 3617 => '𝈂', 3618 => '𝈃', 3619 => '𝈄', 3620 => '𝈅', 3621 => '𝈆', 3622 => '𝈇', 3623 => '𝈈', 3624 => '𝈉', 3625 => '𝈊', 3626 => '𝈋', 3627 => '𝈌', 3628 => '𝈍', 3629 => '𝈎', 3630 => '𝈏', 3631 => '𝈐', 3632 => '𝈑', 3633 => '𝈒', 3634 => '𝈓', 3635 => '𝈔', 3636 => '𝈕', 3637 => '𝈖', 3638 => '𝈗', 3639 => '𝈘', 3640 => '𝈙', 3641 => '𝈚', 3642 => '𝈛', 3643 => '𝈜', 3644 => '𝈝', 3645 => '𝈞', 3646 => '𝈟', 3647 => '𝈠', 3648 => '𝈡', 3649 => '𝈢', 3650 => '𝈣', 3651 => '𝈤', 3652 => '𝈥', 3653 => '𝈦', 3654 => '𝈧', 3655 => '𝈨', 3656 => '𝈩', 3657 => '𝈪', 3658 => '𝈫', 3659 => '𝈬', 3660 => '𝈭', 3661 => '𝈮', 3662 => '𝈯', 3663 => '𝈰', 3664 => '𝈱', 3665 => '𝈲', 3666 => '𝈳', 3667 => '𝈴', 3668 => '𝈵', 3669 => '𝈶', 3670 => '𝈷', 3671 => '𝈸', 3672 => '𝈹', 3673 => '𝈺', 3674 => '𝈻', 3675 => '𝈼', 3676 => '𝈽', 3677 => '𝈾', 3678 => '𝈿', 3679 => '𝉀', 3680 => '𝉁', 3681 => '𝉅', 3682 => '🀀', 3683 => '🀁', 3684 => '🀂', 3685 => '🀃', 3686 => '🀄', 3687 => '🀅', 3688 => '🀆', 3689 => '🀇', 3690 => '🀈', 3691 => '🀉', 3692 => '🀊', 3693 => '🀋', 3694 => '🀌', 3695 => '🀍', 3696 => '🀎', 3697 => '🀏', 3698 => '🀐', 3699 => '🀑', 3700 => '🀒', 3701 => '🀓', 3702 => '🀔', 3703 => '🀕', 3704 => '🀖', 3705 => '🀗', 3706 => '🀘', 3707 => '🀙', 3708 => '🀚', 3709 => '🀛', 3710 => '🀜', 3711 => '🀝', 3712 => '🀞', 3713 => '🀟', 3714 => '🀠', 3715 => '🀡', 3716 => '🀢', 3717 => '🀣', 3718 => '🀤', 3719 => '🀥', 3720 => '🀦', 3721 => '🀧', 3722 => '🀨', 3723 => '🀩', 3724 => '🀪', 3725 => '🀫', 3726 => '🀰', 3727 => '🀱', 3728 => '🀲', 3729 => '🀳', 3730 => '🀴', 3731 => '🀵', 3732 => '🀶', 3733 => '🀷', 3734 => '🀸', 3735 => '🀹', 3736 => '🀺', 3737 => '🀻', 3738 => '🀼', 3739 => '🀽', 3740 => '🀾', 3741 => '🀿', 3742 => '🁀', 3743 => '🁁', 3744 => '🁂', 3745 => '🁃', 3746 => '🁄', 3747 => '🁅', 3748 => '🁆', 3749 => '🁇', 3750 => '🁈', 3751 => '🁉', 3752 => '🁊', 3753 => '🁋', 3754 => '🁌', 3755 => '🁍', 3756 => '🁎', 3757 => '🁏', 3758 => '🁐', 3759 => '🁑', 3760 => '🁒', 3761 => '🁓', 3762 => '🁔', 3763 => '🁕', 3764 => '🁖', 3765 => '🁗', 3766 => '🁘', 3767 => '🁙', 3768 => '🁚', 3769 => '🁛', 3770 => '🁜', 3771 => '🁝', 3772 => '🁞', 3773 => '🁟', 3774 => '🁠', 3775 => '🁡', 3776 => '🁢', 3777 => '🁣', 3778 => '🁤', 3779 => '🁥', 3780 => '🁦', 3781 => '🁧', 3782 => '🁨', 3783 => '🁩', 3784 => '🁪', 3785 => '🁫', 3786 => '🁬', 3787 => '🁭', 3788 => '🁮', 3789 => '🁯', 3790 => '🁰', 3791 => '🁱', 3792 => '🁲', 3793 => '🁳', 3794 => '🁴', 3795 => '🁵', 3796 => '🁶', 3797 => '🁷', 3798 => '🁸', 3799 => '🁹', 3800 => '🁺', 3801 => '🁻', 3802 => '🁼', 3803 => '🁽', 3804 => '🁾', 3805 => '🁿', 3806 => '🂀', 3807 => '🂁', 3808 => '🂂', 3809 => '🂃', 3810 => '🂄', 3811 => '🂅', 3812 => '🂆', 3813 => '🂇', 3814 => '🂈', 3815 => '🂉', 3816 => '🂊', 3817 => '🂋', 3818 => '🂌', 3819 => '🂍', 3820 => '🂎', 3821 => '🂏', 3822 => '🂐', 3823 => '🂑', 3824 => '🂒', 3825 => '🂓', 3826 => '🂠', 3827 => '🂡', 3828 => '🂢', 3829 => '🂣', 3830 => '🂤', 3831 => '🂥', 3832 => '🂦', 3833 => '🂧', 3834 => '🂨', 3835 => '🂩', 3836 => '🂪', 3837 => '🂫', 3838 => '🂬', 3839 => '🂭', 3840 => '🂮', 3841 => '🂱', 3842 => '🂲', 3843 => '🂳', 3844 => '🂴', 3845 => '🂵', 3846 => '🂶', 3847 => '🂷', 3848 => '🂸', 3849 => '🂹', 3850 => '🂺', 3851 =