MediaWiki REL1_39
Parser.php
Go to the documentation of this file.
1<?php
51use Psr\Log\LoggerInterface;
52use Wikimedia\IPUtils;
53use Wikimedia\ScopedCallback;
54
95#[AllowDynamicProperties]
96class Parser {
98
99 # Flags for Parser::setFunctionHook
100 public const SFH_NO_HASH = 1;
101 public const SFH_OBJECT_ARGS = 2;
102
103 # Constants needed for external link processing
104 # Everything except bracket, space, or control characters
105 # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
106 # as well as U+3000 is IDEOGRAPHIC SPACE for T21052
107 # \x{FFFD} is the Unicode replacement character, which the HTML5 spec
108 # uses to replace invalid HTML characters.
109 public const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
110 # Simplified expression to match an IPv4 or IPv6 address, or
111 # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
112 // phpcs:ignore Generic.Files.LineLength
113 private const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
114 # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
115 // phpcs:ignore Generic.Files.LineLength
116 private const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
117 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
118
119 # Regular expression for a non-newline space
120 private const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
121
126 public const PTD_FOR_INCLUSION = Preprocessor::DOM_FOR_INCLUSION;
127
128 # Allowed values for $this->mOutputType
129 # Parameter to startExternalParse().
130 public const OT_HTML = 1; # like parse()
131 public const OT_WIKI = 2; # like preSaveTransform()
132 public const OT_PREPROCESS = 3; # like preprocess()
133 public const OT_MSG = 3;
134 # like extractSections() - portions of the original are returned unchanged.
135 public const OT_PLAIN = 4;
136
154 public const MARKER_SUFFIX = "-QINU`\"'\x7f";
155 public const MARKER_PREFIX = "\x7f'\"`UNIQ-";
156
169 public const TOC_START = '<mw:toc>';
170
176 public const TOC_END = '</mw:toc>';
177
195 public const TOC_PLACEHOLDER = '<mw:tocplace></mw:tocplace>';
196
197 # Persistent:
198 private $mTagHooks = [];
199 private $mFunctionHooks = [];
200 private $mFunctionSynonyms = [ 0 => [], 1 => [] ];
201 private $mStripList = [];
202 private $mVarCache = [];
203 private $mImageParams = [];
204 private $mImageParamsMagicArray = [];
206 public $mMarkerIndex = 0;
207
208 # Initialised by initializeVariables()
209
213 private $mVariables;
214
218 private $mSubstWords;
219
220 # Initialised in constructor
221 private $mExtLinkBracketedRegex;
222
228 private $urlUtils;
229
230 # Initialized in constructor
234 private $mPreprocessor;
235
236 # Cleared with clearState():
240 private $mOutput;
241 private $mAutonumber;
242
246 private $mStripState;
247
251 private $mLinkHolders;
252
256 private $mLinkID;
257 private $mIncludeSizes;
262 private $mTplRedirCache;
268 private $mDoubleUnderscores;
270 public $mExpensiveFunctionCount; # number of expensive parser function calls
271 private $mShowToc;
272 private $mForceTocPosition;
274 private $mTplDomCache;
275
279 private $mUser;
280
281 # Temporary
282 # These are variables reset at least once per parse regardless of $clearState
283
288 public $mOptions;
289
297 public $mTitle; # Title context, used for self-link rendering and similar things
298 private $mOutputType; # Output type, one of the OT_xxx constants
300 public $ot; # Shortcut alias, see setOutputType()
301 private $mRevisionId; # ID to display in {{REVISIONID}} tags
302 private $mRevisionTimestamp; # The timestamp of the specified revision ID
303 private $mRevisionUser; # User to display in {{REVISIONUSER}} tag
304 private $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
305 private $mInputSize = false; # For {{PAGESIZE}} on current page.
306
308 private $mRevisionRecordObject;
309
315 private $mLangLinkLanguages;
316
323 private $currentRevisionCache;
324
329 private $mInParse = false;
330
332 private $mProfiler;
333
337 private $mLinkRenderer;
338
340 private $magicWordFactory;
341
343 private $contLang;
344
346 private $languageConverterFactory;
347
349 private $factory;
350
352 private $specialPageFactory;
353
355 private $titleFormatter;
356
364 private $svcOptions;
365
367 private $linkRendererFactory;
368
370 private $nsInfo;
371
373 private $logger;
374
376 private $badFileLookup;
377
379 private $hookContainer;
380
382 private $hookRunner;
383
385 private $tidy;
386
388 private $userOptionsLookup;
389
391 private $userFactory;
392
394 private $httpRequestFactory;
395
397 private $trackingCategories;
398
400 private $signatureValidatorFactory;
401
403 private $userNameUtils;
404
408 public const CONSTRUCTOR_OPTIONS = [
409 // See documentation for the corresponding config options
410 // Many of these are only used in (eg) CoreMagicVariables
411 MainConfigNames::AllowDisplayTitle,
412 MainConfigNames::AllowSlowParserFunctions,
413 MainConfigNames::ArticlePath,
414 MainConfigNames::EnableScaryTranscluding,
415 MainConfigNames::ExtraInterlanguageLinkPrefixes,
416 MainConfigNames::FragmentMode,
417 MainConfigNames::Localtimezone,
418 MainConfigNames::MaxSigChars,
419 MainConfigNames::MaxTocLevel,
420 MainConfigNames::MiserMode,
421 MainConfigNames::RawHtml,
422 MainConfigNames::ScriptPath,
423 MainConfigNames::Server,
424 MainConfigNames::ServerName,
425 MainConfigNames::ShowHostnames,
426 MainConfigNames::SignatureValidation,
427 MainConfigNames::Sitename,
428 MainConfigNames::StylePath,
429 MainConfigNames::TranscludeCacheExpiry,
430 MainConfigNames::PreprocessorCacheThreshold,
431 ];
432
459 public function __construct(
460 ServiceOptions $svcOptions,
461 MagicWordFactory $magicWordFactory,
462 Language $contLang,
463 ParserFactory $factory,
464 UrlUtils $urlUtils,
465 SpecialPageFactory $spFactory,
466 LinkRendererFactory $linkRendererFactory,
467 NamespaceInfo $nsInfo,
468 LoggerInterface $logger,
469 BadFileLookup $badFileLookup,
470 LanguageConverterFactory $languageConverterFactory,
471 HookContainer $hookContainer,
472 TidyDriverBase $tidy,
473 WANObjectCache $wanCache,
474 UserOptionsLookup $userOptionsLookup,
475 UserFactory $userFactory,
476 TitleFormatter $titleFormatter,
477 HttpRequestFactory $httpRequestFactory,
478 TrackingCategories $trackingCategories,
479 SignatureValidatorFactory $signatureValidatorFactory,
480 UserNameUtils $userNameUtils
481 ) {
482 if ( ParserFactory::$inParserFactory === 0 ) {
483 // Direct construction of Parser was deprecated in 1.34 and
484 // removed in 1.36; use a ParserFactory instead.
485 throw new MWException( 'Direct construction of Parser not allowed' );
486 }
487 $this->deprecatePublicProperty( 'mLinkID', '1.35', __CLASS__ );
488 $this->deprecatePublicProperty( 'mIncludeSizes', '1.35', __CLASS__ );
489 $this->deprecatePublicProperty( 'mDoubleUnderscores', '1.35', __CLASS__ );
490 $this->deprecatePublicProperty( 'mShowToc', '1.35', __CLASS__ );
491 $this->deprecatePublicProperty( 'mRevisionId', '1.35', __CLASS__ );
492 $this->deprecatePublicProperty( 'mRevisionTimestamp', '1.35', __CLASS__ );
493 $this->deprecatePublicProperty( 'mRevisionUser', '1.35', __CLASS__ );
494 $this->deprecatePublicProperty( 'mRevisionSize', '1.35', __CLASS__ );
495 $this->deprecatePublicProperty( 'mInputSize', '1.35', __CLASS__ );
496 $this->deprecatePublicProperty( 'mInParse', '1.35', __CLASS__ );
497 $this->deprecatePublicPropertyFallback( 'mFirstCall', '1.35', static function () {
498 return false;
499 }, static function ( $value ) { /* ignore */
500 } );
501 $this->deprecatePublicPropertyFallback( 'mGeneratedPPNodeCount', '1.35', static function () {
502 return 0;
503 }, static function ( $value ) { /* ignore */
504 } );
505 $svcOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
506 $this->svcOptions = $svcOptions;
507
508 $this->urlUtils = $urlUtils;
509 $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->urlUtils->validProtocols() . ')' .
510 self::EXT_LINK_ADDR .
511 self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
512
513 $this->magicWordFactory = $magicWordFactory;
514
515 $this->contLang = $contLang;
516
517 $this->factory = $factory;
518 $this->specialPageFactory = $spFactory;
519 $this->linkRendererFactory = $linkRendererFactory;
520 $this->nsInfo = $nsInfo;
521 $this->logger = $logger;
522 $this->badFileLookup = $badFileLookup;
523
524 $this->languageConverterFactory = $languageConverterFactory;
525
526 $this->hookContainer = $hookContainer;
527 $this->hookRunner = new HookRunner( $hookContainer );
528
529 $this->tidy = $tidy;
530
531 $this->mPreprocessor = new Preprocessor_Hash(
532 $this,
533 $wanCache,
534 [
535 'cacheThreshold' => $svcOptions->get( MainConfigNames::PreprocessorCacheThreshold ),
536 'disableLangConversion' => $languageConverterFactory->isConversionDisabled(),
537 ]
538 );
539
540 $this->userOptionsLookup = $userOptionsLookup;
541 $this->userFactory = $userFactory;
542 $this->titleFormatter = $titleFormatter;
543 $this->httpRequestFactory = $httpRequestFactory;
544 $this->trackingCategories = $trackingCategories;
545 $this->signatureValidatorFactory = $signatureValidatorFactory;
546 $this->userNameUtils = $userNameUtils;
547
548 // These steps used to be done in "::firstCallInit()"
549 // (if you're chasing a reference from some old code)
550 CoreParserFunctions::register(
551 $this,
552 new ServiceOptions( CoreParserFunctions::REGISTER_OPTIONS, $svcOptions )
553 );
555 $this,
557 );
558 $this->initializeVariables();
559
560 $this->hookRunner->onParserFirstCallInit( $this );
561 }
562
566 public function __destruct() {
567 if ( isset( $this->mLinkHolders ) ) {
568 // @phan-suppress-next-line PhanTypeObjectUnsetDeclaredProperty
569 unset( $this->mLinkHolders );
570 }
571 // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
572 foreach ( $this as $name => $value ) {
573 unset( $this->$name );
574 }
575 }
576
580 public function __clone() {
581 $this->mInParse = false;
582
583 // T58226: When you create a reference "to" an object field, that
584 // makes the object field itself be a reference too (until the other
585 // reference goes out of scope). When cloning, any field that's a
586 // reference is copied as a reference in the new object. Both of these
587 // are defined PHP5 behaviors, as inconvenient as it is for us when old
588 // hooks from PHP4 days are passing fields by reference.
589 foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
590 // Make a non-reference copy of the field, then rebind the field to
591 // reference the new copy.
592 $tmp = $this->$k;
593 $this->$k =& $tmp;
594 unset( $tmp );
595 }
596
597 $this->mPreprocessor = clone $this->mPreprocessor;
598 $this->mPreprocessor->resetParser( $this );
599
600 $this->hookRunner->onParserCloned( $this );
601 }
602
610 public function firstCallInit() {
611 /*
612 * This method should be hard-deprecated once remaining calls are
613 * removed; it no longer does anything.
614 */
615 }
616
622 public function clearState() {
623 $this->resetOutput();
624 $this->mAutonumber = 0;
625 $this->mLinkHolders = new LinkHolderArray(
626 $this,
627 $this->getContentLanguageConverter(),
628 $this->getHookContainer()
629 );
630 $this->mLinkID = 0;
631 $this->mRevisionTimestamp = null;
632 $this->mRevisionId = null;
633 $this->mRevisionUser = null;
634 $this->mRevisionSize = null;
635 $this->mRevisionRecordObject = null;
636 $this->mVarCache = [];
637 $this->mUser = null;
638 $this->mLangLinkLanguages = [];
639 $this->currentRevisionCache = null;
640
641 $this->mStripState = new StripState( $this );
642
643 # Clear these on every parse, T6549
644 $this->mTplRedirCache = [];
645 $this->mTplDomCache = [];
646
647 $this->mShowToc = true;
648 $this->mForceTocPosition = false;
649 $this->mIncludeSizes = [
650 'post-expand' => 0,
651 'arg' => 0,
652 ];
653 $this->mPPNodeCount = 0;
654 $this->mHighestExpansionDepth = 0;
655 $this->mHeadings = [];
656 $this->mDoubleUnderscores = [];
657 $this->mExpensiveFunctionCount = 0;
658
659 $this->mProfiler = new SectionProfiler();
660
661 $this->hookRunner->onParserClearState( $this );
662 }
663
668 public function resetOutput() {
669 $this->mOutput = new ParserOutput;
670 $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
671 }
672
691 public function parse(
692 $text, PageReference $page, ParserOptions $options,
693 $linestart = true, $clearState = true, $revid = null
694 ) {
695 if ( $clearState ) {
696 // We use U+007F DELETE to construct strip markers, so we have to make
697 // sure that this character does not occur in the input text.
698 $text = strtr( $text, "\x7f", "?" );
699 $magicScopeVariable = $this->lock();
700 }
701 // Strip U+0000 NULL (T159174)
702 $text = str_replace( "\000", '', $text );
703
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 [ $nsText, $nsSeparator, $mainText ] = $this->getTargetLanguageConverter()->convertSplitTitle( $page );
745 // In the future, those three pieces could be stored separately rather than joined into $titleText,
746 // and OutputPage would format them and join them together, to resolve T314399.
747 $titleText = self::formatPageTitle( $nsText, $nsSeparator, $mainText );
748 }
749 $this->mOutput->setTitleText( $titleText );
750 }
751
752 # Compute runtime adaptive expiry if set
753 $this->mOutput->finalizeAdaptiveCacheExpiry();
754
755 # Warn if too many heavyweight parser functions were used
756 if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
757 $this->limitationWarn( 'expensive-parserfunction',
758 $this->mExpensiveFunctionCount,
759 $this->mOptions->getExpensiveParserFunctionLimit()
760 );
761 }
762
763 # Information on limits, for the benefit of users who try to skirt them
764 if ( MediaWikiServices::getInstance()->getMainConfig()->get(
765 MainConfigNames::EnableParserLimitReporting ) ) {
766 $this->makeLimitReport();
767 }
768
769 # Wrap non-interface parser output in a <div> so it can be targeted
770 # with CSS (T37247)
771 $class = $this->mOptions->getWrapOutputClass();
772 if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
773 $this->mOutput->addWrapperDivClass( $class );
774 }
775
776 $this->mOutput->setText( $text );
777
778 $this->mRevisionId = $oldRevisionId;
779 $this->mRevisionRecordObject = $oldRevisionRecordObject;
780 $this->mRevisionTimestamp = $oldRevisionTimestamp;
781 $this->mRevisionUser = $oldRevisionUser;
782 $this->mRevisionSize = $oldRevisionSize;
783 $this->mInputSize = false;
784 $this->currentRevisionCache = null;
785
786 return $this->mOutput;
787 }
788
792 protected function makeLimitReport() {
793 $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
794
795 $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
796 if ( $cpuTime !== null ) {
797 $this->mOutput->setLimitReportData( 'limitreport-cputime',
798 sprintf( "%.3f", $cpuTime )
799 );
800 }
801
802 $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
803 $this->mOutput->setLimitReportData( 'limitreport-walltime',
804 sprintf( "%.3f", $wallTime )
805 );
806
807 $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
808 [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
809 );
810 $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
811 [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
812 );
813 $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
814 [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
815 );
816 $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
817 [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
818 );
819 $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
820 [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
821 );
822
823 foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
824 $this->mOutput->setLimitReportData( $key, $value );
825 }
826
827 $this->hookRunner->onParserLimitReportPrepare( $this, $this->mOutput );
828
829 // Add on template profiling data in human/machine readable way
830 $dataByFunc = $this->mProfiler->getFunctionStats();
831 uasort( $dataByFunc, static function ( $a, $b ) {
832 return $b['real'] <=> $a['real']; // descending order
833 } );
834 $profileReport = [];
835 foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
836 $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
837 $item['%real'], $item['real'], $item['calls'],
838 htmlspecialchars( $item['name'] ) );
839 }
840
841 $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
842
843 // Add other cache related metadata
844 if ( $this->svcOptions->get( MainConfigNames::ShowHostnames ) ) {
845 $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
846 }
847 $this->mOutput->setLimitReportData( 'cachereport-timestamp',
848 $this->mOutput->getCacheTime() );
849 $this->mOutput->setLimitReportData( 'cachereport-ttl',
850 $this->mOutput->getCacheExpiry() );
851 $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
852 $this->mOutput->hasReducedExpiry() );
853 }
854
880 public function recursiveTagParse( $text, $frame = false ) {
881 $text = $this->internalParse( $text, false, $frame );
882 return $text;
883 }
884
904 public function recursiveTagParseFully( $text, $frame = false ) {
905 $text = $this->recursiveTagParse( $text, $frame );
906 $text = $this->internalParseHalfParsed( $text, false );
907 return $text;
908 }
909
929 public function parseExtensionTagAsTopLevelDoc( $text ) {
930 $text = $this->recursiveTagParse( $text );
931 $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
932 $text = $this->internalParseHalfParsed( $text, true );
933 return $text;
934 }
935
948 public function preprocess(
949 $text,
950 ?PageReference $page,
951 ParserOptions $options,
952 $revid = null,
953 $frame = false
954 ) {
955 $magicScopeVariable = $this->lock();
956 $this->startParse( $page, $options, self::OT_PREPROCESS, true );
957 if ( $revid !== null ) {
958 $this->mRevisionId = $revid;
959 }
960 $this->hookRunner->onParserBeforePreprocess( $this, $text, $this->mStripState );
961 $text = $this->replaceVariables( $text, $frame );
962 $text = $this->mStripState->unstripBoth( $text );
963 return $text;
964 }
965
975 public function recursivePreprocess( $text, $frame = false ) {
976 $text = $this->replaceVariables( $text, $frame );
977 $text = $this->mStripState->unstripBoth( $text );
978 return $text;
979 }
980
995 public function getPreloadText( $text, PageReference $page, ParserOptions $options, $params = [] ) {
996 $msg = new RawMessage( $text );
997 $text = $msg->params( $params )->plain();
998
999 # Parser (re)initialisation
1000 $magicScopeVariable = $this->lock();
1001 $this->startParse( $page, $options, self::OT_PLAIN, true );
1002
1003 $flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
1004 $dom = $this->preprocessToDom( $text, Preprocessor::DOM_FOR_INCLUSION );
1005 $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
1006 $text = $this->mStripState->unstripBoth( $text );
1007 return $text;
1008 }
1009
1017 public function setUser( ?UserIdentity $user ) {
1018 $this->mUser = $user;
1019 }
1020
1028 public function setTitle( Title $t = null ) {
1029 $this->setPage( $t );
1030 }
1031
1037 public function getTitle(): Title {
1038 if ( !$this->mTitle ) {
1039 $this->mTitle = Title::makeTitle( NS_SPECIAL, 'Badtitle/Parser' );
1040 }
1041 return $this->mTitle;
1042 }
1043
1050 public function setPage( ?PageReference $t = null ) {
1051 if ( !$t ) {
1052 $t = Title::makeTitle( NS_SPECIAL, 'Badtitle/Parser' );
1053 } else {
1054 // For now (early 1.37 alpha), always convert to Title, so we don't have to do it over
1055 // and over again in other methods. Eventually, we will no longer need to have a Title
1056 // instance internally.
1057 $t = Title::castFromPageReference( $t );
1058 }
1059
1060 if ( $t->hasFragment() ) {
1061 # Strip the fragment to avoid various odd effects
1062 $this->mTitle = $t->createFragmentTarget( '' );
1063 } else {
1064 $this->mTitle = $t;
1065 }
1066 }
1067
1073 public function getPage(): ?PageReference {
1074 return $this->mTitle;
1075 }
1076
1082 public function getOutputType(): int {
1083 return $this->mOutputType;
1084 }
1085
1091 public function setOutputType( $ot ): void {
1092 $this->mOutputType = $ot;
1093 # Shortcut alias
1094 $this->ot = [
1095 'html' => $ot == self::OT_HTML,
1096 'wiki' => $ot == self::OT_WIKI,
1097 'pre' => $ot == self::OT_PREPROCESS,
1098 'plain' => $ot == self::OT_PLAIN,
1099 ];
1100 }
1101
1109 public function OutputType( $x = null ) {
1110 wfDeprecated( __METHOD__, '1.35' );
1111 return wfSetVar( $this->mOutputType, $x );
1112 }
1113
1118 public function getOutput() {
1119 return $this->mOutput;
1120 }
1121
1126 public function getOptions() {
1127 return $this->mOptions;
1128 }
1129
1135 public function setOptions( ParserOptions $options ): void {
1136 $this->mOptions = $options;
1137 }
1138
1146 public function Options( $x = null ) {
1147 wfDeprecated( __METHOD__, '1.35' );
1148 return wfSetVar( $this->mOptions, $x );
1149 }
1150
1155 public function nextLinkID() {
1156 return $this->mLinkID++;
1157 }
1158
1163 public function setLinkID( $id ) {
1164 $this->mLinkID = $id;
1165 }
1166
1172 public function getFunctionLang() {
1173 return $this->getTargetLanguage();
1174 }
1175
1184 public function getTargetLanguage() {
1185 $target = $this->mOptions->getTargetLanguage();
1186
1187 if ( $target !== null ) {
1188 return $target;
1189 } elseif ( $this->mOptions->getInterfaceMessage() ) {
1190 return $this->mOptions->getUserLangObj();
1191 }
1192
1193 return $this->getTitle()->getPageLanguage();
1194 }
1195
1203 public function getUserIdentity(): UserIdentity {
1204 return $this->mUser ?? $this->getOptions()->getUserIdentity();
1205 }
1206
1213 public function getPreprocessor() {
1214 return $this->mPreprocessor;
1215 }
1216
1223 public function getLinkRenderer() {
1224 // XXX We make the LinkRenderer with current options and then cache it forever
1225 if ( !$this->mLinkRenderer ) {
1226 $this->mLinkRenderer = $this->linkRendererFactory->create();
1227 }
1228
1229 return $this->mLinkRenderer;
1230 }
1231
1238 public function getMagicWordFactory() {
1239 return $this->magicWordFactory;
1240 }
1241
1248 public function getContentLanguage() {
1249 return $this->contLang;
1250 }
1251
1258 public function getBadFileLookup() {
1259 return $this->badFileLookup;
1260 }
1261
1281 public static function extractTagsAndParams( array $elements, $text, &$matches ) {
1282 static $n = 1;
1283 $stripped = '';
1284 $matches = [];
1285
1286 $taglist = implode( '|', $elements );
1287 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1288
1289 while ( $text != '' ) {
1290 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1291 $stripped .= $p[0];
1292 if ( count( $p ) < 5 ) {
1293 break;
1294 }
1295 if ( count( $p ) > 5 ) {
1296 # comment
1297 $element = $p[4];
1298 $attributes = '';
1299 $close = '';
1300 $inside = $p[5];
1301 } else {
1302 # tag
1303 list( , $element, $attributes, $close, $inside ) = $p;
1304 }
1305
1306 $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1307 $stripped .= $marker;
1308
1309 if ( $close === '/>' ) {
1310 # Empty element tag, <tag />
1311 $content = null;
1312 $text = $inside;
1313 $tail = null;
1314 } else {
1315 if ( $element === '!--' ) {
1316 $end = '/(-->)/';
1317 } else {
1318 $end = "/(<\\/$element\\s*>)/i";
1319 }
1320 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1321 $content = $q[0];
1322 if ( count( $q ) < 3 ) {
1323 # No end tag -- let it run out to the end of the text.
1324 $tail = '';
1325 $text = '';
1326 } else {
1327 list( , $tail, $text ) = $q;
1328 }
1329 }
1330
1331 $matches[$marker] = [ $element,
1332 $content,
1333 Sanitizer::decodeTagAttributes( $attributes ),
1334 "<$element$attributes$close$content$tail" ];
1335 }
1336 return $stripped;
1337 }
1338
1344 public function getStripList() {
1345 return $this->mStripList;
1346 }
1347
1352 public function getStripState() {
1353 return $this->mStripState;
1354 }
1355
1365 public function insertStripItem( $text ) {
1366 $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1367 $this->mMarkerIndex++;
1368 $this->mStripState->addGeneral( $marker, $text );
1369 return $marker;
1370 }
1371
1378 private function handleTables( $text ) {
1379 $lines = StringUtils::explode( "\n", $text );
1380 $out = '';
1381 $td_history = []; # Is currently a td tag open?
1382 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1383 $tr_history = []; # Is currently a tr tag open?
1384 $tr_attributes = []; # history of tr attributes
1385 $has_opened_tr = []; # Did this table open a <tr> element?
1386 $indent_level = 0; # indent level of the table
1387
1388 foreach ( $lines as $outLine ) {
1389 $line = trim( $outLine );
1390
1391 if ( $line === '' ) { # empty line, go to next line
1392 $out .= $outLine . "\n";
1393 continue;
1394 }
1395
1396 $first_character = $line[0];
1397 $first_two = substr( $line, 0, 2 );
1398 $matches = [];
1399
1400 if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1401 # First check if we are starting a new table
1402 $indent_level = strlen( $matches[1] );
1403
1404 $attributes = $this->mStripState->unstripBoth( $matches[2] );
1405 $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1406
1407 $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1408 array_push( $td_history, false );
1409 array_push( $last_tag_history, '' );
1410 array_push( $tr_history, false );
1411 array_push( $tr_attributes, '' );
1412 array_push( $has_opened_tr, false );
1413 } elseif ( count( $td_history ) == 0 ) {
1414 # Don't do any of the following
1415 $out .= $outLine . "\n";
1416 continue;
1417 } elseif ( $first_two === '|}' ) {
1418 # We are ending a table
1419 $line = '</table>' . substr( $line, 2 );
1420 $last_tag = array_pop( $last_tag_history );
1421
1422 if ( !array_pop( $has_opened_tr ) ) {
1423 $line = "<tr><td></td></tr>{$line}";
1424 }
1425
1426 if ( array_pop( $tr_history ) ) {
1427 $line = "</tr>{$line}";
1428 }
1429
1430 if ( array_pop( $td_history ) ) {
1431 $line = "</{$last_tag}>{$line}";
1432 }
1433 array_pop( $tr_attributes );
1434 if ( $indent_level > 0 ) {
1435 $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1436 } else {
1437 $outLine = $line;
1438 }
1439 } elseif ( $first_two === '|-' ) {
1440 # Now we have a table row
1441 $line = preg_replace( '#^\|-+#', '', $line );
1442
1443 # Whats after the tag is now only attributes
1444 $attributes = $this->mStripState->unstripBoth( $line );
1445 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1446 array_pop( $tr_attributes );
1447 array_push( $tr_attributes, $attributes );
1448
1449 $line = '';
1450 $last_tag = array_pop( $last_tag_history );
1451 array_pop( $has_opened_tr );
1452 array_push( $has_opened_tr, true );
1453
1454 if ( array_pop( $tr_history ) ) {
1455 $line = '</tr>';
1456 }
1457
1458 if ( array_pop( $td_history ) ) {
1459 $line = "</{$last_tag}>{$line}";
1460 }
1461
1462 $outLine = $line;
1463 array_push( $tr_history, false );
1464 array_push( $td_history, false );
1465 array_push( $last_tag_history, '' );
1466 } elseif ( $first_character === '|'
1467 || $first_character === '!'
1468 || $first_two === '|+'
1469 ) {
1470 # This might be cell elements, td, th or captions
1471 if ( $first_two === '|+' ) {
1472 $first_character = '+';
1473 $line = substr( $line, 2 );
1474 } else {
1475 $line = substr( $line, 1 );
1476 }
1477
1478 // Implies both are valid for table headings.
1479 if ( $first_character === '!' ) {
1480 $line = StringUtils::replaceMarkup( '!!', '||', $line );
1481 }
1482
1483 # Split up multiple cells on the same line.
1484 # FIXME : This can result in improper nesting of tags processed
1485 # by earlier parser steps.
1486 $cells = explode( '||', $line );
1487
1488 $outLine = '';
1489
1490 # Loop through each table cell
1491 foreach ( $cells as $cell ) {
1492 $previous = '';
1493 if ( $first_character !== '+' ) {
1494 $tr_after = array_pop( $tr_attributes );
1495 if ( !array_pop( $tr_history ) ) {
1496 $previous = "<tr{$tr_after}>\n";
1497 }
1498 array_push( $tr_history, true );
1499 array_push( $tr_attributes, '' );
1500 array_pop( $has_opened_tr );
1501 array_push( $has_opened_tr, true );
1502 }
1503
1504 $last_tag = array_pop( $last_tag_history );
1505
1506 if ( array_pop( $td_history ) ) {
1507 $previous = "</{$last_tag}>\n{$previous}";
1508 }
1509
1510 if ( $first_character === '|' ) {
1511 $last_tag = 'td';
1512 } elseif ( $first_character === '!' ) {
1513 $last_tag = 'th';
1514 } elseif ( $first_character === '+' ) {
1515 $last_tag = 'caption';
1516 } else {
1517 $last_tag = '';
1518 }
1519
1520 array_push( $last_tag_history, $last_tag );
1521
1522 # A cell could contain both parameters and data
1523 $cell_data = explode( '|', $cell, 2 );
1524
1525 # T2553: Note that a '|' inside an invalid link should not
1526 # be mistaken as delimiting cell parameters
1527 # Bug T153140: Neither should language converter markup.
1528 if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1529 $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1530 } elseif ( count( $cell_data ) == 1 ) {
1531 // Whitespace in cells is trimmed
1532 $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1533 } else {
1534 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1535 $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1536 // Whitespace in cells is trimmed
1537 $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1538 }
1539
1540 $outLine .= $cell;
1541 array_push( $td_history, true );
1542 }
1543 }
1544 $out .= $outLine . "\n";
1545 }
1546
1547 # Closing open td, tr && table
1548 while ( count( $td_history ) > 0 ) {
1549 if ( array_pop( $td_history ) ) {
1550 $out .= "</td>\n";
1551 }
1552 if ( array_pop( $tr_history ) ) {
1553 $out .= "</tr>\n";
1554 }
1555 if ( !array_pop( $has_opened_tr ) ) {
1556 $out .= "<tr><td></td></tr>\n";
1557 }
1558
1559 $out .= "</table>\n";
1560 }
1561
1562 # Remove trailing line-ending (b/c)
1563 if ( substr( $out, -1 ) === "\n" ) {
1564 $out = substr( $out, 0, -1 );
1565 }
1566
1567 # special case: don't return empty table
1568 if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1569 $out = '';
1570 }
1571
1572 return $out;
1573 }
1574
1588 public function internalParse( $text, $isMain = true, $frame = false ) {
1589 $origText = $text;
1590
1591 # Hook to suspend the parser in this state
1592 if ( !$this->hookRunner->onParserBeforeInternalParse( $this, $text, $this->mStripState ) ) {
1593 return $text;
1594 }
1595
1596 # if $frame is provided, then use $frame for replacing any variables
1597 if ( $frame ) {
1598 # use frame depth to infer how include/noinclude tags should be handled
1599 # depth=0 means this is the top-level document; otherwise it's an included document
1600 if ( !$frame->depth ) {
1601 $flag = 0;
1602 } else {
1604 }
1605 $dom = $this->preprocessToDom( $text, $flag );
1606 $text = $frame->expand( $dom );
1607 } else {
1608 # if $frame is not provided, then use old-style replaceVariables
1609 $text = $this->replaceVariables( $text );
1610 }
1611
1612 $this->hookRunner->onInternalParseBeforeSanitize( $this, $text, $this->mStripState );
1613 $text = Sanitizer::internalRemoveHtmlTags(
1614 $text,
1615 // Callback from the Sanitizer for expanding items found in
1616 // HTML attribute values, so they can be safely tested and escaped.
1617 function ( &$text, $frame = false ) {
1618 $text = $this->replaceVariables( $text, $frame );
1619 $text = $this->mStripState->unstripBoth( $text );
1620 },
1621 false,
1622 [],
1623 []
1624 );
1625 $this->hookRunner->onInternalParseBeforeLinks( $this, $text, $this->mStripState );
1626
1627 # Tables need to come after variable replacement for things to work
1628 # properly; putting them before other transformations should keep
1629 # exciting things like link expansions from showing up in surprising
1630 # places.
1631 $text = $this->handleTables( $text );
1632
1633 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1634
1635 $text = $this->handleDoubleUnderscore( $text );
1636
1637 $text = $this->handleHeadings( $text );
1638 $text = $this->handleInternalLinks( $text );
1639 $text = $this->handleAllQuotes( $text );
1640 $text = $this->handleExternalLinks( $text );
1641
1642 # handleInternalLinks may sometimes leave behind
1643 # absolute URLs, which have to be masked to hide them from handleExternalLinks
1644 $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1645
1646 $text = $this->handleMagicLinks( $text );
1647 $text = $this->finalizeHeadings( $text, $origText, $isMain );
1648
1649 return $text;
1650 }
1651
1659 return $this->languageConverterFactory->getLanguageConverter(
1660 $this->getTargetLanguage()
1661 );
1662 }
1663
1669 private function getContentLanguageConverter(): ILanguageConverter {
1670 return $this->languageConverterFactory->getLanguageConverter(
1671 $this->getContentLanguage()
1672 );
1673 }
1674
1682 protected function getHookContainer() {
1683 return $this->hookContainer;
1684 }
1685
1694 protected function getHookRunner() {
1695 return $this->hookRunner;
1696 }
1697
1707 private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1708 $text = $this->mStripState->unstripGeneral( $text );
1709
1710 $text = BlockLevelPass::doBlockLevels( $text, $linestart );
1711
1712 $this->replaceLinkHoldersPrivate( $text );
1713
1721 if ( !( $this->mOptions->getDisableContentConversion()
1722 || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1723 && !$this->mOptions->getInterfaceMessage()
1724 ) {
1725 # The position of the convert() call should not be changed. it
1726 # assumes that the links are all replaced and the only thing left
1727 # is the <nowiki> mark.
1728 $text = $this->getTargetLanguageConverter()->convert( $text );
1729 // Record information necessary for language conversion of TOC.
1730 $this->mOutput->setExtensionData(
1731 // T303329: this should migrate out of extension data
1732 'core:target-lang',
1733 $this->getTargetLanguage()->getCode()
1734 );
1735 $this->mOutput->setExtensionData(
1736 // T303329: this should migrate out of extension data
1737 'core:target-lang-variant',
1738 $this->getTargetLanguageConverter()->getPreferredVariant()
1739 );
1740 } else {
1741 $this->mOutput->setOutputFlag( ParserOutputFlags::NO_TOC_CONVERSION );
1742 }
1743
1744 $text = $this->mStripState->unstripNoWiki( $text );
1745
1746 $text = $this->mStripState->unstripGeneral( $text );
1747
1748 $text = $this->tidy->tidy( $text, [ Sanitizer::class, 'armorFrenchSpaces' ] );
1749
1750 if ( $isMain ) {
1751 $this->hookRunner->onParserAfterTidy( $this, $text );
1752 }
1753
1754 return $text;
1755 }
1756
1767 private function handleMagicLinks( $text ) {
1768 $prots = $this->urlUtils->validAbsoluteProtocols();
1769 $urlChar = self::EXT_LINK_URL_CLASS;
1770 $addr = self::EXT_LINK_ADDR;
1771 $space = self::SPACE_NOT_NL; # non-newline space
1772 $spdash = "(?:-|$space)"; # a dash or a non-newline space
1773 $spaces = "$space++"; # possessive match of 1 or more spaces
1774 $text = preg_replace_callback(
1775 '!(?: # Start cases
1776 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1777 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1778 (\b # m[3]: Free external links
1779 (?i:$prots)
1780 ($addr$urlChar*) # m[4]: Post-protocol path
1781 ) |
1782 \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1783 ([0-9]+)\b |
1784 \bISBN $spaces ( # m[6]: ISBN, capture number
1785 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1786 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1787 [0-9Xx] # check digit
1788 )\b
1789 )!xu",
1790 [ $this, 'magicLinkCallback' ],
1791 $text
1792 );
1793 return $text;
1794 }
1795
1801 private function magicLinkCallback( array $m ) {
1802 if ( isset( $m[1] ) && $m[1] !== '' ) {
1803 # Skip anchor
1804 return $m[0];
1805 } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1806 # Skip HTML element
1807 return $m[0];
1808 } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1809 # Free external link
1810 return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1811 } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1812 # RFC or PMID
1813 if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1814 if ( !$this->mOptions->getMagicRFCLinks() ) {
1815 return $m[0];
1816 }
1817 $keyword = 'RFC';
1818 $urlmsg = 'rfcurl';
1819 $cssClass = 'mw-magiclink-rfc';
1820 $trackingCat = 'magiclink-tracking-rfc';
1821 $id = $m[5];
1822 } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1823 if ( !$this->mOptions->getMagicPMIDLinks() ) {
1824 return $m[0];
1825 }
1826 $keyword = 'PMID';
1827 $urlmsg = 'pubmedurl';
1828 $cssClass = 'mw-magiclink-pmid';
1829 $trackingCat = 'magiclink-tracking-pmid';
1830 $id = $m[5];
1831 } else {
1832 // Should never happen
1833 throw new MWException( __METHOD__ . ': unrecognised match type "' .
1834 substr( $m[0], 0, 20 ) . '"' );
1835 }
1836 $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1837 $this->addTrackingCategory( $trackingCat );
1839 $url,
1840 "{$keyword} {$id}",
1841 true,
1842 $cssClass,
1843 [],
1844 $this->getTitle()
1845 );
1846 } elseif ( isset( $m[6] ) && $m[6] !== ''
1847 && $this->mOptions->getMagicISBNLinks()
1848 ) {
1849 # ISBN
1850 $isbn = $m[6];
1851 $space = self::SPACE_NOT_NL; # non-newline space
1852 $isbn = preg_replace( "/$space/", ' ', $isbn );
1853 $num = strtr( $isbn, [
1854 '-' => '',
1855 ' ' => '',
1856 'x' => 'X',
1857 ] );
1858 $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1859 return $this->getLinkRenderer()->makeKnownLink(
1860 SpecialPage::getTitleFor( 'Booksources', $num ),
1861 "ISBN $isbn",
1862 [
1863 'class' => 'internal mw-magiclink-isbn',
1864 'title' => false // suppress title attribute
1865 ]
1866 );
1867 } else {
1868 return $m[0];
1869 }
1870 }
1871
1881 private function makeFreeExternalLink( $url, $numPostProto ) {
1882 $trail = '';
1883
1884 # The characters '<' and '>' (which were escaped by
1885 # internalRemoveHtmlTags()) should not be included in
1886 # URLs, per RFC 2396.
1887 # Make &nbsp; terminate a URL as well (bug T84937)
1888 $m2 = [];
1889 if ( preg_match(
1890 '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1891 $url,
1892 $m2,
1893 PREG_OFFSET_CAPTURE
1894 ) ) {
1895 $trail = substr( $url, $m2[0][1] ) . $trail;
1896 $url = substr( $url, 0, $m2[0][1] );
1897 }
1898
1899 # Move trailing punctuation to $trail
1900 $sep = ',;\.:!?';
1901 # If there is no left bracket, then consider right brackets fair game too
1902 if ( strpos( $url, '(' ) === false ) {
1903 $sep .= ')';
1904 }
1905
1906 $urlRev = strrev( $url );
1907 $numSepChars = strspn( $urlRev, $sep );
1908 # Don't break a trailing HTML entity by moving the ; into $trail
1909 # This is in hot code, so use substr_compare to avoid having to
1910 # create a new string object for the comparison
1911 if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1912 # more optimization: instead of running preg_match with a $
1913 # anchor, which can be slow, do the match on the reversed
1914 # string starting at the desired offset.
1915 # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1916 if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1917 $numSepChars--;
1918 }
1919 }
1920 if ( $numSepChars ) {
1921 $trail = substr( $url, -$numSepChars ) . $trail;
1922 $url = substr( $url, 0, -$numSepChars );
1923 }
1924
1925 # Verify that we still have a real URL after trail removal, and
1926 # not just lone protocol
1927 if ( strlen( $trail ) >= $numPostProto ) {
1928 return $url . $trail;
1929 }
1930
1931 $url = Sanitizer::cleanUrl( $url );
1932
1933 # Is this an external image?
1934 $text = $this->maybeMakeExternalImage( $url );
1935 if ( $text === false ) {
1936 # Not an image, make a link
1938 $url,
1939 $this->getTargetLanguageConverter()->markNoConversion( $url ),
1940 true,
1941 'free',
1942 $this->getExternalLinkAttribs( $url ),
1943 $this->getTitle()
1944 );
1945 # Register it in the output object...
1946 $this->mOutput->addExternalLink( $url );
1947 }
1948 return $text . $trail;
1949 }
1950
1957 private function handleHeadings( $text ) {
1958 for ( $i = 6; $i >= 1; --$i ) {
1959 $h = str_repeat( '=', $i );
1960 // Trim non-newline whitespace from headings
1961 // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1962 $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1963 }
1964 return $text;
1965 }
1966
1974 private function handleAllQuotes( $text ) {
1975 $outtext = '';
1976 $lines = StringUtils::explode( "\n", $text );
1977 foreach ( $lines as $line ) {
1978 $outtext .= $this->doQuotes( $line ) . "\n";
1979 }
1980 $outtext = substr( $outtext, 0, -1 );
1981 return $outtext;
1982 }
1983
1992 public function doQuotes( $text ) {
1993 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1994 $countarr = count( $arr );
1995 if ( $countarr == 1 ) {
1996 return $text;
1997 }
1998
1999 // First, do some preliminary work. This may shift some apostrophes from
2000 // being mark-up to being text. It also counts the number of occurrences
2001 // of bold and italics mark-ups.
2002 $numbold = 0;
2003 $numitalics = 0;
2004 for ( $i = 1; $i < $countarr; $i += 2 ) {
2005 $thislen = strlen( $arr[$i] );
2006 // If there are ever four apostrophes, assume the first is supposed to
2007 // be text, and the remaining three constitute mark-up for bold text.
2008 // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
2009 if ( $thislen == 4 ) {
2010 $arr[$i - 1] .= "'";
2011 $arr[$i] = "'''";
2012 $thislen = 3;
2013 } elseif ( $thislen > 5 ) {
2014 // If there are more than 5 apostrophes in a row, assume they're all
2015 // text except for the last 5.
2016 // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
2017 $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
2018 $arr[$i] = "'''''";
2019 $thislen = 5;
2020 }
2021 // Count the number of occurrences of bold and italics mark-ups.
2022 if ( $thislen == 2 ) {
2023 $numitalics++;
2024 } elseif ( $thislen == 3 ) {
2025 $numbold++;
2026 } elseif ( $thislen == 5 ) {
2027 $numitalics++;
2028 $numbold++;
2029 }
2030 }
2031
2032 // If there is an odd number of both bold and italics, it is likely
2033 // that one of the bold ones was meant to be an apostrophe followed
2034 // by italics. Which one we cannot know for certain, but it is more
2035 // likely to be one that has a single-letter word before it.
2036 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
2037 $firstsingleletterword = -1;
2038 $firstmultiletterword = -1;
2039 $firstspace = -1;
2040 for ( $i = 1; $i < $countarr; $i += 2 ) {
2041 if ( strlen( $arr[$i] ) == 3 ) {
2042 $x1 = substr( $arr[$i - 1], -1 );
2043 $x2 = substr( $arr[$i - 1], -2, 1 );
2044 if ( $x1 === ' ' ) {
2045 if ( $firstspace == -1 ) {
2046 $firstspace = $i;
2047 }
2048 } elseif ( $x2 === ' ' ) {
2049 $firstsingleletterword = $i;
2050 // if $firstsingleletterword is set, we don't
2051 // look at the other options, so we can bail early.
2052 break;
2053 } elseif ( $firstmultiletterword == -1 ) {
2054 $firstmultiletterword = $i;
2055 }
2056 }
2057 }
2058
2059 // If there is a single-letter word, use it!
2060 if ( $firstsingleletterword > -1 ) {
2061 $arr[$firstsingleletterword] = "''";
2062 $arr[$firstsingleletterword - 1] .= "'";
2063 } elseif ( $firstmultiletterword > -1 ) {
2064 // If not, but there's a multi-letter word, use that one.
2065 $arr[$firstmultiletterword] = "''";
2066 $arr[$firstmultiletterword - 1] .= "'";
2067 } elseif ( $firstspace > -1 ) {
2068 // ... otherwise use the first one that has neither.
2069 // (notice that it is possible for all three to be -1 if, for example,
2070 // there is only one pentuple-apostrophe in the line)
2071 $arr[$firstspace] = "''";
2072 $arr[$firstspace - 1] .= "'";
2073 }
2074 }
2075
2076 // Now let's actually convert our apostrophic mush to HTML!
2077 $output = '';
2078 $buffer = '';
2079 $state = '';
2080 $i = 0;
2081 foreach ( $arr as $r ) {
2082 if ( ( $i % 2 ) == 0 ) {
2083 if ( $state === 'both' ) {
2084 $buffer .= $r;
2085 } else {
2086 $output .= $r;
2087 }
2088 } else {
2089 $thislen = strlen( $r );
2090 if ( $thislen == 2 ) {
2091 // two quotes - open or close italics
2092 if ( $state === 'i' ) {
2093 $output .= '</i>';
2094 $state = '';
2095 } elseif ( $state === 'bi' ) {
2096 $output .= '</i>';
2097 $state = 'b';
2098 } elseif ( $state === 'ib' ) {
2099 $output .= '</b></i><b>';
2100 $state = 'b';
2101 } elseif ( $state === 'both' ) {
2102 $output .= '<b><i>' . $buffer . '</i>';
2103 $state = 'b';
2104 } else { // $state can be 'b' or ''
2105 $output .= '<i>';
2106 $state .= 'i';
2107 }
2108 } elseif ( $thislen == 3 ) {
2109 // three quotes - open or close bold
2110 if ( $state === 'b' ) {
2111 $output .= '</b>';
2112 $state = '';
2113 } elseif ( $state === 'bi' ) {
2114 $output .= '</i></b><i>';
2115 $state = 'i';
2116 } elseif ( $state === 'ib' ) {
2117 $output .= '</b>';
2118 $state = 'i';
2119 } elseif ( $state === 'both' ) {
2120 $output .= '<i><b>' . $buffer . '</b>';
2121 $state = 'i';
2122 } else { // $state can be 'i' or ''
2123 $output .= '<b>';
2124 $state .= 'b';
2125 }
2126 } elseif ( $thislen == 5 ) {
2127 // five quotes - open or close both separately
2128 if ( $state === 'b' ) {
2129 $output .= '</b><i>';
2130 $state = 'i';
2131 } elseif ( $state === 'i' ) {
2132 $output .= '</i><b>';
2133 $state = 'b';
2134 } elseif ( $state === 'bi' ) {
2135 $output .= '</i></b>';
2136 $state = '';
2137 } elseif ( $state === 'ib' ) {
2138 $output .= '</b></i>';
2139 $state = '';
2140 } elseif ( $state === 'both' ) {
2141 $output .= '<i><b>' . $buffer . '</b></i>';
2142 $state = '';
2143 } else { // ($state == '')
2144 $buffer = '';
2145 $state = 'both';
2146 }
2147 }
2148 }
2149 $i++;
2150 }
2151 // Now close all remaining tags. Notice that the order is important.
2152 if ( $state === 'b' || $state === 'ib' ) {
2153 $output .= '</b>';
2154 }
2155 if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
2156 $output .= '</i>';
2157 }
2158 if ( $state === 'bi' ) {
2159 $output .= '</b>';
2160 }
2161 // There might be lonely ''''', so make sure we have a buffer
2162 if ( $state === 'both' && $buffer ) {
2163 $output .= '<b><i>' . $buffer . '</i></b>';
2164 }
2165 return $output;
2166 }
2167
2178 private function handleExternalLinks( $text ) {
2179 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
2180 // @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3161
2181 if ( $bits === false ) {
2182 throw new MWException( "PCRE needs to be compiled with "
2183 . "--enable-unicode-properties in order for MediaWiki to function" );
2184 }
2185 $s = array_shift( $bits );
2186
2187 $i = 0;
2188 while ( $i < count( $bits ) ) {
2189 $url = $bits[$i++];
2190 $i++; // protocol
2191 $text = $bits[$i++];
2192 $trail = $bits[$i++];
2193
2194 # The characters '<' and '>' (which were escaped by
2195 # internalRemoveHtmlTags()) should not be included in
2196 # URLs, per RFC 2396.
2197 $m2 = [];
2198 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
2199 $text = substr( $url, $m2[0][1] ) . ' ' . $text;
2200 $url = substr( $url, 0, $m2[0][1] );
2201 }
2202
2203 # If the link text is an image URL, replace it with an <img> tag
2204 # This happened by accident in the original parser, but some people used it extensively
2205 $img = $this->maybeMakeExternalImage( $text );
2206 if ( $img !== false ) {
2207 $text = $img;
2208 }
2209
2210 $dtrail = '';
2211
2212 # Set linktype for CSS
2213 $linktype = 'text';
2214
2215 # No link text, e.g. [http://domain.tld/some.link]
2216 if ( $text == '' ) {
2217 # Autonumber
2218 $langObj = $this->getTargetLanguage();
2219 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
2220 $linktype = 'autonumber';
2221 } else {
2222 # Have link text, e.g. [http://domain.tld/some.link text]s
2223 # Check for trail
2224 list( $dtrail, $trail ) = Linker::splitTrail( $trail );
2225 }
2226
2227 // Excluding protocol-relative URLs may avoid many false positives.
2228 if ( preg_match( '/^(?:' . $this->urlUtils->validAbsoluteProtocols() . ')/', $text ) ) {
2229 $text = $this->getTargetLanguageConverter()->markNoConversion( $text );
2230 }
2231
2232 $url = Sanitizer::cleanUrl( $url );
2233
2234 # Use the encoded URL
2235 # This means that users can paste URLs directly into the text
2236 # Funny characters like ö aren't valid in URLs anyway
2237 # This was changed in August 2004
2238 $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
2239 $this->getExternalLinkAttribs( $url ), $this->getTitle() ) . $dtrail . $trail;
2240
2241 # Register link in the output object.
2242 $this->mOutput->addExternalLink( $url );
2243 }
2244
2245 // @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive from array_shift
2246 return $s;
2247 }
2248
2259 public static function getExternalLinkRel( $url = false, LinkTarget $title = null ) {
2260 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
2261 $noFollowLinks = $mainConfig->get( MainConfigNames::NoFollowLinks );
2262 $noFollowNsExceptions = $mainConfig->get( MainConfigNames::NoFollowNsExceptions );
2263 $noFollowDomainExceptions = $mainConfig->get( MainConfigNames::NoFollowDomainExceptions );
2264 $ns = $title ? $title->getNamespace() : false;
2265 if ( $noFollowLinks && !in_array( $ns, $noFollowNsExceptions )
2266 && !wfMatchesDomainList( $url, $noFollowDomainExceptions )
2267 ) {
2268 return 'nofollow';
2269 }
2270 return null;
2271 }
2272
2284 public function getExternalLinkAttribs( $url ) {
2285 $attribs = [];
2286 $rel = self::getExternalLinkRel( $url, $this->getTitle() );
2287
2288 $target = $this->mOptions->getExternalLinkTarget();
2289 if ( $target ) {
2290 $attribs['target'] = $target;
2291 if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
2292 // T133507. New windows can navigate parent cross-origin.
2293 // Including noreferrer due to lacking browser
2294 // support of noopener. Eventually noreferrer should be removed.
2295 if ( $rel !== '' ) {
2296 $rel .= ' ';
2297 }
2298 $rel .= 'noreferrer noopener';
2299 }
2300 }
2301 $attribs['rel'] = $rel;
2302 return $attribs;
2303 }
2304
2315 public static function normalizeLinkUrl( $url ) {
2316 # Test for RFC 3986 IPv6 syntax
2317 $scheme = '[a-z][a-z0-9+.-]*:';
2318 $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2319 $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2320 if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2321 IPUtils::isValid( rawurldecode( $m[1] ) )
2322 ) {
2323 $isIPv6 = rawurldecode( $m[1] );
2324 } else {
2325 $isIPv6 = false;
2326 }
2327
2328 # Make sure unsafe characters are encoded
2329 $url = preg_replace_callback(
2330 '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
2331 static function ( $m ) {
2332 return rawurlencode( $m[0] );
2333 },
2334 $url
2335 );
2336
2337 $ret = '';
2338 $end = strlen( $url );
2339
2340 # Fragment part - 'fragment'
2341 $start = strpos( $url, '#' );
2342 if ( $start !== false && $start < $end ) {
2343 $ret = self::normalizeUrlComponent(
2344 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2345 $end = $start;
2346 }
2347
2348 # Query part - 'query' minus &=+;
2349 $start = strpos( $url, '?' );
2350 if ( $start !== false && $start < $end ) {
2351 $ret = self::normalizeUrlComponent(
2352 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2353 $end = $start;
2354 }
2355
2356 # Scheme and path part - 'pchar'
2357 # (we assume no userinfo or encoded colons in the host)
2358 $ret = self::normalizeUrlComponent(
2359 substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2360
2361 # Fix IPv6 syntax
2362 if ( $isIPv6 !== false ) {
2363 $ipv6Host = "%5B({$isIPv6})%5D";
2364 $ret = preg_replace(
2365 "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2366 "$1[$2]",
2367 $ret
2368 );
2369 }
2370
2371 return $ret;
2372 }
2373
2374 private static function normalizeUrlComponent( $component, $unsafe ) {
2375 $callback = static function ( $matches ) use ( $unsafe ) {
2376 $char = urldecode( $matches[0] );
2377 $ord = ord( $char );
2378 if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2379 # Unescape it
2380 return $char;
2381 } else {
2382 # Leave it escaped, but use uppercase for a-f
2383 return strtoupper( $matches[0] );
2384 }
2385 };
2386 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2387 }
2388
2397 private function maybeMakeExternalImage( $url ) {
2398 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2399 $imagesexception = !empty( $imagesfrom );
2400 $text = false;
2401 # $imagesfrom could be either a single string or an array of strings, parse out the latter
2402 if ( $imagesexception && is_array( $imagesfrom ) ) {
2403 $imagematch = false;
2404 foreach ( $imagesfrom as $match ) {
2405 if ( strpos( $url, $match ) === 0 ) {
2406 $imagematch = true;
2407 break;
2408 }
2409 }
2410 } elseif ( $imagesexception ) {
2411 $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2412 } else {
2413 $imagematch = false;
2414 }
2415
2416 if ( $this->mOptions->getAllowExternalImages()
2417 || ( $imagesexception && $imagematch )
2418 ) {
2419 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2420 # Image found
2421 $text = Linker::makeExternalImage( $url );
2422 }
2423 }
2424 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2425 && preg_match( self::EXT_IMAGE_REGEX, $url )
2426 ) {
2427 $whitelist = explode(
2428 "\n",
2429 wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2430 );
2431
2432 foreach ( $whitelist as $entry ) {
2433 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2434 if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2435 continue;
2436 }
2437 // @phan-suppress-next-line SecurityCheck-ReDoS preg_quote is not wanted here
2438 if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2439 # Image matches a whitelist entry
2440 $text = Linker::makeExternalImage( $url );
2441 break;
2442 }
2443 }
2444 }
2445 return $text;
2446 }
2447
2455 private function handleInternalLinks( $text ) {
2456 $this->mLinkHolders->merge( $this->handleInternalLinks2( $text ) );
2457 return $text;
2458 }
2459
2465 private function handleInternalLinks2( &$s ) {
2466 static $tc = false, $e1, $e1_img;
2467 # the % is needed to support urlencoded titles as well
2468 if ( !$tc ) {
2469 $tc = Title::legalChars() . '#%';
2470 # Match a link having the form [[namespace:link|alternate]]trail
2471 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2472 # Match cases where there is no "]]", which might still be images
2473 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2474 }
2475
2476 $holders = new LinkHolderArray(
2477 $this,
2478 $this->getContentLanguageConverter(),
2479 $this->getHookContainer() );
2480
2481 # split the entire text string on occurrences of [[
2482 $a = StringUtils::explode( '[[', ' ' . $s );
2483 # get the first element (all text up to first [[), and remove the space we added
2484 $s = $a->current();
2485 $a->next();
2486 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2487 $s = substr( $s, 1 );
2488
2489 $nottalk = !$this->getTitle()->isTalkPage();
2490
2491 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2492 $e2 = null;
2493 if ( $useLinkPrefixExtension ) {
2494 # Match the end of a line for a word that's not followed by whitespace,
2495 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2496 $charset = $this->contLang->linkPrefixCharset();
2497 $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2498 $m = [];
2499 if ( preg_match( $e2, $s, $m ) ) {
2500 $first_prefix = $m[2];
2501 } else {
2502 $first_prefix = false;
2503 }
2504 $prefix = false;
2505 } else {
2506 $first_prefix = false;
2507 $prefix = '';
2508 }
2509
2510 # Some namespaces don't allow subpages
2511 $useSubpages = $this->nsInfo->hasSubpages(
2512 $this->getTitle()->getNamespace()
2513 );
2514
2515 # Loop for each link
2516 for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2517 # Check for excessive memory usage
2518 if ( $holders->isBig() ) {
2519 # Too big
2520 # Do the existence check, replace the link holders and clear the array
2521 $holders->replace( $s );
2522 $holders->clear();
2523 }
2524
2525 if ( $useLinkPrefixExtension ) {
2526 // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal $e2 is set under this condition
2527 if ( preg_match( $e2, $s, $m ) ) {
2528 list( , $s, $prefix ) = $m;
2529 } else {
2530 $prefix = '';
2531 }
2532 # first link
2533 if ( $first_prefix ) {
2534 $prefix = $first_prefix;
2535 $first_prefix = false;
2536 }
2537 }
2538
2539 $might_be_img = false;
2540
2541 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2542 $text = $m[2];
2543 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2544 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2545 # the real problem is with the $e1 regex
2546 # See T1500.
2547 # Still some problems for cases where the ] is meant to be outside punctuation,
2548 # and no image is in sight. See T4095.
2549 if ( $text !== ''
2550 && substr( $m[3], 0, 1 ) === ']'
2551 && strpos( $text, '[' ) !== false
2552 ) {
2553 $text .= ']'; # so that handleExternalLinks($text) works later
2554 $m[3] = substr( $m[3], 1 );
2555 }
2556 # fix up urlencoded title texts
2557 if ( strpos( $m[1], '%' ) !== false ) {
2558 # Should anchors '#' also be rejected?
2559 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2560 }
2561 $trail = $m[3];
2562 } elseif ( preg_match( $e1_img, $line, $m ) ) {
2563 # Invalid, but might be an image with a link in its caption
2564 $might_be_img = true;
2565 $text = $m[2];
2566 if ( strpos( $m[1], '%' ) !== false ) {
2567 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2568 }
2569 $trail = "";
2570 } else { # Invalid form; output directly
2571 $s .= $prefix . '[[' . $line;
2572 continue;
2573 }
2574
2575 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset preg_match success when reached here
2576 $origLink = ltrim( $m[1], ' ' );
2577
2578 # Don't allow internal links to pages containing
2579 # PROTO: where PROTO is a valid URL protocol; these
2580 # should be external links.
2581 if ( preg_match( '/^(?i:' . $this->urlUtils->validProtocols() . ')/', $origLink ) ) {
2582 $s .= $prefix . '[[' . $line;
2583 continue;
2584 }
2585
2586 # Make subpage if necessary
2587 if ( $useSubpages ) {
2589 $this->getTitle(), $origLink, $text
2590 );
2591 } else {
2592 $link = $origLink;
2593 }
2594
2595 // \x7f isn't a default legal title char, so most likely strip
2596 // markers will force us into the "invalid form" path above. But,
2597 // just in case, let's assert that xmlish tags aren't valid in
2598 // the title position.
2599 $unstrip = $this->mStripState->killMarkers( $link );
2600 $noMarkers = ( $unstrip === $link );
2601
2602 $nt = $noMarkers ? Title::newFromText( $link ) : null;
2603 if ( $nt === null ) {
2604 $s .= $prefix . '[[' . $line;
2605 continue;
2606 }
2607
2608 $ns = $nt->getNamespace();
2609 $iw = $nt->getInterwiki();
2610
2611 $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2612
2613 if ( $might_be_img ) { # if this is actually an invalid link
2614 if ( $ns === NS_FILE && $noforce ) { # but might be an image
2615 $found = false;
2616 while ( true ) {
2617 # look at the next 'line' to see if we can close it there
2618 $a->next();
2619 $next_line = $a->current();
2620 if ( $next_line === false || $next_line === null ) {
2621 break;
2622 }
2623 $m = explode( ']]', $next_line, 3 );
2624 if ( count( $m ) == 3 ) {
2625 # the first ]] closes the inner link, the second the image
2626 $found = true;
2627 $text .= "[[{$m[0]}]]{$m[1]}";
2628 $trail = $m[2];
2629 break;
2630 } elseif ( count( $m ) == 2 ) {
2631 # if there's exactly one ]] that's fine, we'll keep looking
2632 $text .= "[[{$m[0]}]]{$m[1]}";
2633 } else {
2634 # if $next_line is invalid too, we need look no further
2635 $text .= '[[' . $next_line;
2636 break;
2637 }
2638 }
2639 if ( !$found ) {
2640 # we couldn't find the end of this imageLink, so output it raw
2641 # but don't ignore what might be perfectly normal links in the text we've examined
2642 $holders->merge( $this->handleInternalLinks2( $text ) );
2643 $s .= "{$prefix}[[$link|$text";
2644 # note: no $trail, because without an end, there *is* no trail
2645 continue;
2646 }
2647 } else { # it's not an image, so output it raw
2648 $s .= "{$prefix}[[$link|$text";
2649 # note: no $trail, because without an end, there *is* no trail
2650 continue;
2651 }
2652 }
2653
2654 $wasblank = ( $text == '' );
2655 if ( $wasblank ) {
2656 $text = $link;
2657 if ( !$noforce ) {
2658 # Strip off leading ':'
2659 $text = substr( $text, 1 );
2660 }
2661 } else {
2662 # T6598 madness. Handle the quotes only if they come from the alternate part
2663 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2664 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2665 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2666 $text = $this->doQuotes( $text );
2667 }
2668
2669 # Link not escaped by : , create the various objects
2670 if ( $noforce && !$nt->wasLocalInterwiki() ) {
2671 # Interwikis
2672 if (
2673 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2674 MediaWikiServices::getInstance()->getLanguageNameUtils()
2675 ->getLanguageName(
2676 $iw,
2677 LanguageNameUtils::AUTONYMS,
2678 LanguageNameUtils::DEFINED
2679 )
2680 || in_array( $iw, $this->svcOptions->get( MainConfigNames::ExtraInterlanguageLinkPrefixes ) )
2681 )
2682 ) {
2683 # T26502: filter duplicates
2684 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2685 $this->mLangLinkLanguages[$iw] = true;
2686 $this->mOutput->addLanguageLink( $nt->getFullText() );
2687 }
2688
2692 $s = rtrim( $s . $prefix ) . $trail; # T175416
2693 continue;
2694 }
2695
2696 if ( $ns === NS_FILE ) {
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 } elseif ( $ns === NS_CATEGORY ) {
2718 $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2719
2720 if ( $wasblank ) {
2721 $sortkey = $this->mOutput->getPageProperty( 'defaultsort' ) ?? '';
2722 } else {
2723 $sortkey = $text;
2724 }
2725 $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2726 $sortkey = str_replace( "\n", '', $sortkey );
2727 $sortkey = $this->getTargetLanguageConverter()->convertCategoryKey( $sortkey );
2728 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2729
2730 continue;
2731 }
2732 }
2733
2734 # Self-link checking. For some languages, variants of the title are checked in
2735 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2736 # for linking to a different variant.
2737 if ( $ns !== NS_SPECIAL && $nt->equals( $this->getTitle() ) && !$nt->hasFragment() ) {
2738 $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2739 continue;
2740 }
2741
2742 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2743 # @todo FIXME: Should do batch file existence checks, see comment below
2744 if ( $ns === NS_MEDIA ) {
2745 # Give extensions a chance to select the file revision for us
2746 $options = [];
2747 $descQuery = false;
2748 $this->hookRunner->onBeforeParserFetchFileAndTitle(
2749 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
2750 $this, $nt, $options, $descQuery
2751 );
2752 # Fetch and register the file (file title may be different via hooks)
2753 list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2754 # Cloak with NOPARSE to avoid replacement in handleExternalLinks
2755 $s .= $prefix . $this->armorLinks(
2756 Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2757 continue;
2758 }
2759
2760 # Some titles, such as valid special pages or files in foreign repos, should
2761 # be shown as bluelinks even though they're not included in the page table
2762 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2763 # batch file existence checks for NS_FILE and NS_MEDIA
2764 if ( $iw == '' && $nt->isAlwaysKnown() ) {
2765 $this->mOutput->addLink( $nt );
2766 $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2767 } else {
2768 # Links will be added to the output link list after checking
2769 $s .= $holders->makeHolder( $nt, $text, $trail, $prefix );
2770 }
2771 }
2772 return $holders;
2773 }
2774
2788 private function makeKnownLinkHolder( LinkTarget $nt, $text = '', $trail = '', $prefix = '' ) {
2789 list( $inside, $trail ) = Linker::splitTrail( $trail );
2790
2791 if ( $text == '' ) {
2792 $text = htmlspecialchars( $this->titleFormatter->getPrefixedText( $nt ) );
2793 }
2794
2795 $link = $this->getLinkRenderer()->makeKnownLink(
2796 $nt, new HtmlArmor( "$prefix$text$inside" )
2797 );
2798
2799 return $this->armorLinks( $link ) . $trail;
2800 }
2801
2812 private function armorLinks( $text ) {
2813 return preg_replace( '/\b((?i)' . $this->urlUtils->validProtocols() . ')/',
2814 self::MARKER_PREFIX . "NOPARSE$1", $text );
2815 }
2816
2826 public function doBlockLevels( $text, $linestart ) {
2827 wfDeprecated( __METHOD__, '1.35' );
2828 return BlockLevelPass::doBlockLevels( $text, $linestart );
2829 }
2830
2839 private function expandMagicVariable( $index, $frame = false ) {
2844 if ( isset( $this->mVarCache[$index] ) ) {
2845 return $this->mVarCache[$index];
2846 }
2847
2848 $ts = new MWTimestamp( $this->mOptions->getTimestamp() /* TS_MW */ );
2849 if ( $this->hookContainer->isRegistered( 'ParserGetVariableValueTs' ) ) {
2850 $s = $ts->getTimestamp( TS_UNIX );
2851 $this->hookRunner->onParserGetVariableValueTs( $this, $s );
2852 $ts = new MWTimestamp( $s );
2853 }
2854
2856 $this, $index, $ts, $this->nsInfo, $this->svcOptions, $this->logger
2857 );
2858
2859 if ( $value === null ) {
2860 // Not a defined core magic word
2861 // Don't give this hook unrestricted access to mVarCache
2862 $fakeCache = [];
2863 $this->hookRunner->onParserGetVariableValueSwitch(
2864 // @phan-suppress-next-line PhanTypeMismatchArgument $value is passed as null but returned as string
2865 $this, $fakeCache, $index, $value, $frame
2866 );
2867 // Cache the value returned by the hook by falling through here.
2868 // Assert the the hook returned a non-null value for this MV
2869 '@phan-var string $value';
2870 }
2871
2872 $this->mVarCache[$index] = $value;
2873
2874 return $value;
2875 }
2876
2881 private function initializeVariables() {
2882 $variableIDs = $this->magicWordFactory->getVariableIDs();
2883 $substIDs = $this->magicWordFactory->getSubstIDs();
2884
2885 $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
2886 $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
2887 }
2888
2907 public function preprocessToDom( $text, $flags = 0 ) {
2908 return $this->getPreprocessor()->preprocessToObj( $text, $flags );
2909 }
2910
2932 public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2933 # Is there any text? Also, Prevent too big inclusions!
2934 $textSize = strlen( $text );
2935 if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2936 return $text;
2937 }
2938
2939 if ( $frame === false ) {
2940 $frame = $this->getPreprocessor()->newFrame();
2941 } elseif ( !( $frame instanceof PPFrame ) ) {
2942 $this->logger->debug(
2943 __METHOD__ . " called using plain parameters instead of " .
2944 "a PPFrame instance. Creating custom frame."
2945 );
2946 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
2947 }
2948
2949 $dom = $this->preprocessToDom( $text );
2950 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2951 $text = $frame->expand( $dom, $flags );
2952
2953 return $text;
2954 }
2955
2983 public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2984 # does no harm if $current and $max are present but are unnecessary for the message
2985 # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2986 # only during preview, and that would split the parser cache unnecessarily.
2987 $this->mOutput->addWarningMsg(
2988 "$limitationType-warning",
2989 Message::numParam( $current ),
2990 Message::numParam( $max )
2991 );
2992 $this->addTrackingCategory( "$limitationType-category" );
2993 }
2994
3008 public function braceSubstitution( array $piece, PPFrame $frame ) {
3009 // Flags
3010
3011 // $text has been filled
3012 $found = false;
3013 $text = '';
3014 // wiki markup in $text should be escaped
3015 $nowiki = false;
3016 // $text is HTML, armour it against wikitext transformation
3017 $isHTML = false;
3018 // Force interwiki transclusion to be done in raw mode not rendered
3019 $forceRawInterwiki = false;
3020 // $text is a DOM node needing expansion in a child frame
3021 $isChildObj = false;
3022 // $text is a DOM node needing expansion in the current frame
3023 $isLocalObj = false;
3024
3025 # Title object, where $text came from
3026 $title = false;
3027
3028 # $part1 is the bit before the first |, and must contain only title characters.
3029 # Various prefixes will be stripped from it later.
3030 $titleWithSpaces = $frame->expand( $piece['title'] );
3031 $part1 = trim( $titleWithSpaces );
3032 $titleText = false;
3033
3034 # Original title text preserved for various purposes
3035 $originalTitle = $part1;
3036
3037 # $args is a list of argument nodes, starting from index 0, not including $part1
3038 # @todo FIXME: If piece['parts'] is null then the call to getLength()
3039 # below won't work b/c this $args isn't an object
3040 $args = ( $piece['parts'] == null ) ? [] : $piece['parts'];
3041
3042 $profileSection = null; // profile templates
3043
3044 $sawDeprecatedTemplateEquals = false; // T91154
3045
3046 # SUBST
3047 // @phan-suppress-next-line PhanImpossibleCondition
3048 if ( !$found ) {
3049 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3050
3051 # Possibilities for substMatch: "subst", "safesubst" or FALSE
3052 # Decide whether to expand template or keep wikitext as-is.
3053 if ( $this->ot['wiki'] ) {
3054 if ( $substMatch === false ) {
3055 $literal = true; # literal when in PST with no prefix
3056 } else {
3057 $literal = false; # expand when in PST with subst: or safesubst:
3058 }
3059 } else {
3060 if ( $substMatch == 'subst' ) {
3061 $literal = true; # literal when not in PST with plain subst:
3062 } else {
3063 $literal = false; # expand when not in PST with safesubst: or no prefix
3064 }
3065 }
3066 if ( $literal ) {
3067 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3068 $isLocalObj = true;
3069 $found = true;
3070 }
3071 }
3072
3073 # Variables
3074 if ( !$found && $args->getLength() == 0 ) {
3075 $id = $this->mVariables->matchStartToEnd( $part1 );
3076 if ( $id !== false ) {
3077 if ( strpos( $part1, ':' ) !== false ) {
3079 'Registering a magic variable with a name including a colon',
3080 '1.39', false, false
3081 );
3082 }
3083 $text = $this->expandMagicVariable( $id, $frame );
3084 if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
3085 $this->mOutput->updateCacheExpiry(
3086 $this->magicWordFactory->getCacheTTL( $id ) );
3087 }
3088 $found = true;
3089 }
3090 }
3091
3092 # MSG, MSGNW and RAW
3093 if ( !$found ) {
3094 # Check for MSGNW:
3095 $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
3096 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3097 $nowiki = true;
3098 } else {
3099 # Remove obsolete MSG:
3100 $mwMsg = $this->magicWordFactory->get( 'msg' );
3101 $mwMsg->matchStartAndRemove( $part1 );
3102 }
3103
3104 # Check for RAW:
3105 $mwRaw = $this->magicWordFactory->get( 'raw' );
3106 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3107 $forceRawInterwiki = true;
3108 }
3109 }
3110
3111 # Parser functions
3112 if ( !$found ) {
3113 $colonPos = strpos( $part1, ':' );
3114 if ( $colonPos !== false ) {
3115 $func = substr( $part1, 0, $colonPos );
3116 $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3117 $argsLength = $args->getLength();
3118 for ( $i = 0; $i < $argsLength; $i++ ) {
3119 $funcArgs[] = $args->item( $i );
3120 }
3121
3122 $result = $this->callParserFunction( $frame, $func, $funcArgs );
3123
3124 // Extract any forwarded flags
3125 if ( isset( $result['title'] ) ) {
3126 $title = $result['title'];
3127 }
3128 if ( isset( $result['found'] ) ) {
3129 $found = $result['found'];
3130 }
3131 if ( array_key_exists( 'text', $result ) ) {
3132 // a string or null
3133 $text = $result['text'];
3134 }
3135 if ( isset( $result['nowiki'] ) ) {
3136 $nowiki = $result['nowiki'];
3137 }
3138 if ( isset( $result['isHTML'] ) ) {
3139 $isHTML = $result['isHTML'];
3140 }
3141 if ( isset( $result['forceRawInterwiki'] ) ) {
3142 $forceRawInterwiki = $result['forceRawInterwiki'];
3143 }
3144 if ( isset( $result['isChildObj'] ) ) {
3145 $isChildObj = $result['isChildObj'];
3146 }
3147 if ( isset( $result['isLocalObj'] ) ) {
3148 $isLocalObj = $result['isLocalObj'];
3149 }
3150 }
3151 }
3152
3153 # Finish mangling title and then check for loops.
3154 # Set $title to a Title object and $titleText to the PDBK
3155 if ( !$found ) {
3156 $ns = NS_TEMPLATE;
3157 # Split the title into page and subpage
3158 $subpage = '';
3159 $relative = Linker::normalizeSubpageLink(
3160 $this->getTitle(), $part1, $subpage
3161 );
3162 if ( $part1 !== $relative ) {
3163 $part1 = $relative;
3164 $ns = $this->getTitle()->getNamespace();
3165 }
3166 $title = Title::newFromText( $part1, $ns );
3167 if ( $title ) {
3168 $titleText = $title->getPrefixedText();
3169 # Check for language variants if the template is not found
3170 if ( $this->getTargetLanguageConverter()->hasVariants() && $title->getArticleID() == 0 ) {
3171 $this->getTargetLanguageConverter()->findVariantLink( $part1, $title, true );
3172 }
3173 # Do recursion depth check
3174 $limit = $this->mOptions->getMaxTemplateDepth();
3175 if ( $frame->depth >= $limit ) {
3176 $found = true;
3177 $text = '<span class="error">'
3178 . wfMessage( 'parser-template-recursion-depth-warning' )
3179 ->numParams( $limit )->inContentLanguage()->text()
3180 . '</span>';
3181 }
3182 }
3183 }
3184
3185 # Load from database
3186 if ( !$found && $title ) {
3187 $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3188 if ( !$title->isExternal() ) {
3189 if ( $title->isSpecialPage()
3190 && $this->mOptions->getAllowSpecialInclusion()
3191 && $this->ot['html']
3192 ) {
3193 $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
3194 // Pass the template arguments as URL parameters.
3195 // "uselang" will have no effect since the Language object
3196 // is forced to the one defined in ParserOptions.
3197 $pageArgs = [];
3198 $argsLength = $args->getLength();
3199 for ( $i = 0; $i < $argsLength; $i++ ) {
3200 $bits = $args->item( $i )->splitArg();
3201 if ( strval( $bits['index'] ) === '' ) {
3202 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3203 $value = trim( $frame->expand( $bits['value'] ) );
3204 $pageArgs[$name] = $value;
3205 }
3206 }
3207
3208 // Create a new context to execute the special page
3209 $context = new RequestContext;
3210 $context->setTitle( $title );
3211 $context->setRequest( new FauxRequest( $pageArgs ) );
3212 if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3213 $context->setUser( $this->userFactory->newFromUserIdentity( $this->getUserIdentity() ) );
3214 } else {
3215 // If this page is cached, then we better not be per user.
3216 $context->setUser( User::newFromName( '127.0.0.1', false ) );
3217 }
3218 $context->setLanguage( $this->mOptions->getUserLangObj() );
3219 $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
3220 if ( $ret ) {
3221 $text = $context->getOutput()->getHTML();
3222 $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3223 $found = true;
3224 $isHTML = true;
3225 if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3226 $this->mOutput->updateRuntimeAdaptiveExpiry(
3227 $specialPage->maxIncludeCacheTime()
3228 );
3229 }
3230 }
3231 } elseif ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
3232 $found = false; # access denied
3233 $this->logger->debug(
3234 __METHOD__ .
3235 ": template inclusion denied for " . $title->getPrefixedDBkey()
3236 );
3237 } else {
3238 list( $text, $title ) = $this->getTemplateDom( $title );
3239 if ( $text !== false ) {
3240 $found = true;
3241 $isChildObj = true;
3242 if (
3243 $title->getNamespace() === NS_TEMPLATE &&
3244 $title->getDBkey() === '=' &&
3245 $originalTitle === '='
3246 ) {
3247 // Note that we won't get here if `=` is evaluated
3248 // (in the future) as a parser function, nor if
3249 // the Template namespace is given explicitly,
3250 // ie `{{Template:=}}`. Only `{{=}}` triggers.
3251 $sawDeprecatedTemplateEquals = true; // T91154
3252 }
3253 }
3254 }
3255
3256 # If the title is valid but undisplayable, make a link to it
3257 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3258 $text = "[[:$titleText]]";
3259 $found = true;
3260 }
3261 } elseif ( $title->isTrans() ) {
3262 # Interwiki transclusion
3263 if ( $this->ot['html'] && !$forceRawInterwiki ) {
3264 $text = $this->interwikiTransclude( $title, 'render' );
3265 $isHTML = true;
3266 } else {
3267 $text = $this->interwikiTransclude( $title, 'raw' );
3268 # Preprocess it like a template
3269 $text = $this->preprocessToDom( $text, Preprocessor::DOM_FOR_INCLUSION );
3270 $isChildObj = true;
3271 }
3272 $found = true;
3273 }
3274
3275 # Do infinite loop check
3276 # This has to be done after redirect resolution to avoid infinite loops via redirects
3277 if ( !$frame->loopCheck( $title ) ) {
3278 $found = true;
3279 $text = '<span class="error">'
3280 . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3281 . '</span>';
3282 $this->addTrackingCategory( 'template-loop-category' );
3283 $this->mOutput->addWarningMsg(
3284 'template-loop-warning',
3285 Message::plaintextParam( $titleText )
3286 );
3287 $this->logger->debug( __METHOD__ . ": template loop broken at '$titleText'" );
3288 }
3289 }
3290
3291 # If we haven't found text to substitute by now, we're done
3292 # Recover the source wikitext and return it
3293 if ( !$found ) {
3294 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3295 if ( $profileSection ) {
3296 $this->mProfiler->scopedProfileOut( $profileSection );
3297 }
3298 return [ 'object' => $text ];
3299 }
3300
3301 # Expand DOM-style return values in a child frame
3302 if ( $isChildObj ) {
3303 # Clean up argument array
3304 $newFrame = $frame->newChild( $args, $title );
3305
3306 if ( $nowiki ) {
3307 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3308 } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3309 # Expansion is eligible for the empty-frame cache
3310 $text = $newFrame->cachedExpand( $titleText, $text );
3311 } else {
3312 # Uncached expansion
3313 $text = $newFrame->expand( $text );
3314 }
3315 }
3316 if ( $isLocalObj && $nowiki ) {
3317 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3318 $isLocalObj = false;
3319 }
3320
3321 if ( $profileSection ) {
3322 $this->mProfiler->scopedProfileOut( $profileSection );
3323 }
3324 if (
3325 $sawDeprecatedTemplateEquals &&
3326 $this->mStripState->unstripBoth( $text ) !== '='
3327 ) {
3328 // T91154: {{=}} is deprecated when it doesn't expand to `=`;
3329 // use {{Template:=}} if you must.
3330 $this->addTrackingCategory( 'template-equals-category' );
3331 $this->mOutput->addWarningMsg( 'template-equals-warning' );
3332 }
3333
3334 # Replace raw HTML by a placeholder
3335 if ( $isHTML ) {
3336 $text = $this->insertStripItem( $text );
3337 } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3338 # Escape nowiki-style return values
3339 $text = wfEscapeWikiText( $text );
3340 } elseif ( is_string( $text )
3341 && !$piece['lineStart']
3342 && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3343 ) {
3344 # T2529: if the template begins with a table or block-level
3345 # element, it should be treated as beginning a new line.
3346 # This behavior is somewhat controversial.
3347 $text = "\n" . $text;
3348 }
3349
3350 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3351 # Error, oversize inclusion
3352 if ( $titleText !== false ) {
3353 # Make a working, properly escaped link if possible (T25588)
3354 $text = "[[:$titleText]]";
3355 } else {
3356 # This will probably not be a working link, but at least it may
3357 # provide some hint of where the problem is
3358 $originalTitle = preg_replace( '/^:/', '', $originalTitle );
3359 $text = "[[:$originalTitle]]";
3360 }
3361 $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3362 . 'post-expand include size too large -->' );
3363 $this->limitationWarn( 'post-expand-template-inclusion' );
3364 }
3365
3366 if ( $isLocalObj ) {
3367 $ret = [ 'object' => $text ];
3368 } else {
3369 $ret = [ 'text' => $text ];
3370 }
3371
3372 return $ret;
3373 }
3374
3393 public function callParserFunction( PPFrame $frame, $function, array $args = [] ) {
3394 # Case sensitive functions
3395 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3396 $function = $this->mFunctionSynonyms[1][$function];
3397 } else {
3398 # Case insensitive functions
3399 $function = $this->contLang->lc( $function );
3400 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3401 $function = $this->mFunctionSynonyms[0][$function];
3402 } else {
3403 return [ 'found' => false ];
3404 }
3405 }
3406
3407 list( $callback, $flags ) = $this->mFunctionHooks[$function];
3408
3409 $allArgs = [ $this ];
3410 if ( $flags & self::SFH_OBJECT_ARGS ) {
3411 # Convert arguments to PPNodes and collect for appending to $allArgs
3412 $funcArgs = [];
3413 foreach ( $args as $k => $v ) {
3414 if ( $v instanceof PPNode || $k === 0 ) {
3415 $funcArgs[] = $v;
3416 } else {
3417 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3418 }
3419 }
3420
3421 # Add a frame parameter, and pass the arguments as an array
3422 $allArgs[] = $frame;
3423 $allArgs[] = $funcArgs;
3424 } else {
3425 # Convert arguments to plain text and append to $allArgs
3426 foreach ( $args as $k => $v ) {
3427 if ( $v instanceof PPNode ) {
3428 $allArgs[] = trim( $frame->expand( $v ) );
3429 } elseif ( is_int( $k ) && $k >= 0 ) {
3430 $allArgs[] = trim( $v );
3431 } else {
3432 $allArgs[] = trim( "$k=$v" );
3433 }
3434 }
3435 }
3436
3437 $result = $callback( ...$allArgs );
3438
3439 # The interface for function hooks allows them to return a wikitext
3440 # string or an array containing the string and any flags. This mungs
3441 # things around to match what this method should return.
3442 if ( !is_array( $result ) ) {
3443 $result = [
3444 'found' => true,
3445 'text' => $result,
3446 ];
3447 } else {
3448 if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3449 $result['text'] = $result[0];
3450 }
3451 unset( $result[0] );
3452 $result += [
3453 'found' => true,
3454 ];
3455 }
3456
3457 $noparse = true;
3458 $preprocessFlags = 0;
3459 if ( isset( $result['noparse'] ) ) {
3460 $noparse = $result['noparse'];
3461 }
3462 if ( isset( $result['preprocessFlags'] ) ) {
3463 $preprocessFlags = $result['preprocessFlags'];
3464 }
3465
3466 if ( !$noparse ) {
3467 $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3468 $result['isChildObj'] = true;
3469 }
3470
3471 return $result;
3472 }
3473
3483 public function getTemplateDom( LinkTarget $title ) {
3484 $cacheTitle = $title;
3485 $titleKey = CacheKeyHelper::getKeyForPage( $title );
3486
3487 if ( isset( $this->mTplRedirCache[$titleKey] ) ) {
3488 list( $ns, $dbk ) = $this->mTplRedirCache[$titleKey];
3489 $title = Title::makeTitle( $ns, $dbk );
3490 $titleKey = CacheKeyHelper::getKeyForPage( $title );
3491 }
3492 if ( isset( $this->mTplDomCache[$titleKey] ) ) {
3493 return [ $this->mTplDomCache[$titleKey], $title ];
3494 }
3495
3496 # Cache miss, go to the database
3497 list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3498
3499 if ( $text === false ) {
3500 $this->mTplDomCache[$titleKey] = false;
3501 return [ false, $title ];
3502 }
3503
3504 $dom = $this->preprocessToDom( $text, Preprocessor::DOM_FOR_INCLUSION );
3505 $this->mTplDomCache[$titleKey] = $dom;
3506
3507 if ( !$title->isSamePageAs( $cacheTitle ) ) {
3508 $this->mTplRedirCache[ CacheKeyHelper::getKeyForPage( $cacheTitle ) ] =
3509 [ $title->getNamespace(), $title->getDBkey() ];
3510 }
3511
3512 return [ $dom, $title ];
3513 }
3514
3529 $cacheKey = CacheKeyHelper::getKeyForPage( $link );
3530 if ( !$this->currentRevisionCache ) {
3531 $this->currentRevisionCache = new MapCacheLRU( 100 );
3532 }
3533 if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3534 $title = Title::castFromLinkTarget( $link ); // hook signature compat
3535 $revisionRecord =
3536 // Defaults to Parser::statelessFetchRevisionRecord()
3537 call_user_func(
3538 $this->mOptions->getCurrentRevisionRecordCallback(),
3539 $title,
3540 $this
3541 );
3542 if ( $revisionRecord === false ) {
3543 // Parser::statelessFetchRevisionRecord() can return false;
3544 // normalize it to null.
3545 $revisionRecord = null;
3546 }
3547 $this->currentRevisionCache->set( $cacheKey, $revisionRecord );
3548 }
3549 return $this->currentRevisionCache->get( $cacheKey );
3550 }
3551
3559 $key = CacheKeyHelper::getKeyForPage( $link );
3560 return (
3561 $this->currentRevisionCache &&
3562 $this->currentRevisionCache->has( $key )
3563 );
3564 }
3565
3574 public static function statelessFetchRevisionRecord( LinkTarget $link, $parser = null ) {
3575 if ( $link instanceof PageIdentity ) {
3576 // probably a Title, just use it.
3577 $page = $link;
3578 } else {
3579 // XXX: use RevisionStore::getPageForLink()!
3580 // ...but get the info for the current revision at the same time?
3581 // Should RevisionStore::getKnownCurrentRevision accept a LinkTarget?
3582 $page = Title::castFromLinkTarget( $link );
3583 }
3584
3585 $revRecord = MediaWikiServices::getInstance()
3586 ->getRevisionLookup()
3587 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
3588 ->getKnownCurrentRevision( $page );
3589 return $revRecord;
3590 }
3591
3598 public function fetchTemplateAndTitle( LinkTarget $link ) {
3599 // Use Title for compatibility with callbacks and return type
3600 $title = Title::castFromLinkTarget( $link );
3601
3602 // Defaults to Parser::statelessFetchTemplate()
3603 $templateCb = $this->mOptions->getTemplateCallback();
3604 $stuff = $templateCb( $title, $this );
3605 $revRecord = $stuff['revision-record'] ?? null;
3606
3607 $text = $stuff['text'];
3608 if ( is_string( $stuff['text'] ) ) {
3609 // We use U+007F DELETE to distinguish strip markers from regular text
3610 $text = strtr( $text, "\x7f", "?" );
3611 }
3612 $finalTitle = $stuff['finalTitle'] ?? $title;
3613 foreach ( ( $stuff['deps'] ?? [] ) as $dep ) {
3614 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3615 if ( $dep['title']->equals( $this->getTitle() ) && $revRecord instanceof RevisionRecord ) {
3616 // Self-transclusion; final result may change based on the new page version
3617 try {
3618 $sha1 = $revRecord->getSha1();
3619 } catch ( RevisionAccessException $e ) {
3620 $sha1 = null;
3621 }
3622 $this->setOutputFlag( ParserOutputFlags::VARY_REVISION_SHA1, 'Self transclusion' );
3623 $this->getOutput()->setRevisionUsedSha1Base36( $sha1 );
3624 }
3625 }
3626
3627 return [ $text, $finalTitle ];
3628 }
3629
3640 public static function statelessFetchTemplate( $page, $parser = false ) {
3641 $title = Title::castFromLinkTarget( $page ); // for compatibility with return type
3642 $text = $skip = false;
3643 $finalTitle = $title;
3644 $deps = [];
3645 $revRecord = null;
3646 $contextTitle = $parser ? $parser->getTitle() : null;
3647
3648 # Loop to fetch the article, with up to 2 redirects
3649 $revLookup = MediaWikiServices::getInstance()->getRevisionLookup();
3650 for ( $i = 0; $i < 3 && is_object( $title ); $i++ ) {
3651 # Give extensions a chance to select the revision instead
3652 $revRecord = null; # Assume no hook
3653 $id = false; # Assume current
3654 $origTitle = $title;
3655 $titleChanged = false;
3656 Hooks::runner()->onBeforeParserFetchTemplateRevisionRecord(
3657 # The $title is a not a PageIdentity, as it may
3658 # contain fragments or even represent an attempt to transclude
3659 # a broken or otherwise-missing Title, which the hook may
3660 # fix up. Similarly, the $contextTitle may represent a special
3661 # page or other page which "exists" as a parsing context but
3662 # is not in the DB.
3663 $contextTitle, $title,
3664 $skip, $revRecord
3665 );
3666 if ( !$skip && !$revRecord ) {
3667 # Deprecated legacy hook
3668 Hooks::runner()->onBeforeParserFetchTemplateAndtitle(
3669 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
3670 $parser, $title, $skip, $id
3671 );
3672 }
3673
3674 if ( $skip ) {
3675 $text = false;
3676 $deps[] = [
3677 'title' => $title,
3678 'page_id' => $title->getArticleID(),
3679 'rev_id' => null
3680 ];
3681 break;
3682 }
3683 # Get the revision
3684 if ( !$revRecord ) {
3685 if ( $id ) {
3686 # Handle $id returned by deprecated legacy hook
3687 $revRecord = $revLookup->getRevisionById( $id );
3688 } elseif ( $parser ) {
3689 $revRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
3690 } else {
3691 $revRecord = $revLookup->getRevisionByTitle( $title );
3692 }
3693 }
3694 if ( $revRecord ) {
3695 # Update title, as $revRecord may have been changed by hook
3696 $title = Title::newFromLinkTarget(
3697 $revRecord->getPageAsLinkTarget()
3698 );
3699 $deps[] = [
3700 'title' => $title,
3701 'page_id' => $revRecord->getPageId(),
3702 'rev_id' => $revRecord->getId(),
3703 ];
3704 } else {
3705 $deps[] = [
3706 'title' => $title,
3707 'page_id' => $title->getArticleID(),
3708 'rev_id' => null,
3709 ];
3710 }
3711 if ( !$title->equals( $origTitle ) ) {
3712 # If we fetched a rev from a different title, register
3713 # the original title too...
3714 $deps[] = [
3715 'title' => $origTitle,
3716 'page_id' => $origTitle->getArticleID(),
3717 'rev_id' => null,
3718 ];
3719 $titleChanged = true;
3720 }
3721 # If there is no current revision, there is no page
3722 if ( $revRecord === null || $revRecord->getId() === null ) {
3723 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3724 $linkCache->addBadLinkObj( $title );
3725 }
3726 if ( $revRecord ) {
3727 if ( $titleChanged && !$revRecord->hasSlot( SlotRecord::MAIN ) ) {
3728 // We've added this (missing) title to the dependencies;
3729 // give the hook another chance to redirect it to an
3730 // actual page.
3731 $text = false;
3732 $finalTitle = $title;
3733 continue;
3734 }
3735 if ( $revRecord->hasSlot( SlotRecord::MAIN ) ) { // T276476
3736 $content = $revRecord->getContent( SlotRecord::MAIN );
3737 $text = $content ? $content->getWikitextForTransclusion() : null;
3738 } else {
3739 $text = false;
3740 }
3741
3742 if ( $text === false || $text === null ) {
3743 $text = false;
3744 break;
3745 }
3746 } elseif ( $title->getNamespace() === NS_MEDIAWIKI ) {
3747 $message = wfMessage( MediaWikiServices::getInstance()->getContentLanguage()->
3748 lcfirst( $title->getText() ) )->inContentLanguage();
3749 if ( !$message->exists() ) {
3750 $text = false;
3751 break;
3752 }
3753 $text = $message->plain();
3754 break;
3755 } else {
3756 break;
3757 }
3758 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Only reached when content is set
3759 if ( !$content ) {
3760 break;
3761 }
3762 # Redirect?
3763 $finalTitle = $title;
3764 $title = $content->getRedirectTarget();
3765 }
3766
3767 $retValues = [
3768 // previously, when this also returned a Revision object, we set
3769 // 'revision-record' to false instead of null if it was unavailable,
3770 // so that callers to use isset and then rely on the revision-record
3771 // key instead of the revision key, even if there was no corresponding
3772 // object - we continue to set to false here for backwards compatability
3773 'revision-record' => $revRecord ?: false,
3774 'text' => $text,
3775 'finalTitle' => $finalTitle,
3776 'deps' => $deps
3777 ];
3778 return $retValues;
3779 }
3780
3789 public function fetchFileAndTitle( LinkTarget $link, array $options = [] ) {
3790 $file = $this->fetchFileNoRegister( $link, $options );
3791
3792 $time = $file ? $file->getTimestamp() : false;
3793 $sha1 = $file ? $file->getSha1() : false;
3794 # Register the file as a dependency...
3795 $this->mOutput->addImage( $link->getDBkey(), $time, $sha1 );
3796 if ( $file && !$link->isSameLinkAs( $file->getTitle() ) ) {
3797 # Update fetched file title
3798 $page = $file->getTitle();
3799 $this->mOutput->addImage( $page->getDBkey(), $time, $sha1 );
3800 }
3801
3802 $title = Title::castFromLinkTarget( $link ); // for return type compat
3803 return [ $file, $title ];
3804 }
3805
3816 protected function fetchFileNoRegister( LinkTarget $link, array $options = [] ) {
3817 if ( isset( $options['broken'] ) ) {
3818 $file = false; // broken thumbnail forced by hook
3819 } else {
3820 $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
3821 if ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3822 $file = $repoGroup->findFileFromKey( $options['sha1'], $options );
3823 } else { // get by (name,timestamp)
3824 $file = $repoGroup->findFile( $link, $options );
3825 }
3826 }
3827 return $file;
3828 }
3829
3839 public function interwikiTransclude( LinkTarget $link, $action ) {
3840 if ( !$this->svcOptions->get( MainConfigNames::EnableScaryTranscluding ) ) {
3841 return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3842 }
3843
3844 // TODO: extract relevant functionality from Title
3845 $title = Title::castFromLinkTarget( $link );
3846
3847 $url = $title->getFullURL( [ 'action' => $action ] );
3848 if ( strlen( $url ) > 1024 ) {
3849 return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3850 }
3851
3852 $wikiId = $title->getTransWikiID(); // remote wiki ID or false
3853
3854 $fname = __METHOD__;
3855 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3856
3857 $data = $cache->getWithSetCallback(
3858 $cache->makeGlobalKey(
3859 'interwiki-transclude',
3860 ( $wikiId !== false ) ? $wikiId : 'external',
3861 sha1( $url )
3862 ),
3863 $this->svcOptions->get( MainConfigNames::TranscludeCacheExpiry ),
3864 function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
3865 $req = $this->httpRequestFactory->create( $url, [], $fname );
3866
3867 $status = $req->execute(); // Status object
3868 if ( !$status->isOK() ) {
3869 $ttl = $cache::TTL_UNCACHEABLE;
3870 } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
3871 $ttl = min( $cache::TTL_LAGGED, $ttl );
3872 }
3873
3874 return [
3875 'text' => $status->isOK() ? $req->getContent() : null,
3876 'code' => $req->getStatus()
3877 ];
3878 },
3879 [
3880 'checkKeys' => ( $wikiId !== false )
3881 ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
3882 : [],
3883 'pcGroup' => 'interwiki-transclude:5',
3884 'pcTTL' => $cache::TTL_PROC_LONG
3885 ]
3886 );
3887
3888 if ( is_string( $data['text'] ) ) {
3889 $text = $data['text'];
3890 } elseif ( $data['code'] != 200 ) {
3891 // Though we failed to fetch the content, this status is useless.
3892 $text = wfMessage( 'scarytranscludefailed-httpstatus' )
3893 ->params( $url, $data['code'] )->inContentLanguage()->text();
3894 } else {
3895 $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3896 }
3897
3898 return $text;
3899 }
3900
3911 public function argSubstitution( array $piece, PPFrame $frame ) {
3912 $error = false;
3913 $parts = $piece['parts'];
3914 $nameWithSpaces = $frame->expand( $piece['title'] );
3915 $argName = trim( $nameWithSpaces );
3916 $object = false;
3917 $text = $frame->getArgument( $argName );
3918 if ( $text === false && $parts->getLength() > 0
3919 && ( $this->ot['html']
3920 || $this->ot['pre']
3921 || ( $this->ot['wiki'] && $frame->isTemplate() )
3922 )
3923 ) {
3924 # No match in frame, use the supplied default
3925 $object = $parts->item( 0 )->getChildren();
3926 }
3927 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3928 $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3929 $this->limitationWarn( 'post-expand-template-argument' );
3930 }
3931
3932 if ( $text === false && $object === false ) {
3933 # No match anywhere
3934 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3935 }
3936 if ( $error !== false ) {
3937 $text .= $error;
3938 }
3939 if ( $object !== false ) {
3940 $ret = [ 'object' => $object ];
3941 } else {
3942 $ret = [ 'text' => $text ];
3943 }
3944
3945 return $ret;
3946 }
3947
3952 public function tagNeedsNowikiStrippedInTagPF( string $lowerTagName ): bool {
3953 $parsoidSiteConfig = MediaWikiServices::getInstance()->getParsoidSiteConfig();
3954 return $parsoidSiteConfig->tagNeedsNowikiStrippedInTagPF( $lowerTagName );
3955 }
3956
3978 public function extensionSubstitution( array $params, PPFrame $frame, bool $processNowiki = false ) {
3979 static $errorStr = '<span class="error">';
3980 static $errorLen = 20;
3981
3982 $name = $frame->expand( $params['name'] );
3983 if ( substr( $name, 0, $errorLen ) === $errorStr ) {
3984 // Probably expansion depth or node count exceeded. Just punt the
3985 // error up.
3986 return $name;
3987 }
3988
3989 $attrText = !isset( $params['attr'] ) ? '' : $frame->expand( $params['attr'] );
3990 if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
3991 // See above
3992 return $attrText;
3993 }
3994
3995 // We can't safely check if the expansion for $content resulted in an
3996 // error, because the content could happen to be the error string
3997 // (T149622).
3998 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3999
4000 $marker = self::MARKER_PREFIX . "-$name-"
4001 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
4002
4003 $normalizedName = strtolower( $name );
4004 $isNowiki = $normalizedName === 'nowiki';
4005 $markerType = $isNowiki ? 'nowiki' : 'general';
4006 if ( $this->ot['html'] || ( $processNowiki && $isNowiki ) ) {
4007 $name = $normalizedName;
4008 $attributes = Sanitizer::decodeTagAttributes( $attrText );
4009 if ( isset( $params['attributes'] ) ) {
4010 $attributes += $params['attributes'];
4011 }
4012
4013 if ( isset( $this->mTagHooks[$name] ) ) {
4014 // Note that $content may be null here, for example if the
4015 // tag is self-closed.
4016 $output = call_user_func_array( $this->mTagHooks[$name],
4017 [ $content, $attributes, $this, $frame ] );
4018 } else {
4019 $output = '<span class="error">Invalid tag extension name: ' .
4020 htmlspecialchars( $name ) . '</span>';
4021 }
4022
4023 if ( is_array( $output ) ) {
4024 // Extract flags
4025 $flags = $output;
4026 $output = $flags[0];
4027 if ( isset( $flags['markerType'] ) ) {
4028 $markerType = $flags['markerType'];
4029 }
4030 }
4031 } else {
4032 if ( isset( $params['attributes'] ) ) {
4033 foreach ( $params['attributes'] as $attrName => $attrValue ) {
4034 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
4035 htmlspecialchars( $attrValue, ENT_COMPAT ) . '"';
4036 }
4037 }
4038 if ( $content === null ) {
4039 $output = "<$name$attrText/>";
4040 } else {
4041 $close = $params['close'] === null ? '' : $frame->expand( $params['close'] );
4042 if ( substr( $close, 0, $errorLen ) === $errorStr ) {
4043 // See above
4044 return $close;
4045 }
4046 $output = "<$name$attrText>$content$close";
4047 }
4048 }
4049
4050 if ( $markerType === 'none' ) {
4051 return $output;
4052 } elseif ( $markerType === 'nowiki' ) {
4053 $this->mStripState->addNoWiki( $marker, $output );
4054 } elseif ( $markerType === 'general' ) {
4055 $this->mStripState->addGeneral( $marker, $output );
4056 } else {
4057 throw new MWException( __METHOD__ . ': invalid marker type' );
4058 }
4059 return $marker;
4060 }
4061
4069 private function incrementIncludeSize( $type, $size ) {
4070 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4071 return false;
4072 } else {
4073 $this->mIncludeSizes[$type] += $size;
4074 return true;
4075 }
4076 }
4077
4083 $this->mExpensiveFunctionCount++;
4084 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4085 }
4086
4094 private function handleDoubleUnderscore( $text ) {
4095 # The position of __TOC__ needs to be recorded
4096 $mw = $this->magicWordFactory->get( 'toc' );
4097 if ( $mw->match( $text ) ) {
4098 $this->mShowToc = true;
4099 $this->mForceTocPosition = true;
4100
4101 # Set a placeholder. At the end we'll fill it in with the TOC.
4102 $text = $mw->replace( self::TOC_PLACEHOLDER, $text, 1 );
4103
4104 # Only keep the first one.
4105 $text = $mw->replace( '', $text );
4106 }
4107
4108 # Now match and remove the rest of them
4109 $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4110 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4111
4112 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4113 $this->mOutput->setNoGallery( true );
4114 }
4115 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4116 $this->mShowToc = false;
4117 }
4118 if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4119 && $this->getTitle()->getNamespace() === NS_CATEGORY
4120 ) {
4121 $this->addTrackingCategory( 'hidden-category-category' );
4122 }
4123 # (T10068) Allow control over whether robots index a page.
4124 # __INDEX__ always overrides __NOINDEX__, see T16899
4125 if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->getTitle()->canUseNoindex() ) {
4126 $this->mOutput->setIndexPolicy( 'noindex' );
4127 $this->addTrackingCategory( 'noindex-category' );
4128 }
4129 if ( isset( $this->mDoubleUnderscores['index'] ) && $this->getTitle()->canUseNoindex() ) {
4130 $this->mOutput->setIndexPolicy( 'index' );
4131 $this->addTrackingCategory( 'index-category' );
4132 }
4133
4134 # Cache all double underscores in the database
4135 foreach ( $this->mDoubleUnderscores as $key => $val ) {
4136 $this->mOutput->setPageProperty( $key, '' );
4137 }
4138
4139 return $text;
4140 }
4141
4148 public function addTrackingCategory( $msg ) {
4149 return $this->trackingCategories->addTrackingCategory(
4150 $this->mOutput, $msg, $this->getPage()
4151 );
4152 }
4153
4169 private function finalizeHeadings( $text, $origText, $isMain = true ) {
4170 # Inhibit editsection links if requested in the page
4171 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4172 $maybeShowEditLink = false;
4173 } else {
4174 $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4175 }
4176
4177 # Get all headlines for numbering them and adding funky stuff like [edit]
4178 # links - this is for later, but we need the number of headlines right now
4179 # NOTE: white space in headings have been trimmed in handleHeadings. They shouldn't
4180 # be trimmed here since whitespace in HTML headings is significant.
4181 $matches = [];
4182 $numMatches = preg_match_all(
4183 '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4184 $text,
4185 $matches
4186 );
4187
4188 # if there are fewer than 4 headlines in the article, do not show TOC
4189 # unless it's been explicitly enabled.
4190 $enoughToc = $this->mShowToc &&
4191 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4192
4193 # Allow user to stipulate that a page should have a "new section"
4194 # link added via __NEWSECTIONLINK__
4195 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4196 $this->mOutput->setNewSection( true );
4197 }
4198
4199 # Allow user to remove the "new section"
4200 # link via __NONEWSECTIONLINK__
4201 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4202 $this->mOutput->setHideNewSection( true );
4203 }
4204
4205 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4206 # override above conditions and always show TOC above first header
4207 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4208 $this->mShowToc = true;
4209 $enoughToc = true;
4210 }
4211
4212 # headline counter
4213 $headlineCount = 0;
4214 $numVisible = 0;
4215
4216 # Ugh .. the TOC should have neat indentation levels which can be
4217 # passed to the skin functions. These are determined here
4218 $toc = '';
4219 $full = '';
4220 $head = [];
4221 $sublevelCount = [];
4222 $levelCount = [];
4223 $level = 0;
4224 $prevlevel = 0;
4225 $toclevel = 0;
4226 $prevtoclevel = 0;
4227 $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4228 $baseTitleText = $this->getTitle()->getPrefixedDBkey();
4229 $oldType = $this->mOutputType;
4230 $this->setOutputType( self::OT_WIKI );
4231 $frame = $this->getPreprocessor()->newFrame();
4232 $root = $this->preprocessToDom( $origText );
4233 $node = $root->getFirstChild();
4234 $byteOffset = 0;
4235 $tocraw = [];
4236 $refers = [];
4237
4238 $headlines = $numMatches !== false ? $matches[3] : [];
4239
4240 $maxTocLevel = $this->svcOptions->get( MainConfigNames::MaxTocLevel );
4241 foreach ( $headlines as $headline ) {
4242 $isTemplate = false;
4243 $titleText = false;
4244 $sectionIndex = false;
4245 $numbering = '';
4246 $markerMatches = [];
4247 if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4248 $serial = (int)$markerMatches[1];
4249 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4250 $isTemplate = ( $titleText != $baseTitleText );
4251 $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4252 }
4253
4254 if ( $toclevel ) {
4255 $prevlevel = $level;
4256 }
4257 $level = (int)$matches[1][$headlineCount];
4258
4259 if ( $level > $prevlevel ) {
4260 # Increase TOC level
4261 $toclevel++;
4262 $sublevelCount[$toclevel] = 0;
4263 if ( $toclevel < $maxTocLevel ) {
4264 $prevtoclevel = $toclevel;
4265 $toc .= Linker::tocIndent();
4266 $numVisible++;
4267 }
4268 } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4269 # Decrease TOC level, find level to jump to
4270
4271 for ( $i = $toclevel; $i > 0; $i-- ) {
4272 // @phan-suppress-next-line PhanTypeInvalidDimOffset
4273 if ( $levelCount[$i] == $level ) {
4274 # Found last matching level
4275 $toclevel = $i;
4276 break;
4277 } elseif ( $levelCount[$i] < $level ) {
4278 // @phan-suppress-previous-line PhanTypeInvalidDimOffset
4279 # Found first matching level below current level
4280 $toclevel = $i + 1;
4281 break;
4282 }
4283 }
4284 if ( $i == 0 ) {
4285 $toclevel = 1;
4286 }
4287 if ( $toclevel < $maxTocLevel ) {
4288 if ( $prevtoclevel < $maxTocLevel ) {
4289 # Unindent only if the previous toc level was shown :p
4290 $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4291 $prevtoclevel = $toclevel;
4292 } else {
4293 $toc .= Linker::tocLineEnd();
4294 }
4295 }
4296 } else {
4297 # No change in level, end TOC line
4298 if ( $toclevel < $maxTocLevel ) {
4299 $toc .= Linker::tocLineEnd();
4300 }
4301 }
4302
4303 $levelCount[$toclevel] = $level;
4304
4305 # count number of headlines for each level
4306 $sublevelCount[$toclevel]++;
4307 $dot = 0;
4308 for ( $i = 1; $i <= $toclevel; $i++ ) {
4309 if ( !empty( $sublevelCount[$i] ) ) {
4310 if ( $dot ) {
4311 $numbering .= '.';
4312 }
4313 $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4314 $dot = 1;
4315 }
4316 }
4317
4318 # The safe header is a version of the header text safe to use for links
4319
4320 # Remove link placeholders by the link text.
4321 # <!--LINK number-->
4322 # turns into
4323 # link text with suffix
4324 # Do this before unstrip since link text can contain strip markers
4325 $safeHeadline = $this->replaceLinkHoldersText( $headline );
4326
4327 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4328 $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4329
4330 # Remove any <style> or <script> tags (T198618)
4331 $safeHeadline = preg_replace(
4332 '#<(style|script)(?: [^>]*[^>/])?>.*?</\1>#is',
4333 '',
4334 $safeHeadline
4335 );
4336
4337 # Strip out HTML (first regex removes any tag not allowed)
4338 # Allowed tags are:
4339 # * <sup> and <sub> (T10393)
4340 # * <i> (T28375)
4341 # * <b> (r105284)
4342 # * <bdi> (T74884)
4343 # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4344 # * <s> and <strike> (T35715)
4345 # * <q> (T251672)
4346 # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4347 # to allow setting directionality in toc items.
4348 $tocline = preg_replace(
4349 [
4350 '#<(?!/?(span|sup|sub|bdi|i|b|s|strike|q)(?: [^>]*)?>).*?>#',
4351 '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4352 ],
4353 [ '', '<$1>' ],
4354 $safeHeadline
4355 );
4356
4357 # Strip '<span></span>', which is the result from the above if
4358 # <span id="foo"></span> is used to produce an additional anchor
4359 # for a section.
4360 $tocline = str_replace( '<span></span>', '', $tocline );
4361
4362 $tocline = trim( $tocline );
4363
4364 # For the anchor, strip out HTML-y stuff period
4365 $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4366 $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4367
4368 # Save headline for section edit hint before it's escaped
4369 $headlineHint = $safeHeadline;
4370
4371 # Decode HTML entities
4372 $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4373
4374 $safeHeadline = self::normalizeSectionName( $safeHeadline );
4375
4376 $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4377 $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4378 $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4379 if ( $fallbackHeadline === $safeHeadline ) {
4380 # No reason to have both (in fact, we can't)
4381 $fallbackHeadline = false;
4382 }
4383
4384 # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4385 $arrayKey = strtolower( $safeHeadline );
4386 if ( $fallbackHeadline === false ) {
4387 $fallbackArrayKey = false;
4388 } else {
4389 $fallbackArrayKey = strtolower( $fallbackHeadline );
4390 }
4391
4392 # Create the anchor for linking from the TOC to the section
4393 $anchor = $safeHeadline;
4394 $fallbackAnchor = $fallbackHeadline;
4395 if ( isset( $refers[$arrayKey] ) ) {
4396 for ( $i = 2; isset( $refers["{$arrayKey}_$i"] ); ++$i );
4397 $anchor .= "_$i";
4398 $linkAnchor .= "_$i";
4399 $refers["{$arrayKey}_$i"] = true;
4400 } else {
4401 $refers[$arrayKey] = true;
4402 }
4403 if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
4404 for ( $i = 2; isset( $refers["{$fallbackArrayKey}_$i"] ); ++$i );
4405 $fallbackAnchor .= "_$i";
4406 $refers["{$fallbackArrayKey}_$i"] = true;
4407 } else {
4408 $refers[$fallbackArrayKey] = true;
4409 }
4410
4411 if ( $enoughToc && ( !isset( $maxTocLevel ) || $toclevel < $maxTocLevel ) ) {
4412 $toc .= Linker::tocLine(
4413 $linkAnchor,
4414 $tocline,
4415 $numbering,
4416 $toclevel,
4417 ( $isTemplate ? false : $sectionIndex )
4418 );
4419 }
4420
4421 # Add the section to the section tree
4422 # Find the DOM node for this header
4423 $noOffset = ( $isTemplate || $sectionIndex === false );
4424 while ( $node && !$noOffset ) {
4425 if ( $node->getName() === 'h' ) {
4426 $bits = $node->splitHeading();
4427 if ( $bits['i'] == $sectionIndex ) {
4428 break;
4429 }
4430 }
4431 $byteOffset += mb_strlen(
4432 $this->mStripState->unstripBoth(
4433 $frame->expand( $node, PPFrame::RECOVER_ORIG )
4434 )
4435 );
4436 $node = $node->getNextSibling();
4437 }
4438 $tocraw[] = [
4439 'toclevel' => $toclevel,
4440 // cast $level to string in order to keep b/c for the parse api
4441 'level' => (string)$level,
4442 'line' => $tocline,
4443 'number' => $numbering,
4444 'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4445 'fromtitle' => $titleText,
4446 'byteoffset' => ( $noOffset ? null : $byteOffset ),
4447 'anchor' => $anchor,
4448 ];
4449
4450 # give headline the correct <h#> tag
4451 if ( $maybeShowEditLink && $sectionIndex !== false ) {
4452 // Output edit section links as markers with styles that can be customized by skins
4453 if ( $isTemplate ) {
4454 # Put a T flag in the section identifier, to indicate to extractSections()
4455 # that sections inside <includeonly> should be counted.
4456 $editsectionPage = $titleText;
4457 $editsectionSection = "T-$sectionIndex";
4458 } else {
4459 $editsectionPage = $this->getTitle()->getPrefixedText();
4460 $editsectionSection = $sectionIndex;
4461 }
4462 $editsectionContent = $headlineHint;
4463 // We use a bit of pesudo-xml for editsection markers. The
4464 // language converter is run later on. Using a UNIQ style marker
4465 // leads to the converter screwing up the tokens when it
4466 // converts stuff. And trying to insert strip tags fails too. At
4467 // this point all real inputted tags have already been escaped,
4468 // so we don't have to worry about a user trying to input one of
4469 // these markers directly. We use a page and section attribute
4470 // to stop the language converter from converting these
4471 // important bits of data, but put the headline hint inside a
4472 // content block because the language converter is supposed to
4473 // be able to convert that piece of data.
4474 // Gets replaced with html in ParserOutput::getText
4475 $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage, ENT_COMPAT );
4476 $editlink .= '" section="' . htmlspecialchars( $editsectionSection, ENT_COMPAT ) . '"';
4477 $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4478 } else {
4479 $editlink = '';
4480 }
4481 $head[$headlineCount] = Linker::makeHeadline(
4482 $level,
4483 $matches['attrib'][$headlineCount],
4484 $anchor,
4485 $headline,
4486 $editlink,
4487 $fallbackAnchor
4488 );
4489
4490 $headlineCount++;
4491 }
4492
4493 $this->setOutputType( $oldType );
4494
4495 # Never ever show TOC if no headers (or suppressed)
4496 $suppressToc = $this->mOptions->getSuppressTOC();
4497 if ( $numVisible < 1 || $suppressToc ) {
4498 $enoughToc = false;
4499 }
4500
4501 if ( $enoughToc ) {
4502 if ( $prevtoclevel > 0 && $prevtoclevel < $maxTocLevel ) {
4503 $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4504 }
4505 $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4506 $this->mOutput->setTOCHTML( $toc );
4507 // Record the fact that the TOC should be shown. T294950
4508 // (We shouldn't be looking at ::getTOCHTML() for this because
4509 // eventually that will be replaced (T293513) and
4510 // ::getSections() will contain sections even if there aren't
4511 // $enoughToc to show.)
4512 $this->mOutput->setOutputFlag( ParserOutputFlags::SHOW_TOC );
4513 }
4514
4515 if ( $isMain && !$suppressToc ) {
4516 // We generally output the section information via the API
4517 // even if there isn't "enough" of a ToC to merit showing
4518 // it -- but the "suppress TOC" parser option is set when
4519 // any sections that might be found aren't "really there"
4520 // (ie, JavaScript content that might have spurious === or
4521 // <h2>: T307691) so we will *not* set section information
4522 // in that case.
4523 $this->mOutput->setSections( $tocraw );
4524 }
4525
4526 # split up and insert constructed headlines
4527 $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4528 $i = 0;
4529
4530 // build an array of document sections
4531 $sections = [];
4532 foreach ( $blocks as $block ) {
4533 // $head is zero-based, sections aren't.
4534 if ( empty( $head[$i - 1] ) ) {
4535 $sections[$i] = $block;
4536 } else {
4537 $sections[$i] = $head[$i - 1] . $block;
4538 }
4539
4550 $this->hookRunner->onParserSectionCreate( $this, $i, $sections[$i], $maybeShowEditLink );
4551
4552 $i++;
4553 }
4554
4555 if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4556 // append the TOC at the beginning
4557 // Top anchor now in skin
4558 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset At least one element when enoughToc is true
4559 $sections[0] .= self::TOC_PLACEHOLDER . "\n";
4560 }
4561
4562 $full .= implode( '', $sections );
4563
4564 return $full;
4565 }
4566
4579 public function preSaveTransform(
4580 $text,
4581 PageReference $page,
4582 UserIdentity $user,
4583 ParserOptions $options,
4584 $clearState = true
4585 ) {
4586 if ( $clearState ) {
4587 $magicScopeVariable = $this->lock();
4588 }
4589 $this->startParse( $page, $options, self::OT_WIKI, $clearState );
4590 $this->setUser( $user );
4591
4592 // Strip U+0000 NULL (T159174)
4593 $text = str_replace( "\000", '', $text );
4594
4595 // We still normalize line endings (including trimming trailing whitespace) for
4596 // backwards-compatibility with other code that just calls PST, but this should already
4597 // be handled in TextContent subclasses
4598 $text = TextContent::normalizeLineEndings( $text );
4599
4600 if ( $options->getPreSaveTransform() ) {
4601 $text = $this->pstPass2( $text, $user );
4602 }
4603 $text = $this->mStripState->unstripBoth( $text );
4604
4605 // Trim trailing whitespace again, because the previous steps can introduce it.
4606 $text = rtrim( $text );
4607
4608 $this->hookRunner->onParserPreSaveTransformComplete( $this, $text );
4609
4610 $this->setUser( null ); # Reset
4611
4612 return $text;
4613 }
4614
4623 private function pstPass2( $text, UserIdentity $user ) {
4624 # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4625 # $this->contLang here in order to give everyone the same signature and use the default one
4626 # rather than the one selected in each user's preferences. (see also T14815)
4627 $ts = $this->mOptions->getTimestamp();
4628 $timestamp = MWTimestamp::getLocalInstance( $ts );
4629 $ts = $timestamp->format( 'YmdHis' );
4630 $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4631
4632 $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4633
4634 # Variable replacement
4635 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4636 $text = $this->replaceVariables( $text );
4637
4638 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4639 # which may corrupt this parser instance via its wfMessage()->text() call-
4640
4641 # Signatures
4642 if ( strpos( $text, '~~~' ) !== false ) {
4643 $sigText = $this->getUserSig( $user );
4644 $text = strtr( $text, [
4645 '~~~~~' => $d,
4646 '~~~~' => "$sigText $d",
4647 '~~~' => $sigText
4648 ] );
4649 # The main two signature forms used above are time-sensitive
4650 $this->setOutputFlag( ParserOutputFlags::USER_SIGNATURE, 'User signature detected' );
4651 }
4652
4653 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4654 $tc = '[' . Title::legalChars() . ']';
4655 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4656
4657 // [[ns:page (context)|]]
4658 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4659 // [[ns:page(context)|]] (double-width brackets, added in r40257)
4660 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4661 // [[ns:page (context), context|]] (using single, double-width or Arabic comma)
4662 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,|، )$tc+|)\\|]]/";
4663 // [[|page]] (reverse pipe trick: add context from page title)
4664 $p2 = "/\[\[\\|($tc+)]]/";
4665
4666 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4667 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4668 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4669 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4670
4671 $t = $this->getTitle()->getText();
4672 $m = [];
4673 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4674 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4675 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4676 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4677 } else {
4678 # if there's no context, don't bother duplicating the title
4679 $text = preg_replace( $p2, '[[\\1]]', $text );
4680 }
4681
4682 return $text;
4683 }
4684
4700 public function getUserSig( UserIdentity $user, $nickname = false, $fancySig = null ) {
4701 $username = $user->getName();
4702
4703 # If not given, retrieve from the user object.
4704 if ( $nickname === false ) {
4705 $nickname = $this->userOptionsLookup->getOption( $user, 'nickname' );
4706 }
4707
4708 if ( $fancySig === null ) {
4709 $fancySig = $this->userOptionsLookup->getBoolOption( $user, 'fancysig' );
4710 }
4711
4712 if ( $nickname === null || $nickname === '' ) {
4713 // Empty value results in the default signature (even when fancysig is enabled)
4714 $nickname = $username;
4715 } elseif ( mb_strlen( $nickname ) > $this->svcOptions->get( MainConfigNames::MaxSigChars ) ) {
4716 $nickname = $username;
4717 $this->logger->debug( __METHOD__ . ": $username has overlong signature." );
4718 } elseif ( $fancySig !== false ) {
4719 # Sig. might contain markup; validate this
4720 $isValid = $this->validateSig( $nickname ) !== false;
4721
4722 # New validator
4723 $sigValidation = $this->svcOptions->get( MainConfigNames::SignatureValidation );
4724 if ( $isValid && $sigValidation === 'disallow' ) {
4725 $parserOpts = new ParserOptions(
4726 $this->mOptions->getUserIdentity(),
4727 $this->contLang
4728 );
4729 $validator = $this->signatureValidatorFactory
4730 ->newSignatureValidator( $user, null, $parserOpts );
4731 $isValid = !$validator->validateSignature( $nickname );
4732 }
4733
4734 if ( $isValid ) {
4735 # Validated; clean up (if needed) and return it
4736 return $this->cleanSig( $nickname, true );
4737 } else {
4738 # Failed to validate; fall back to the default
4739 $nickname = $username;
4740 $this->logger->debug( __METHOD__ . ": $username has invalid signature." );
4741 }
4742 }
4743
4744 # Make sure nickname doesnt get a sig in a sig
4745 $nickname = self::cleanSigInSig( $nickname );
4746
4747 # If we're still here, make it a link to the user page
4748 $userText = wfEscapeWikiText( $username );
4749 $nickText = wfEscapeWikiText( $nickname );
4750 if ( $this->userNameUtils->isTemp( $username ) ) {
4751 $msgName = 'signature-temp';
4752 } elseif ( $user->isRegistered() ) {
4753 $msgName = 'signature';
4754 } else {
4755 $msgName = 'signature-anon';
4756 }
4757
4758 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4759 ->page( $this->getPage() )->text();
4760 }
4761
4769 public function validateSig( $text ) {
4770 return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4771 }
4772
4784 public function cleanSig( $text, $parsing = false ) {
4785 if ( !$parsing ) {
4786 global $wgTitle;
4787 $magicScopeVariable = $this->lock();
4788 $this->startParse(
4789 $wgTitle,
4790 ParserOptions::newFromUser( RequestContext::getMain()->getUser() ),
4791 self::OT_PREPROCESS,
4792 true
4793 );
4794 }
4795
4796 # Option to disable this feature
4797 if ( !$this->mOptions->getCleanSignatures() ) {
4798 return $text;
4799 }
4800
4801 # @todo FIXME: Regex doesn't respect extension tags or nowiki
4802 # => Move this logic to braceSubstitution()
4803 $substWord = $this->magicWordFactory->get( 'subst' );
4804 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4805 $substText = '{{' . $substWord->getSynonym( 0 );
4806
4807 $text = preg_replace( $substRegex, $substText, $text );
4808 $text = self::cleanSigInSig( $text );
4809 $dom = $this->preprocessToDom( $text );
4810 $frame = $this->getPreprocessor()->newFrame();
4811 $text = $frame->expand( $dom );
4812
4813 if ( !$parsing ) {
4814 $text = $this->mStripState->unstripBoth( $text );
4815 }
4816
4817 return $text;
4818 }
4819
4827 public static function cleanSigInSig( $text ) {
4828 $text = preg_replace( '/~{3,5}/', '', $text );
4829 return $text;
4830 }
4831
4848 public static function replaceTableOfContentsMarker( $text, $toc ) {
4849 return str_replace(
4850 self::TOC_PLACEHOLDER,
4851 $toc,
4852 // For forwards compatibility during transition period,
4853 // also replace "new" TOC_PLACEHOLDER value (to be used
4854 // in the future, but might show up in the cache
4855 // during a rollback to this version).
4856 str_replace( '<meta property="mw:PageProp/toc" />', $toc, $text )
4857 );
4858 }
4859
4871 public function startExternalParse( ?PageReference $page, ParserOptions $options,
4872 $outputType, $clearState = true, $revId = null
4873 ) {
4874 $this->startParse( $page, $options, $outputType, $clearState );
4875 if ( $revId !== null ) {
4876 $this->mRevisionId = $revId;
4877 }
4878 }
4879
4886 private function startParse( ?PageReference $page, ParserOptions $options,
4887 $outputType, $clearState = true
4888 ) {
4889 $this->setPage( $page );
4890 $this->mOptions = $options;
4891 $this->setOutputType( $outputType );
4892 if ( $clearState ) {
4893 $this->clearState();
4894 }
4895 }
4896
4906 public function transformMsg( $text, ParserOptions $options, ?PageReference $page = null ) {
4907 static $executing = false;
4908
4909 # Guard against infinite recursion
4910 if ( $executing ) {
4911 return $text;
4912 }
4913 $executing = true;
4914
4915 if ( !$page ) {
4916 global $wgTitle;
4917 $page = $wgTitle;
4918 }
4919
4920 $text = $this->preprocess( $text, $page, $options );
4921
4922 $executing = false;
4923 return $text;
4924 }
4925
4949 public function setHook( $tag, callable $callback ) {
4950 $tag = strtolower( $tag );
4951 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4952 throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4953 }
4954 $oldVal = $this->mTagHooks[$tag] ?? null;
4955 $this->mTagHooks[$tag] = $callback;
4956 if ( !in_array( $tag, $this->mStripList ) ) {
4957 $this->mStripList[] = $tag;
4958 }
4959
4960 return $oldVal;
4961 }
4962
4967 public function clearTagHooks() {
4968 $this->mTagHooks = [];
4969 $this->mStripList = [];
4970 }
4971
5016 public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
5017 $oldVal = $this->mFunctionHooks[$id][0] ?? null;
5018 $this->mFunctionHooks[$id] = [ $callback, $flags ];
5019
5020 # Add to function cache
5021 $mw = $this->magicWordFactory->get( $id );
5022 if ( !$mw ) {
5023 throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
5024 }
5025
5026 $synonyms = $mw->getSynonyms();
5027 $sensitive = intval( $mw->isCaseSensitive() );
5028
5029 foreach ( $synonyms as $syn ) {
5030 # Case
5031 if ( !$sensitive ) {
5032 $syn = $this->contLang->lc( $syn );
5033 }
5034 # Add leading hash
5035 if ( !( $flags & self::SFH_NO_HASH ) ) {
5036 $syn = '#' . $syn;
5037 }
5038 # Remove trailing colon
5039 if ( substr( $syn, -1, 1 ) === ':' ) {
5040 $syn = substr( $syn, 0, -1 );
5041 }
5042 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5043 }
5044 return $oldVal;
5045 }
5046
5053 public function getFunctionHooks() {
5054 return array_keys( $this->mFunctionHooks );
5055 }
5056
5065 public function replaceLinkHolders( &$text, $options = 0 ) {
5066 $this->replaceLinkHoldersPrivate( $text, $options );
5067 }
5068
5076 private function replaceLinkHoldersPrivate( &$text, $options = 0 ) {
5077 $this->mLinkHolders->replace( $text );
5078 }
5079
5087 private function replaceLinkHoldersText( $text ) {
5088 return $this->mLinkHolders->replaceText( $text );
5089 }
5090
5105 public function renderImageGallery( $text, array $params ) {
5106 $mode = false;
5107 if ( isset( $params['mode'] ) ) {
5108 $mode = $params['mode'];
5109 }
5110
5111 try {
5112 $ig = ImageGalleryBase::factory( $mode );
5113 } catch ( ImageGalleryClassNotFoundException $e ) {
5114 // If invalid type set, fallback to default.
5115 $ig = ImageGalleryBase::factory( false );
5116 }
5117
5118 $ig->setContextTitle( $this->getTitle() );
5119 $ig->setShowBytes( false );
5120 $ig->setShowDimensions( false );
5121 $ig->setShowFilename( false );
5122 $ig->setParser( $this );
5123 $ig->setHideBadImages();
5124 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
5125
5126 if ( isset( $params['showfilename'] ) ) {
5127 $ig->setShowFilename( true );
5128 } else {
5129 $ig->setShowFilename( false );
5130 }
5131 if ( isset( $params['caption'] ) ) {
5132 // NOTE: We aren't passing a frame here or below. Frame info
5133 // is currently opaque to Parsoid, which acts on OT_PREPROCESS.
5134 // See T107332#4030581
5135 $caption = $this->recursiveTagParse( $params['caption'] );
5136 $ig->setCaptionHtml( $caption );
5137 }
5138 if ( isset( $params['perrow'] ) ) {
5139 $ig->setPerRow( $params['perrow'] );
5140 }
5141 if ( isset( $params['widths'] ) ) {
5142 $ig->setWidths( $params['widths'] );
5143 }
5144 if ( isset( $params['heights'] ) ) {
5145 $ig->setHeights( $params['heights'] );
5146 }
5147 $ig->setAdditionalOptions( $params );
5148
5149 $this->hookRunner->onBeforeParserrenderImageGallery( $this, $ig );
5150
5151 $lines = StringUtils::explode( "\n", $text );
5152 foreach ( $lines as $line ) {
5153 # match lines like these:
5154 # Image:someimage.jpg|This is some image
5155 $matches = [];
5156 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5157 # Skip empty lines
5158 if ( count( $matches ) == 0 ) {
5159 continue;
5160 }
5161
5162 if ( strpos( $matches[0], '%' ) !== false ) {
5163 $matches[1] = rawurldecode( $matches[1] );
5164 }
5165 $title = Title::newFromText( $matches[1], NS_FILE );
5166 if ( $title === null ) {
5167 # Bogus title. Ignore these so we don't bomb out later.
5168 continue;
5169 }
5170
5171 # We need to get what handler the file uses, to figure out parameters.
5172 # Note, a hook can override the file name, and chose an entirely different
5173 # file (which potentially could be of a different type and have different handler).
5174 $options = [];
5175 $descQuery = false;
5176 $this->hookRunner->onBeforeParserFetchFileAndTitle(
5177 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
5178 $this, $title, $options, $descQuery
5179 );
5180 # Don't register it now, as TraditionalImageGallery does that later.
5181 $file = $this->fetchFileNoRegister( $title, $options );
5182 $handler = $file ? $file->getHandler() : false;
5183
5184 $paramMap = [
5185 'img_alt' => 'gallery-internal-alt',
5186 'img_link' => 'gallery-internal-link',
5187 ];
5188 if ( $handler ) {
5189 $paramMap += $handler->getParamMap();
5190 // We don't want people to specify per-image widths.
5191 // Additionally the width parameter would need special casing anyhow.
5192 unset( $paramMap['img_width'] );
5193 }
5194
5195 $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5196
5197 $label = '';
5198 $alt = '';
5199 $handlerOptions = [];
5200 $imageOptions = [];
5201 $hasAlt = false;
5202
5203 if ( isset( $matches[3] ) ) {
5204 // look for an |alt= definition while trying not to break existing
5205 // captions with multiple pipes (|) in it, until a more sensible grammar
5206 // is defined for images in galleries
5207
5208 // FIXME: Doing recursiveTagParse at this stage, and the trim before
5209 // splitting on '|' is a bit odd, and different from makeImage.
5210 $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5211 // Protect LanguageConverter markup
5212 $parameterMatches = StringUtils::delimiterExplode(
5213 '-{', '}-',
5214 '|',
5215 $matches[3],
5216 true /* nested */
5217 );
5218
5219 foreach ( $parameterMatches as $parameterMatch ) {
5220 list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5221 if ( !$magicName ) {
5222 // Last pipe wins.
5223 $label = $parameterMatch;
5224 continue;
5225 }
5226
5227 $paramName = $paramMap[$magicName];
5228 switch ( $paramName ) {
5229 case 'gallery-internal-alt':
5230 $hasAlt = true;
5231 $alt = $this->stripAltText( $match, false );
5232 break;
5233 case 'gallery-internal-link':
5234 $linkValue = $this->stripAltText( $match, false );
5235 if ( preg_match( '/^-{R\|(.*)}-$/', $linkValue ) ) {
5236 // Result of LanguageConverter::markNoConversion
5237 // invoked on an external link.
5238 $linkValue = substr( $linkValue, 4, -2 );
5239 }
5240 list( $type, $target ) = $this->parseLinkParameter( $linkValue );
5241 if ( $type ) {
5242 if ( $type === 'no-link' ) {
5243 $target = true;
5244 }
5245 $imageOptions[$type] = $target;
5246 }
5247 break;
5248 default:
5249 // Must be a handler specific parameter.
5250 if ( $handler->validateParam( $paramName, $match ) ) {
5251 $handlerOptions[$paramName] = $match;
5252 } else {
5253 // Guess not, consider it as caption.
5254 $this->logger->debug(
5255 "$parameterMatch failed parameter validation" );
5256 $label = $parameterMatch;
5257 }
5258 }
5259 }
5260 }
5261
5262 // Match makeImage when !$hasVisibleCaption
5263 if ( !$hasAlt ) {
5264 if ( $label !== '' ) {
5265 $alt = $this->stripAltText( $label, false );
5266 } else {
5267 $alt = $title->getText();
5268 }
5269 }
5270 $imageOptions['title'] = $this->stripAltText( $label, false );
5271
5272 $ig->add(
5273 $title, $label, $alt, '', $handlerOptions,
5274 ImageGalleryBase::LOADING_DEFAULT, $imageOptions
5275 );
5276 }
5277 $html = $ig->toHTML();
5278 $this->hookRunner->onAfterParserFetchFileAndTitle( $this, $ig, $html );
5279 return $html;
5280 }
5281
5286 private function getImageParams( $handler ) {
5287 if ( $handler ) {
5288 $handlerClass = get_class( $handler );
5289 } else {
5290 $handlerClass = '';
5291 }
5292 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5293 # Initialise static lists
5294 static $internalParamNames = [
5295 'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5296 'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5297 'bottom', 'text-bottom' ],
5298 'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5299 'upright', 'border', 'link', 'alt', 'class' ],
5300 ];
5301 static $internalParamMap;
5302 if ( !$internalParamMap ) {
5303 $internalParamMap = [];
5304 foreach ( $internalParamNames as $type => $names ) {
5305 foreach ( $names as $name ) {
5306 // For grep: img_left, img_right, img_center, img_none,
5307 // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5308 // img_bottom, img_text_bottom,
5309 // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5310 // img_border, img_link, img_alt, img_class
5311 $magicName = str_replace( '-', '_', "img_$name" );
5312 $internalParamMap[$magicName] = [ $type, $name ];
5313 }
5314 }
5315 }
5316
5317 # Add handler params
5318 $paramMap = $internalParamMap;
5319 if ( $handler ) {
5320 $handlerParamMap = $handler->getParamMap();
5321 foreach ( $handlerParamMap as $magic => $paramName ) {
5322 $paramMap[$magic] = [ 'handler', $paramName ];
5323 }
5324 } else {
5325 // Parse the size for non-existent files. See T273013
5326 $paramMap[ 'img_width' ] = [ 'handler', 'width' ];
5327 }
5328 $this->mImageParams[$handlerClass] = $paramMap;
5329 $this->mImageParamsMagicArray[$handlerClass] =
5330 $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5331 }
5332 return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5333 }
5334
5344 public function makeImage( LinkTarget $link, $options, $holders = false ) {
5345 # Check if the options text is of the form "options|alt text"
5346 # Options are:
5347 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5348 # * left no resizing, just left align. label is used for alt= only
5349 # * right same, but right aligned
5350 # * none same, but not aligned
5351 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5352 # * center center the image
5353 # * framed Keep original image size, no magnify-button.
5354 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5355 # * upright reduce width for upright images, rounded to full __0 px
5356 # * border draw a 1px border around the image
5357 # * alt Text for HTML alt attribute (defaults to empty)
5358 # * class Set a class for img node
5359 # * link Set the target of the image link. Can be external, interwiki, or local
5360 # vertical-align values (no % or length right now):
5361 # * baseline
5362 # * sub
5363 # * super
5364 # * top
5365 # * text-top
5366 # * middle
5367 # * bottom
5368 # * text-bottom
5369
5370 # Protect LanguageConverter markup when splitting into parts
5372 '-{', '}-', '|', $options, true /* allow nesting */
5373 );
5374
5375 # Give extensions a chance to select the file revision for us
5376 $options = [];
5377 $descQuery = false;
5378 $title = Title::castFromLinkTarget( $link ); // hook signature compat
5379 $this->hookRunner->onBeforeParserFetchFileAndTitle(
5380 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
5381 $this, $title, $options, $descQuery
5382 );
5383 # Fetch and register the file (file title may be different via hooks)
5384 list( $file, $link ) = $this->fetchFileAndTitle( $link, $options );
5385
5386 # Get parameter map
5387 $handler = $file ? $file->getHandler() : false;
5388
5389 list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5390
5391 if ( !$file ) {
5392 $this->addTrackingCategory( 'broken-file-category' );
5393 }
5394
5395 # Process the input parameters
5396 $caption = '';
5397 $params = [ 'frame' => [], 'handler' => [],
5398 'horizAlign' => [], 'vertAlign' => [] ];
5399 $seenformat = false;
5400 foreach ( $parts as $part ) {
5401 $part = trim( $part );
5402 list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5403 $validated = false;
5404 if ( isset( $paramMap[$magicName] ) ) {
5405 list( $type, $paramName ) = $paramMap[$magicName];
5406
5407 # Special case; width and height come in one variable together
5408 if ( $type === 'handler' && $paramName === 'width' ) {
5409 $parsedWidthParam = self::parseWidthParam( $value );
5410 // Parsoid applies data-(width|height) attributes to broken
5411 // media spans, for client use. See T273013
5412 $validateFunc = static function ( $name, $value ) use ( $handler ) {
5413 return $handler
5414 ? $handler->validateParam( $name, $value )
5415 : $value > 0;
5416 };
5417 if ( isset( $parsedWidthParam['width'] ) ) {
5418 $width = $parsedWidthParam['width'];
5419 if ( $validateFunc( 'width', $width ) ) {
5420 $params[$type]['width'] = $width;
5421 $validated = true;
5422 }
5423 }
5424 if ( isset( $parsedWidthParam['height'] ) ) {
5425 $height = $parsedWidthParam['height'];
5426 if ( $validateFunc( 'height', $height ) ) {
5427 $params[$type]['height'] = $height;
5428 $validated = true;
5429 }
5430 }
5431 # else no validation -- T15436
5432 } else {
5433 if ( $type === 'handler' ) {
5434 # Validate handler parameter
5435 $validated = $handler->validateParam( $paramName, $value );
5436 } else {
5437 # Validate internal parameters
5438 switch ( $paramName ) {
5439 case 'alt':
5440 case 'class':
5441 $validated = true;
5442 $value = $this->stripAltText( $value, $holders );
5443 break;
5444 case 'link':
5445 list( $paramName, $value ) =
5446 $this->parseLinkParameter(
5447 $this->stripAltText( $value, $holders )
5448 );
5449 if ( $paramName ) {
5450 $validated = true;
5451 if ( $paramName === 'no-link' ) {
5452 $value = true;
5453 }
5454 }
5455 break;
5456 case 'manualthumb':
5457 # @todo FIXME: Possibly check validity here for
5458 # manualthumb? downstream behavior seems odd with
5459 # missing manual thumbs.
5460 $value = $this->stripAltText( $value, $holders );
5461 // fall through
5462 case 'frameless':
5463 case 'framed':
5464 case 'thumbnail':
5465 // use first appearing option, discard others.
5466 $validated = !$seenformat;
5467 $seenformat = true;
5468 break;
5469 default:
5470 # Most other things appear to be empty or numeric...
5471 $validated = ( $value === false || is_numeric( trim( $value ) ) );
5472 }
5473 }
5474
5475 if ( $validated ) {
5476 $params[$type][$paramName] = $value;
5477 }
5478 }
5479 }
5480 if ( !$validated ) {
5481 $caption = $part;
5482 }
5483 }
5484
5485 # Process alignment parameters
5486 if ( $params['horizAlign'] !== [] ) {
5487 $params['frame']['align'] = key( $params['horizAlign'] );
5488 }
5489 if ( $params['vertAlign'] !== [] ) {
5490 $params['frame']['valign'] = key( $params['vertAlign'] );
5491 }
5492
5493 $params['frame']['caption'] = $caption;
5494
5495 # Will the image be presented in a frame, with the caption below?
5496 // @phan-suppress-next-line PhanImpossibleCondition
5497 $hasVisibleCaption = isset( $params['frame']['framed'] )
5498 // @phan-suppress-next-line PhanImpossibleCondition
5499 || isset( $params['frame']['thumbnail'] )
5500 // @phan-suppress-next-line PhanImpossibleCondition
5501 || isset( $params['frame']['manualthumb'] );
5502
5503 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5504 # came to also set the caption, ordinary text after the image -- which
5505 # makes no sense, because that just repeats the text multiple times in
5506 # screen readers. It *also* came to set the title attribute.
5507 # Now that we have an alt attribute, we should not set the alt text to
5508 # equal the caption: that's worse than useless, it just repeats the
5509 # text. This is the framed/thumbnail case. If there's no caption, we
5510 # use the unnamed parameter for alt text as well, just for the time be-
5511 # ing, if the unnamed param is set and the alt param is not.
5512 # For the future, we need to figure out if we want to tweak this more,
5513 # e.g., introducing a title= parameter for the title; ignoring the un-
5514 # named parameter entirely for images without a caption; adding an ex-
5515 # plicit caption= parameter and preserving the old magic unnamed para-
5516 # meter for BC; ...
5517 if ( $hasVisibleCaption ) {
5518 // @phan-suppress-next-line PhanImpossibleCondition
5519 if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5520 # No caption or alt text, add the filename as the alt text so
5521 # that screen readers at least get some description of the image
5522 $params['frame']['alt'] = $link->getText();
5523 }
5524 # Do not set $params['frame']['title'] because tooltips are unnecessary
5525 # for framed images, the caption is visible
5526 } else {
5527 // @phan-suppress-next-line PhanImpossibleCondition
5528 if ( !isset( $params['frame']['alt'] ) ) {
5529 # No alt text, use the "caption" for the alt text
5530 if ( $caption !== '' ) {
5531 $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5532 } else {
5533 # No caption, fall back to using the filename for the
5534 # alt text
5535 $params['frame']['alt'] = $link->getText();
5536 }
5537 }
5538 # Use the "caption" for the tooltip text
5539 $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5540 }
5541 $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
5542
5543 // hook signature compat again, $link may have changed
5544 $title = Title::castFromLinkTarget( $link );
5545 $this->hookRunner->onParserMakeImageParams( $title, $file, $params, $this );
5546
5547 # Linker does the rest
5548 $time = $options['time'] ?? false;
5549 $ret = Linker::makeImageLink( $this, $link, $file, $params['frame'], $params['handler'],
5550 $time, $descQuery, $this->mOptions->getThumbSize() );
5551
5552 # Give the handler a chance to modify the parser object
5553 if ( $handler ) {
5554 $handler->parserTransformHook( $this, $file );
5555 }
5556 if ( $file ) {
5557 $this->modifyImageHtml( $file, $params, $ret );
5558 }
5559
5560 return $ret;
5561 }
5562
5581 private function parseLinkParameter( $value ) {
5582 $chars = self::EXT_LINK_URL_CLASS;
5583 $addr = self::EXT_LINK_ADDR;
5584 $prots = $this->urlUtils->validProtocols();
5585 $type = null;
5586 $target = false;
5587 if ( $value === '' ) {
5588 $type = 'no-link';
5589 } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5590 if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5591 $this->mOutput->addExternalLink( $value );
5592 $type = 'link-url';
5593 $target = $value;
5594 }
5595 } else {
5596 // Percent-decode link arguments for consistency with wikilink
5597 // handling (T216003#7836261).
5598 //
5599 // There's slight concern here though. The |link= option supports
5600 // two formats, link=Test%22test vs link=[[Test%22test]], both of
5601 // which are about to be decoded.
5602 //
5603 // In the former case, the decoding here is straightforward and
5604 // desirable.
5605 //
5606 // In the latter case, there's a potential for double decoding,
5607 // because the wikilink syntax has a higher precedence and has
5608 // already been parsed as a link before we get here. $value
5609 // has had stripAltText() called on it, which in turn calls
5610 // replaceLinkHoldersText() on the link. So, the text we're
5611 // getting at this point has already been percent decoded.
5612 //
5613 // The problematic case is if %25 is in the title, since that
5614 // decodes to %, which could combine with trailing characters.
5615 // However, % is not a valid link title character, so it would
5616 // not parse as a link and the string we received here would
5617 // still contain the encoded %25.
5618 //
5619 // Hence, double decoded is not an issue. See the test,
5620 // "Should not double decode the link option"
5621 if ( strpos( $value, '%' ) !== false ) {
5622 $value = rawurldecode( $value );
5623 }
5624 $linkTitle = Title::newFromText( $value );
5625 if ( $linkTitle ) {
5626 $this->mOutput->addLink( $linkTitle );
5627 $type = 'link-title';
5628 $target = $linkTitle;
5629 }
5630 }
5631 return [ $type, $target ];
5632 }
5633
5641 public function modifyImageHtml( File $file, array $params, string &$html ) {
5642 $this->hookRunner->onParserModifyImageHTML( $this, $file, $params, $html );
5643 }
5644
5650 private function stripAltText( $caption, $holders ) {
5651 # Strip bad stuff out of the title (tooltip). We can't just use
5652 # replaceLinkHoldersText() here, because if this function is called
5653 # from handleInternalLinks2(), mLinkHolders won't be up-to-date.
5654 if ( $holders ) {
5655 $tooltip = $holders->replaceText( $caption );
5656 } else {
5657 $tooltip = $this->replaceLinkHoldersText( $caption );
5658 }
5659
5660 # make sure there are no placeholders in thumbnail attributes
5661 # that are later expanded to html- so expand them now and
5662 # remove the tags
5663 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5664 # Compatibility hack! In HTML certain entity references not terminated
5665 # by a semicolon are decoded (but not if we're in an attribute; that's
5666 # how link URLs get away without properly escaping & in queries).
5667 # But wikitext has always required semicolon-termination of entities,
5668 # so encode & where needed to avoid decode of semicolon-less entities.
5669 # See T209236 and
5670 # https://www.w3.org/TR/html5/syntax.html#named-character-references
5671 # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
5672 $tooltip = preg_replace( "/
5673 & # 1. entity prefix
5674 (?= # 2. followed by:
5675 (?: # a. one of the legacy semicolon-less named entities
5676 A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
5677 C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
5678 GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
5679 O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
5680 U(?:acute|circ|grave|uml)|Yacute|
5681 a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
5682 c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
5683 divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
5684 frac(?:1(?:2|4)|34)|
5685 gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
5686 i(?:acute|circ|excl|grave|quest|uml)|laquo|
5687 lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
5688 m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
5689 not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
5690 o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
5691 p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
5692 s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
5693 u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
5694 )
5695 (?:[^;]|$)) # b. and not followed by a semicolon
5696 # S = study, for efficiency
5697 /Sx", '&amp;', $tooltip );
5698 $tooltip = Sanitizer::stripAllTags( $tooltip );
5699
5700 return $tooltip;
5701 }
5702
5712 public function attributeStripCallback( &$text, $frame = false ) {
5713 wfDeprecated( __METHOD__, '1.35' );
5714 $text = $this->replaceVariables( $text, $frame );
5715 $text = $this->mStripState->unstripBoth( $text );
5716 return $text;
5717 }
5718
5725 public function getTags() {
5726 return array_keys( $this->mTagHooks );
5727 }
5728
5733 public function getFunctionSynonyms() {
5734 return $this->mFunctionSynonyms;
5735 }
5736
5741 public function getUrlProtocols() {
5742 return $this->urlUtils->validProtocols();
5743 }
5744
5774 private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5775 global $wgTitle; # not generally used but removes an ugly failure mode
5776
5777 $magicScopeVariable = $this->lock();
5778 $this->startParse(
5779 $wgTitle,
5780 ParserOptions::newFromUser( RequestContext::getMain()->getUser() ),
5781 self::OT_PLAIN,
5782 true
5783 );
5784 $outText = '';
5785 $frame = $this->getPreprocessor()->newFrame();
5786
5787 # Process section extraction flags
5788 $flags = 0;
5789 $sectionParts = explode( '-', $sectionId );
5790 // The section ID may either be a magic string such as 'new' (which should be treated as 0),
5791 // or a numbered section ID in the format of "T-<section index>".
5792 // Explicitly coerce the section index into a number accordingly. (T323373)
5793 $sectionIndex = (int)array_pop( $sectionParts );
5794 foreach ( $sectionParts as $part ) {
5795 if ( $part === 'T' ) {
5797 }
5798 }
5799
5800 # Check for empty input
5801 if ( strval( $text ) === '' ) {
5802 # Only sections 0 and T-0 exist in an empty document
5803 if ( $sectionIndex === 0 ) {
5804 if ( $mode === 'get' ) {
5805 return '';
5806 }
5807
5808 return $newText;
5809 } else {
5810 if ( $mode === 'get' ) {
5811 return $newText;
5812 }
5813
5814 return $text;
5815 }
5816 }
5817
5818 # Preprocess the text
5819 $root = $this->preprocessToDom( $text, $flags );
5820
5821 # <h> nodes indicate section breaks
5822 # They can only occur at the top level, so we can find them by iterating the root's children
5823 $node = $root->getFirstChild();
5824
5825 # Find the target section
5826 if ( $sectionIndex === 0 ) {
5827 # Section zero doesn't nest, level=big
5828 $targetLevel = 1000;
5829 } else {
5830 while ( $node ) {
5831 if ( $node->getName() === 'h' ) {
5832 $bits = $node->splitHeading();
5833 if ( $bits['i'] == $sectionIndex ) {
5834 $targetLevel = $bits['level'];
5835 break;
5836 }
5837 }
5838 if ( $mode === 'replace' ) {
5839 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5840 }
5841 $node = $node->getNextSibling();
5842 }
5843 }
5844
5845 if ( !$node ) {
5846 # Not found
5847 if ( $mode === 'get' ) {
5848 return $newText;
5849 } else {
5850 return $text;
5851 }
5852 }
5853
5854 # Find the end of the section, including nested sections
5855 do {
5856 if ( $node->getName() === 'h' ) {
5857 $bits = $node->splitHeading();
5858 $curLevel = $bits['level'];
5859 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
5860 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5861 break;
5862 }
5863 }
5864 if ( $mode === 'get' ) {
5865 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5866 }
5867 $node = $node->getNextSibling();
5868 } while ( $node );
5869
5870 # Write out the remainder (in replace mode only)
5871 if ( $mode === 'replace' ) {
5872 # Output the replacement text
5873 # Add two newlines on -- trailing whitespace in $newText is conventionally
5874 # stripped by the editor, so we need both newlines to restore the paragraph gap
5875 # Only add trailing whitespace if there is newText
5876 if ( $newText != "" ) {
5877 $outText .= $newText . "\n\n";
5878 }
5879
5880 while ( $node ) {
5881 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5882 $node = $node->getNextSibling();
5883 }
5884 }
5885
5886 # Re-insert stripped tags
5887 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5888
5889 return $outText;
5890 }
5891
5907 public function getSection( $text, $sectionId, $defaultText = '' ) {
5908 return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5909 }
5910
5924 public function replaceSection( $oldText, $sectionId, $newText ) {
5925 return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5926 }
5927
5957 public function getFlatSectionInfo( $text ) {
5958 $magicScopeVariable = $this->lock();
5959 $this->startParse(
5960 null,
5961 ParserOptions::newFromUser( RequestContext::getMain()->getUser() ),
5962 self::OT_PLAIN,
5963 true
5964 );
5965 $frame = $this->getPreprocessor()->newFrame();
5966 $root = $this->preprocessToDom( $text, 0 );
5967 $node = $root->getFirstChild();
5968 $offset = 0;
5969 $currentSection = [
5970 'index' => 0,
5971 'level' => 0,
5972 'offset' => 0,
5973 'heading' => '',
5974 'text' => ''
5975 ];
5976 $sections = [];
5977
5978 while ( $node ) {
5979 $nodeText = $frame->expand( $node, PPFrame::RECOVER_ORIG );
5980 if ( $node->getName() === 'h' ) {
5981 $bits = $node->splitHeading();
5982 $sections[] = $currentSection;
5983 $currentSection = [
5984 'index' => $bits['i'],
5985 'level' => $bits['level'],
5986 'offset' => $offset,
5987 'heading' => $nodeText,
5988 'text' => $nodeText
5989 ];
5990 } else {
5991 $currentSection['text'] .= $nodeText;
5992 }
5993 $offset += strlen( $nodeText );
5994 $node = $node->getNextSibling();
5995 }
5996 $sections[] = $currentSection;
5997 return $sections;
5998 }
5999
6011 public function getRevisionId() {
6012 return $this->mRevisionId;
6013 }
6014
6021 public function getRevisionRecordObject() {
6022 if ( $this->mRevisionRecordObject ) {
6023 return $this->mRevisionRecordObject;
6024 }
6025
6026 // NOTE: try to get the RevisionRecord object even if mRevisionId is null.
6027 // This is useful when parsing a revision that has not yet been saved.
6028 // However, if we get back a saved revision even though we are in
6029 // preview mode, we'll have to ignore it, see below.
6030 // NOTE: This callback may be used to inject an OLD revision that was
6031 // already loaded, so "current" is a bit of a misnomer. We can't just
6032 // skip it if mRevisionId is set.
6033 $rev = call_user_func(
6034 $this->mOptions->getCurrentRevisionRecordCallback(),
6035 $this->getTitle(),
6036 $this
6037 );
6038
6039 if ( $rev === false ) {
6040 // The revision record callback returns `false` (not null) to
6041 // indicate that the revision is missing. (See for example
6042 // Parser::statelessFetchRevisionRecord(), the default callback.)
6043 // This API expects `null` instead. (T251952)
6044 $rev = null;
6045 }
6046
6047 if ( $this->mRevisionId === null && $rev && $rev->getId() ) {
6048 // We are in preview mode (mRevisionId is null), and the current revision callback
6049 // returned an existing revision. Ignore it and return null, it's probably the page's
6050 // current revision, which is not what we want here. Note that we do want to call the
6051 // callback to allow the unsaved revision to be injected here, e.g. for
6052 // self-transclusion previews.
6053 return null;
6054 }
6055
6056 // If the parse is for a new revision, then the callback should have
6057 // already been set to force the object and should match mRevisionId.
6058 // If not, try to fetch by mRevisionId instead.
6059 if ( $this->mRevisionId && $rev && $rev->getId() != $this->mRevisionId ) {
6060 $rev = MediaWikiServices::getInstance()
6061 ->getRevisionLookup()
6062 ->getRevisionById( $this->mRevisionId );
6063 }
6064
6065 $this->mRevisionRecordObject = $rev;
6066
6067 return $this->mRevisionRecordObject;
6068 }
6069
6076 public function getRevisionTimestamp() {
6077 if ( $this->mRevisionTimestamp !== null ) {
6078 return $this->mRevisionTimestamp;
6079 }
6080
6081 # Use specified revision timestamp, falling back to the current timestamp
6082 $revObject = $this->getRevisionRecordObject();
6083 $timestamp = $revObject && $revObject->getTimestamp()
6084 ? $revObject->getTimestamp()
6085 : $this->mOptions->getTimestamp();
6086 $this->mOutput->setRevisionTimestampUsed( $timestamp ); // unadjusted time zone
6087
6088 # The cryptic '' timezone parameter tells to use the site-default
6089 # timezone offset instead of the user settings.
6090 # Since this value will be saved into the parser cache, served
6091 # to other users, and potentially even used inside links and such,
6092 # it needs to be consistent for all visitors.
6093 $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
6094
6095 return $this->mRevisionTimestamp;
6096 }
6097
6104 public function getRevisionUser(): ?string {
6105 if ( $this->mRevisionUser === null ) {
6106 $revObject = $this->getRevisionRecordObject();
6107
6108 # if this template is subst: the revision id will be blank,
6109 # so just use the current user's name
6110 if ( $revObject && $revObject->getUser() ) {
6111 $this->mRevisionUser = $revObject->getUser()->getName();
6112 } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6113 $this->mRevisionUser = $this->getUserIdentity()->getName();
6114 } else {
6115 # Note that we fall through here with
6116 # $this->mRevisionUser still null
6117 }
6118 }
6119 return $this->mRevisionUser;
6120 }
6121
6128 public function getRevisionSize() {
6129 if ( $this->mRevisionSize === null ) {
6130 $revObject = $this->getRevisionRecordObject();
6131
6132 # if this variable is subst: the revision id will be blank,
6133 # so just use the parser input size, because the own substitution
6134 # will change the size.
6135 if ( $revObject ) {
6136 $this->mRevisionSize = $revObject->getSize();
6137 } else {
6138 $this->mRevisionSize = $this->mInputSize;
6139 }
6140 }
6141 return $this->mRevisionSize;
6142 }
6143
6152 public function setDefaultSort( $sort ) {
6153 wfDeprecated( __METHOD__, '1.38' );
6154 $this->mOutput->setPageProperty( 'defaultsort', $sort );
6155 }
6156
6170 public function getDefaultSort() {
6171 wfDeprecated( __METHOD__, '1.38' );
6172 return $this->mOutput->getPageProperty( 'defaultsort' ) ?? '';
6173 }
6174
6184 public function getCustomDefaultSort() {
6185 wfDeprecated( __METHOD__, '1.38' );
6186 return $this->mOutput->getPageProperty( 'defaultsort' ) ?? false;
6187 }
6188
6189 private static function getSectionNameFromStrippedText( $text ) {
6190 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
6191 $text = Sanitizer::decodeCharReferences( $text );
6192 $text = self::normalizeSectionName( $text );
6193 return $text;
6194 }
6195
6196 private static function makeAnchor( $sectionName ) {
6197 return '#' . Sanitizer::escapeIdForLink( $sectionName );
6198 }
6199
6200 private function makeLegacyAnchor( $sectionName ) {
6201 $fragmentMode = $this->svcOptions->get( MainConfigNames::FragmentMode );
6202 if ( isset( $fragmentMode[1] ) && $fragmentMode[1] === 'legacy' ) {
6203 // ForAttribute() and ForLink() are the same for legacy encoding
6204 $id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
6205 } else {
6206 $id = Sanitizer::escapeIdForLink( $sectionName );
6207 }
6208
6209 return "#$id";
6210 }
6211
6221 public function guessSectionNameFromWikiText( $text ) {
6222 # Strip out wikitext links(they break the anchor)
6223 $text = $this->stripSectionName( $text );
6224 $sectionName = self::getSectionNameFromStrippedText( $text );
6225 return self::makeAnchor( $sectionName );
6226 }
6227
6238 public function guessLegacySectionNameFromWikiText( $text ) {
6239 # Strip out wikitext links(they break the anchor)
6240 $text = $this->stripSectionName( $text );
6241 $sectionName = self::getSectionNameFromStrippedText( $text );
6242 return $this->makeLegacyAnchor( $sectionName );
6243 }
6244
6251 public static function guessSectionNameFromStrippedText( $text ) {
6252 $sectionName = self::getSectionNameFromStrippedText( $text );
6253 return self::makeAnchor( $sectionName );
6254 }
6255
6262 private static function normalizeSectionName( $text ) {
6263 # T90902: ensure the same normalization is applied for IDs as to links
6265 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6266 '@phan-var MediaWikiTitleCodec $titleParser';
6267 try {
6268
6269 $parts = $titleParser->splitTitleString( "#$text" );
6270 } catch ( MalformedTitleException $ex ) {
6271 return $text;
6272 }
6273 return $parts['fragment'];
6274 }
6275
6291 public function stripSectionName( $text ) {
6292 # Strip internal link markup
6293 $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6294 $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6295
6296 # Strip external link markup
6297 # @todo FIXME: Not tolerant to blank link text
6298 # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6299 # on how many empty links there are on the page - need to figure that out.
6300 $text = preg_replace(
6301 '/\[(?i:' . $this->urlUtils->validProtocols() . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6302
6303 # Parse wikitext quotes (italics & bold)
6304 $text = $this->doQuotes( $text );
6305
6306 # Strip HTML tags
6307 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6308 return $text;
6309 }
6310
6324 private function fuzzTestSrvus( $text, PageReference $page, ParserOptions $options,
6325 $outputType = self::OT_HTML
6326 ) {
6327 $magicScopeVariable = $this->lock();
6328 $this->startParse( $page, $options, $outputType, true );
6329
6330 $text = $this->replaceVariables( $text );
6331 $text = $this->mStripState->unstripBoth( $text );
6332 $text = Sanitizer::internalRemoveHtmlTags( $text );
6333 return $text;
6334 }
6335
6347 private function fuzzTestPst( $text, PageReference $page, ParserOptions $options ) {
6348 return $this->preSaveTransform( $text, $page, $options->getUserIdentity(), $options );
6349 }
6350
6362 private function fuzzTestPreprocess( $text, PageReference $page, ParserOptions $options ) {
6363 return $this->fuzzTestSrvus( $text, $page, $options, self::OT_PREPROCESS );
6364 }
6365
6384 public function markerSkipCallback( $s, callable $callback ) {
6385 $i = 0;
6386 $out = '';
6387 while ( $i < strlen( $s ) ) {
6388 $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6389 if ( $markerStart === false ) {
6390 $out .= call_user_func( $callback, substr( $s, $i ) );
6391 break;
6392 } else {
6393 $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6394 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6395 if ( $markerEnd === false ) {
6396 $out .= substr( $s, $markerStart );
6397 break;
6398 } else {
6399 $markerEnd += strlen( self::MARKER_SUFFIX );
6400 $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6401 $i = $markerEnd;
6402 }
6403 }
6404 }
6405 return $out;
6406 }
6407
6415 public function killMarkers( $text ) {
6416 return $this->mStripState->killMarkers( $text );
6417 }
6418
6429 public static function parseWidthParam( $value, $parseHeight = true ) {
6430 $parsedWidthParam = [];
6431 if ( $value === '' ) {
6432 return $parsedWidthParam;
6433 }
6434 $m = [];
6435 # (T15500) In both cases (width/height and width only),
6436 # permit trailing "px" for backward compatibility.
6437 if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6438 $width = intval( $m[1] );
6439 $height = intval( $m[2] );
6440 $parsedWidthParam['width'] = $width;
6441 $parsedWidthParam['height'] = $height;
6442 } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6443 $width = intval( $value );
6444 $parsedWidthParam['width'] = $width;
6445 }
6446 return $parsedWidthParam;
6447 }
6448
6458 protected function lock() {
6459 if ( $this->mInParse ) {
6460 throw new MWException( "Parser state cleared while parsing. "
6461 . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6462 }
6463
6464 // Save the backtrace when locking, so that if some code tries locking again,
6465 // we can print the lock owner's backtrace for easier debugging
6466 $e = new Exception;
6467 $this->mInParse = $e->getTraceAsString();
6468
6469 $recursiveCheck = new ScopedCallback( function () {
6470 $this->mInParse = false;
6471 } );
6472
6473 return $recursiveCheck;
6474 }
6475
6483 public function isLocked() {
6484 return (bool)$this->mInParse;
6485 }
6486
6497 public static function stripOuterParagraph( $html ) {
6498 $m = [];
6499 if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && strpos( $m[1], '</p>' ) === false ) {
6500 $html = $m[1];
6501 }
6502
6503 return $html;
6504 }
6505
6516 public static function formatPageTitle( $nsText, $nsSeparator, $mainText ): string {
6517 $html = '';
6518 if ( $nsText !== '' ) {
6519 $html .= '<span class="mw-page-title-namespace">' . HtmlArmor::getHtml( $nsText ) . '</span>';
6520 $html .= '<span class="mw-page-title-separator">' . HtmlArmor::getHtml( $nsSeparator ) . '</span>';
6521 }
6522 $html .= '<span class="mw-page-title-main">' . HtmlArmor::getHtml( $mainText ) . '</span>';
6523 return $html;
6524 }
6525
6537 public function getFreshParser() {
6538 if ( $this->mInParse ) {
6539 return $this->factory->create();
6540 } else {
6541 return $this;
6542 }
6543 }
6544
6552 public function enableOOUI() {
6553 wfDeprecated( __METHOD__, '1.35' );
6554 OutputPage::setupOOUI();
6555 $this->mOutput->setEnableOOUI( true );
6556 }
6557
6564 private function setOutputFlag( string $flag, string $reason ): void {
6565 $this->mOutput->setOutputFlag( $flag );
6566 $name = $this->getTitle()->getPrefixedText();
6567 $this->logger->debug( __METHOD__ . ": set $flag flag on '$name'; $reason" );
6568 }
6569}
getUser()
const OT_WIKI
Definition Defines.php:159
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:162
const OT_PREPROCESS
Definition Defines.php:160
const OT_HTML
Definition Defines.php:158
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.
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgTitle
Definition Setup.php:497
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)
const REGISTER_OPTIONS
WebRequest clone which takes values from a provided array.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:67
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
Class for exceptions thrown by ImageGalleryBase::factory().
Base class for language-specific code.
Definition Language.php:53
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition Linker.php:993
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition Linker.php:1664
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition Linker.php:244
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1488
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition Linker.php:165
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:301
static tocIndent()
Add another level to the Table of Contents.
Definition Linker.php:1638
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:1796
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:1061
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition Linker.php:1775
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
Definition Linker.php:1649
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:1700
static tocLineEnd()
End a Table Of Contents line.
Definition Linker.php:1688
MediaWiki exception.
Library for creating and parsing MW-style timestamps.
Class for handling an array of magic words.
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.
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...
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.
Factory for handling the special page list and generating SpecialPage objects.
Base class for HTML cleanup utilities.
Creates User objects.
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:355
static plaintextParam( $plaintext)
Definition Message.php:1266
static numParam( $num)
Definition Message.php:1145
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
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?
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:96
addTrackingCategory( $msg)
Definition Parser.php:4148
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:1281
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:1184
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:6076
setDefaultSort( $sort)
Mutator for the 'defaultsort' page property.
Definition Parser.php:6152
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6497
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition Parser.php:2315
__clone()
Allow extensions to clean up when the parser is cloned.
Definition Parser.php:580
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:4827
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition Parser.php:1238
$mHighestExpansionDepth
Definition Parser.php:261
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:691
getCustomDefaultSort()
Accessor for the 'defaultsort' page property.
Definition Parser.php:6184
ParserOptions null $mOptions
Definition Parser.php:288
getOptions()
Definition Parser.php:1126
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition Parser.php:4784
getStripState()
Definition Parser.php:1352
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:6104
isCurrentRevisionOfTitleCached(LinkTarget $link)
Definition Parser.php:3558
replaceVariables( $text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition Parser.php:2932
getFunctionSynonyms()
Definition Parser.php:5733
extensionSubstitution(array $params, PPFrame $frame, bool $processNowiki=false)
Return the text to be used for a given extension tag.
Definition Parser.php:3978
interwikiTransclude(LinkTarget $link, $action)
Transclude an interwiki link.
Definition Parser.php:3839
const OT_HTML
Definition Parser.php:130
$mHeadings
Definition Parser.php:264
getPreloadText( $text, PageReference $page, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition Parser.php:995
setTitle(Title $t=null)
Set the context title.
Definition Parser.php:1028
static guessSectionNameFromStrippedText( $text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
Definition Parser.php:6251
const OT_WIKI
Definition Parser.php:131
getFlatSectionInfo( $text)
Get an array of preprocessor section information.
Definition Parser.php:5957
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:2983
modifyImageHtml(File $file, array $params, string &$html)
Give hooks a chance to modify image thumbnail HTML.
Definition Parser.php:5641
fetchTemplateAndTitle(LinkTarget $link)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3598
Title null $mTitle
Since 1.34, leaving mTitle uninitialized or setting mTitle to null is deprecated.
Definition Parser.php:297
setUser(?UserIdentity $user)
Set the current user.
Definition Parser.php:1017
getRevisionSize()
Get the size of the revision.
Definition Parser.php:6128
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition Parser.php:6291
tagNeedsNowikiStrippedInTagPF(string $lowerTagName)
Definition Parser.php:3952
getOutputType()
Accessor for the output type.
Definition Parser.php:1082
fetchCurrentRevisionRecordOfTitle(LinkTarget $link)
Fetch the current revision of a given title as a RevisionRecord.
Definition Parser.php:3528
fetchFileAndTitle(LinkTarget $link, array $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3789
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition Parser.php:975
fetchFileNoRegister(LinkTarget $link, array $options=[])
Helper function for fetchFileAndTitle.
Definition Parser.php:3816
getUrlProtocols()
Definition Parser.php:5741
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:5016
getHookRunner()
Get a HookRunner for calling core hooks.
Definition Parser.php:1694
getTitle()
Definition Parser.php:1037
braceSubstitution(array $piece, PPFrame $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
Definition Parser.php:3008
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition Parser.php:1223
preprocess( $text, ?PageReference $page, ParserOptions $options, $revid=null, $frame=false)
Expand templates and variables in the text, producing valid, static wikitext.
Definition Parser.php:948
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
Definition Parser.php:2284
setOutputType( $ot)
Mutator for the output type.
Definition Parser.php:1091
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
Definition Parser.php:1172
callParserFunction(PPFrame $frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
Definition Parser.php:3393
makeLimitReport()
Set the limit report data in the current ParserOutput.
Definition Parser.php:792
parseExtensionTagAsTopLevelDoc( $text)
Needed by Parsoid/PHP to ensure all the hooks for extensions are run in the right order.
Definition Parser.php:929
transformMsg( $text, ParserOptions $options, ?PageReference $page=null)
Wrapper for preprocess()
Definition Parser.php:4906
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:4871
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1344
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition Parser.php:4949
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
Definition Parser.php:1258
getTags()
Accessor.
Definition Parser.php:5725
static statelessFetchRevisionRecord(LinkTarget $link, $parser=null)
Wrapper around RevisionLookup::getKnownCurrentRevision.
Definition Parser.php:3574
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition Parser.php:880
getTargetLanguageConverter()
Shorthand for getting a Language Converter for Target language.
Definition Parser.php:1658
static replaceTableOfContentsMarker( $text, $toc)
Replace table of contents marker in parsed HTML.
Definition Parser.php:4848
const OT_PLAIN
Definition Parser.php:135
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
Definition Parser.php:5907
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition Parser.php:1588
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition Parser.php:5924
setPage(?PageReference $t=null)
Set the page used as context for parsing, e.g.
Definition Parser.php:1050
getUserIdentity()
Get a user either from the user set on Parser if it's set, or from the ParserOptions object otherwise...
Definition Parser.php:1203
doBlockLevels( $text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
Definition Parser.php:2826
$mPPNodeCount
Definition Parser.php:259
getDefaultSort()
Accessor for the 'defaultsort' page property.
Definition Parser.php:6170
const MARKER_PREFIX
Definition Parser.php:155
isLocked()
Will entry points such as parse() throw an exception due to the parser already being active?
Definition Parser.php:6483
getHookContainer()
Get a HookContainer capable of returning metadata about hooks or running extension hooks.
Definition Parser.php:1682
getFunctionHooks()
Get all registered function hook identifiers.
Definition Parser.php:5053
$mMarkerIndex
Definition Parser.php:206
getPage()
Returns the page used as context for parsing, e.g.
Definition Parser.php:1073
static getExternalLinkRel( $url=false, LinkTarget $title=null)
Get the rel attribute for a particular external link.
Definition Parser.php:2259
$mExpensiveFunctionCount
Definition Parser.php:270
getContentLanguage()
Get the content language that this Parser is using.
Definition Parser.php:1248
getOutput()
Definition Parser.php:1118
Options( $x=null)
Accessor/mutator for the ParserOptions object.
Definition Parser.php:1146
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:904
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:6221
clearTagHooks()
Remove all tag hooks.
Definition Parser.php:4967
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:6011
setOptions(ParserOptions $options)
Mutator for the ParserOptions object.
Definition Parser.php:1135
static parseWidthParam( $value, $parseHeight=true)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6429
validateSig( $text)
Check that the user's signature contains no bad XML.
Definition Parser.php:4769
getPreprocessor()
Get a preprocessor object.
Definition Parser.php:1213
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
Definition Parser.php:6238
const EXT_LINK_URL_CLASS
Definition Parser.php:109
OutputType( $x=null)
Accessor/mutator for the output type.
Definition Parser.php:1109
const SFH_OBJECT_ARGS
Definition Parser.php:101
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition Parser.php:566
resetOutput()
Reset the ParserOutput.
Definition Parser.php:668
setLinkID( $id)
Definition Parser.php:1163
doQuotes( $text)
Helper function for handleAllQuotes()
Definition Parser.php:1992
getTemplateDom(LinkTarget $title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
Definition Parser.php:3483
static formatPageTitle( $nsText, $nsSeparator, $mainText)
Add HTML tags marking the parts of a page title, to be displayed in the first heading of the page.
Definition Parser.php:6516
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition Parser.php:5065
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:1365
const CONSTRUCTOR_OPTIONS
Definition Parser.php:408
const OT_MSG
Definition Parser.php:133
incrementExpensiveFunctionCount()
Definition Parser.php:4082
makeImage(LinkTarget $link, $options, $holders=false)
Parse image options text and use it to make an image.
Definition Parser.php:5344
const OT_PREPROCESS
Definition Parser.php:132
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:4700
__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:459
firstCallInit()
Used to do various kinds of initialisation on the first call of the parser.
Definition Parser.php:610
preprocessToDom( $text, $flags=0)
Get the document object model for the given wikitext.
Definition Parser.php:2907
markerSkipCallback( $s, callable $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:6384
clearState()
Clear Parser state.
Definition Parser.php:622
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:6415
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
Definition Parser.php:6552
nextLinkID()
Definition Parser.php:1155
getRevisionRecordObject()
Get the revision record object for $this->mRevisionId.
Definition Parser.php:6021
attributeStripCallback(&$text, $frame=false)
Callback from the Sanitizer for expanding items found in HTML attribute values, so they can be safely...
Definition Parser.php:5712
argSubstitution(array $piece, PPFrame $frame)
Triple brace replacement – used for template arguments.
Definition Parser.php:3911
const SFH_NO_HASH
Definition Parser.php:100
renderImageGallery( $text, array $params)
Renders an image gallery from a text with one line per image.
Definition Parser.php:5105
lock()
Lock the current instance of the parser.
Definition Parser.php:6458
static statelessFetchTemplate( $page, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
Definition Parser.php:3640
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
Definition Parser.php:6537
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:4579
Differences from DOM schema:
const DOM_FOR_INCLUSION
Transclusion mode flag for Preprocessor::preprocessToObj()
Variant of the Message class.
Group all the pieces relevant to the context of a request into one instance.
setTitle(Title $title=null)
static fixTagAttributes( $text, $element, $sorted=false)
Take a tag soup fragment listing an HTML element's attributes and normalize it to well-formed XML,...
static cleanUrl( $url)
static escapeIdForLink( $id)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid URL...
static normalizeSectionNameWhitespace( $section)
Normalizes whitespace in a section name, such as might be returned by Parser::stripSectionName(),...
static decodeCharReferences( $text)
Decode any character references, numeric or named entities, in the text and return a UTF-8 string.
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...
static stripAllTags( $html)
Take a fragment of (potentially invalid) HTML and return a version with any tags removed,...
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...
Represents a title within MediaWiki.
Definition Title.php:49
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition Title.php:734
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
This class performs some operations related to tracking categories, such as creating a list of all su...
internal since 1.36
Definition User.php:70
static newFromName( $name, $validate='valid')
Definition User.php:598
Multi-datacenter aware caching interface.
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 => '🂻', 3852 => '🂼', 3853 => '🂽', 3854 => '🂾', 3855 => '🃁', 3856 => '🃂', 3857 => '🃃', 3858 => '🃄', 3859 => '🃅', 3860 => '🃆', 3861 => '🃇', 3862 => '🃈', 3863 => '🃉', 3864 => '🃊', 3865 => '🃋', 3866 => '🃌', 3867 => '🃍', 3868 => '🃎', 3869 => '🃏', 3870 => '🃑', 3871 => '🃒', 3872 => '🃓', 3873 => '🃔', 3874 => '🃕', 3875 => '🃖', 3876 => '🃗', 3877 => '🃘', 3878 => '🃙', 3879 => '🃚', 3880 => '🃛', 3881 => '🃜', 3882 => '🃝', 3883 => '🃞', 3884 => '🃟', 3885 => '🌀', 3886 => '🌁', 3887 => '🌂', 3888 => '🌃', 3889 => '🌄', 3890 => '🌅', 3891 => '🌆', 3892 => '🌇', 3893 => '🌈', 3894 => '🌉', 3895 => '🌊', 3896 => '🌋', 3897 => '🌌', 3898 => '🌍', 3899 => '🌎', 3900 => '🌏', 3901 => '🌐', 3902 => '🌑', 3903 => '🌒', 3904 => '🌓', 3905 => '🌔', 3906 => '🌕', 3907 => '🌖', 3908 => '🌗', 3909 => '🌘', 3910 => '🌙', 3911 => '🌚', 3912 => '🌛', 3913 => '🌜', 3914 => '🌝', 3915 => '🌞', 3916 => '🌟', 3917 => '🌠', 3918 => '🌰', 3919 => '🌱', 3920 => '🌲', 3921 => '🌳', 3922 => '🌴', 3923 => '🌵', 3924 => '🌷', 3925 => '🌸', 3926 => '🌹', 3927 => '🌺', 3928 => '🌻', 3929 => '🌼', 3930 => '🌽', 3931 => '🌾', 3932 => '🌿', 3933 => '🍀', 3934 => '🍁', 3935 => '🍂', 3936 => '🍃', 3937 => '🍄', 3938 => '🍅', 3939 => '🍆', 3940 => '🍇', 3941 => '🍈', 3942 => '🍉', 3943 => '🍊', 3944 => '🍋', 3945 => '🍌', 3946 => '🍍', 3947 => '🍎', 3948 => '🍏', 3949 => '🍐', 3950 => '🍑', 3951 => '🍒', 3952 => '🍓', 3953 => '🍔', 3954 => '🍕', 3955 => '🍖', 3956 => '🍗', 3957 => '🍘', 3958 => '🍙', 3959 => '🍚', 3960 => '🍛', 3961 => '🍜', 3962 => '🍝', 3963 => '🍞', 3964 => '🍟', 3965 => '🍠', 3966 => '🍡', 3967 => '🍢', 3968 => '🍣', 3969 => '🍤', 3970 => '🍥', 3971 => '🍦', 3972 => '🍧', 3973 => '🍨', 3974 => '🍩', 3975 => '🍪', 3976 => '🍫', 3977 => '🍬', 3978 => '🍭', 3979 => '🍮', 3980 => '🍯', 3981 => '🍰', 3982 => '🍱', 3983 => '🍲', 3984 => '🍳', 3985 => '🍴', 3986 => '🍵', 3987 => '🍶', 3988 => '🍷', 3989 => '🍸', 3990 => '🍹', 3991 => '🍺', 3992 => '🍻', 3993 => '🍼', 3994 => '🎀', 3995 => '🎁', 3996 => '🎂', 3997 => '🎃', 3998 => '🎄', 3999 => '🎅', 4000 => '🎆', 4001 => '🎇', 4002 => '🎈', 4003 => '🎉', 4004 => '🎊', 4005 => '🎋', 4006 => '🎌', 4007 => '🎍', 4008 => '🎎', 4009 => '🎏', 4010 => '🎐', 4011 => '🎑', 4012 => '🎒', 4013 => '🎓', 4014 => '🎠', 4015 => '🎡', 4016 => '🎢', 4017 => '🎣', 4018 => '🎤', 4019 => '🎥', 4020 => '🎦', 4021 => '🎧', 4022 => '🎨', 4023 => '🎩', 4024 => '🎪', 4025 => '🎫', 4026 => '🎬', 4027 => '🎭', 4028 => '🎮', 4029 => '🎯', 4030 => '🎰', 4031 => '🎱', 4032 => '🎲', 4033 => '🎳', 4034 => '🎴', 4035 => '🎵', 4036 => '🎶', 4037 => '🎷', 4038 => '🎸', 4039 => '🎹', 4040 => '🎺', 4041 => '🎻', 4042 => '🎼', 4043 => '🎽', 4044 => '🎾', 4045 => '🎿', 4046 => '🏀', 4047 => '🏁', 4048 => '🏂', 4049 => '🏃', 4050 => '🏄', 4051 => '🏆', 4052 => '🏇', 4053 => '🏈', 4054 => '🏉', 4055 => '🏊', 4056 => '🏠', 4057 => '🏡', 4058 => '🏢', 4059 => '🏣', 4060 => '🏤', 4061 => '🏥', 4062 => '🏦', 4063 => '🏧', 4064 => '🏨', 4065 => '🏩', 4066 => '🏪', 4067 => '🏫', 4068 => '🏬', 4069 => '🏭', 4070 => '🏮', 4071 => '🏯', 4072 => '🏰', 4073 => '🐀', 4074 => '🐁', 4075 => '🐂', 4076 => '🐃', 4077 => '🐄', 4078 => '🐅', 4079 => '🐆', 4080 => '🐇', 4081 => '🐈', 4082 => '🐉', 4083 => '🐊', 4084 => '🐋', 4085 => '🐌', 4086 => '🐍', 4087 => '🐎', 4088 => '🐏', 4089 => '🐐', 4090 => '🐑', 4091 => '🐒', 4092 => '🐓', 4093 => '🐔', 4094 => '🐕', 4095 => '🐖', 4096 => '🐗', 4097 => '🐘', 4098 => '🐙', 4099 => '🐚', 4100 => '🐛', 4101 => '🐜', 4102 => '🐝', 4103 => '🐞', 4104 => '🐟', 4105 => '🐠', 4106 => '🐡', 4107 => '🐢', 4108 => '🐣', 4109 => '🐤', 4110 => '🐥', 4111 => '🐦', 4112 => '🐧', 4113 => '🐨', 4114 => '🐩', 4115 => '🐪', 4116 => '🐫', 4117 => '🐬', 4118 => '🐭', 4119 => '🐮', 4120 => '🐯', 4121 => '🐰', 4122 => '🐱', 4123 => '🐲', 4124 => '🐳', 4125 => '🐴', 4126 => '🐵', 4127 => '🐶', 4128 => '🐷', 4129 => '🐸', 4130 => '🐹', 4131 => '🐺', 4132 => '🐻', 4133 => '🐼', 4134 => '🐽', 4135 => '🐾', 4136 => '👀', 4137 => '👂', 4138 => '👃', 4139 => '👄', 4140 => '👅', 4141 => '👆', 4142 => '👇', 4143 => '👈', 4144 => '👉', 4145 => '👊', 4146 => '👋', 4147 => '👌', 4148 => '👍', 4149 => '👎', 4150 => '👏', 4151 => '👐', 4152 => '👑', 4153 => '👒', 4154 => '👓', 4155 => '👔', 4156 => '👕', 4157 => '👖', 4158 => '👗', 4159 => '👘', 4160 => '👙', 4161 => '👚', 4162 => '👛', 4163 => '👜', 4164 => '👝', 4165 => '👞', 4166 => '👟', 4167 => '👠', 4168 => '👡', 4169 => '👢', 4170 => '👣', 4171 => '👤', 4172 => '👥', 4173 => '👦', 4174 => '👧', 4175 => '👨', 4176 => '👩', 4177 => '👪', 4178 => '👫', 4179 => '👬', 4180 => '👭', 4181 => '👮', 4182 => '👯', 4183 => '👰', 4184 => '👱', 4185 => '👲', 4186 => '👳', 4187 => '👴', 4188 => '👵', 4189 => '👶', 4190 => '👷', 4191 => '👸', 4192 => '👹', 4193 => '👺', 4194 => '👻', 4195 => '👼', 4196 => '👽', 4197 => '👾', 4198 => '👿', 4199 => '💀', 4200 => '💁', 4201 => '💂', 4202 => '💃', 4203 => '💄', 4204 => '💅', 4205 => '💆', 4206 => '💇', 4207 => '💈', 4208 => '💉', 4209 => '💊', 4210 => '💋', 4211 => '💌', 4212 => '💍', 4213 => '💎', 4214 => '💏', 4215 => '💐', 4216 => '💑', 4217 => '💒', 4218 => '💓', 4219 => '💔', 4220 => '💕', 4221 => '💖', 4222 => '💗', 4223 => '💘', 4224 => '💙', 4225 => '💚', 4226 => '💛', 4227 => '💜', 4228 => '💝', 4229 => '💞', 4230 => '💟', 4231 => '💠', 4232 => '💡', 4233 => '💢', 4234 => '💣', 4235 => '💤', 4236 => '💥', 4237 => '💦', 4238 => '💧', 4239 => '💨', 4240 => '💩', 4241 => '💪', 4242 => '💫', 4243 => '💬', 4244 => '💭', 4245 => '💮', 4246 => '💯', 4247 => '💰', 4248 => '💱', 4249 => '💲', 4250 => '💳', 4251 => '💴', 4252 => '💵', 4253 => '💶', 4254 => '💷', 4255 => '💸', 4256 => '💹', 4257 => '💺', 4258 => '💻', 4259 => '💼', 4260 => '💽', 4261 => '💾', 4262 => '💿', 4263 => '📀', 4264 => '📁', 4265 => '📂', 4266 => '📃', 4267 => '📄', 4268 => '📅', 4269 => '📆', 4270 => '📇', 4271 => '📈', 4272 => '📉', 4273 => '📊', 4274 => '📋', 4275 => '📌', 4276 => '📍', 4277 => '📎', 4278 => '📏', 4279 => '📐', 4280 => '📑', 4281 => '📒', 4282 => '📓', 4283 => '📔', 4284 => '📕', 4285 => '📖', 4286 => '📗', 4287 => '📘', 4288 => '📙', 4289 => '📚', 4290 => '📛', 4291 => '📜', 4292 => '📝', 4293 => '📞', 4294 => '📟', 4295 => '📠', 4296 => '📡', 4297 => '📢', 4298 => '📣', 4299 => '📤', 4300 => '📥', 4301 => '📦', 4302 => '📧', 4303 => '📨', 4304 => '📩', 4305 => '📪', 4306 => '📫', 4307 => '📬', 4308 => '📭', 4309 => '📮', 4310 => '📯', 4311 => '📰', 4312 => '📱', 4313 => '📲', 4314 => '📳', 4315 => '📴', 4316 => '📵', 4317 => '📶', 4318 => '📷', 4319 => '📹', 4320 => '📺', 4321 => '📻', 4322 => '📼', 4323 => '🔀', 4324 => '🔁', 4325 => '🔂', 4326 => '🔃', 4327 => '🔄', 4328 => '🔅', 4329 => '🔆', 4330 => '🔇', 4331 => '🔈', 4332 => '🔉', 4333 => '🔊', 4334 => '🔋', 4335 => '🔌', 4336 => '🔍', 4337 => '🔎', 4338 => '🔏', 4339 => '🔐', 4340 => '🔑', 4341 => '🔒', 4342 => '🔓', 4343 => '🔔', 4344 => '🔕', 4345 => '🔖', 4346 => '🔗', 4347 => '🔘', 4348 => '🔙', 4349 => '🔚', 4350 => '🔛', 4351 => '🔜', 4352 => '🔝', 4353 => '🔞', 4354 => '🔟', 4355 => '🔠', 4356 => '🔡', 4357 => '🔢', 4358 => '🔣', 4359 => '🔤', 4360 => '🔥', 4361 => '🔦', 4362 => '🔧', 4363 => '🔨', 4364 => '🔩', 4365 => '🔪', 4366 => '🔫', 4367 => '🔬', 4368 => '🔭', 4369 => '🔮', 4370 => '🔯', 4371 => '🔰', 4372 => '🔱', 4373 => '🔲', 4374 => '🔳', 4375 => '🔴', 4376 => '🔵', 4377 => '🔶', 4378 => '🔷', 4379 => '🔸', 4380 => '🔹', 4381 => '🔺', 4382 => '🔻', 4383 => '🔼', 4384 => '🔽', 4385 => '🕐', 4386 => '🕑', 4387 => '🕒', 4388 => '🕓', 4389 => '🕔', 4390 => '🕕', 4391 => '🕖', 4392 => '🕗', 4393 => '🕘', 4394 => '🕙', 4395 => '🕚', 4396 => '🕛', 4397 => '🕜', 4398 => '🕝', 4399 => '🕞', 4400 => '🕟', 4401 => '🕠', 4402 => '🕡', 4403 => '🕢', 4404 => '🕣', 4405 => '🕤', 4406 => '🕥', 4407 => '🕦', 4408 => '🕧', 4409 => '🗻', 4410 => '🗼', 4411 => '🗽', 4412 => '🗾', 4413 => '🗿', 4414 => '😁', 4415 => '😂', 4416 => '😃', 4417 => '😄', 4418 => '😅', 4419 => '😆', 4420 => '😇', 4421 => '😈', 4422 => '😉', 4423 => '😊', 4424 => '😋', 4425 => '😌', 4426 => '😍', 4427 => '😎', 4428 => '😏', 4429 => '😐', 4430 => '😒', 4431 => '😓', 4432 => '😔', 4433 => '😖', 4434 => '😘', 4435 => '😚', 4436 => '😜', 4437 => '😝', 4438 => '😞', 4439 => '😠', 4440 => '😡', 4441 => '😢', 4442 => '😣', 4443 => '😤', 4444 => '😥', 4445 => '😨', 4446 => '😩', 4447 => '😪', 4448 => '😫', 4449 => '😭', 4450 => '😰', 4451 => '😱', 4452 => '😲', 4453 => '😳', 4454 => '😵', 4455 => '😶', 4456 => '😷', 4457 => '😸', 4458 => '😹', 4459 => '😺', 4460 => '😻', 4461 => '😼', 4462 => '😽', 4463 => '😾', 4464 => '😿', 4465 => '🙀', 4466 => '🙅', 4467 => '🙆', 4468 => '🙇', 4469 => '🙈', 4470 => '🙉', 4471 => '🙊', 4472 => '🙋', 4473 => '🙌', 4474 => '🙍', 4475 => '🙎', 4476 => '🙏', 4477 => '🚀', 4478 => '🚁', 4479 => '🚂', 4480 => '🚃', 4481 => '🚄', 4482 => '🚅', 4483 => '🚆', 4484 => '🚇', 4485 => '🚈', 4486 => '🚉', 4487 => '🚊', 4488 => '🚋', 4489 => '🚌', 4490 => '🚍', 4491 => '🚎', 4492 => '🚏', 4493 => '🚐', 4494 => '🚑', 4495 => '🚒', 4496 => '🚓', 4497 => '🚔', 4498 => '🚕', 4499 => '🚖', 4500 => '🚗', 4501 => '🚘', 4502 => '🚙', 4503 => '🚚', 4504 => '🚛', 4505 => '🚜', 4506 => '🚝', 4507 => '🚞', 4508 => '🚟', 4509 => '🚠', 4510 => '🚡', 4511 => '🚢', 4512 => '🚣', 4513 => '🚤', 4514 => '🚥', 4515 => '🚦', 4516 => '🚧', 4517 => '🚨', 4518 => '🚩', 4519 => '🚪', 4520 => '🚫', 4521 => '🚬', 4522 => '🚭', 4523 => '🚮', 4524 => '🚯', 4525 => '🚰', 4526 => '🚱', 4527 => '🚲', 4528 => '🚳', 4529 => '🚴', 4530 => '🚵', 4531 => '🚶', 4532 => '🚷', 4533 => '🚸', 4534 => '🚹', 4535 => '🚺', 4536 => '🚻', 4537 => '🚼', 4538 => '🚽', 4539 => '🚾', 4540 => '🚿', 4541 => '🛀', 4542 => '🛁', 4543 => '🛂', 4544 => '🛃', 4545 => '🛄', 4546 => '🛅', 4547 => '🜀', 4548 => '🜁', 4549 => '🜂', 4550 => '🜃', 4551 => '🜄', 4552 => '🜅', 4553 => '🜆', 4554 => '🜇', 4555 => '🜈', 4556 => '🜉', 4557 => '🜊', 4558 => '🜋', 4559 => '🜌', 4560 => '🜍', 4561 => '🜎', 4562 => '🜏', 4563 => '🜐', 4564 => '🜑', 4565 => '🜒', 4566 => '🜓', 4567 => '🜔', 4568 => '🜕', 4569 => '🜖', 4570 => '🜗', 4571 => '🜘', 4572 => '🜙', 4573 => '🜚', 4574 => '🜛', 4575 => '🜜', 4576 => '🜝', 4577 => '🜞', 4578 => '🜟', 4579 => '🜠', 4580 => '🜡', 4581 => '🜢', 4582 => '🜣', 4583 => '🜤', 4584 => '🜥', 4585 => '🜦', 4586 => '🜧', 4587 => '🜨', 4588 => '🜩', 4589 => '🜪', 4590 => '🜫', 4591 => '🜬', 4592 => '🜭', 4593 => '🜮', 4594 => '🜯', 4595 => '🜰', 4596 => '🜱', 4597 => '🜲', 4598 => '🜳', 4599 => '🜴', 4600 => '🜵', 4601 => '🜶', 4602 => '🜷', 4603 => '🜸', 4604 => '🜹', 4605 => '🜺', 4606 => '🜻', 4607 => '🜼', 4608 => '🜽', 4609 => '🜾', 4610 => '🜿', 4611 => '🝀', 4612 => '🝁', 4613 => '🝂', 4614 => '🝃', 4615 => '🝄', 4616 => '🝅', 4617 => '🝆', 4618 => '🝇', 4619 => '🝈', 4620 => '🝉', 4621 => '🝊', 4622 => '🝋', 4623 => '🝌', 4624 => '🝍', 4625 => '🝎', 4626 => '🝏', 4627 => '🝐', 4628 => '🝑', 4629 => '🝒', 4630 => '🝓', 4631 => '🝔', 4632 => '🝕', 4633 => '🝖', 4634 => '🝗', 4635 => '🝘', 4636 => '🝙', 4637 => '🝚', 4638 => '🝛', 4639 => '🝜', 4640 => '🝝', 4641 => '🝞', 4642 => '🝟', 4643 => '🝠', 4644 => '🝡', 4645 => '🝢', 4646 => '🝣', 4647 => '🝤', 4648 => '🝥', 4649 => '🝦', 4650 => '🝧', 4651 => '🝨', 4652 => '🝩', 4653 => '🝪', 4654 => '🝫', 4655 => '🝬', 4656 => '🝭', 4657 => '🝮', 4658 => '🝯', 4659 => '🝰', 4660 => '🝱', 4661 => '🝲', 4662 => '🝳', 4663 => '㆐', 4664 => '㆑', 4665 => '', 4666 => '�', 4667 => '৴', 4668 => '৵', 4669 => '৶', 4670 => '৷', 4671 => '৸', 4672 => '৹', 4673 => '୲', 4674 => '୳', 4675 => '୴', 4676 => '୵', 4677 => '୶', 4678 => '୷', 4679 => '꠰', 4680 => '꠱', 4681 => '꠲', 4682 => '꠳', 4683 => '꠴', 4684 => '꠵', 4685 => '௰', 4686 => '௱', 4687 => '௲', 4688 => '൰', 4689 => '൱', 4690 => '൲', 4691 => '൳', 4692 => '൴', 4693 => '൵', 4694 => '፲', 4695 => '፳', 4696 => '፴', 4697 => '፵', 4698 => '፶', 4699 => '፷', 4700 => '፸', 4701 => '፹', 4702 => '፺', 4703 => '፻', 4704 => '፼', 4705 => 'ↀ', 4706 => 'ↁ', 4707 => 'ↂ', 4708 => 'ↆ', 4709 => 'ↇ', 4710 => 'ↈ', 4711 => '𐹩', 4712 => '𐹪', 4713 => '𐹫', 4714 => '𐹬', 4715 => '𐹭', 4716 => '𐹮', 4717 => '𐹯', 4718 => '𐹰', 4719 => '𐹱', 4720 => '𐹲', 4721 => '𐹳', 4722 => '𐹴', 4723 => '𐹵', 4724 => '𐹶', 4725 => '𐹷', 4726 => '𐹸', 4727 => '𐹹', 4728 => '𐹺', 4729 => '𐹻', 4730 => '𐹼', 4731 => '𐹽', 4732 => '𐹾', 4733 => '⳽', 4734 => '𐌢', 4735 => '𐌣', 4736 => '𐄐', 4737 => '𐄑', 4738 => '𐄒', 4739 => '𐄓', 4740 => '𐄔', 4741 => '𐄕', 4742 => '𐄖', 4743 => '𐄗', 4744 => '𐄘', 4745 => '𐄙', 4746 => '𐄚', 4747 => '𐄛', 4748 => '𐄜', 4749 => '𐄝', 4750 => '𐄞', 4751 => '𐄟', 4752 => '𐄠', 4753 => '𐄡', 4754 => '𐄢', 4755 => '𐄣', 4756 => '𐄤', 4757 => '𐄥', 4758 => '𐄦', 4759 => '𐄧', 4760 => '𐄨', 4761 => '𐄩', 4762 => '𐄪', 4763 => '𐄫', 4764 => '𐄬', 4765 => '𐄭', 4766 => '𐄮', 4767 => '𐄯', 4768 => '𐄰', 4769 => '𐄱', 4770 => '𐄲', 4771 => '𐄳', 4772 => '𐅀', 4773 => '𐅁', 4774 => '𐅄', 4775 => '𐅅', 4776 => '𐅆', 4777 => '𐅇', 4778 => '𐅉', 4779 => '𐅊', 4780 => '𐅋', 4781 => '𐅌', 4782 => '𐅍', 4783 => '𐅎', 4784 => '𐅐', 4785 => '𐅑', 4786 => '𐅒', 4787 => '𐅓', 4788 => '𐅔', 4789 => '𐅕', 4790 => '𐅖', 4791 => '𐅗', 4792 => '𐅠', 4793 => '𐅡', 4794 => '𐅢', 4795 => '𐅣', 4796 => '𐅤', 4797 => '𐅥', 4798 => '𐅦', 4799 => '𐅧', 4800 => '𐅨', 4801 => '𐅩', 4802 => '𐅪', 4803 => '𐅫', 4804 => '𐅬', 4805 => '𐅭', 4806 => '𐅮', 4807 => '𐅯', 4808 => '𐅰', 4809 => '𐅱', 4810 => '𐅲', 4811 => '𐅴', 4812 => '𐅵', 4813 => '𐅶', 4814 => '𐅷', 4815 => '𐅸', 4816 => '𐏓', 4817 => '𐏔', 4818 => '𐏕', 4819 => '𐩾', 4820 => '𐩿', 4821 => '𐤗', 4822 => '𐤘', 4823 => '𐤙', 4824 => '𐡛', 4825 => '𐡜', 4826 => '𐡝', 4827 => '𐡞', 4828 => '𐡟', 4829 => '𐭜', 4830 => '𐭝', 4831 => '𐭞', 4832 => '𐭟', 4833 => '𐭼', 4834 => '𐭽', 4835 => '𐭾', 4836 => '𐭿', 4837 => '𑁛', 4838 => '𑁜', 4839 => '𑁝', 4840 => '𑁞', 4841 => '𑁟', 4842 => '𑁠', 4843 => '𑁡', 4844 => '𑁢', 4845 => '𑁣', 4846 => '𑁤', 4847 => '𑁥', 4848 => '𐩄', 4849 => '𐩅', 4850 => '𐩆', 4851 => '𐩇', 4852 => '𒐲', 4853 => '𒐳', 4854 => '𒑖', 4855 => '𒑗', 4856 => '𒑚', 4857 => '𒑛', 4858 => '𒑜', 4859 => '𒑝', 4860 => '𒑞', 4861 => '𒑟', 4862 => '𒑠', 4863 => '𒑡', 4864 => '𒑢', 4865 => '𝍩', 4866 => '𝍪', 4867 => '𝍫', 4868 => '𝍬', 4869 => '𝍭', 4870 => '𝍮', 4871 => '𝍯', 4872 => '𝍰', 4873 => '𝍱', 4874 => 'ː',