MediaWiki REL1_35
Parser.php
Go to the documentation of this file.
1<?php
39use Psr\Log\LoggerInterface;
40use Psr\Log\NullLogger;
41use Wikimedia\IPUtils;
42use Wikimedia\ScopedCallback;
43
84#[AllowDynamicProperties]
85class Parser {
91 public const VERSION = '1.6.4';
92
93 # Flags for Parser::setFunctionHook
94 public const SFH_NO_HASH = 1;
95 public const SFH_OBJECT_ARGS = 2;
96
97 # Constants needed for external link processing
98 # Everything except bracket, space, or control characters
99 # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
100 # as well as U+3000 is IDEOGRAPHIC SPACE for T21052
101 # \x{FFFD} is the Unicode replacement character, which the HTML5 spec
102 # uses to replace invalid HTML characters.
103 public const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
104 # Simplified expression to match an IPv4 or IPv6 address, or
105 # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
106 // phpcs:ignore Generic.Files.LineLength
107 private const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
108 # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
109 // phpcs:ignore Generic.Files.LineLength
110 private const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
111 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
112
113 # Regular expression for a non-newline space
114 private const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
115
116 # Flags for preprocessToDom
117 public const PTD_FOR_INCLUSION = 1;
118
119 # Allowed values for $this->mOutputType
120 # Parameter to startExternalParse().
121 public const OT_HTML = 1; # like parse()
122 public const OT_WIKI = 2; # like preSaveTransform()
123 public const OT_PREPROCESS = 3; # like preprocess()
124 public const OT_MSG = 3;
125 # like extractSections() - portions of the original are returned unchanged.
126 public const OT_PLAIN = 4;
127
145 public const MARKER_SUFFIX = "-QINU`\"'\x7f";
146 public const MARKER_PREFIX = "\x7f'\"`UNIQ-";
147
148 # Markers used for wrapping the table of contents
149 public const TOC_START = '<mw:toc>';
150 public const TOC_END = '</mw:toc>';
151
152 # Persistent:
154 public $mTagHooks = [];
156 public $mFunctionHooks = [];
157 private $mFunctionSynonyms = [ 0 => [], 1 => [] ];
158 private $mFunctionTagHooks = [];
159 private $mStripList = [];
160 private $mVarCache = [];
161 private $mImageParams = [];
164 public $mMarkerIndex = 0;
169 public $mFirstCall = true;
170
171 # Initialised by initializeVariables()
172
176 private $mVariables;
177
182
187 private $mConf;
188
189 # Initialised in constructor
191
192 # Initialized in getPreprocessor()
198
199 # Cleared with clearState():
204 public $mOutput;
206
212
217
222 public $mLinkID;
241 public $mExpensiveFunctionCount; # number of expensive parser function calls
243 public $mShowToc;
247
252 public $mUser; # User object; only used when doing pre-save transform
253
254 # Temporary
255 # These are variables reset at least once per parse regardless of $clearState
256
261 public $mOptions;
262
270 public $mTitle; # Title context, used for self-link rendering and similar things
271 private $mOutputType; # Output type, one of the OT_xxx constants
273 public $ot; # Shortcut alias, see setOutputType()
275 public $mRevisionObject; # The revision object of the specified revision ID
277 public $mRevisionId; # ID to display in {{REVISIONID}} tags
279 public $mRevisionTimestamp; # The timestamp of the specified revision ID
281 public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
283 public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
285 public $mInputSize = false; # For {{PAGESIZE}} on current page.
286
289
296
304
311 public $mInParse = false;
312
314 private $mProfiler;
315
320
323
325 private $contLang;
326
329
331 private $factory;
332
335
343 private $svcOptions;
344
347
349 private $nsInfo;
350
352 private $logger;
353
356
359
361 private $hookRunner;
362
367 public const CONSTRUCTOR_OPTIONS = [
368 // Deprecated and unused; from $wgParserConf
369 'class',
370 // See documentation for the corresponding config options
371 'ArticlePath',
372 'EnableScaryTranscluding',
373 'ExtraInterlanguageLinkPrefixes',
374 'FragmentMode',
375 'LanguageCode',
376 'MaxSigChars',
377 'MaxTocLevel',
378 'MiserMode',
379 'ScriptPath',
380 'Server',
381 'ServerName',
382 'ShowHostnames',
383 'SignatureValidation',
384 'Sitename',
385 'StylePath',
386 'TranscludeCacheExpiry',
387 ];
388
406 public function __construct(
407 $svcOptions = null,
408 MagicWordFactory $magicWordFactory = null,
409 Language $contLang = null,
410 ParserFactory $factory = null,
411 $urlProtocols = null,
412 SpecialPageFactory $spFactory = null,
413 $linkRendererFactory = null,
414 $nsInfo = null,
415 $logger = null,
416 BadFileLookup $badFileLookup = null,
417 LanguageConverterFactory $languageConverterFactory = null,
418 HookContainer $hookContainer = null
419 ) {
420 if ( ParserFactory::$inParserFactory === 0 ) {
421 // Direct construction of Parser is deprecated; use a ParserFactory
422 wfDeprecated( __METHOD__, '1.34' );
423 }
424 if ( !$svcOptions || is_array( $svcOptions ) ) {
425 wfDeprecated( 'old calling convention for ' . __METHOD__, '1.34' );
426 // Pre-1.34 calling convention is the first parameter is just ParserConf, the seventh is
427 // Config, and the eighth is LinkRendererFactory.
428 $this->mConf = (array)$svcOptions;
429 if ( empty( $this->mConf['class'] ) ) {
430 $this->mConf['class'] = self::class;
431 }
432 $this->svcOptions = new ServiceOptions( self::CONSTRUCTOR_OPTIONS,
433 $this->mConf, func_num_args() > 6
434 ? func_get_arg( 6 ) : MediaWikiServices::getInstance()->getMainConfig()
435 );
436 $linkRendererFactory = func_num_args() > 7 ? func_get_arg( 7 ) : null;
437 $nsInfo = func_num_args() > 8 ? func_get_arg( 8 ) : null;
438 } else {
439 // New calling convention
440 $svcOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
441 // $this->mConf is public, so we'll keep the option there for
442 // compatibility until it's removed
443 $this->mConf = [
444 'class' => $svcOptions->get( 'class' ),
445 ];
446 $this->svcOptions = $svcOptions;
447 }
448
449 $this->mUrlProtocols = $urlProtocols ?? wfUrlProtocols();
450 $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
451 self::EXT_LINK_ADDR .
452 self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
453
454 $this->magicWordFactory = $magicWordFactory ??
455 MediaWikiServices::getInstance()->getMagicWordFactory();
456
457 $this->contLang = $contLang ?? MediaWikiServices::getInstance()->getContentLanguage();
458
459 $this->factory = $factory ?? MediaWikiServices::getInstance()->getParserFactory();
460 $this->specialPageFactory = $spFactory ??
461 MediaWikiServices::getInstance()->getSpecialPageFactory();
462 $this->linkRendererFactory = $linkRendererFactory ??
463 MediaWikiServices::getInstance()->getLinkRendererFactory();
464 $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo();
465 $this->logger = $logger ?: new NullLogger();
466 $this->badFileLookup = $badFileLookup ??
467 MediaWikiServices::getInstance()->getBadFileLookup();
468
469 $this->languageConverterFactory = $languageConverterFactory ??
470 MediaWikiServices::getInstance()->getLanguageConverterFactory();
471
472 $this->hookContainer = $hookContainer ??
473 MediaWikiServices::getInstance()->getHookContainer();
474 $this->hookRunner = new HookRunner( $this->hookContainer );
475
476 // T250444: This will eventually be inlined here and the
477 // standalone method removed.
478 $this->firstCallInit();
479 }
480
484 public function __destruct() {
485 if ( isset( $this->mLinkHolders ) ) {
486 // @phan-suppress-next-line PhanTypeObjectUnsetDeclaredProperty
487 unset( $this->mLinkHolders );
488 }
489 // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
490 foreach ( $this as $name => $value ) {
491 unset( $this->$name );
492 }
493 }
494
498 public function __clone() {
499 $this->mInParse = false;
500
501 // T58226: When you create a reference "to" an object field, that
502 // makes the object field itself be a reference too (until the other
503 // reference goes out of scope). When cloning, any field that's a
504 // reference is copied as a reference in the new object. Both of these
505 // are defined PHP5 behaviors, as inconvenient as it is for us when old
506 // hooks from PHP4 days are passing fields by reference.
507 foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
508 // Make a non-reference copy of the field, then rebind the field to
509 // reference the new copy.
510 $tmp = $this->$k;
511 $this->$k =& $tmp;
512 unset( $tmp );
513 }
514
515 $this->hookRunner->onParserCloned( $this );
516 }
517
523 public function firstCallInit() {
524 if ( !$this->mFirstCall ) {
525 return;
526 }
527 $this->mFirstCall = false;
528
530 CoreTagHooks::register( $this );
531 $this->initializeVariables();
532
533 $this->hookRunner->onParserFirstCallInit( $this );
534 }
535
541 public function clearState() {
542 $this->firstCallInit();
543 $this->resetOutput();
544 $this->mAutonumber = 0;
545 $this->mLinkHolders = new LinkHolderArray(
546 $this,
548 $this->getHookContainer()
549 );
550 $this->mLinkID = 0;
551 $this->mRevisionObject = $this->mRevisionTimestamp =
552 $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
553 $this->mRevisionRecordObject = null;
554 $this->mVarCache = [];
555 $this->mUser = null;
556 $this->mLangLinkLanguages = [];
557 $this->currentRevisionCache = null;
558
559 $this->mStripState = new StripState( $this );
560
561 # Clear these on every parse, T6549
562 $this->mTplRedirCache = $this->mTplDomCache = [];
563
564 $this->mShowToc = true;
565 $this->mForceTocPosition = false;
566 $this->mIncludeSizes = [
567 'post-expand' => 0,
568 'arg' => 0,
569 ];
570 $this->mPPNodeCount = 0;
571 $this->mGeneratedPPNodeCount = 0;
572 $this->mHighestExpansionDepth = 0;
573 $this->mDefaultSort = false;
574 $this->mHeadings = [];
575 $this->mDoubleUnderscores = [];
576 $this->mExpensiveFunctionCount = 0;
577
578 # Fix cloning
579 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
580 $this->mPreprocessor = null;
581 }
582
583 $this->mProfiler = new SectionProfiler();
584
585 $this->hookRunner->onParserClearState( $this );
586 }
587
591 public function resetOutput() {
592 $this->mOutput = new ParserOutput;
593 $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
594 }
595
613 public function parse(
614 $text, Title $title, ParserOptions $options,
615 $linestart = true, $clearState = true, $revid = null
616 ) {
617 if ( $clearState ) {
618 // We use U+007F DELETE to construct strip markers, so we have to make
619 // sure that this character does not occur in the input text.
620 $text = strtr( $text, "\x7f", "?" );
621 $magicScopeVariable = $this->lock();
622 }
623 // Strip U+0000 NULL (T159174)
624 $text = str_replace( "\000", '', $text );
625
626 $this->startParse( $title, $options, self::OT_HTML, $clearState );
627
628 $this->currentRevisionCache = null;
629 $this->mInputSize = strlen( $text );
630 if ( $this->mOptions->getEnableLimitReport() ) {
631 $this->mOutput->resetParseStartTime();
632 }
633
634 $oldRevisionId = $this->mRevisionId;
635 $oldRevisionObject = $this->mRevisionObject;
636 $oldRevisionRecordObject = $this->mRevisionRecordObject;
637 $oldRevisionTimestamp = $this->mRevisionTimestamp;
638 $oldRevisionUser = $this->mRevisionUser;
639 $oldRevisionSize = $this->mRevisionSize;
640 if ( $revid !== null ) {
641 $this->mRevisionId = $revid;
642 $this->mRevisionObject = null;
643 $this->mRevisionRecordObject = null;
644 $this->mRevisionTimestamp = null;
645 $this->mRevisionUser = null;
646 $this->mRevisionSize = null;
647 }
648
649 $this->hookRunner->onParserBeforeStrip( $this, $text, $this->mStripState );
650 # No more strip!
651 $this->hookRunner->onParserAfterStrip( $this, $text, $this->mStripState );
652 $text = $this->internalParse( $text );
653 $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
654
655 $text = $this->internalParseHalfParsed( $text, true, $linestart );
656
664 if ( !( $options->getDisableTitleConversion()
665 || isset( $this->mDoubleUnderscores['nocontentconvert'] )
666 || isset( $this->mDoubleUnderscores['notitleconvert'] )
667 || $this->mOutput->getDisplayTitle() !== false )
668 ) {
669 $convruletitle = $this->getTargetLanguageConverter()->getConvRuleTitle();
670 if ( $convruletitle ) {
671 $this->mOutput->setTitleText( $convruletitle );
672 } else {
673 $titleText = $this->getTargetLanguageConverter()->convertTitle( $title );
674 $this->mOutput->setTitleText( $titleText );
675 }
676 }
677
678 # Compute runtime adaptive expiry if set
679 $this->mOutput->finalizeAdaptiveCacheExpiry();
680
681 # Warn if too many heavyweight parser functions were used
682 if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
683 $this->limitationWarn( 'expensive-parserfunction',
684 $this->mExpensiveFunctionCount,
685 $this->mOptions->getExpensiveParserFunctionLimit()
686 );
687 }
688
689 # Information on limits, for the benefit of users who try to skirt them
690 if ( $this->mOptions->getEnableLimitReport() ) {
691 $text .= $this->makeLimitReport();
692 }
693
694 # Wrap non-interface parser output in a <div> so it can be targeted
695 # with CSS (T37247)
696 $class = $this->mOptions->getWrapOutputClass();
697 if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
698 $this->mOutput->addWrapperDivClass( $class );
699 }
700
701 $this->mOutput->setText( $text );
702
703 $this->mRevisionId = $oldRevisionId;
704 $this->mRevisionObject = $oldRevisionObject;
705 $this->mRevisionRecordObject = $oldRevisionRecordObject;
706 $this->mRevisionTimestamp = $oldRevisionTimestamp;
707 $this->mRevisionUser = $oldRevisionUser;
708 $this->mRevisionSize = $oldRevisionSize;
709 $this->mInputSize = false;
710 $this->currentRevisionCache = null;
711
712 return $this->mOutput;
713 }
714
721 protected function makeLimitReport() {
722 $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
723
724 $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
725 if ( $cpuTime !== null ) {
726 $this->mOutput->setLimitReportData( 'limitreport-cputime',
727 sprintf( "%.3f", $cpuTime )
728 );
729 }
730
731 $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
732 $this->mOutput->setLimitReportData( 'limitreport-walltime',
733 sprintf( "%.3f", $wallTime )
734 );
735
736 $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
737 [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
738 );
739 $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
740 [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
741 );
742 $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
743 [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
744 );
745 $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
746 [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
747 );
748 $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
749 [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
750 );
751
752 foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
753 $this->mOutput->setLimitReportData( $key, $value );
754 }
755
756 $this->hookRunner->onParserLimitReportPrepare( $this, $this->mOutput );
757
758 $limitReport = "NewPP limit report\n";
759 if ( $this->svcOptions->get( 'ShowHostnames' ) ) {
760 $limitReport .= 'Parsed by ' . wfHostname() . "\n";
761 }
762 $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
763 $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
764 $limitReport .= 'Dynamic content: ' .
765 ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
766 "\n";
767 $limitReport .= 'Complications: [' . implode( ', ', $this->mOutput->getAllFlags() ) . "]\n";
768
769 foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
770 if ( $this->hookRunner->onParserLimitReportFormat(
771 $key, $value, $limitReport, false, false )
772 ) {
773 $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
774 $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
775 ->inLanguage( 'en' )->useDatabase( false );
776 if ( !$valueMsg->exists() ) {
777 $valueMsg = new RawMessage( '$1' );
778 }
779 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
780 $valueMsg->params( $value );
781 $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
782 }
783 }
784 }
785 // Since we're not really outputting HTML, decode the entities and
786 // then re-encode the things that need hiding inside HTML comments.
787 $limitReport = htmlspecialchars_decode( $limitReport );
788
789 // Sanitize for comment. Note '‐' in the replacement is U+2010,
790 // which looks much like the problematic '-'.
791 $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
792 $text = "\n<!-- \n$limitReport-->\n";
793
794 // Add on template profiling data in human/machine readable way
795 $dataByFunc = $this->mProfiler->getFunctionStats();
796 uasort( $dataByFunc, function ( $a, $b ) {
797 return $b['real'] <=> $a['real']; // descending order
798 } );
799 $profileReport = [];
800 foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
801 $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
802 $item['%real'], $item['real'], $item['calls'],
803 htmlspecialchars( $item['name'] ) );
804 }
805 $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
806 $text .= implode( "\n", $profileReport ) . "\n-->\n";
807
808 $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
809
810 // Add other cache related metadata
811 if ( $this->svcOptions->get( 'ShowHostnames' ) ) {
812 $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
813 }
814 $this->mOutput->setLimitReportData( 'cachereport-timestamp',
815 $this->mOutput->getCacheTime() );
816 $this->mOutput->setLimitReportData( 'cachereport-ttl',
817 $this->mOutput->getCacheExpiry() );
818 $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
819 $this->mOutput->hasDynamicContent() );
820
821 return $text;
822 }
823
848 public function recursiveTagParse( $text, $frame = false ) {
849 $this->hookRunner->onParserBeforeStrip( $this, $text, $this->mStripState );
850 $this->hookRunner->onParserAfterStrip( $this, $text, $this->mStripState );
851 $text = $this->internalParse( $text, false, $frame );
852 return $text;
853 }
854
874 public function recursiveTagParseFully( $text, $frame = false ) {
875 $text = $this->recursiveTagParse( $text, $frame );
876 $text = $this->internalParseHalfParsed( $text, false );
877 return $text;
878 }
879
899 public function parseExtensionTagAsTopLevelDoc( $text ) {
900 $text = $this->recursiveTagParse( $text );
901 $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
902 $text = $this->internalParseHalfParsed( $text, true );
903 return $text;
904 }
905
917 public function preprocess( $text, ?Title $title,
918 ParserOptions $options, $revid = null, $frame = false
919 ) {
920 $magicScopeVariable = $this->lock();
921 $this->startParse( $title, $options, self::OT_PREPROCESS, true );
922 if ( $revid !== null ) {
923 $this->mRevisionId = $revid;
924 }
925 $this->hookRunner->onParserBeforeStrip( $this, $text, $this->mStripState );
926 $this->hookRunner->onParserAfterStrip( $this, $text, $this->mStripState );
927 $this->hookRunner->onParserBeforePreprocess( $this, $text, $this->mStripState );
928 $text = $this->replaceVariables( $text, $frame );
929 $text = $this->mStripState->unstripBoth( $text );
930 return $text;
931 }
932
942 public function recursivePreprocess( $text, $frame = false ) {
943 $text = $this->replaceVariables( $text, $frame );
944 $text = $this->mStripState->unstripBoth( $text );
945 return $text;
946 }
947
961 public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
962 $msg = new RawMessage( $text );
963 $text = $msg->params( $params )->plain();
964
965 # Parser (re)initialisation
966 $magicScopeVariable = $this->lock();
967 $this->startParse( $title, $options, self::OT_PLAIN, true );
968
969 $flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
970 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
971 $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
972 $text = $this->mStripState->unstripBoth( $text );
973 return $text;
974 }
975
982 public function setUser( ?User $user ) {
983 $this->mUser = $user;
984 }
985
991 public function setTitle( Title $t = null ) {
992 if ( !$t ) {
993 $t = Title::makeTitle( NS_SPECIAL, 'Badtitle/Parser' );
994 }
995
996 if ( $t->hasFragment() ) {
997 # Strip the fragment to avoid various odd effects
998 $this->mTitle = $t->createFragmentTarget( '' );
999 } else {
1000 $this->mTitle = $t;
1001 }
1002 }
1003
1007 public function getTitle() : ?Title {
1008 return $this->mTitle;
1009 }
1010
1018 public function Title( Title $x = null ) : ?Title {
1019 wfDeprecated( __METHOD__, '1.35' );
1020 return wfSetVar( $this->mTitle, $x );
1021 }
1022
1028 public function getOutputType(): int {
1029 return $this->mOutputType;
1030 }
1031
1036 public function setOutputType( $ot ): void {
1037 $this->mOutputType = $ot;
1038 # Shortcut alias
1039 $this->ot = [
1040 'html' => $ot == self::OT_HTML,
1041 'wiki' => $ot == self::OT_WIKI,
1042 'pre' => $ot == self::OT_PREPROCESS,
1043 'plain' => $ot == self::OT_PLAIN,
1044 ];
1045 }
1046
1054 public function OutputType( $x = null ) {
1055 wfDeprecated( __METHOD__, '1.35' );
1056 return wfSetVar( $this->mOutputType, $x );
1057 }
1058
1062 public function getOutput() {
1063 return $this->mOutput;
1064 }
1065
1069 public function getOptions() {
1070 return $this->mOptions;
1071 }
1072
1078 public function setOptions( ParserOptions $options ): void {
1079 $this->mOptions = $options;
1080 }
1081
1089 public function Options( $x = null ) {
1090 wfDeprecated( __METHOD__, '1.35' );
1091 return wfSetVar( $this->mOptions, $x );
1092 }
1093
1097 public function nextLinkID() {
1098 return $this->mLinkID++;
1099 }
1100
1104 public function setLinkID( $id ) {
1105 $this->mLinkID = $id;
1106 }
1107
1112 public function getFunctionLang() {
1113 return $this->getTargetLanguage();
1114 }
1115
1124 public function getTargetLanguage() {
1125 $target = $this->mOptions->getTargetLanguage();
1126
1127 if ( $target !== null ) {
1128 return $target;
1129 } elseif ( $this->mOptions->getInterfaceMessage() ) {
1130 return $this->mOptions->getUserLangObj();
1131 }
1132
1133 return $this->getTitle()->getPageLanguage();
1134 }
1135
1142 public function getUser() {
1143 if ( $this->mUser !== null ) {
1144 return $this->mUser;
1145 }
1146 return $this->mOptions->getUser();
1147 }
1148
1154 public function getPreprocessor() {
1155 if ( !isset( $this->mPreprocessor ) ) {
1156 $this->mPreprocessor = new Preprocessor_Hash( $this );
1157 }
1158 return $this->mPreprocessor;
1159 }
1160
1167 public function getLinkRenderer() {
1168 // XXX We make the LinkRenderer with current options and then cache it forever
1169 if ( !$this->mLinkRenderer ) {
1170 $this->mLinkRenderer = $this->linkRendererFactory->create();
1171 $this->mLinkRenderer->setStubThreshold(
1172 $this->getOptions()->getStubThreshold()
1173 );
1174 }
1175
1176 return $this->mLinkRenderer;
1177 }
1178
1185 public function getMagicWordFactory() {
1186 return $this->magicWordFactory;
1187 }
1188
1195 public function getContentLanguage() {
1196 return $this->contLang;
1197 }
1198
1205 public function getBadFileLookup() {
1206 return $this->badFileLookup;
1207 }
1208
1228 public static function extractTagsAndParams( array $elements, $text, &$matches ) {
1229 static $n = 1;
1230 $stripped = '';
1231 $matches = [];
1232
1233 $taglist = implode( '|', $elements );
1234 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1235
1236 while ( $text != '' ) {
1237 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1238 $stripped .= $p[0];
1239 if ( count( $p ) < 5 ) {
1240 break;
1241 }
1242 if ( count( $p ) > 5 ) {
1243 # comment
1244 $element = $p[4];
1245 $attributes = '';
1246 $close = '';
1247 $inside = $p[5];
1248 } else {
1249 # tag
1250 list( , $element, $attributes, $close, $inside ) = $p;
1251 }
1252
1253 $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1254 $stripped .= $marker;
1255
1256 if ( $close === '/>' ) {
1257 # Empty element tag, <tag />
1258 $content = null;
1259 $text = $inside;
1260 $tail = null;
1261 } else {
1262 if ( $element === '!--' ) {
1263 $end = '/(-->)/';
1264 } else {
1265 $end = "/(<\\/$element\\s*>)/i";
1266 }
1267 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1268 $content = $q[0];
1269 if ( count( $q ) < 3 ) {
1270 # No end tag -- let it run out to the end of the text.
1271 $tail = '';
1272 $text = '';
1273 } else {
1274 list( , $tail, $text ) = $q;
1275 }
1276 }
1277
1278 $matches[$marker] = [ $element,
1279 $content,
1280 Sanitizer::decodeTagAttributes( $attributes ),
1281 "<$element$attributes$close$content$tail" ];
1282 }
1283 return $stripped;
1284 }
1285
1291 public function getStripList() {
1292 return $this->mStripList;
1293 }
1294
1300 public function getStripState() {
1301 return $this->mStripState;
1302 }
1303
1313 public function insertStripItem( $text ) {
1314 $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1315 $this->mMarkerIndex++;
1316 $this->mStripState->addGeneral( $marker, $text );
1317 return $marker;
1318 }
1319
1326 private function handleTables( $text ) {
1327 $lines = StringUtils::explode( "\n", $text );
1328 $out = '';
1329 $td_history = []; # Is currently a td tag open?
1330 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1331 $tr_history = []; # Is currently a tr tag open?
1332 $tr_attributes = []; # history of tr attributes
1333 $has_opened_tr = []; # Did this table open a <tr> element?
1334 $indent_level = 0; # indent level of the table
1335
1336 foreach ( $lines as $outLine ) {
1337 $line = trim( $outLine );
1338
1339 if ( $line === '' ) { # empty line, go to next line
1340 $out .= $outLine . "\n";
1341 continue;
1342 }
1343
1344 $first_character = $line[0];
1345 $first_two = substr( $line, 0, 2 );
1346 $matches = [];
1347
1348 if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1349 # First check if we are starting a new table
1350 $indent_level = strlen( $matches[1] );
1351
1352 $attributes = $this->mStripState->unstripBoth( $matches[2] );
1353 $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1354
1355 $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1356 array_push( $td_history, false );
1357 array_push( $last_tag_history, '' );
1358 array_push( $tr_history, false );
1359 array_push( $tr_attributes, '' );
1360 array_push( $has_opened_tr, false );
1361 } elseif ( count( $td_history ) == 0 ) {
1362 # Don't do any of the following
1363 $out .= $outLine . "\n";
1364 continue;
1365 } elseif ( $first_two === '|}' ) {
1366 # We are ending a table
1367 $line = '</table>' . substr( $line, 2 );
1368 $last_tag = array_pop( $last_tag_history );
1369
1370 if ( !array_pop( $has_opened_tr ) ) {
1371 $line = "<tr><td></td></tr>{$line}";
1372 }
1373
1374 if ( array_pop( $tr_history ) ) {
1375 $line = "</tr>{$line}";
1376 }
1377
1378 if ( array_pop( $td_history ) ) {
1379 $line = "</{$last_tag}>{$line}";
1380 }
1381 array_pop( $tr_attributes );
1382 if ( $indent_level > 0 ) {
1383 $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1384 } else {
1385 $outLine = $line;
1386 }
1387 } elseif ( $first_two === '|-' ) {
1388 # Now we have a table row
1389 $line = preg_replace( '#^\|-+#', '', $line );
1390
1391 # Whats after the tag is now only attributes
1392 $attributes = $this->mStripState->unstripBoth( $line );
1393 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1394 array_pop( $tr_attributes );
1395 array_push( $tr_attributes, $attributes );
1396
1397 $line = '';
1398 $last_tag = array_pop( $last_tag_history );
1399 array_pop( $has_opened_tr );
1400 array_push( $has_opened_tr, true );
1401
1402 if ( array_pop( $tr_history ) ) {
1403 $line = '</tr>';
1404 }
1405
1406 if ( array_pop( $td_history ) ) {
1407 $line = "</{$last_tag}>{$line}";
1408 }
1409
1410 $outLine = $line;
1411 array_push( $tr_history, false );
1412 array_push( $td_history, false );
1413 array_push( $last_tag_history, '' );
1414 } elseif ( $first_character === '|'
1415 || $first_character === '!'
1416 || $first_two === '|+'
1417 ) {
1418 # This might be cell elements, td, th or captions
1419 if ( $first_two === '|+' ) {
1420 $first_character = '+';
1421 $line = substr( $line, 2 );
1422 } else {
1423 $line = substr( $line, 1 );
1424 }
1425
1426 // Implies both are valid for table headings.
1427 if ( $first_character === '!' ) {
1428 $line = StringUtils::replaceMarkup( '!!', '||', $line );
1429 }
1430
1431 # Split up multiple cells on the same line.
1432 # FIXME : This can result in improper nesting of tags processed
1433 # by earlier parser steps.
1434 $cells = explode( '||', $line );
1435
1436 $outLine = '';
1437
1438 # Loop through each table cell
1439 foreach ( $cells as $cell ) {
1440 $previous = '';
1441 if ( $first_character !== '+' ) {
1442 $tr_after = array_pop( $tr_attributes );
1443 if ( !array_pop( $tr_history ) ) {
1444 $previous = "<tr{$tr_after}>\n";
1445 }
1446 array_push( $tr_history, true );
1447 array_push( $tr_attributes, '' );
1448 array_pop( $has_opened_tr );
1449 array_push( $has_opened_tr, true );
1450 }
1451
1452 $last_tag = array_pop( $last_tag_history );
1453
1454 if ( array_pop( $td_history ) ) {
1455 $previous = "</{$last_tag}>\n{$previous}";
1456 }
1457
1458 if ( $first_character === '|' ) {
1459 $last_tag = 'td';
1460 } elseif ( $first_character === '!' ) {
1461 $last_tag = 'th';
1462 } elseif ( $first_character === '+' ) {
1463 $last_tag = 'caption';
1464 } else {
1465 $last_tag = '';
1466 }
1467
1468 array_push( $last_tag_history, $last_tag );
1469
1470 # A cell could contain both parameters and data
1471 $cell_data = explode( '|', $cell, 2 );
1472
1473 # T2553: Note that a '|' inside an invalid link should not
1474 # be mistaken as delimiting cell parameters
1475 # Bug T153140: Neither should language converter markup.
1476 if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1477 $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1478 } elseif ( count( $cell_data ) == 1 ) {
1479 // Whitespace in cells is trimmed
1480 $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1481 } else {
1482 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1483 $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1484 // Whitespace in cells is trimmed
1485 $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1486 }
1487
1488 $outLine .= $cell;
1489 array_push( $td_history, true );
1490 }
1491 }
1492 $out .= $outLine . "\n";
1493 }
1494
1495 # Closing open td, tr && table
1496 while ( count( $td_history ) > 0 ) {
1497 if ( array_pop( $td_history ) ) {
1498 $out .= "</td>\n";
1499 }
1500 if ( array_pop( $tr_history ) ) {
1501 $out .= "</tr>\n";
1502 }
1503 if ( !array_pop( $has_opened_tr ) ) {
1504 $out .= "<tr><td></td></tr>\n";
1505 }
1506
1507 $out .= "</table>\n";
1508 }
1509
1510 # Remove trailing line-ending (b/c)
1511 if ( substr( $out, -1 ) === "\n" ) {
1512 $out = substr( $out, 0, -1 );
1513 }
1514
1515 # special case: don't return empty table
1516 if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1517 $out = '';
1518 }
1519
1520 return $out;
1521 }
1522
1536 public function internalParse( $text, $isMain = true, $frame = false ) {
1537 $origText = $text;
1538
1539 # Hook to suspend the parser in this state
1540 if ( !$this->hookRunner->onParserBeforeInternalParse( $this, $text, $this->mStripState ) ) {
1541 return $text;
1542 }
1543
1544 # if $frame is provided, then use $frame for replacing any variables
1545 if ( $frame ) {
1546 # use frame depth to infer how include/noinclude tags should be handled
1547 # depth=0 means this is the top-level document; otherwise it's an included document
1548 if ( !$frame->depth ) {
1549 $flag = 0;
1550 } else {
1551 $flag = self::PTD_FOR_INCLUSION;
1552 }
1553 $dom = $this->preprocessToDom( $text, $flag );
1554 $text = $frame->expand( $dom );
1555 } else {
1556 # if $frame is not provided, then use old-style replaceVariables
1557 $text = $this->replaceVariables( $text );
1558 }
1559
1560 $this->hookRunner->onInternalParseBeforeSanitize( $this, $text, $this->mStripState );
1561 $text = Sanitizer::removeHTMLtags(
1562 $text,
1563 // Callback from the Sanitizer for expanding items found in
1564 // HTML attribute values, so they can be safely tested and escaped.
1565 function ( &$text, $frame = false ) {
1566 $text = $this->replaceVariables( $text, $frame );
1567 $text = $this->mStripState->unstripBoth( $text );
1568 },
1569 false,
1570 [],
1571 []
1572 );
1573 $this->hookRunner->onInternalParseBeforeLinks( $this, $text, $this->mStripState );
1574
1575 # Tables need to come after variable replacement for things to work
1576 # properly; putting them before other transformations should keep
1577 # exciting things like link expansions from showing up in surprising
1578 # places.
1579 $text = $this->handleTables( $text );
1580
1581 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1582
1583 $text = $this->handleDoubleUnderscore( $text );
1584
1585 $text = $this->handleHeadings( $text );
1586 $text = $this->handleInternalLinks( $text );
1587 $text = $this->handleAllQuotes( $text );
1588 $text = $this->handleExternalLinks( $text );
1589
1590 # handleInternalLinks may sometimes leave behind
1591 # absolute URLs, which have to be masked to hide them from handleExternalLinks
1592 $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1593
1594 $text = $this->handleMagicLinks( $text );
1595 $text = $this->finalizeHeadings( $text, $origText, $isMain );
1596
1597 return $text;
1598 }
1599
1606 return $this->languageConverterFactory->getLanguageConverter(
1607 $this->getTargetLanguage()
1608 );
1609 }
1610
1617 return $this->languageConverterFactory->getLanguageConverter(
1618 $this->getContentLanguage()
1619 );
1620 }
1621
1629 protected function getHookContainer() {
1630 return $this->hookContainer;
1631 }
1632
1641 protected function getHookRunner() {
1642 return $this->hookRunner;
1643 }
1644
1654 private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1655 $text = $this->mStripState->unstripGeneral( $text );
1656
1657 $text = BlockLevelPass::doBlockLevels( $text, $linestart );
1658
1659 $this->replaceLinkHoldersPrivate( $text );
1660
1668 if ( !( $this->mOptions->getDisableContentConversion()
1669 || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1670 && !$this->mOptions->getInterfaceMessage()
1671 ) {
1672 # The position of the convert() call should not be changed. it
1673 # assumes that the links are all replaced and the only thing left
1674 # is the <nowiki> mark.
1675 $text = $this->getTargetLanguageConverter()->convert( $text );
1676 }
1677
1678 $text = $this->mStripState->unstripNoWiki( $text );
1679
1680 if ( $isMain ) {
1681 $this->hookRunner->onParserBeforeTidy( $this, $text );
1682 }
1683
1684 $text = $this->mStripState->unstripGeneral( $text );
1685
1686 # Clean up special characters, only run once, after doBlockLevels
1687 $text = Sanitizer::armorFrenchSpaces( $text );
1688
1689 $text = Sanitizer::normalizeCharReferences( $text );
1690
1691 $text = MWTidy::tidy( $text );
1692
1693 if ( $isMain ) {
1694 $this->hookRunner->onParserAfterTidy( $this, $text );
1695 }
1696
1697 return $text;
1698 }
1699
1710 private function handleMagicLinks( $text ) {
1712 $urlChar = self::EXT_LINK_URL_CLASS;
1713 $addr = self::EXT_LINK_ADDR;
1714 $space = self::SPACE_NOT_NL; # non-newline space
1715 $spdash = "(?:-|$space)"; # a dash or a non-newline space
1716 $spaces = "$space++"; # possessive match of 1 or more spaces
1717 $text = preg_replace_callback(
1718 '!(?: # Start cases
1719 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1720 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1721 (\b # m[3]: Free external links
1722 (?i:$prots)
1723 ($addr$urlChar*) # m[4]: Post-protocol path
1724 ) |
1725 \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1726 ([0-9]+)\b |
1727 \bISBN $spaces ( # m[6]: ISBN, capture number
1728 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1729 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1730 [0-9Xx] # check digit
1731 )\b
1732 )!xu", [ $this, 'magicLinkCallback' ], $text );
1733 return $text;
1734 }
1735
1741 private function magicLinkCallback( array $m ) {
1742 if ( isset( $m[1] ) && $m[1] !== '' ) {
1743 # Skip anchor
1744 return $m[0];
1745 } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1746 # Skip HTML element
1747 return $m[0];
1748 } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1749 # Free external link
1750 return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1751 } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1752 # RFC or PMID
1753 if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1754 if ( !$this->mOptions->getMagicRFCLinks() ) {
1755 return $m[0];
1756 }
1757 $keyword = 'RFC';
1758 $urlmsg = 'rfcurl';
1759 $cssClass = 'mw-magiclink-rfc';
1760 $trackingCat = 'magiclink-tracking-rfc';
1761 $id = $m[5];
1762 } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1763 if ( !$this->mOptions->getMagicPMIDLinks() ) {
1764 return $m[0];
1765 }
1766 $keyword = 'PMID';
1767 $urlmsg = 'pubmedurl';
1768 $cssClass = 'mw-magiclink-pmid';
1769 $trackingCat = 'magiclink-tracking-pmid';
1770 $id = $m[5];
1771 } else {
1772 throw new MWException( __METHOD__ . ': unrecognised match type "' .
1773 substr( $m[0], 0, 20 ) . '"' );
1774 }
1775 $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1776 $this->addTrackingCategory( $trackingCat );
1778 $url,
1779 "{$keyword} {$id}",
1780 true,
1781 $cssClass,
1782 [],
1783 $this->getTitle()
1784 );
1785 } elseif ( isset( $m[6] ) && $m[6] !== ''
1786 && $this->mOptions->getMagicISBNLinks()
1787 ) {
1788 # ISBN
1789 $isbn = $m[6];
1790 $space = self::SPACE_NOT_NL; # non-newline space
1791 $isbn = preg_replace( "/$space/", ' ', $isbn );
1792 $num = strtr( $isbn, [
1793 '-' => '',
1794 ' ' => '',
1795 'x' => 'X',
1796 ] );
1797 $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1798 return $this->getLinkRenderer()->makeKnownLink(
1799 SpecialPage::getTitleFor( 'Booksources', $num ),
1800 "ISBN $isbn",
1801 [
1802 'class' => 'internal mw-magiclink-isbn',
1803 'title' => false // suppress title attribute
1804 ]
1805 );
1806 } else {
1807 return $m[0];
1808 }
1809 }
1810
1820 private function makeFreeExternalLink( $url, $numPostProto ) {
1821 $trail = '';
1822
1823 # The characters '<' and '>' (which were escaped by
1824 # removeHTMLtags()) should not be included in
1825 # URLs, per RFC 2396.
1826 # Make &nbsp; terminate a URL as well (bug T84937)
1827 $m2 = [];
1828 if ( preg_match(
1829 '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1830 $url,
1831 $m2,
1832 PREG_OFFSET_CAPTURE
1833 ) ) {
1834 $trail = substr( $url, $m2[0][1] ) . $trail;
1835 $url = substr( $url, 0, $m2[0][1] );
1836 }
1837
1838 # Move trailing punctuation to $trail
1839 $sep = ',;\.:!?';
1840 # If there is no left bracket, then consider right brackets fair game too
1841 if ( strpos( $url, '(' ) === false ) {
1842 $sep .= ')';
1843 }
1844
1845 $urlRev = strrev( $url );
1846 $numSepChars = strspn( $urlRev, $sep );
1847 # Don't break a trailing HTML entity by moving the ; into $trail
1848 # This is in hot code, so use substr_compare to avoid having to
1849 # create a new string object for the comparison
1850 if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1851 # more optimization: instead of running preg_match with a $
1852 # anchor, which can be slow, do the match on the reversed
1853 # string starting at the desired offset.
1854 # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1855 if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1856 $numSepChars--;
1857 }
1858 }
1859 if ( $numSepChars ) {
1860 $trail = substr( $url, -$numSepChars ) . $trail;
1861 $url = substr( $url, 0, -$numSepChars );
1862 }
1863
1864 # Verify that we still have a real URL after trail removal, and
1865 # not just lone protocol
1866 if ( strlen( $trail ) >= $numPostProto ) {
1867 return $url . $trail;
1868 }
1869
1870 $url = Sanitizer::cleanUrl( $url );
1871
1872 # Is this an external image?
1873 $text = $this->maybeMakeExternalImage( $url );
1874 if ( $text === false ) {
1875 # Not an image, make a link
1876 $text = Linker::makeExternalLink( $url,
1877 $this->getTargetLanguageConverter()->markNoConversion( $url ),
1878 true, 'free',
1879 $this->getExternalLinkAttribs( $url ), $this->getTitle() );
1880 # Register it in the output object...
1881 $this->mOutput->addExternalLink( $url );
1882 }
1883 return $text . $trail;
1884 }
1885
1892 private function handleHeadings( $text ) {
1893 for ( $i = 6; $i >= 1; --$i ) {
1894 $h = str_repeat( '=', $i );
1895 // Trim non-newline whitespace from headings
1896 // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1897 $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1898 }
1899 return $text;
1900 }
1901
1909 private function handleAllQuotes( $text ) {
1910 $outtext = '';
1911 $lines = StringUtils::explode( "\n", $text );
1912 foreach ( $lines as $line ) {
1913 $outtext .= $this->doQuotes( $line ) . "\n";
1914 }
1915 $outtext = substr( $outtext, 0, -1 );
1916 return $outtext;
1917 }
1918
1927 public function doQuotes( $text ) {
1928 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1929 $countarr = count( $arr );
1930 if ( $countarr == 1 ) {
1931 return $text;
1932 }
1933
1934 // First, do some preliminary work. This may shift some apostrophes from
1935 // being mark-up to being text. It also counts the number of occurrences
1936 // of bold and italics mark-ups.
1937 $numbold = 0;
1938 $numitalics = 0;
1939 for ( $i = 1; $i < $countarr; $i += 2 ) {
1940 $thislen = strlen( $arr[$i] );
1941 // If there are ever four apostrophes, assume the first is supposed to
1942 // be text, and the remaining three constitute mark-up for bold text.
1943 // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1944 if ( $thislen == 4 ) {
1945 $arr[$i - 1] .= "'";
1946 $arr[$i] = "'''";
1947 $thislen = 3;
1948 } elseif ( $thislen > 5 ) {
1949 // If there are more than 5 apostrophes in a row, assume they're all
1950 // text except for the last 5.
1951 // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1952 $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1953 $arr[$i] = "'''''";
1954 $thislen = 5;
1955 }
1956 // Count the number of occurrences of bold and italics mark-ups.
1957 if ( $thislen == 2 ) {
1958 $numitalics++;
1959 } elseif ( $thislen == 3 ) {
1960 $numbold++;
1961 } elseif ( $thislen == 5 ) {
1962 $numitalics++;
1963 $numbold++;
1964 }
1965 }
1966
1967 // If there is an odd number of both bold and italics, it is likely
1968 // that one of the bold ones was meant to be an apostrophe followed
1969 // by italics. Which one we cannot know for certain, but it is more
1970 // likely to be one that has a single-letter word before it.
1971 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1972 $firstsingleletterword = -1;
1973 $firstmultiletterword = -1;
1974 $firstspace = -1;
1975 for ( $i = 1; $i < $countarr; $i += 2 ) {
1976 if ( strlen( $arr[$i] ) == 3 ) {
1977 $x1 = substr( $arr[$i - 1], -1 );
1978 $x2 = substr( $arr[$i - 1], -2, 1 );
1979 if ( $x1 === ' ' ) {
1980 if ( $firstspace == -1 ) {
1981 $firstspace = $i;
1982 }
1983 } elseif ( $x2 === ' ' ) {
1984 $firstsingleletterword = $i;
1985 // if $firstsingleletterword is set, we don't
1986 // look at the other options, so we can bail early.
1987 break;
1988 } elseif ( $firstmultiletterword == -1 ) {
1989 $firstmultiletterword = $i;
1990 }
1991 }
1992 }
1993
1994 // If there is a single-letter word, use it!
1995 if ( $firstsingleletterword > -1 ) {
1996 $arr[$firstsingleletterword] = "''";
1997 $arr[$firstsingleletterword - 1] .= "'";
1998 } elseif ( $firstmultiletterword > -1 ) {
1999 // If not, but there's a multi-letter word, use that one.
2000 $arr[$firstmultiletterword] = "''";
2001 $arr[$firstmultiletterword - 1] .= "'";
2002 } elseif ( $firstspace > -1 ) {
2003 // ... otherwise use the first one that has neither.
2004 // (notice that it is possible for all three to be -1 if, for example,
2005 // there is only one pentuple-apostrophe in the line)
2006 $arr[$firstspace] = "''";
2007 $arr[$firstspace - 1] .= "'";
2008 }
2009 }
2010
2011 // Now let's actually convert our apostrophic mush to HTML!
2012 $output = '';
2013 $buffer = '';
2014 $state = '';
2015 $i = 0;
2016 foreach ( $arr as $r ) {
2017 if ( ( $i % 2 ) == 0 ) {
2018 if ( $state === 'both' ) {
2019 $buffer .= $r;
2020 } else {
2021 $output .= $r;
2022 }
2023 } else {
2024 $thislen = strlen( $r );
2025 if ( $thislen == 2 ) {
2026 if ( $state === 'i' ) {
2027 $output .= '</i>';
2028 $state = '';
2029 } elseif ( $state === 'bi' ) {
2030 $output .= '</i>';
2031 $state = 'b';
2032 } elseif ( $state === 'ib' ) {
2033 $output .= '</b></i><b>';
2034 $state = 'b';
2035 } elseif ( $state === 'both' ) {
2036 $output .= '<b><i>' . $buffer . '</i>';
2037 $state = 'b';
2038 } else { // $state can be 'b' or ''
2039 $output .= '<i>';
2040 $state .= 'i';
2041 }
2042 } elseif ( $thislen == 3 ) {
2043 if ( $state === 'b' ) {
2044 $output .= '</b>';
2045 $state = '';
2046 } elseif ( $state === 'bi' ) {
2047 $output .= '</i></b><i>';
2048 $state = 'i';
2049 } elseif ( $state === 'ib' ) {
2050 $output .= '</b>';
2051 $state = 'i';
2052 } elseif ( $state === 'both' ) {
2053 $output .= '<i><b>' . $buffer . '</b>';
2054 $state = 'i';
2055 } else { // $state can be 'i' or ''
2056 $output .= '<b>';
2057 $state .= 'b';
2058 }
2059 } elseif ( $thislen == 5 ) {
2060 if ( $state === 'b' ) {
2061 $output .= '</b><i>';
2062 $state = 'i';
2063 } elseif ( $state === 'i' ) {
2064 $output .= '</i><b>';
2065 $state = 'b';
2066 } elseif ( $state === 'bi' ) {
2067 $output .= '</i></b>';
2068 $state = '';
2069 } elseif ( $state === 'ib' ) {
2070 $output .= '</b></i>';
2071 $state = '';
2072 } elseif ( $state === 'both' ) {
2073 $output .= '<i><b>' . $buffer . '</b></i>';
2074 $state = '';
2075 } else { // ($state == '')
2076 $buffer = '';
2077 $state = 'both';
2078 }
2079 }
2080 }
2081 $i++;
2082 }
2083 // Now close all remaining tags. Notice that the order is important.
2084 if ( $state === 'b' || $state === 'ib' ) {
2085 $output .= '</b>';
2086 }
2087 if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
2088 $output .= '</i>';
2089 }
2090 if ( $state === 'bi' ) {
2091 $output .= '</b>';
2092 }
2093 // There might be lonely ''''', so make sure we have a buffer
2094 if ( $state === 'both' && $buffer ) {
2095 $output .= '<b><i>' . $buffer . '</i></b>';
2096 }
2097 return $output;
2098 }
2099
2110 private function handleExternalLinks( $text ) {
2111 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
2112 // @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3161
2113 if ( $bits === false ) {
2114 throw new MWException( "PCRE needs to be compiled with "
2115 . "--enable-unicode-properties in order for MediaWiki to function" );
2116 }
2117 $s = array_shift( $bits );
2118
2119 $i = 0;
2120 while ( $i < count( $bits ) ) {
2121 $url = $bits[$i++];
2122 $i++; // protocol
2123 $text = $bits[$i++];
2124 $trail = $bits[$i++];
2125
2126 # The characters '<' and '>' (which were escaped by
2127 # removeHTMLtags()) should not be included in
2128 # URLs, per RFC 2396.
2129 $m2 = [];
2130 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
2131 $text = substr( $url, $m2[0][1] ) . ' ' . $text;
2132 $url = substr( $url, 0, $m2[0][1] );
2133 }
2134
2135 # If the link text is an image URL, replace it with an <img> tag
2136 # This happened by accident in the original parser, but some people used it extensively
2137 $img = $this->maybeMakeExternalImage( $text );
2138 if ( $img !== false ) {
2139 $text = $img;
2140 }
2141
2142 $dtrail = '';
2143
2144 # Set linktype for CSS
2145 $linktype = 'text';
2146
2147 # No link text, e.g. [http://domain.tld/some.link]
2148 if ( $text == '' ) {
2149 # Autonumber
2150 $langObj = $this->getTargetLanguage();
2151 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
2152 $linktype = 'autonumber';
2153 } else {
2154 # Have link text, e.g. [http://domain.tld/some.link text]s
2155 # Check for trail
2156 list( $dtrail, $trail ) = Linker::splitTrail( $trail );
2157 }
2158
2159 // Excluding protocol-relative URLs may avoid many false positives.
2160 if ( preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
2161 $text = $this->getTargetLanguageConverter()->markNoConversion( $text );
2162 }
2163
2164 $url = Sanitizer::cleanUrl( $url );
2165
2166 # Use the encoded URL
2167 # This means that users can paste URLs directly into the text
2168 # Funny characters like ö aren't valid in URLs anyway
2169 # This was changed in August 2004
2170 $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
2171 $this->getExternalLinkAttribs( $url ), $this->getTitle() ) . $dtrail . $trail;
2172
2173 # Register link in the output object.
2174 $this->mOutput->addExternalLink( $url );
2175 }
2176
2177 return $s;
2178 }
2179
2190 public static function getExternalLinkRel( $url = false, LinkTarget $title = null ) {
2192 $ns = $title ? $title->getNamespace() : false;
2193 if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
2195 ) {
2196 return 'nofollow';
2197 }
2198 return null;
2199 }
2200
2212 public function getExternalLinkAttribs( $url ) {
2213 $attribs = [];
2214 $rel = self::getExternalLinkRel( $url, $this->getTitle() );
2215
2216 $target = $this->mOptions->getExternalLinkTarget();
2217 if ( $target ) {
2218 $attribs['target'] = $target;
2219 if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
2220 // T133507. New windows can navigate parent cross-origin.
2221 // Including noreferrer due to lacking browser
2222 // support of noopener. Eventually noreferrer should be removed.
2223 if ( $rel !== '' ) {
2224 $rel .= ' ';
2225 }
2226 $rel .= 'noreferrer noopener';
2227 }
2228 }
2229 $attribs['rel'] = $rel;
2230 return $attribs;
2231 }
2232
2243 public static function normalizeLinkUrl( $url ) {
2244 # Test for RFC 3986 IPv6 syntax
2245 $scheme = '[a-z][a-z0-9+.-]*:';
2246 $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2247 $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2248 if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2249 IPUtils::isValid( rawurldecode( $m[1] ) )
2250 ) {
2251 $isIPv6 = rawurldecode( $m[1] );
2252 } else {
2253 $isIPv6 = false;
2254 }
2255
2256 # Make sure unsafe characters are encoded
2257 $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
2258 function ( $m ) {
2259 return rawurlencode( $m[0] );
2260 },
2261 $url
2262 );
2263
2264 $ret = '';
2265 $end = strlen( $url );
2266
2267 # Fragment part - 'fragment'
2268 $start = strpos( $url, '#' );
2269 if ( $start !== false && $start < $end ) {
2270 $ret = self::normalizeUrlComponent(
2271 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2272 $end = $start;
2273 }
2274
2275 # Query part - 'query' minus &=+;
2276 $start = strpos( $url, '?' );
2277 if ( $start !== false && $start < $end ) {
2278 $ret = self::normalizeUrlComponent(
2279 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2280 $end = $start;
2281 }
2282
2283 # Scheme and path part - 'pchar'
2284 # (we assume no userinfo or encoded colons in the host)
2285 $ret = self::normalizeUrlComponent(
2286 substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2287
2288 # Fix IPv6 syntax
2289 if ( $isIPv6 !== false ) {
2290 $ipv6Host = "%5B({$isIPv6})%5D";
2291 $ret = preg_replace(
2292 "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2293 "$1[$2]",
2294 $ret
2295 );
2296 }
2297
2298 return $ret;
2299 }
2300
2301 private static function normalizeUrlComponent( $component, $unsafe ) {
2302 $callback = function ( $matches ) use ( $unsafe ) {
2303 $char = urldecode( $matches[0] );
2304 $ord = ord( $char );
2305 if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2306 # Unescape it
2307 return $char;
2308 } else {
2309 # Leave it escaped, but use uppercase for a-f
2310 return strtoupper( $matches[0] );
2311 }
2312 };
2313 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2314 }
2315
2324 private function maybeMakeExternalImage( $url ) {
2325 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2326 $imagesexception = !empty( $imagesfrom );
2327 $text = false;
2328 # $imagesfrom could be either a single string or an array of strings, parse out the latter
2329 if ( $imagesexception && is_array( $imagesfrom ) ) {
2330 $imagematch = false;
2331 foreach ( $imagesfrom as $match ) {
2332 if ( strpos( $url, $match ) === 0 ) {
2333 $imagematch = true;
2334 break;
2335 }
2336 }
2337 } elseif ( $imagesexception ) {
2338 $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2339 } else {
2340 $imagematch = false;
2341 }
2342
2343 if ( $this->mOptions->getAllowExternalImages()
2344 || ( $imagesexception && $imagematch )
2345 ) {
2346 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2347 # Image found
2348 $text = Linker::makeExternalImage( $url );
2349 }
2350 }
2351 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2352 && preg_match( self::EXT_IMAGE_REGEX, $url )
2353 ) {
2354 $whitelist = explode(
2355 "\n",
2356 wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2357 );
2358
2359 foreach ( $whitelist as $entry ) {
2360 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2361 if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2362 continue;
2363 }
2364 if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2365 # Image matches a whitelist entry
2366 $text = Linker::makeExternalImage( $url );
2367 break;
2368 }
2369 }
2370 }
2371 return $text;
2372 }
2373
2381 private function handleInternalLinks( $text ) {
2382 $this->mLinkHolders->merge( $this->handleInternalLinks2( $text ) );
2383 return $text;
2384 }
2385
2391 private function handleInternalLinks2( &$s ) {
2392 static $tc = false, $e1, $e1_img;
2393 # the % is needed to support urlencoded titles as well
2394 if ( !$tc ) {
2395 $tc = Title::legalChars() . '#%';
2396 # Match a link having the form [[namespace:link|alternate]]trail
2397 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2398 # Match cases where there is no "]]", which might still be images
2399 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2400 }
2401
2402 $holders = new LinkHolderArray(
2403 $this,
2404 $this->getContentLanguageConverter(),
2405 $this->getHookContainer() );
2406
2407 # split the entire text string on occurrences of [[
2408 $a = StringUtils::explode( '[[', ' ' . $s );
2409 # get the first element (all text up to first [[), and remove the space we added
2410 $s = $a->current();
2411 $a->next();
2412 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2413 $s = substr( $s, 1 );
2414
2415 $nottalk = !$this->getTitle()->isTalkPage();
2416
2417 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2418 $e2 = null;
2419 if ( $useLinkPrefixExtension ) {
2420 # Match the end of a line for a word that's not followed by whitespace,
2421 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2422 $charset = $this->contLang->linkPrefixCharset();
2423 $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2424 $m = [];
2425 if ( preg_match( $e2, $s, $m ) ) {
2426 $first_prefix = $m[2];
2427 } else {
2428 $first_prefix = false;
2429 }
2430 } else {
2431 $prefix = '';
2432 }
2433
2434 # Some namespaces don't allow subpages
2435 $useSubpages = $this->nsInfo->hasSubpages(
2436 $this->getTitle()->getNamespace()
2437 );
2438
2439 # Loop for each link
2440 for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2441 # Check for excessive memory usage
2442 if ( $holders->isBig() ) {
2443 # Too big
2444 # Do the existence check, replace the link holders and clear the array
2445 $holders->replace( $s );
2446 $holders->clear();
2447 }
2448
2449 if ( $useLinkPrefixExtension ) {
2450 if ( preg_match( $e2, $s, $m ) ) {
2451 list( , $s, $prefix ) = $m;
2452 } else {
2453 $prefix = '';
2454 }
2455 # first link
2456 if ( $first_prefix ) {
2457 $prefix = $first_prefix;
2458 $first_prefix = false;
2459 }
2460 }
2461
2462 $might_be_img = false;
2463
2464 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2465 $text = $m[2];
2466 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2467 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2468 # the real problem is with the $e1 regex
2469 # See T1500.
2470 # Still some problems for cases where the ] is meant to be outside punctuation,
2471 # and no image is in sight. See T4095.
2472 if ( $text !== ''
2473 && substr( $m[3], 0, 1 ) === ']'
2474 && strpos( $text, '[' ) !== false
2475 ) {
2476 $text .= ']'; # so that handleExternalLinks($text) works later
2477 $m[3] = substr( $m[3], 1 );
2478 }
2479 # fix up urlencoded title texts
2480 if ( strpos( $m[1], '%' ) !== false ) {
2481 # Should anchors '#' also be rejected?
2482 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2483 }
2484 $trail = $m[3];
2485 } elseif ( preg_match( $e1_img, $line, $m ) ) {
2486 # Invalid, but might be an image with a link in its caption
2487 $might_be_img = true;
2488 $text = $m[2];
2489 if ( strpos( $m[1], '%' ) !== false ) {
2490 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2491 }
2492 $trail = "";
2493 } else { # Invalid form; output directly
2494 $s .= $prefix . '[[' . $line;
2495 continue;
2496 }
2497
2498 $origLink = ltrim( $m[1], ' ' );
2499
2500 # Don't allow internal links to pages containing
2501 # PROTO: where PROTO is a valid URL protocol; these
2502 # should be external links.
2503 if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2504 $s .= $prefix . '[[' . $line;
2505 continue;
2506 }
2507
2508 # Make subpage if necessary
2509 if ( $useSubpages ) {
2511 $this->getTitle(), $origLink, $text
2512 );
2513 } else {
2514 $link = $origLink;
2515 }
2516
2517 // \x7f isn't a default legal title char, so most likely strip
2518 // markers will force us into the "invalid form" path above. But,
2519 // just in case, let's assert that xmlish tags aren't valid in
2520 // the title position.
2521 $unstrip = $this->mStripState->killMarkers( $link );
2522 $noMarkers = ( $unstrip === $link );
2523
2524 $nt = $noMarkers ? Title::newFromText( $link ) : null;
2525 if ( $nt === null ) {
2526 $s .= $prefix . '[[' . $line;
2527 continue;
2528 }
2529
2530 $ns = $nt->getNamespace();
2531 $iw = $nt->getInterwiki();
2532
2533 $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2534
2535 if ( $might_be_img ) { # if this is actually an invalid link
2536 if ( $ns == NS_FILE && $noforce ) { # but might be an image
2537 $found = false;
2538 while ( true ) {
2539 # look at the next 'line' to see if we can close it there
2540 $a->next();
2541 $next_line = $a->current();
2542 if ( $next_line === false || $next_line === null ) {
2543 break;
2544 }
2545 $m = explode( ']]', $next_line, 3 );
2546 if ( count( $m ) == 3 ) {
2547 # the first ]] closes the inner link, the second the image
2548 $found = true;
2549 $text .= "[[{$m[0]}]]{$m[1]}";
2550 $trail = $m[2];
2551 break;
2552 } elseif ( count( $m ) == 2 ) {
2553 # if there's exactly one ]] that's fine, we'll keep looking
2554 $text .= "[[{$m[0]}]]{$m[1]}";
2555 } else {
2556 # if $next_line is invalid too, we need look no further
2557 $text .= '[[' . $next_line;
2558 break;
2559 }
2560 }
2561 if ( !$found ) {
2562 # we couldn't find the end of this imageLink, so output it raw
2563 # but don't ignore what might be perfectly normal links in the text we've examined
2564 $holders->merge( $this->handleInternalLinks2( $text ) );
2565 $s .= "{$prefix}[[$link|$text";
2566 # note: no $trail, because without an end, there *is* no trail
2567 continue;
2568 }
2569 } else { # it's not an image, so output it raw
2570 $s .= "{$prefix}[[$link|$text";
2571 # note: no $trail, because without an end, there *is* no trail
2572 continue;
2573 }
2574 }
2575
2576 $wasblank = ( $text == '' );
2577 if ( $wasblank ) {
2578 $text = $link;
2579 if ( !$noforce ) {
2580 # Strip off leading ':'
2581 $text = substr( $text, 1 );
2582 }
2583 } else {
2584 # T6598 madness. Handle the quotes only if they come from the alternate part
2585 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2586 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2587 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2588 $text = $this->doQuotes( $text );
2589 }
2590
2591 # Link not escaped by : , create the various objects
2592 if ( $noforce && !$nt->wasLocalInterwiki() ) {
2593 # Interwikis
2594 if (
2595 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2596 MediaWikiServices::getInstance()->getLanguageNameUtils()
2597 ->getLanguageName(
2598 $iw,
2599 LanguageNameUtils::AUTONYMS,
2600 LanguageNameUtils::DEFINED
2601 )
2602 || in_array( $iw, $this->svcOptions->get( 'ExtraInterlanguageLinkPrefixes' ) )
2603 )
2604 ) {
2605 # T26502: filter duplicates
2606 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2607 $this->mLangLinkLanguages[$iw] = true;
2608 $this->mOutput->addLanguageLink( $nt->getFullText() );
2609 }
2610
2614 $s = rtrim( $s . $prefix ) . $trail; # T175416
2615 continue;
2616 }
2617
2618 if ( $ns == NS_FILE ) {
2619 if ( $wasblank ) {
2620 # if no parameters were passed, $text
2621 # becomes something like "File:Foo.png",
2622 # which we don't want to pass on to the
2623 # image generator
2624 $text = '';
2625 } else {
2626 # recursively parse links inside the image caption
2627 # actually, this will parse them in any other parameters, too,
2628 # but it might be hard to fix that, and it doesn't matter ATM
2629 $text = $this->handleExternalLinks( $text );
2630 $holders->merge( $this->handleInternalLinks2( $text ) );
2631 }
2632 # cloak any absolute URLs inside the image markup, so handleExternalLinks() won't touch them
2633 $s .= $prefix . $this->armorLinks(
2634 $this->makeImage( $nt, $text, $holders ) ) . $trail;
2635 continue;
2636 } elseif ( $ns == NS_CATEGORY ) {
2640 $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2641
2642 if ( $wasblank ) {
2643 $sortkey = $this->getDefaultSort();
2644 } else {
2645 $sortkey = $text;
2646 }
2647 $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2648 $sortkey = str_replace( "\n", '', $sortkey );
2649 $sortkey = $this->getTargetLanguageConverter()->convertCategoryKey( $sortkey );
2650 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2651
2652 continue;
2653 }
2654 }
2655
2656 # Self-link checking. For some languages, variants of the title are checked in
2657 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2658 # for linking to a different variant.
2659 if ( $ns != NS_SPECIAL && $nt->equals( $this->getTitle() ) && !$nt->hasFragment() ) {
2660 $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2661 continue;
2662 }
2663
2664 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2665 # @todo FIXME: Should do batch file existence checks, see comment below
2666 if ( $ns == NS_MEDIA ) {
2667 # Give extensions a chance to select the file revision for us
2668 $options = [];
2669 $descQuery = false;
2670 $this->hookRunner->onBeforeParserFetchFileAndTitle(
2671 $this, $nt, $options, $descQuery );
2672 # Fetch and register the file (file title may be different via hooks)
2673 list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2674 # Cloak with NOPARSE to avoid replacement in handleExternalLinks
2675 $s .= $prefix . $this->armorLinks(
2676 Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2677 continue;
2678 }
2679
2680 # Some titles, such as valid special pages or files in foreign repos, should
2681 # be shown as bluelinks even though they're not included in the page table
2682 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2683 # batch file existence checks for NS_FILE and NS_MEDIA
2684 if ( $iw == '' && $nt->isAlwaysKnown() ) {
2685 $this->mOutput->addLink( $nt );
2686 $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2687 } else {
2688 # Links will be added to the output link list after checking
2689 $s .= $holders->makeHolder( $nt, $text, $trail, $prefix );
2690 }
2691 }
2692 return $holders;
2693 }
2694
2708 private function makeKnownLinkHolder( Title $nt, $text = '', $trail = '', $prefix = '' ) {
2709 list( $inside, $trail ) = Linker::splitTrail( $trail );
2710
2711 if ( $text == '' ) {
2712 $text = htmlspecialchars( $nt->getPrefixedText() );
2713 }
2714
2715 $link = $this->getLinkRenderer()->makeKnownLink(
2716 $nt, new HtmlArmor( "$prefix$text$inside" )
2717 );
2718
2719 return $this->armorLinks( $link ) . $trail;
2720 }
2721
2732 private function armorLinks( $text ) {
2733 return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2734 self::MARKER_PREFIX . "NOPARSE$1", $text );
2735 }
2736
2746 public function doBlockLevels( $text, $linestart ) {
2747 wfDeprecated( __METHOD__, '1.35' );
2748 return BlockLevelPass::doBlockLevels( $text, $linestart );
2749 }
2750
2759 private function expandMagicVariable( $index, $frame = false ) {
2764 if (
2765 $this->hookRunner->onParserGetVariableValueVarCache( $this, $this->mVarCache ) &&
2766 isset( $this->mVarCache[$index] )
2767 ) {
2768 return $this->mVarCache[$index];
2769 }
2770
2771 $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2772 $this->hookRunner->onParserGetVariableValueTs( $this, $ts );
2773
2775 $this, $index, $ts, $this->nsInfo, $this->svcOptions, $this->logger
2776 );
2777
2778 if ( $value === null ) {
2779 // Not a defined core magic word
2780 $ret = null;
2781 $originalIndex = $index;
2782 $this->hookRunner->onParserGetVariableValueSwitch( $this,
2783 $this->mVarCache, $index, $ret, $frame );
2784 if ( $index !== $originalIndex ) {
2786 'A ParserGetVariableValueSwitch hook handler modified $index, ' .
2787 'this is deprecated since MediaWiki 1.35',
2788 '1.35', false, false
2789 );
2790 }
2791 if ( !isset( $this->mVarCache[$originalIndex] ) ||
2792 $this->mVarCache[$originalIndex] !== $ret ) {
2794 'A ParserGetVariableValueSwitch hook handler bypassed the cache, ' .
2795 'this is deprecated since MediaWiki 1.35', '1.35', false, false
2796 );
2797 }// FIXME: in the future, don't give this hook unrestricted
2798 // access to mVarCache; we can cache it ourselves by falling
2799 // through here.
2800 return $ret;
2801 }
2802
2803 $this->mVarCache[$index] = $value;
2804
2805 return $value;
2806 }
2807
2812 private function initializeVariables() {
2813 $variableIDs = $this->magicWordFactory->getVariableIDs();
2814 $substIDs = $this->magicWordFactory->getSubstIDs();
2815
2816 $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
2817 $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
2818 }
2819
2842 public function preprocessToDom( $text, $flags = 0 ) {
2843 $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2844 return $dom;
2845 }
2846
2867 public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2868 # Is there any text? Also, Prevent too big inclusions!
2869 $textSize = strlen( $text );
2870 if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2871 return $text;
2872 }
2873
2874 if ( $frame === false ) {
2875 $frame = $this->getPreprocessor()->newFrame();
2876 } elseif ( !( $frame instanceof PPFrame ) ) {
2877 $this->logger->debug(
2878 __METHOD__ . " called using plain parameters instead of " .
2879 "a PPFrame instance. Creating custom frame."
2880 );
2881 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
2882 }
2883
2884 $dom = $this->preprocessToDom( $text );
2885 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2886 $text = $frame->expand( $dom, $flags );
2887
2888 return $text;
2889 }
2890
2918 public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2919 # does no harm if $current and $max are present but are unnecessary for the message
2920 # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2921 # only during preview, and that would split the parser cache unnecessarily.
2922 $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
2923 ->text();
2924 $this->mOutput->addWarning( $warning );
2925 $this->addTrackingCategory( "$limitationType-category" );
2926 }
2927
2941 public function braceSubstitution( array $piece, PPFrame $frame ) {
2942 // Flags
2943
2944 // $text has been filled
2945 $found = false;
2946 // wiki markup in $text should be escaped
2947 $nowiki = false;
2948 // $text is HTML, armour it against wikitext transformation
2949 $isHTML = false;
2950 // Force interwiki transclusion to be done in raw mode not rendered
2951 $forceRawInterwiki = false;
2952 // $text is a DOM node needing expansion in a child frame
2953 $isChildObj = false;
2954 // $text is a DOM node needing expansion in the current frame
2955 $isLocalObj = false;
2956
2957 # Title object, where $text came from
2958 $title = false;
2959
2960 # $part1 is the bit before the first |, and must contain only title characters.
2961 # Various prefixes will be stripped from it later.
2962 $titleWithSpaces = $frame->expand( $piece['title'] );
2963 $part1 = trim( $titleWithSpaces );
2964 $titleText = false;
2965
2966 # Original title text preserved for various purposes
2967 $originalTitle = $part1;
2968
2969 # $args is a list of argument nodes, starting from index 0, not including $part1
2970 # @todo FIXME: If piece['parts'] is null then the call to getLength()
2971 # below won't work b/c this $args isn't an object
2972 $args = ( $piece['parts'] == null ) ? [] : $piece['parts'];
2973
2974 $profileSection = null; // profile templates
2975
2976 # SUBST
2977 // @phan-suppress-next-line PhanImpossibleCondition
2978 if ( !$found ) {
2979 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
2980
2981 # Possibilities for substMatch: "subst", "safesubst" or FALSE
2982 # Decide whether to expand template or keep wikitext as-is.
2983 if ( $this->ot['wiki'] ) {
2984 if ( $substMatch === false ) {
2985 $literal = true; # literal when in PST with no prefix
2986 } else {
2987 $literal = false; # expand when in PST with subst: or safesubst:
2988 }
2989 } else {
2990 if ( $substMatch == 'subst' ) {
2991 $literal = true; # literal when not in PST with plain subst:
2992 } else {
2993 $literal = false; # expand when not in PST with safesubst: or no prefix
2994 }
2995 }
2996 if ( $literal ) {
2997 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
2998 $isLocalObj = true;
2999 $found = true;
3000 }
3001 }
3002
3003 # Variables
3004 if ( !$found && $args->getLength() == 0 ) {
3005 $id = $this->mVariables->matchStartToEnd( $part1 );
3006 if ( $id !== false ) {
3007 $text = $this->expandMagicVariable( $id, $frame );
3008 if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
3009 $this->mOutput->updateCacheExpiry(
3010 $this->magicWordFactory->getCacheTTL( $id ) );
3011 }
3012 $found = true;
3013 }
3014 }
3015
3016 # MSG, MSGNW and RAW
3017 if ( !$found ) {
3018 # Check for MSGNW:
3019 $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
3020 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3021 $nowiki = true;
3022 } else {
3023 # Remove obsolete MSG:
3024 $mwMsg = $this->magicWordFactory->get( 'msg' );
3025 $mwMsg->matchStartAndRemove( $part1 );
3026 }
3027
3028 # Check for RAW:
3029 $mwRaw = $this->magicWordFactory->get( 'raw' );
3030 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3031 $forceRawInterwiki = true;
3032 }
3033 }
3034
3035 # Parser functions
3036 if ( !$found ) {
3037 $colonPos = strpos( $part1, ':' );
3038 if ( $colonPos !== false ) {
3039 $func = substr( $part1, 0, $colonPos );
3040 $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3041 $argsLength = $args->getLength();
3042 for ( $i = 0; $i < $argsLength; $i++ ) {
3043 $funcArgs[] = $args->item( $i );
3044 }
3045
3046 $result = $this->callParserFunction( $frame, $func, $funcArgs );
3047
3048 // Extract any forwarded flags
3049 if ( isset( $result['title'] ) ) {
3050 $title = $result['title'];
3051 }
3052 if ( isset( $result['found'] ) ) {
3053 $found = $result['found'];
3054 }
3055 if ( array_key_exists( 'text', $result ) ) {
3056 // a string or null
3057 $text = $result['text'];
3058 }
3059 if ( isset( $result['nowiki'] ) ) {
3060 $nowiki = $result['nowiki'];
3061 }
3062 if ( isset( $result['isHTML'] ) ) {
3063 $isHTML = $result['isHTML'];
3064 }
3065 if ( isset( $result['forceRawInterwiki'] ) ) {
3066 $forceRawInterwiki = $result['forceRawInterwiki'];
3067 }
3068 if ( isset( $result['isChildObj'] ) ) {
3069 $isChildObj = $result['isChildObj'];
3070 }
3071 if ( isset( $result['isLocalObj'] ) ) {
3072 $isLocalObj = $result['isLocalObj'];
3073 }
3074 }
3075 }
3076
3077 # Finish mangling title and then check for loops.
3078 # Set $title to a Title object and $titleText to the PDBK
3079 if ( !$found ) {
3080 $ns = NS_TEMPLATE;
3081 # Split the title into page and subpage
3082 $subpage = '';
3083 $relative = Linker::normalizeSubpageLink(
3084 $this->getTitle(), $part1, $subpage
3085 );
3086 if ( $part1 !== $relative ) {
3087 $part1 = $relative;
3088 $ns = $this->getTitle()->getNamespace();
3089 }
3090 $title = Title::newFromText( $part1, $ns );
3091 if ( $title ) {
3092 $titleText = $title->getPrefixedText();
3093 # Check for language variants if the template is not found
3094 if ( $this->getTargetLanguageConverter()->hasVariants() && $title->getArticleID() == 0 ) {
3095 $this->getTargetLanguageConverter()->findVariantLink( $part1, $title, true );
3096 }
3097 # Do recursion depth check
3098 $limit = $this->mOptions->getMaxTemplateDepth();
3099 if ( $frame->depth >= $limit ) {
3100 $found = true;
3101 $text = '<span class="error">'
3102 . wfMessage( 'parser-template-recursion-depth-warning' )
3103 ->numParams( $limit )->inContentLanguage()->text()
3104 . '</span>';
3105 }
3106 }
3107 }
3108
3109 # Load from database
3110 if ( !$found && $title ) {
3111 $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3112 if ( !$title->isExternal() ) {
3113 if ( $title->isSpecialPage()
3114 && $this->mOptions->getAllowSpecialInclusion()
3115 && $this->ot['html']
3116 ) {
3117 $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
3118 // Pass the template arguments as URL parameters.
3119 // "uselang" will have no effect since the Language object
3120 // is forced to the one defined in ParserOptions.
3121 $pageArgs = [];
3122 $argsLength = $args->getLength();
3123 for ( $i = 0; $i < $argsLength; $i++ ) {
3124 $bits = $args->item( $i )->splitArg();
3125 if ( strval( $bits['index'] ) === '' ) {
3126 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3127 $value = trim( $frame->expand( $bits['value'] ) );
3128 $pageArgs[$name] = $value;
3129 }
3130 }
3131
3132 // Create a new context to execute the special page
3133 $context = new RequestContext;
3134 $context->setTitle( $title );
3135 $context->setRequest( new FauxRequest( $pageArgs ) );
3136 if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3137 $context->setUser( $this->getUser() );
3138 } else {
3139 // If this page is cached, then we better not be per user.
3140 $context->setUser( User::newFromName( '127.0.0.1', false ) );
3141 }
3142 $context->setLanguage( $this->mOptions->getUserLangObj() );
3143 $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
3144 if ( $ret ) {
3145 $text = $context->getOutput()->getHTML();
3146 $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3147 $found = true;
3148 $isHTML = true;
3149 if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3150 $this->mOutput->updateRuntimeAdaptiveExpiry(
3151 $specialPage->maxIncludeCacheTime()
3152 );
3153 }
3154 }
3155 } elseif ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
3156 $found = false; # access denied
3157 $this->logger->debug(
3158 __METHOD__ .
3159 ": template inclusion denied for " . $title->getPrefixedDBkey()
3160 );
3161 } else {
3162 list( $text, $title ) = $this->getTemplateDom( $title );
3163 if ( $text !== false ) {
3164 $found = true;
3165 $isChildObj = true;
3166 }
3167 }
3168
3169 # If the title is valid but undisplayable, make a link to it
3170 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3171 $text = "[[:$titleText]]";
3172 $found = true;
3173 }
3174 } elseif ( $title->isTrans() ) {
3175 # Interwiki transclusion
3176 if ( $this->ot['html'] && !$forceRawInterwiki ) {
3177 $text = $this->interwikiTransclude( $title, 'render' );
3178 $isHTML = true;
3179 } else {
3180 $text = $this->interwikiTransclude( $title, 'raw' );
3181 # Preprocess it like a template
3182 $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3183 $isChildObj = true;
3184 }
3185 $found = true;
3186 }
3187
3188 # Do infinite loop check
3189 # This has to be done after redirect resolution to avoid infinite loops via redirects
3190 if ( !$frame->loopCheck( $title ) ) {
3191 $found = true;
3192 $text = '<span class="error">'
3193 . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3194 . '</span>';
3195 $this->addTrackingCategory( 'template-loop-category' );
3196 $this->mOutput->addWarning( wfMessage( 'template-loop-warning',
3197 wfEscapeWikiText( $titleText ) )->text() );
3198 $this->logger->debug( __METHOD__ . ": template loop broken at '$titleText'" );
3199 }
3200 }
3201
3202 # If we haven't found text to substitute by now, we're done
3203 # Recover the source wikitext and return it
3204 if ( !$found ) {
3205 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3206 if ( $profileSection ) {
3207 $this->mProfiler->scopedProfileOut( $profileSection );
3208 }
3209 return [ 'object' => $text ];
3210 }
3211
3212 # Expand DOM-style return values in a child frame
3213 if ( $isChildObj ) {
3214 # Clean up argument array
3215 $newFrame = $frame->newChild( $args, $title );
3216
3217 if ( $nowiki ) {
3218 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3219 } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3220 # Expansion is eligible for the empty-frame cache
3221 $text = $newFrame->cachedExpand( $titleText, $text );
3222 } else {
3223 # Uncached expansion
3224 $text = $newFrame->expand( $text );
3225 }
3226 }
3227 if ( $isLocalObj && $nowiki ) {
3228 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3229 $isLocalObj = false;
3230 }
3231
3232 if ( $profileSection ) {
3233 $this->mProfiler->scopedProfileOut( $profileSection );
3234 }
3235
3236 # Replace raw HTML by a placeholder
3237 if ( $isHTML ) {
3238 $text = $this->insertStripItem( $text );
3239 } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3240 # Escape nowiki-style return values
3241 $text = wfEscapeWikiText( $text );
3242 } elseif ( is_string( $text )
3243 && !$piece['lineStart']
3244 && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3245 ) {
3246 # T2529: if the template begins with a table or block-level
3247 # element, it should be treated as beginning a new line.
3248 # This behavior is somewhat controversial.
3249 $text = "\n" . $text;
3250 }
3251
3252 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3253 # Error, oversize inclusion
3254 if ( $titleText !== false ) {
3255 # Make a working, properly escaped link if possible (T25588)
3256 $text = "[[:$titleText]]";
3257 } else {
3258 # This will probably not be a working link, but at least it may
3259 # provide some hint of where the problem is
3260 preg_replace( '/^:/', '', $originalTitle );
3261 $text = "[[:$originalTitle]]";
3262 }
3263 $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3264 . 'post-expand include size too large -->' );
3265 $this->limitationWarn( 'post-expand-template-inclusion' );
3266 }
3267
3268 if ( $isLocalObj ) {
3269 $ret = [ 'object' => $text ];
3270 } else {
3271 $ret = [ 'text' => $text ];
3272 }
3273
3274 return $ret;
3275 }
3276
3295 public function callParserFunction( PPFrame $frame, $function, array $args = [] ) {
3296 # Case sensitive functions
3297 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3298 $function = $this->mFunctionSynonyms[1][$function];
3299 } else {
3300 # Case insensitive functions
3301 $function = $this->contLang->lc( $function );
3302 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3303 $function = $this->mFunctionSynonyms[0][$function];
3304 } else {
3305 return [ 'found' => false ];
3306 }
3307 }
3308
3309 list( $callback, $flags ) = $this->mFunctionHooks[$function];
3310
3311 $allArgs = [ $this ];
3312 if ( $flags & self::SFH_OBJECT_ARGS ) {
3313 # Convert arguments to PPNodes and collect for appending to $allArgs
3314 $funcArgs = [];
3315 foreach ( $args as $k => $v ) {
3316 if ( $v instanceof PPNode || $k === 0 ) {
3317 $funcArgs[] = $v;
3318 } else {
3319 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3320 }
3321 }
3322
3323 # Add a frame parameter, and pass the arguments as an array
3324 $allArgs[] = $frame;
3325 $allArgs[] = $funcArgs;
3326 } else {
3327 # Convert arguments to plain text and append to $allArgs
3328 foreach ( $args as $k => $v ) {
3329 if ( $v instanceof PPNode ) {
3330 $allArgs[] = trim( $frame->expand( $v ) );
3331 } elseif ( is_int( $k ) && $k >= 0 ) {
3332 $allArgs[] = trim( $v );
3333 } else {
3334 $allArgs[] = trim( "$k=$v" );
3335 }
3336 }
3337 }
3338
3339 $result = $callback( ...$allArgs );
3340
3341 # The interface for function hooks allows them to return a wikitext
3342 # string or an array containing the string and any flags. This mungs
3343 # things around to match what this method should return.
3344 if ( !is_array( $result ) ) {
3345 $result = [
3346 'found' => true,
3347 'text' => $result,
3348 ];
3349 } else {
3350 if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3351 $result['text'] = $result[0];
3352 }
3353 unset( $result[0] );
3354 $result += [
3355 'found' => true,
3356 ];
3357 }
3358
3359 $noparse = true;
3360 $preprocessFlags = 0;
3361 if ( isset( $result['noparse'] ) ) {
3362 $noparse = $result['noparse'];
3363 }
3364 if ( isset( $result['preprocessFlags'] ) ) {
3365 $preprocessFlags = $result['preprocessFlags'];
3366 }
3367
3368 if ( !$noparse ) {
3369 $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3370 $result['isChildObj'] = true;
3371 }
3372
3373 return $result;
3374 }
3375
3384 public function getTemplateDom( Title $title ) {
3385 $cacheTitle = $title;
3386 $titleText = $title->getPrefixedDBkey();
3387
3388 if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3389 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3390 $title = Title::makeTitle( $ns, $dbk );
3391 $titleText = $title->getPrefixedDBkey();
3392 }
3393 if ( isset( $this->mTplDomCache[$titleText] ) ) {
3394 return [ $this->mTplDomCache[$titleText], $title ];
3395 }
3396
3397 # Cache miss, go to the database
3398 list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3399
3400 if ( $text === false ) {
3401 $this->mTplDomCache[$titleText] = false;
3402 return [ false, $title ];
3403 }
3404
3405 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3406 $this->mTplDomCache[$titleText] = $dom;
3407
3408 if ( !$title->equals( $cacheTitle ) ) {
3409 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3410 [ $title->getNamespace(), $title->getDBkey() ];
3411 }
3412
3413 return [ $dom, $title ];
3414 }
3415
3431 wfDeprecated( __METHOD__, '1.35' );
3432 $revisionRecord = $this->fetchCurrentRevisionRecordOfTitle( $title );
3433 if ( $revisionRecord ) {
3434 return new Revision( $revisionRecord );
3435 }
3436 return $revisionRecord;
3437 }
3438
3453 $cacheKey = $title->getPrefixedDBkey();
3454 if ( !$this->currentRevisionCache ) {
3455 $this->currentRevisionCache = new MapCacheLRU( 100 );
3456 }
3457 if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3458 $revisionRecord =
3459 // Defaults to Parser::statelessFetchRevisionRecord()
3460 call_user_func(
3461 $this->mOptions->getCurrentRevisionRecordCallback(),
3462 $title,
3463 $this
3464 );
3465 if ( !$revisionRecord ) {
3466 // Parser::statelessFetchRevisionRecord() can return false;
3467 // normalize it to null.
3468 $revisionRecord = null;
3469 }
3470 $this->currentRevisionCache->set( $cacheKey, $revisionRecord );
3471 }
3472 return $this->currentRevisionCache->get( $cacheKey );
3473 }
3474
3482 return (
3483 $this->currentRevisionCache &&
3484 $this->currentRevisionCache->has( $title->getPrefixedText() )
3485 );
3486 }
3487
3498 public static function statelessFetchRevision( Title $title, $parser = false ) {
3499 wfDeprecated( __METHOD__, '1.35' );
3500 $revRecord = MediaWikiServices::getInstance()
3501 ->getRevisionLookup()
3502 ->getKnownCurrentRevision( $title );
3503 return $revRecord ? new Revision( $revRecord ) : false;
3504 }
3505
3515 public static function statelessFetchRevisionRecord( Title $title, $parser = null ) {
3516 $revRecord = MediaWikiServices::getInstance()
3517 ->getRevisionLookup()
3518 ->getKnownCurrentRevision( $title );
3519 return $revRecord;
3520 }
3521
3528 // Defaults to Parser::statelessFetchTemplate()
3529 $templateCb = $this->mOptions->getTemplateCallback();
3530 $stuff = call_user_func( $templateCb, $title, $this );
3531 if ( isset( $stuff['revision-record'] ) ) {
3532 $revRecord = $stuff['revision-record'];
3533 } else {
3534 // Triggers deprecation warnings via DeprecatablePropertyArray
3535 $rev = $stuff['revision'] ?? null;
3536 if ( $rev instanceof Revision ) {
3537 $revRecord = $rev->getRevisionRecord();
3538 } else {
3539 $revRecord = null;
3540 }
3541 }
3542
3543 $text = $stuff['text'];
3544 if ( is_string( $stuff['text'] ) ) {
3545 // We use U+007F DELETE to distinguish strip markers from regular text
3546 $text = strtr( $text, "\x7f", "?" );
3547 }
3548 $finalTitle = $stuff['finalTitle'] ?? $title;
3549 foreach ( ( $stuff['deps'] ?? [] ) as $dep ) {
3550 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3551 if ( $dep['title']->equals( $this->getTitle() ) && $revRecord instanceof RevisionRecord ) {
3552 // Self-transclusion; final result may change based on the new page version
3553 try {
3554 $sha1 = $revRecord->getSha1();
3555 } catch ( RevisionAccessException $e ) {
3556 $sha1 = null;
3557 }
3558 $this->setOutputFlag( 'vary-revision-sha1', 'Self transclusion' );
3559 $this->getOutput()->setRevisionUsedSha1Base36( $sha1 );
3560 }
3561 }
3562
3563 return [ $text, $finalTitle ];
3564 }
3565
3572 public function fetchTemplate( Title $title ) {
3573 wfDeprecated( __METHOD__, '1.35' );
3574 return $this->fetchTemplateAndTitle( $title )[0];
3575 }
3576
3586 public static function statelessFetchTemplate( $title, $parser = false ) {
3587 $text = $skip = false;
3588 $finalTitle = $title;
3589 $deps = [];
3590 $revRecord = null;
3591
3592 # Loop to fetch the article, with up to 1 redirect
3593 $revLookup = MediaWikiServices::getInstance()->getRevisionLookup();
3594 for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3595 # Give extensions a chance to select the revision instead
3596 $id = false; # Assume current
3597 Hooks::runner()->onBeforeParserFetchTemplateAndtitle(
3598 $parser, $title, $skip, $id );
3599
3600 if ( $skip ) {
3601 $text = false;
3602 $deps[] = [
3603 'title' => $title,
3604 'page_id' => $title->getArticleID(),
3605 'rev_id' => null
3606 ];
3607 break;
3608 }
3609 # Get the revision
3610 # TODO rewrite using only RevisionRecord objects
3611 if ( $id ) {
3612 $revRecord = $revLookup->getRevisionById( $id );
3613 } elseif ( $parser ) {
3614 $revRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
3615 } else {
3616 $revRecord = $revLookup->getRevisionByTitle( $title );
3617 }
3618 $rev_id = $revRecord ? $revRecord->getId() : 0;
3619 # If there is no current revision, there is no page
3620 if ( $id === false && !$revRecord ) {
3621 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3622 $linkCache->addBadLinkObj( $title );
3623 }
3624
3625 $deps[] = [
3626 'title' => $title,
3627 'page_id' => $title->getArticleID(),
3628 'rev_id' => $rev_id
3629 ];
3630 if ( $revRecord ) {
3631 $revTitle = Title::newFromLinkTarget(
3632 $revRecord->getPageAsLinkTarget()
3633 );
3634 if ( !$title->equals( $revTitle ) ) {
3635 # We fetched a rev from a different title; register it too...
3636 $deps[] = [
3637 'title' => $revTitle,
3638 'page_id' => $revRecord->getPageId(),
3639 'rev_id' => $rev_id
3640 ];
3641 }
3642 }
3643
3644 if ( $revRecord ) {
3645 $content = $revRecord->getContent( SlotRecord::MAIN );
3646 $text = $content ? $content->getWikitextForTransclusion() : null;
3647
3648 // Hook is hard deprecated since 1.35
3649 if ( Hooks::isRegistered( 'ParserFetchTemplate' ) ) {
3650 // Only create the Revision object if needed
3651 $legacyRevision = new Revision( $revRecord );
3652 Hooks::runner()->onParserFetchTemplate(
3653 $parser,
3654 $title,
3655 $legacyRevision,
3656 $text,
3657 $deps
3658 );
3659 }
3660
3661 if ( $text === false || $text === null ) {
3662 $text = false;
3663 break;
3664 }
3665 } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3666 $message = wfMessage( MediaWikiServices::getInstance()->getContentLanguage()->
3667 lcfirst( $title->getText() ) )->inContentLanguage();
3668 if ( !$message->exists() ) {
3669 $text = false;
3670 break;
3671 }
3672 $content = $message->content();
3673 $text = $message->plain();
3674 } else {
3675 break;
3676 }
3677 if ( !$content ) {
3678 break;
3679 }
3680 # Redirect?
3681 $finalTitle = $title;
3682 $title = $content->getRedirectTarget();
3683 }
3684
3685 $legacyRevision = function () use ( $revRecord ) {
3686 return $revRecord ? new Revision( $revRecord ) : null;
3687 };
3688 $retValues = [
3689 'revision' => $legacyRevision,
3690 'revision-record' => $revRecord ?: false, // So isset works
3691 'text' => $text,
3692 'finalTitle' => $finalTitle,
3693 'deps' => $deps
3694 ];
3695 $propertyArray = new DeprecatablePropertyArray(
3696 $retValues,
3697 [ 'revision' => '1.35' ],
3698 __METHOD__
3699 );
3700 return $propertyArray;
3701 }
3702
3710 public function fetchFileAndTitle( Title $title, array $options = [] ) {
3711 $file = $this->fetchFileNoRegister( $title, $options );
3712
3713 $time = $file ? $file->getTimestamp() : false;
3714 $sha1 = $file ? $file->getSha1() : false;
3715 # Register the file as a dependency...
3716 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3717 if ( $file && !$title->equals( $file->getTitle() ) ) {
3718 # Update fetched file title
3719 $title = $file->getTitle();
3720 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3721 }
3722 return [ $file, $title ];
3723 }
3724
3735 protected function fetchFileNoRegister( Title $title, array $options = [] ) {
3736 if ( isset( $options['broken'] ) ) {
3737 $file = false; // broken thumbnail forced by hook
3738 } else {
3739 $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
3740 if ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3741 $file = $repoGroup->findFileFromKey( $options['sha1'], $options );
3742 } else { // get by (name,timestamp)
3743 $file = $repoGroup->findFile( $title, $options );
3744 }
3745 }
3746 return $file;
3747 }
3748
3758 public function interwikiTransclude( Title $title, $action ) {
3759 if ( !$this->svcOptions->get( 'EnableScaryTranscluding' ) ) {
3760 return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3761 }
3762
3763 $url = $title->getFullURL( [ 'action' => $action ] );
3764 if ( strlen( $url ) > 1024 ) {
3765 return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3766 }
3767
3768 $wikiId = $title->getTransWikiID(); // remote wiki ID or false
3769
3770 $fname = __METHOD__;
3771 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3772
3773 $data = $cache->getWithSetCallback(
3774 $cache->makeGlobalKey(
3775 'interwiki-transclude',
3776 ( $wikiId !== false ) ? $wikiId : 'external',
3777 sha1( $url )
3778 ),
3779 $this->svcOptions->get( 'TranscludeCacheExpiry' ),
3780 function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
3781 $req = MWHttpRequest::factory( $url, [], $fname );
3782
3783 $status = $req->execute(); // Status object
3784 if ( !$status->isOK() ) {
3785 $ttl = $cache::TTL_UNCACHEABLE;
3786 } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
3787 $ttl = min( $cache::TTL_LAGGED, $ttl );
3788 }
3789
3790 return [
3791 'text' => $status->isOK() ? $req->getContent() : null,
3792 'code' => $req->getStatus()
3793 ];
3794 },
3795 [
3796 'checkKeys' => ( $wikiId !== false )
3797 ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
3798 : [],
3799 'pcGroup' => 'interwiki-transclude:5',
3800 'pcTTL' => $cache::TTL_PROC_LONG
3801 ]
3802 );
3803
3804 if ( is_string( $data['text'] ) ) {
3805 $text = $data['text'];
3806 } elseif ( $data['code'] != 200 ) {
3807 // Though we failed to fetch the content, this status is useless.
3808 $text = wfMessage( 'scarytranscludefailed-httpstatus' )
3809 ->params( $url, $data['code'] )->inContentLanguage()->text();
3810 } else {
3811 $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3812 }
3813
3814 return $text;
3815 }
3816
3827 public function argSubstitution( array $piece, PPFrame $frame ) {
3828 $error = false;
3829 $parts = $piece['parts'];
3830 $nameWithSpaces = $frame->expand( $piece['title'] );
3831 $argName = trim( $nameWithSpaces );
3832 $object = false;
3833 $text = $frame->getArgument( $argName );
3834 if ( $text === false && $parts->getLength() > 0
3835 && ( $this->ot['html']
3836 || $this->ot['pre']
3837 || ( $this->ot['wiki'] && $frame->isTemplate() )
3838 )
3839 ) {
3840 # No match in frame, use the supplied default
3841 $object = $parts->item( 0 )->getChildren();
3842 }
3843 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3844 $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3845 $this->limitationWarn( 'post-expand-template-argument' );
3846 }
3847
3848 if ( $text === false && $object === false ) {
3849 # No match anywhere
3850 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3851 }
3852 if ( $error !== false ) {
3853 $text .= $error;
3854 }
3855 if ( $object !== false ) {
3856 $ret = [ 'object' => $object ];
3857 } else {
3858 $ret = [ 'text' => $text ];
3859 }
3860
3861 return $ret;
3862 }
3863
3880 public function extensionSubstitution( array $params, PPFrame $frame ) {
3881 static $errorStr = '<span class="error">';
3882 static $errorLen = 20;
3883
3884 $name = $frame->expand( $params['name'] );
3885 if ( substr( $name, 0, $errorLen ) === $errorStr ) {
3886 // Probably expansion depth or node count exceeded. Just punt the
3887 // error up.
3888 return $name;
3889 }
3890
3891 $attrText = !isset( $params['attr'] ) ? '' : $frame->expand( $params['attr'] );
3892 if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
3893 // See above
3894 return $attrText;
3895 }
3896
3897 // We can't safely check if the expansion for $content resulted in an
3898 // error, because the content could happen to be the error string
3899 // (T149622).
3900 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3901
3902 $marker = self::MARKER_PREFIX . "-$name-"
3903 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3904
3905 $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
3906 ( $this->ot['html'] || $this->ot['pre'] );
3907 if ( $isFunctionTag ) {
3908 $markerType = 'none';
3909 } else {
3910 $markerType = 'general';
3911 }
3912 if ( $this->ot['html'] || $isFunctionTag ) {
3913 $name = strtolower( $name );
3914 $attributes = Sanitizer::decodeTagAttributes( $attrText );
3915 if ( isset( $params['attributes'] ) ) {
3916 $attributes += $params['attributes'];
3917 }
3918
3919 if ( isset( $this->mTagHooks[$name] ) ) {
3920 $output = call_user_func_array( $this->mTagHooks[$name],
3921 [ $content, $attributes, $this, $frame ] );
3922 } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
3923 list( $callback, ) = $this->mFunctionTagHooks[$name];
3924
3925 $output = $callback( $this, $frame, $content, $attributes );
3926 } else {
3927 $output = '<span class="error">Invalid tag extension name: ' .
3928 htmlspecialchars( $name ) . '</span>';
3929 }
3930
3931 if ( is_array( $output ) ) {
3932 // Extract flags
3933 $flags = $output;
3934 $output = $flags[0];
3935 if ( isset( $flags['markerType'] ) ) {
3936 $markerType = $flags['markerType'];
3937 }
3938 }
3939 } else {
3940 if ( isset( $params['attributes'] ) ) {
3941 foreach ( $params['attributes'] as $attrName => $attrValue ) {
3942 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
3943 htmlspecialchars( $attrValue ) . '"';
3944 }
3945 }
3946 if ( $content === null ) {
3947 $output = "<$name$attrText/>";
3948 } else {
3949 $close = $params['close'] === null ? '' : $frame->expand( $params['close'] );
3950 if ( substr( $close, 0, $errorLen ) === $errorStr ) {
3951 // See above
3952 return $close;
3953 }
3954 $output = "<$name$attrText>$content$close";
3955 }
3956 }
3957
3958 if ( $markerType === 'none' ) {
3959 return $output;
3960 } elseif ( $markerType === 'nowiki' ) {
3961 $this->mStripState->addNoWiki( $marker, $output );
3962 } elseif ( $markerType === 'general' ) {
3963 $this->mStripState->addGeneral( $marker, $output );
3964 } else {
3965 throw new MWException( __METHOD__ . ': invalid marker type' );
3966 }
3967 return $marker;
3968 }
3969
3977 private function incrementIncludeSize( $type, $size ) {
3978 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
3979 return false;
3980 } else {
3981 $this->mIncludeSizes[$type] += $size;
3982 return true;
3983 }
3984 }
3985
3992 $this->mExpensiveFunctionCount++;
3993 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
3994 }
3995
4003 private function handleDoubleUnderscore( $text ) {
4004 # The position of __TOC__ needs to be recorded
4005 $mw = $this->magicWordFactory->get( 'toc' );
4006 if ( $mw->match( $text ) ) {
4007 $this->mShowToc = true;
4008 $this->mForceTocPosition = true;
4009
4010 # Set a placeholder. At the end we'll fill it in with the TOC.
4011 $text = $mw->replace( '<!--MWTOC\'"-->', $text, 1 );
4012
4013 # Only keep the first one.
4014 $text = $mw->replace( '', $text );
4015 }
4016
4017 # Now match and remove the rest of them
4018 $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4019 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4020
4021 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4022 $this->mOutput->mNoGallery = true;
4023 }
4024 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4025 $this->mShowToc = false;
4026 }
4027 if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4028 && $this->getTitle()->getNamespace() == NS_CATEGORY
4029 ) {
4030 $this->addTrackingCategory( 'hidden-category-category' );
4031 }
4032 # (T10068) Allow control over whether robots index a page.
4033 # __INDEX__ always overrides __NOINDEX__, see T16899
4034 if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->getTitle()->canUseNoindex() ) {
4035 $this->mOutput->setIndexPolicy( 'noindex' );
4036 $this->addTrackingCategory( 'noindex-category' );
4037 }
4038 if ( isset( $this->mDoubleUnderscores['index'] ) && $this->getTitle()->canUseNoindex() ) {
4039 $this->mOutput->setIndexPolicy( 'index' );
4040 $this->addTrackingCategory( 'index-category' );
4041 }
4042
4043 # Cache all double underscores in the database
4044 foreach ( $this->mDoubleUnderscores as $key => $val ) {
4045 $this->mOutput->setProperty( $key, '' );
4046 }
4047
4048 return $text;
4049 }
4050
4056 public function addTrackingCategory( $msg ) {
4057 return $this->mOutput->addTrackingCategory( $msg, $this->getTitle() );
4058 }
4059
4075 private function finalizeHeadings( $text, $origText, $isMain = true ) {
4076 # Inhibit editsection links if requested in the page
4077 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4078 $maybeShowEditLink = false;
4079 } else {
4080 $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4081 }
4082
4083 # Get all headlines for numbering them and adding funky stuff like [edit]
4084 # links - this is for later, but we need the number of headlines right now
4085 # NOTE: white space in headings have been trimmed in handleHeadings. They shouldn't
4086 # be trimmed here since whitespace in HTML headings is significant.
4087 $matches = [];
4088 $numMatches = preg_match_all(
4089 '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4090 $text,
4091 $matches
4092 );
4093
4094 # if there are fewer than 4 headlines in the article, do not show TOC
4095 # unless it's been explicitly enabled.
4096 $enoughToc = $this->mShowToc &&
4097 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4098
4099 # Allow user to stipulate that a page should have a "new section"
4100 # link added via __NEWSECTIONLINK__
4101 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4102 $this->mOutput->setNewSection( true );
4103 }
4104
4105 # Allow user to remove the "new section"
4106 # link via __NONEWSECTIONLINK__
4107 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4108 $this->mOutput->hideNewSection( true );
4109 }
4110
4111 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4112 # override above conditions and always show TOC above first header
4113 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4114 $this->mShowToc = true;
4115 $enoughToc = true;
4116 }
4117
4118 # headline counter
4119 $headlineCount = 0;
4120 $numVisible = 0;
4121
4122 # Ugh .. the TOC should have neat indentation levels which can be
4123 # passed to the skin functions. These are determined here
4124 $toc = '';
4125 $full = '';
4126 $head = [];
4127 $sublevelCount = [];
4128 $levelCount = [];
4129 $level = 0;
4130 $prevlevel = 0;
4131 $toclevel = 0;
4132 $prevtoclevel = 0;
4133 $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4134 $baseTitleText = $this->getTitle()->getPrefixedDBkey();
4135 $oldType = $this->mOutputType;
4136 $this->setOutputType( self::OT_WIKI );
4137 $frame = $this->getPreprocessor()->newFrame();
4138 $root = $this->preprocessToDom( $origText );
4139 $node = $root->getFirstChild();
4140 $byteOffset = 0;
4141 $tocraw = [];
4142 $refers = [];
4143
4144 $headlines = $numMatches !== false ? $matches[3] : [];
4145
4146 $maxTocLevel = $this->svcOptions->get( 'MaxTocLevel' );
4147 foreach ( $headlines as $headline ) {
4148 $isTemplate = false;
4149 $titleText = false;
4150 $sectionIndex = false;
4151 $numbering = '';
4152 $markerMatches = [];
4153 if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4154 $serial = $markerMatches[1];
4155 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4156 $isTemplate = ( $titleText != $baseTitleText );
4157 $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4158 }
4159
4160 if ( $toclevel ) {
4161 $prevlevel = $level;
4162 }
4163 $level = $matches[1][$headlineCount];
4164
4165 if ( $level > $prevlevel ) {
4166 # Increase TOC level
4167 $toclevel++;
4168 $sublevelCount[$toclevel] = 0;
4169 if ( $toclevel < $maxTocLevel ) {
4170 $prevtoclevel = $toclevel;
4171 $toc .= Linker::tocIndent();
4172 $numVisible++;
4173 }
4174 } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4175 # Decrease TOC level, find level to jump to
4176
4177 for ( $i = $toclevel; $i > 0; $i-- ) {
4178 // @phan-suppress-next-line PhanTypeInvalidDimOffset
4179 if ( $levelCount[$i] == $level ) {
4180 # Found last matching level
4181 $toclevel = $i;
4182 break;
4183 } elseif ( $levelCount[$i] < $level ) {
4184 // @phan-suppress-previous-line PhanTypeInvalidDimOffset
4185 # Found first matching level below current level
4186 $toclevel = $i + 1;
4187 break;
4188 }
4189 }
4190 if ( $i == 0 ) {
4191 $toclevel = 1;
4192 }
4193 if ( $toclevel < $maxTocLevel ) {
4194 if ( $prevtoclevel < $maxTocLevel ) {
4195 # Unindent only if the previous toc level was shown :p
4196 $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4197 $prevtoclevel = $toclevel;
4198 } else {
4199 $toc .= Linker::tocLineEnd();
4200 }
4201 }
4202 } else {
4203 # No change in level, end TOC line
4204 if ( $toclevel < $maxTocLevel ) {
4205 $toc .= Linker::tocLineEnd();
4206 }
4207 }
4208
4209 $levelCount[$toclevel] = $level;
4210
4211 # count number of headlines for each level
4212 $sublevelCount[$toclevel]++;
4213 $dot = 0;
4214 for ( $i = 1; $i <= $toclevel; $i++ ) {
4215 if ( !empty( $sublevelCount[$i] ) ) {
4216 if ( $dot ) {
4217 $numbering .= '.';
4218 }
4219 $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4220 $dot = 1;
4221 }
4222 }
4223
4224 # The safe header is a version of the header text safe to use for links
4225
4226 # Remove link placeholders by the link text.
4227 # <!--LINK number-->
4228 # turns into
4229 # link text with suffix
4230 # Do this before unstrip since link text can contain strip markers
4231 $safeHeadline = $this->replaceLinkHoldersText( $headline );
4232
4233 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4234 $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4235
4236 # Remove any <style> or <script> tags (T198618)
4237 $safeHeadline = preg_replace(
4238 '#<(style|script)(?: [^>]*[^>/])?>.*?</\1>#is',
4239 '',
4240 $safeHeadline
4241 );
4242
4243 # Strip out HTML (first regex removes any tag not allowed)
4244 # Allowed tags are:
4245 # * <sup> and <sub> (T10393)
4246 # * <i> (T28375)
4247 # * <b> (r105284)
4248 # * <bdi> (T74884)
4249 # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4250 # * <s> and <strike> (T35715)
4251 # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4252 # to allow setting directionality in toc items.
4253 $tocline = preg_replace(
4254 [
4255 '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4256 '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4257 ],
4258 [ '', '<$1>' ],
4259 $safeHeadline
4260 );
4261
4262 # Strip '<span></span>', which is the result from the above if
4263 # <span id="foo"></span> is used to produce an additional anchor
4264 # for a section.
4265 $tocline = str_replace( '<span></span>', '', $tocline );
4266
4267 $tocline = trim( $tocline );
4268
4269 # For the anchor, strip out HTML-y stuff period
4270 $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4271 $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4272
4273 # Save headline for section edit hint before it's escaped
4274 $headlineHint = $safeHeadline;
4275
4276 # Decode HTML entities
4277 $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4278
4279 $safeHeadline = self::normalizeSectionName( $safeHeadline );
4280
4281 $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4282 $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4283 $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4284 if ( $fallbackHeadline === $safeHeadline ) {
4285 # No reason to have both (in fact, we can't)
4286 $fallbackHeadline = false;
4287 }
4288
4289 # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4290 # @todo FIXME: We may be changing them depending on the current locale.
4291 $arrayKey = strtolower( $safeHeadline );
4292 if ( $fallbackHeadline === false ) {
4293 $fallbackArrayKey = false;
4294 } else {
4295 $fallbackArrayKey = strtolower( $fallbackHeadline );
4296 }
4297
4298 # Create the anchor for linking from the TOC to the section
4299 $anchor = $safeHeadline;
4300 $fallbackAnchor = $fallbackHeadline;
4301 if ( isset( $refers[$arrayKey] ) ) {
4302 // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4303 for ( $i = 2; isset( $refers["{$arrayKey}_$i"] ); ++$i );
4304 $anchor .= "_$i";
4305 $linkAnchor .= "_$i";
4306 $refers["{$arrayKey}_$i"] = true;
4307 } else {
4308 $refers[$arrayKey] = true;
4309 }
4310 if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
4311 // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4312 for ( $i = 2; isset( $refers["{$fallbackArrayKey}_$i"] ); ++$i );
4313 $fallbackAnchor .= "_$i";
4314 $refers["{$fallbackArrayKey}_$i"] = true;
4315 } else {
4316 $refers[$fallbackArrayKey] = true;
4317 }
4318
4319 # Don't number the heading if it is the only one (looks silly)
4320 if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4321 # the two are different if the line contains a link
4322 $headline = Html::element(
4323 'span',
4324 [ 'class' => 'mw-headline-number' ],
4325 $numbering
4326 ) . ' ' . $headline;
4327 }
4328
4329 if ( $enoughToc && ( !isset( $maxTocLevel ) || $toclevel < $maxTocLevel ) ) {
4330 $toc .= Linker::tocLine( $linkAnchor, $tocline,
4331 $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4332 }
4333
4334 # Add the section to the section tree
4335 # Find the DOM node for this header
4336 $noOffset = ( $isTemplate || $sectionIndex === false );
4337 while ( $node && !$noOffset ) {
4338 if ( $node->getName() === 'h' ) {
4339 $bits = $node->splitHeading();
4340 if ( $bits['i'] == $sectionIndex ) {
4341 break;
4342 }
4343 }
4344 $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4345 $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4346 $node = $node->getNextSibling();
4347 }
4348 $tocraw[] = [
4349 'toclevel' => $toclevel,
4350 'level' => $level,
4351 'line' => $tocline,
4352 'number' => $numbering,
4353 'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4354 'fromtitle' => $titleText,
4355 'byteoffset' => ( $noOffset ? null : $byteOffset ),
4356 'anchor' => $anchor,
4357 ];
4358
4359 # give headline the correct <h#> tag
4360 if ( $maybeShowEditLink && $sectionIndex !== false ) {
4361 // Output edit section links as markers with styles that can be customized by skins
4362 if ( $isTemplate ) {
4363 # Put a T flag in the section identifier, to indicate to extractSections()
4364 # that sections inside <includeonly> should be counted.
4365 $editsectionPage = $titleText;
4366 $editsectionSection = "T-$sectionIndex";
4367 $editsectionContent = null;
4368 } else {
4369 $editsectionPage = $this->getTitle()->getPrefixedText();
4370 $editsectionSection = $sectionIndex;
4371 $editsectionContent = $headlineHint;
4372 }
4373 // We use a bit of pesudo-xml for editsection markers. The
4374 // language converter is run later on. Using a UNIQ style marker
4375 // leads to the converter screwing up the tokens when it
4376 // converts stuff. And trying to insert strip tags fails too. At
4377 // this point all real inputted tags have already been escaped,
4378 // so we don't have to worry about a user trying to input one of
4379 // these markers directly. We use a page and section attribute
4380 // to stop the language converter from converting these
4381 // important bits of data, but put the headline hint inside a
4382 // content block because the language converter is supposed to
4383 // be able to convert that piece of data.
4384 // Gets replaced with html in ParserOutput::getText
4385 $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4386 $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4387 if ( $editsectionContent !== null ) {
4388 $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4389 } else {
4390 $editlink .= '/>';
4391 }
4392 } else {
4393 $editlink = '';
4394 }
4395 $head[$headlineCount] = Linker::makeHeadline( $level,
4396 $matches['attrib'][$headlineCount], $anchor, $headline,
4397 $editlink, $fallbackAnchor );
4398
4399 $headlineCount++;
4400 }
4401
4402 $this->setOutputType( $oldType );
4403
4404 # Never ever show TOC if no headers
4405 if ( $numVisible < 1 ) {
4406 $enoughToc = false;
4407 }
4408
4409 if ( $enoughToc ) {
4410 if ( $prevtoclevel > 0 && $prevtoclevel < $maxTocLevel ) {
4411 $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4412 }
4413 $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4414 $this->mOutput->setTOCHTML( $toc );
4415 $toc = self::TOC_START . $toc . self::TOC_END;
4416 }
4417
4418 if ( $isMain ) {
4419 $this->mOutput->setSections( $tocraw );
4420 }
4421
4422 # split up and insert constructed headlines
4423 $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4424 $i = 0;
4425
4426 // build an array of document sections
4427 $sections = [];
4428 foreach ( $blocks as $block ) {
4429 // $head is zero-based, sections aren't.
4430 if ( empty( $head[$i - 1] ) ) {
4431 $sections[$i] = $block;
4432 } else {
4433 $sections[$i] = $head[$i - 1] . $block;
4434 }
4435
4446 $this->hookRunner->onParserSectionCreate( $this, $i, $sections[$i], $maybeShowEditLink );
4447
4448 $i++;
4449 }
4450
4451 if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4452 // append the TOC at the beginning
4453 // Top anchor now in skin
4454 $sections[0] .= $toc . "\n";
4455 }
4456
4457 $full .= implode( '', $sections );
4458
4459 if ( $this->mForceTocPosition ) {
4460 return str_replace( '<!--MWTOC\'"-->', $toc, $full );
4461 } else {
4462 return $full;
4463 }
4464 }
4465
4477 public function preSaveTransform( $text, Title $title, User $user,
4478 ParserOptions $options, $clearState = true
4479 ) {
4480 if ( $clearState ) {
4481 $magicScopeVariable = $this->lock();
4482 }
4483 $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4484 $this->setUser( $user );
4485
4486 // Strip U+0000 NULL (T159174)
4487 $text = str_replace( "\000", '', $text );
4488
4489 // We still normalize line endings (including trimming trailing whitespace) for
4490 // backwards-compatibility with other code that just calls PST, but this should already
4491 // be handled in TextContent subclasses
4492 $text = TextContent::normalizeLineEndings( $text );
4493
4494 if ( $options->getPreSaveTransform() ) {
4495 $text = $this->pstPass2( $text, $user );
4496 }
4497 $text = $this->mStripState->unstripBoth( $text );
4498
4499 // Trim trailing whitespace again, because the previous steps can introduce it.
4500 $text = rtrim( $text );
4501
4502 $this->hookRunner->onParserPreSaveTransformComplete( $this, $text );
4503
4504 $this->setUser( null ); # Reset
4505
4506 return $text;
4507 }
4508
4517 private function pstPass2( $text, User $user ) {
4518 # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4519 # $this->contLang here in order to give everyone the same signature and use the default one
4520 # rather than the one selected in each user's preferences. (see also T14815)
4521 $ts = $this->mOptions->getTimestamp();
4522 $timestamp = MWTimestamp::getLocalInstance( $ts );
4523 $ts = $timestamp->format( 'YmdHis' );
4524 $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4525
4526 $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4527
4528 # Variable replacement
4529 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4530 $text = $this->replaceVariables( $text );
4531
4532 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4533 # which may corrupt this parser instance via its wfMessage()->text() call-
4534
4535 # Signatures
4536 if ( strpos( $text, '~~~' ) !== false ) {
4537 $sigText = $this->getUserSig( $user );
4538 $text = strtr( $text, [
4539 '~~~~~' => $d,
4540 '~~~~' => "$sigText $d",
4541 '~~~' => $sigText
4542 ] );
4543 # The main two signature forms used above are time-sensitive
4544 $this->setOutputFlag( 'user-signature', 'User signature detected' );
4545 }
4546
4547 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4548 $tc = '[' . Title::legalChars() . ']';
4549 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4550
4551 // [[ns:page (context)|]]
4552 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4553 // [[ns:page(context)|]] (double-width brackets, added in r40257)
4554 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4555 // [[ns:page (context), context|]] (using either single or double-width comma)
4556 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4557 // [[|page]] (reverse pipe trick: add context from page title)
4558 $p2 = "/\[\[\\|($tc+)]]/";
4559
4560 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4561 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4562 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4563 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4564
4565 $t = $this->getTitle()->getText();
4566 $m = [];
4567 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4568 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4569 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4570 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4571 } else {
4572 # if there's no context, don't bother duplicating the title
4573 $text = preg_replace( $p2, '[[\\1]]', $text );
4574 }
4575
4576 return $text;
4577 }
4578
4593 public function getUserSig( User $user, $nickname = false, $fancySig = null ) {
4594 $username = $user->getName();
4595
4596 # If not given, retrieve from the user object.
4597 if ( $nickname === false ) {
4598 $nickname = $user->getOption( 'nickname' );
4599 }
4600
4601 if ( $fancySig === null ) {
4602 $fancySig = $user->getBoolOption( 'fancysig' );
4603 }
4604
4605 if ( $nickname === null || $nickname === '' ) {
4606 $nickname = $username;
4607 } elseif ( mb_strlen( $nickname ) > $this->svcOptions->get( 'MaxSigChars' ) ) {
4608 $nickname = $username;
4609 $this->logger->debug( __METHOD__ . ": $username has overlong signature." );
4610 } elseif ( $fancySig !== false ) {
4611 # Sig. might contain markup; validate this
4612 $isValid = $this->validateSig( $nickname ) !== false;
4613
4614 # New validator
4615 $sigValidation = $this->svcOptions->get( 'SignatureValidation' );
4616 if ( $isValid && $sigValidation === 'disallow' ) {
4617 $validator = new SignatureValidator(
4618 $user,
4619 null,
4620 $this->mOptions
4621 );
4622 $isValid = !$validator->validateSignature( $nickname );
4623 }
4624
4625 if ( $isValid ) {
4626 # Validated; clean up (if needed) and return it
4627 return $this->cleanSig( $nickname, true );
4628 } else {
4629 # Failed to validate; fall back to the default
4630 $nickname = $username;
4631 $this->logger->debug( __METHOD__ . ": $username has invalid signature." );
4632 }
4633 }
4634
4635 # Make sure nickname doesnt get a sig in a sig
4636 $nickname = self::cleanSigInSig( $nickname );
4637
4638 # If we're still here, make it a link to the user page
4639 $userText = wfEscapeWikiText( $username );
4640 $nickText = wfEscapeWikiText( $nickname );
4641 $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4642
4643 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4644 ->title( $this->getTitle() )->text();
4645 }
4646
4653 public function validateSig( $text ) {
4654 return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4655 }
4656
4667 public function cleanSig( $text, $parsing = false ) {
4668 if ( !$parsing ) {
4669 global $wgTitle;
4670 $magicScopeVariable = $this->lock();
4671 $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4672 }
4673
4674 # Option to disable this feature
4675 if ( !$this->mOptions->getCleanSignatures() ) {
4676 return $text;
4677 }
4678
4679 # @todo FIXME: Regex doesn't respect extension tags or nowiki
4680 # => Move this logic to braceSubstitution()
4681 $substWord = $this->magicWordFactory->get( 'subst' );
4682 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4683 $substText = '{{' . $substWord->getSynonym( 0 );
4684
4685 $text = preg_replace( $substRegex, $substText, $text );
4686 $text = self::cleanSigInSig( $text );
4687 $dom = $this->preprocessToDom( $text );
4688 $frame = $this->getPreprocessor()->newFrame();
4689 $text = $frame->expand( $dom );
4690
4691 if ( !$parsing ) {
4692 $text = $this->mStripState->unstripBoth( $text );
4693 }
4694
4695 return $text;
4696 }
4697
4704 public static function cleanSigInSig( $text ) {
4705 $text = preg_replace( '/~{3,5}/', '', $text );
4706 return $text;
4707 }
4708
4719 public function startExternalParse( ?Title $title, ParserOptions $options,
4720 $outputType, $clearState = true, $revId = null
4721 ) {
4722 $this->startParse( $title, $options, $outputType, $clearState );
4723 if ( $revId !== null ) {
4724 $this->mRevisionId = $revId;
4725 }
4726 }
4727
4734 private function startParse( ?Title $title, ParserOptions $options,
4735 $outputType, $clearState = true
4736 ) {
4737 $this->setTitle( $title );
4738 $this->mOptions = $options;
4739 $this->setOutputType( $outputType );
4740 if ( $clearState ) {
4741 $this->clearState();
4742 }
4743 }
4744
4753 public function transformMsg( $text, ParserOptions $options, Title $title = null ) {
4754 static $executing = false;
4755
4756 # Guard against infinite recursion
4757 if ( $executing ) {
4758 return $text;
4759 }
4760 $executing = true;
4761
4762 if ( !$title ) {
4763 global $wgTitle;
4764 $title = $wgTitle;
4765 }
4766
4767 $text = $this->preprocess( $text, $title, $options );
4768
4769 $executing = false;
4770 return $text;
4771 }
4772
4797 public function setHook( $tag, callable $callback ) {
4798 $tag = strtolower( $tag );
4799 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4800 throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4801 }
4802 $oldVal = $this->mTagHooks[$tag] ?? null;
4803 $this->mTagHooks[$tag] = $callback;
4804 if ( !in_array( $tag, $this->mStripList ) ) {
4805 $this->mStripList[] = $tag;
4806 }
4807
4808 return $oldVal;
4809 }
4810
4814 public function clearTagHooks() {
4815 $this->mTagHooks = [];
4816 $this->mFunctionTagHooks = [];
4817 $this->mStripList = [];
4818 }
4819
4863 public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
4864 $oldVal = $this->mFunctionHooks[$id][0] ?? null;
4865 $this->mFunctionHooks[$id] = [ $callback, $flags ];
4866
4867 # Add to function cache
4868 $mw = $this->magicWordFactory->get( $id );
4869 if ( !$mw ) {
4870 throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4871 }
4872
4873 $synonyms = $mw->getSynonyms();
4874 $sensitive = intval( $mw->isCaseSensitive() );
4875
4876 foreach ( $synonyms as $syn ) {
4877 # Case
4878 if ( !$sensitive ) {
4879 $syn = $this->contLang->lc( $syn );
4880 }
4881 # Add leading hash
4882 if ( !( $flags & self::SFH_NO_HASH ) ) {
4883 $syn = '#' . $syn;
4884 }
4885 # Remove trailing colon
4886 if ( substr( $syn, -1, 1 ) === ':' ) {
4887 $syn = substr( $syn, 0, -1 );
4888 }
4889 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
4890 }
4891 return $oldVal;
4892 }
4893
4899 public function getFunctionHooks() {
4900 $this->firstCallInit();
4901 return array_keys( $this->mFunctionHooks );
4902 }
4903
4915 public function setFunctionTagHook( $tag, callable $callback, $flags ) {
4916 wfDeprecated( __METHOD__, '1.35' );
4917 $tag = strtolower( $tag );
4918 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4919 throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
4920 }
4921 $old = $this->mFunctionTagHooks[$tag] ?? null;
4922 $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
4923
4924 if ( !in_array( $tag, $this->mStripList ) ) {
4925 $this->mStripList[] = $tag;
4926 }
4927
4928 return $old;
4929 }
4930
4939 public function replaceLinkHolders( &$text, $options = 0 ) {
4940 $this->replaceLinkHoldersPrivate( $text, $options );
4941 }
4942
4950 private function replaceLinkHoldersPrivate( &$text, $options = 0 ) {
4951 $this->mLinkHolders->replace( $text );
4952 }
4953
4961 private function replaceLinkHoldersText( $text ) {
4962 return $this->mLinkHolders->replaceText( $text );
4963 }
4964
4979 public function renderImageGallery( $text, array $params ) {
4980 $mode = false;
4981 if ( isset( $params['mode'] ) ) {
4982 $mode = $params['mode'];
4983 }
4984
4985 try {
4986 $ig = ImageGalleryBase::factory( $mode );
4987 } catch ( Exception $e ) {
4988 // If invalid type set, fallback to default.
4989 $ig = ImageGalleryBase::factory( false );
4990 }
4991
4992 $ig->setContextTitle( $this->getTitle() );
4993 $ig->setShowBytes( false );
4994 $ig->setShowDimensions( false );
4995 $ig->setShowFilename( false );
4996 $ig->setParser( $this );
4997 $ig->setHideBadImages();
4998 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
4999
5000 if ( isset( $params['showfilename'] ) ) {
5001 $ig->setShowFilename( true );
5002 } else {
5003 $ig->setShowFilename( false );
5004 }
5005 if ( isset( $params['caption'] ) ) {
5006 // NOTE: We aren't passing a frame here or below. Frame info
5007 // is currently opaque to Parsoid, which acts on OT_PREPROCESS.
5008 // See T107332#4030581
5009 $caption = $this->recursiveTagParse( $params['caption'] );
5010 $ig->setCaptionHtml( $caption );
5011 }
5012 if ( isset( $params['perrow'] ) ) {
5013 $ig->setPerRow( $params['perrow'] );
5014 }
5015 if ( isset( $params['widths'] ) ) {
5016 $ig->setWidths( $params['widths'] );
5017 }
5018 if ( isset( $params['heights'] ) ) {
5019 $ig->setHeights( $params['heights'] );
5020 }
5021 $ig->setAdditionalOptions( $params );
5022
5023 $this->hookRunner->onBeforeParserrenderImageGallery( $this, $ig );
5024
5025 $lines = StringUtils::explode( "\n", $text );
5026 foreach ( $lines as $line ) {
5027 # match lines like these:
5028 # Image:someimage.jpg|This is some image
5029 $matches = [];
5030 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5031 # Skip empty lines
5032 if ( count( $matches ) == 0 ) {
5033 continue;
5034 }
5035
5036 if ( strpos( $matches[0], '%' ) !== false ) {
5037 $matches[1] = rawurldecode( $matches[1] );
5038 }
5039 $title = Title::newFromText( $matches[1], NS_FILE );
5040 if ( $title === null ) {
5041 # Bogus title. Ignore these so we don't bomb out later.
5042 continue;
5043 }
5044
5045 # We need to get what handler the file uses, to figure out parameters.
5046 # Note, a hook can overide the file name, and chose an entirely different
5047 # file (which potentially could be of a different type and have different handler).
5048 $options = [];
5049 $descQuery = false;
5050 $this->hookRunner->onBeforeParserFetchFileAndTitle(
5051 $this, $title, $options, $descQuery );
5052 # Don't register it now, as TraditionalImageGallery does that later.
5053 $file = $this->fetchFileNoRegister( $title, $options );
5054 $handler = $file ? $file->getHandler() : false;
5055
5056 $paramMap = [
5057 'img_alt' => 'gallery-internal-alt',
5058 'img_link' => 'gallery-internal-link',
5059 ];
5060 if ( $handler ) {
5061 $paramMap += $handler->getParamMap();
5062 // We don't want people to specify per-image widths.
5063 // Additionally the width parameter would need special casing anyhow.
5064 unset( $paramMap['img_width'] );
5065 }
5066
5067 $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5068
5069 $label = '';
5070 $alt = '';
5071 $link = '';
5072 $handlerOptions = [];
5073 if ( isset( $matches[3] ) ) {
5074 // look for an |alt= definition while trying not to break existing
5075 // captions with multiple pipes (|) in it, until a more sensible grammar
5076 // is defined for images in galleries
5077
5078 // FIXME: Doing recursiveTagParse at this stage, and the trim before
5079 // splitting on '|' is a bit odd, and different from makeImage.
5080 $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5081 // Protect LanguageConverter markup
5082 $parameterMatches = StringUtils::delimiterExplode(
5083 '-{', '}-', '|', $matches[3], true /* nested */
5084 );
5085
5086 foreach ( $parameterMatches as $parameterMatch ) {
5087 list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5088 if ( $magicName ) {
5089 $paramName = $paramMap[$magicName];
5090
5091 switch ( $paramName ) {
5092 case 'gallery-internal-alt':
5093 $alt = $this->stripAltText( $match, false );
5094 break;
5095 case 'gallery-internal-link':
5096 $linkValue = $this->stripAltText( $match, false );
5097 if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
5098 // Result of LanguageConverter::markNoConversion
5099 // invoked on an external link.
5100 $linkValue = substr( $linkValue, 4, -2 );
5101 }
5102 list( $type, $target ) = $this->parseLinkParameter( $linkValue );
5103 if ( $type === 'link-url' ) {
5104 $link = $target;
5105 $this->mOutput->addExternalLink( $target );
5106 } elseif ( $type === 'link-title' ) {
5107 $link = $target->getLinkURL();
5108 $this->mOutput->addLink( $target );
5109 }
5110 break;
5111 default:
5112 // Must be a handler specific parameter.
5113 if ( $handler->validateParam( $paramName, $match ) ) {
5114 $handlerOptions[$paramName] = $match;
5115 } else {
5116 // Guess not, consider it as caption.
5117 $this->logger->debug(
5118 "$parameterMatch failed parameter validation" );
5119 $label = $parameterMatch;
5120 }
5121 }
5122
5123 } else {
5124 // Last pipe wins.
5125 $label = $parameterMatch;
5126 }
5127 }
5128 }
5129
5130 $ig->add( $title, $label, $alt, $link, $handlerOptions );
5131 }
5132 $html = $ig->toHTML();
5133 $this->hookRunner->onAfterParserFetchFileAndTitle( $this, $ig, $html );
5134 return $html;
5135 }
5136
5141 private function getImageParams( $handler ) {
5142 if ( $handler ) {
5143 $handlerClass = get_class( $handler );
5144 } else {
5145 $handlerClass = '';
5146 }
5147 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5148 # Initialise static lists
5149 static $internalParamNames = [
5150 'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5151 'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5152 'bottom', 'text-bottom' ],
5153 'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5154 'upright', 'border', 'link', 'alt', 'class' ],
5155 ];
5156 static $internalParamMap;
5157 if ( !$internalParamMap ) {
5158 $internalParamMap = [];
5159 foreach ( $internalParamNames as $type => $names ) {
5160 foreach ( $names as $name ) {
5161 // For grep: img_left, img_right, img_center, img_none,
5162 // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5163 // img_bottom, img_text_bottom,
5164 // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5165 // img_border, img_link, img_alt, img_class
5166 $magicName = str_replace( '-', '_', "img_$name" );
5167 $internalParamMap[$magicName] = [ $type, $name ];
5168 }
5169 }
5170 }
5171
5172 # Add handler params
5173 $paramMap = $internalParamMap;
5174 if ( $handler ) {
5175 $handlerParamMap = $handler->getParamMap();
5176 foreach ( $handlerParamMap as $magic => $paramName ) {
5177 $paramMap[$magic] = [ 'handler', $paramName ];
5178 }
5179 }
5180 $this->mImageParams[$handlerClass] = $paramMap;
5181 $this->mImageParamsMagicArray[$handlerClass] =
5182 $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5183 }
5184 return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5185 }
5186
5195 public function makeImage( Title $title, $options, $holders = false ) {
5196 # Check if the options text is of the form "options|alt text"
5197 # Options are:
5198 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5199 # * left no resizing, just left align. label is used for alt= only
5200 # * right same, but right aligned
5201 # * none same, but not aligned
5202 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5203 # * center center the image
5204 # * frame Keep original image size, no magnify-button.
5205 # * framed Same as "frame"
5206 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5207 # * upright reduce width for upright images, rounded to full __0 px
5208 # * border draw a 1px border around the image
5209 # * alt Text for HTML alt attribute (defaults to empty)
5210 # * class Set a class for img node
5211 # * link Set the target of the image link. Can be external, interwiki, or local
5212 # vertical-align values (no % or length right now):
5213 # * baseline
5214 # * sub
5215 # * super
5216 # * top
5217 # * text-top
5218 # * middle
5219 # * bottom
5220 # * text-bottom
5221
5222 # Protect LanguageConverter markup when splitting into parts
5224 '-{', '}-', '|', $options, true /* allow nesting */
5225 );
5226
5227 # Give extensions a chance to select the file revision for us
5228 $options = [];
5229 $descQuery = false;
5230 $this->hookRunner->onBeforeParserFetchFileAndTitle(
5231 $this, $title, $options, $descQuery );
5232 # Fetch and register the file (file title may be different via hooks)
5233 list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5234
5235 # Get parameter map
5236 $handler = $file ? $file->getHandler() : false;
5237
5238 list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5239
5240 if ( !$file ) {
5241 $this->addTrackingCategory( 'broken-file-category' );
5242 }
5243
5244 # Process the input parameters
5245 $caption = '';
5246 $params = [ 'frame' => [], 'handler' => [],
5247 'horizAlign' => [], 'vertAlign' => [] ];
5248 $seenformat = false;
5249 foreach ( $parts as $part ) {
5250 $part = trim( $part );
5251 list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5252 $validated = false;
5253 if ( isset( $paramMap[$magicName] ) ) {
5254 list( $type, $paramName ) = $paramMap[$magicName];
5255
5256 # Special case; width and height come in one variable together
5257 if ( $type === 'handler' && $paramName === 'width' ) {
5258 $parsedWidthParam = self::parseWidthParam( $value );
5259 if ( isset( $parsedWidthParam['width'] ) ) {
5260 $width = $parsedWidthParam['width'];
5261 if ( $handler->validateParam( 'width', $width ) ) {
5262 $params[$type]['width'] = $width;
5263 $validated = true;
5264 }
5265 }
5266 if ( isset( $parsedWidthParam['height'] ) ) {
5267 $height = $parsedWidthParam['height'];
5268 if ( $handler->validateParam( 'height', $height ) ) {
5269 $params[$type]['height'] = $height;
5270 $validated = true;
5271 }
5272 }
5273 # else no validation -- T15436
5274 } else {
5275 if ( $type === 'handler' ) {
5276 # Validate handler parameter
5277 $validated = $handler->validateParam( $paramName, $value );
5278 } else {
5279 # Validate internal parameters
5280 switch ( $paramName ) {
5281 case 'manualthumb':
5282 case 'alt':
5283 case 'class':
5284 # @todo FIXME: Possibly check validity here for
5285 # manualthumb? downstream behavior seems odd with
5286 # missing manual thumbs.
5287 $validated = true;
5288 $value = $this->stripAltText( $value, $holders );
5289 break;
5290 case 'link':
5291 list( $paramName, $value ) =
5292 $this->parseLinkParameter(
5293 $this->stripAltText( $value, $holders )
5294 );
5295 if ( $paramName ) {
5296 $validated = true;
5297 if ( $paramName === 'no-link' ) {
5298 $value = true;
5299 }
5300 if ( ( $paramName === 'link-url' ) && $this->mOptions->getExternalLinkTarget() ) {
5301 $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5302 }
5303 }
5304 break;
5305 case 'frameless':
5306 case 'framed':
5307 case 'thumbnail':
5308 // use first appearing option, discard others.
5309 $validated = !$seenformat;
5310 $seenformat = true;
5311 break;
5312 default:
5313 # Most other things appear to be empty or numeric...
5314 $validated = ( $value === false || is_numeric( trim( $value ) ) );
5315 }
5316 }
5317
5318 if ( $validated ) {
5319 $params[$type][$paramName] = $value;
5320 }
5321 }
5322 }
5323 if ( !$validated ) {
5324 $caption = $part;
5325 }
5326 }
5327
5328 # Process alignment parameters
5329 // @phan-suppress-next-line PhanImpossibleCondition
5330 if ( $params['horizAlign'] ) {
5331 $params['frame']['align'] = key( $params['horizAlign'] );
5332 }
5333 // @phan-suppress-next-line PhanImpossibleCondition
5334 if ( $params['vertAlign'] ) {
5335 $params['frame']['valign'] = key( $params['vertAlign'] );
5336 }
5337
5338 $params['frame']['caption'] = $caption;
5339
5340 # Will the image be presented in a frame, with the caption below?
5341 $imageIsFramed = isset( $params['frame']['frame'] )
5342 || isset( $params['frame']['framed'] )
5343 || isset( $params['frame']['thumbnail'] )
5344 || isset( $params['frame']['manualthumb'] );
5345
5346 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5347 # came to also set the caption, ordinary text after the image -- which
5348 # makes no sense, because that just repeats the text multiple times in
5349 # screen readers. It *also* came to set the title attribute.
5350 # Now that we have an alt attribute, we should not set the alt text to
5351 # equal the caption: that's worse than useless, it just repeats the
5352 # text. This is the framed/thumbnail case. If there's no caption, we
5353 # use the unnamed parameter for alt text as well, just for the time be-
5354 # ing, if the unnamed param is set and the alt param is not.
5355 # For the future, we need to figure out if we want to tweak this more,
5356 # e.g., introducing a title= parameter for the title; ignoring the un-
5357 # named parameter entirely for images without a caption; adding an ex-
5358 # plicit caption= parameter and preserving the old magic unnamed para-
5359 # meter for BC; ...
5360 if ( $imageIsFramed ) { # Framed image
5361 if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5362 # No caption or alt text, add the filename as the alt text so
5363 # that screen readers at least get some description of the image
5364 $params['frame']['alt'] = $title->getText();
5365 }
5366 # Do not set $params['frame']['title'] because tooltips don't make sense
5367 # for framed images
5368 } else { # Inline image
5369 if ( !isset( $params['frame']['alt'] ) ) {
5370 # No alt text, use the "caption" for the alt text
5371 if ( $caption !== '' ) {
5372 $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5373 } else {
5374 # No caption, fall back to using the filename for the
5375 # alt text
5376 $params['frame']['alt'] = $title->getText();
5377 }
5378 }
5379 # Use the "caption" for the tooltip text
5380 $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5381 }
5382 $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
5383
5384 $this->hookRunner->onParserMakeImageParams( $title, $file, $params, $this );
5385
5386 # Linker does the rest
5387 $time = $options['time'] ?? false;
5388 $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5389 $time, $descQuery, $this->mOptions->getThumbSize() );
5390
5391 # Give the handler a chance to modify the parser object
5392 if ( $handler ) {
5393 $handler->parserTransformHook( $this, $file );
5394 }
5395
5396 return $ret;
5397 }
5398
5417 private function parseLinkParameter( $value ) {
5418 $chars = self::EXT_LINK_URL_CLASS;
5419 $addr = self::EXT_LINK_ADDR;
5420 $prots = $this->mUrlProtocols;
5421 $type = null;
5422 $target = false;
5423 if ( $value === '' ) {
5424 $type = 'no-link';
5425 } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5426 if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5427 $this->mOutput->addExternalLink( $value );
5428 $type = 'link-url';
5429 $target = $value;
5430 }
5431 } else {
5432 $linkTitle = Title::newFromText( $value );
5433 if ( $linkTitle ) {
5434 $this->mOutput->addLink( $linkTitle );
5435 $type = 'link-title';
5436 $target = $linkTitle;
5437 }
5438 }
5439 return [ $type, $target ];
5440 }
5441
5447 private function stripAltText( $caption, $holders ) {
5448 # Strip bad stuff out of the title (tooltip). We can't just use
5449 # replaceLinkHoldersText() here, because if this function is called
5450 # from handleInternalLinks2(), mLinkHolders won't be up-to-date.
5451 if ( $holders ) {
5452 $tooltip = $holders->replaceText( $caption );
5453 } else {
5454 $tooltip = $this->replaceLinkHoldersText( $caption );
5455 }
5456
5457 # make sure there are no placeholders in thumbnail attributes
5458 # that are later expanded to html- so expand them now and
5459 # remove the tags
5460 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5461 # Compatibility hack! In HTML certain entity references not terminated
5462 # by a semicolon are decoded (but not if we're in an attribute; that's
5463 # how link URLs get away without properly escaping & in queries).
5464 # But wikitext has always required semicolon-termination of entities,
5465 # so encode & where needed to avoid decode of semicolon-less entities.
5466 # See T209236 and
5467 # https://www.w3.org/TR/html5/syntax.html#named-character-references
5468 # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
5469 $tooltip = preg_replace( "/
5470 & # 1. entity prefix
5471 (?= # 2. followed by:
5472 (?: # a. one of the legacy semicolon-less named entities
5473 A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
5474 C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
5475 GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
5476 O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
5477 U(?:acute|circ|grave|uml)|Yacute|
5478 a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
5479 c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
5480 divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
5481 frac(?:1(?:2|4)|34)|
5482 gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
5483 i(?:acute|circ|excl|grave|quest|uml)|laquo|
5484 lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
5485 m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
5486 not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
5487 o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
5488 p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
5489 s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
5490 u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
5491 )
5492 (?:[^;]|$)) # b. and not followed by a semicolon
5493 # S = study, for efficiency
5494 /Sx", '&amp;', $tooltip );
5495 $tooltip = Sanitizer::stripAllTags( $tooltip );
5496
5497 return $tooltip;
5498 }
5499
5509 public function attributeStripCallback( &$text, $frame = false ) {
5510 wfDeprecated( __METHOD__, '1.35' );
5511 $text = $this->replaceVariables( $text, $frame );
5512 $text = $this->mStripState->unstripBoth( $text );
5513 return $text;
5514 }
5515
5521 public function getTags() {
5522 $this->firstCallInit();
5523 return array_merge(
5524 array_keys( $this->mTagHooks ),
5525 array_keys( $this->mFunctionTagHooks )
5526 );
5527 }
5528
5533 public function getFunctionSynonyms() {
5534 $this->firstCallInit();
5535 return $this->mFunctionSynonyms;
5536 }
5537
5542 public function getUrlProtocols() {
5543 return $this->mUrlProtocols;
5544 }
5545
5575 private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5576 global $wgTitle; # not generally used but removes an ugly failure mode
5577
5578 $magicScopeVariable = $this->lock();
5579 $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5580 $outText = '';
5581 $frame = $this->getPreprocessor()->newFrame();
5582
5583 # Process section extraction flags
5584 $flags = 0;
5585 $sectionParts = explode( '-', $sectionId );
5586 // The section ID may either be a magic string such as 'new' (which should be treated as 0),
5587 // or a numbered section ID in the format of "T-<section index>".
5588 // Explicitly coerce the section index into a number accordingly. (T323373)
5589 $sectionIndex = (int)array_pop( $sectionParts );
5590 foreach ( $sectionParts as $part ) {
5591 if ( $part === 'T' ) {
5592 $flags |= self::PTD_FOR_INCLUSION;
5593 }
5594 }
5595
5596 # Check for empty input
5597 if ( strval( $text ) === '' ) {
5598 # Only sections 0 and T-0 exist in an empty document
5599 if ( $sectionIndex === 0 ) {
5600 if ( $mode === 'get' ) {
5601 return '';
5602 }
5603
5604 return $newText;
5605 } else {
5606 if ( $mode === 'get' ) {
5607 return $newText;
5608 }
5609
5610 return $text;
5611 }
5612 }
5613
5614 # Preprocess the text
5615 $root = $this->preprocessToDom( $text, $flags );
5616
5617 # <h> nodes indicate section breaks
5618 # They can only occur at the top level, so we can find them by iterating the root's children
5619 $node = $root->getFirstChild();
5620
5621 # Find the target section
5622 if ( $sectionIndex === 0 ) {
5623 # Section zero doesn't nest, level=big
5624 $targetLevel = 1000;
5625 } else {
5626 while ( $node ) {
5627 if ( $node->getName() === 'h' ) {
5628 $bits = $node->splitHeading();
5629 if ( $bits['i'] == $sectionIndex ) {
5630 $targetLevel = $bits['level'];
5631 break;
5632 }
5633 }
5634 if ( $mode === 'replace' ) {
5635 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5636 }
5637 $node = $node->getNextSibling();
5638 }
5639 }
5640
5641 if ( !$node ) {
5642 # Not found
5643 if ( $mode === 'get' ) {
5644 return $newText;
5645 } else {
5646 return $text;
5647 }
5648 }
5649
5650 # Find the end of the section, including nested sections
5651 do {
5652 if ( $node->getName() === 'h' ) {
5653 $bits = $node->splitHeading();
5654 $curLevel = $bits['level'];
5655 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5656 break;
5657 }
5658 }
5659 if ( $mode === 'get' ) {
5660 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5661 }
5662 $node = $node->getNextSibling();
5663 } while ( $node );
5664
5665 # Write out the remainder (in replace mode only)
5666 if ( $mode === 'replace' ) {
5667 # Output the replacement text
5668 # Add two newlines on -- trailing whitespace in $newText is conventionally
5669 # stripped by the editor, so we need both newlines to restore the paragraph gap
5670 # Only add trailing whitespace if there is newText
5671 if ( $newText != "" ) {
5672 $outText .= $newText . "\n\n";
5673 }
5674
5675 while ( $node ) {
5676 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5677 $node = $node->getNextSibling();
5678 }
5679 }
5680
5681 # Re-insert stripped tags
5682 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5683
5684 return $outText;
5685 }
5686
5701 public function getSection( $text, $sectionId, $defaultText = '' ) {
5702 return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5703 }
5704
5717 public function replaceSection( $oldText, $sectionId, $newText ) {
5718 return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5719 }
5720
5750 public function getFlatSectionInfo( $text ) {
5751 $magicScopeVariable = $this->lock();
5752 $this->startParse( null, new ParserOptions, self::OT_PLAIN, true );
5753 $frame = $this->getPreprocessor()->newFrame();
5754 $root = $this->preprocessToDom( $text, 0 );
5755 $node = $root->getFirstChild();
5756 $offset = 0;
5757 $currentSection = [
5758 'index' => 0,
5759 'level' => 0,
5760 'offset' => 0,
5761 'heading' => '',
5762 'text' => ''
5763 ];
5764 $sections = [];
5765
5766 while ( $node ) {
5767 $nodeText = $frame->expand( $node, PPFrame::RECOVER_ORIG );
5768 if ( $node->getName() === 'h' ) {
5769 $bits = $node->splitHeading();
5770 $sections[] = $currentSection;
5771 $currentSection = [
5772 'index' => $bits['i'],
5773 'level' => $bits['level'],
5774 'offset' => $offset,
5775 'heading' => $nodeText,
5776 'text' => $nodeText
5777 ];
5778 } else {
5779 $currentSection['text'] .= $nodeText;
5780 }
5781 $offset += strlen( $nodeText );
5782 $node = $node->getNextSibling();
5783 }
5784 $sections[] = $currentSection;
5785 return $sections;
5786 }
5787
5798 public function getRevisionId() {
5799 return $this->mRevisionId;
5800 }
5801
5809 public function getRevisionObject() {
5810 wfDeprecated( __METHOD__, '1.35' );
5811
5812 if ( $this->mRevisionObject ) {
5813 return $this->mRevisionObject;
5814 }
5815
5816 $this->mRevisionObject = null;
5817
5818 $revRecord = $this->getRevisionRecordObject();
5819 if ( $revRecord ) {
5820 $this->mRevisionObject = new Revision( $revRecord );
5821 }
5822
5823 return $this->mRevisionObject;
5824 }
5825
5832 public function getRevisionRecordObject() {
5833 if ( $this->mRevisionRecordObject ) {
5834 return $this->mRevisionRecordObject;
5835 }
5836
5837 // NOTE: try to get the RevisionObject even if mRevisionId is null.
5838 // This is useful when parsing a revision that has not yet been saved.
5839 // However, if we get back a saved revision even though we are in
5840 // preview mode, we'll have to ignore it, see below.
5841 // NOTE: This callback may be used to inject an OLD revision that was
5842 // already loaded, so "current" is a bit of a misnomer. We can't just
5843 // skip it if mRevisionId is set.
5844 $rev = call_user_func(
5845 $this->mOptions->getCurrentRevisionRecordCallback(),
5846 $this->getTitle(),
5847 $this
5848 );
5849
5850 if ( $rev === false ) {
5851 // The revision record callback returns `false` (not null) to
5852 // indicate that the revision is missing. (See for example
5853 // Parser::statelessFetchRevisionRecord(), the default callback.)
5854 // This API expects `null` instead. (T251952)
5855 $rev = null;
5856 }
5857
5858 if ( $this->mRevisionId === null && $rev && $rev->getId() ) {
5859 // We are in preview mode (mRevisionId is null), and the current revision callback
5860 // returned an existing revision. Ignore it and return null, it's probably the page's
5861 // current revision, which is not what we want here. Note that we do want to call the
5862 // callback to allow the unsaved revision to be injected here, e.g. for
5863 // self-transclusion previews.
5864 return null;
5865 }
5866
5867 // If the parse is for a new revision, then the callback should have
5868 // already been set to force the object and should match mRevisionId.
5869 // If not, try to fetch by mRevisionId for sanity.
5870 if ( $this->mRevisionId && $rev && $rev->getId() != $this->mRevisionId ) {
5871 $rev = MediaWikiServices::getInstance()
5872 ->getRevisionLookup()
5873 ->getRevisionById( $this->mRevisionId );
5874 }
5875
5876 $this->mRevisionRecordObject = $rev;
5877
5878 return $this->mRevisionRecordObject;
5879 }
5880
5886 public function getRevisionTimestamp() {
5887 if ( $this->mRevisionTimestamp !== null ) {
5888 return $this->mRevisionTimestamp;
5889 }
5890
5891 # Use specified revision timestamp, falling back to the current timestamp
5892 $revObject = $this->getRevisionRecordObject();
5893 $timestamp = $revObject && $revObject->getTimestamp()
5894 ? $revObject->getTimestamp()
5895 : $this->mOptions->getTimestamp();
5896 $this->mOutput->setRevisionTimestampUsed( $timestamp ); // unadjusted time zone
5897
5898 # The cryptic '' timezone parameter tells to use the site-default
5899 # timezone offset instead of the user settings.
5900 # Since this value will be saved into the parser cache, served
5901 # to other users, and potentially even used inside links and such,
5902 # it needs to be consistent for all visitors.
5903 $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
5904
5905 return $this->mRevisionTimestamp;
5906 }
5907
5913 public function getRevisionUser(): ?string {
5914 if ( $this->mRevisionUser === null ) {
5915 $revObject = $this->getRevisionRecordObject();
5916
5917 # if this template is subst: the revision id will be blank,
5918 # so just use the current user's name
5919 if ( $revObject && $revObject->getUser() ) {
5920 $this->mRevisionUser = $revObject->getUser()->getName();
5921 } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5922 $this->mRevisionUser = $this->getUser()->getName();
5923 } else {
5924 # Note that we fall through here with
5925 # $this->mRevisionUser still null
5926 }
5927 }
5928 return $this->mRevisionUser;
5929 }
5930
5936 public function getRevisionSize() {
5937 if ( $this->mRevisionSize === null ) {
5938 $revObject = $this->getRevisionRecordObject();
5939
5940 # if this variable is subst: the revision id will be blank,
5941 # so just use the parser input size, because the own substituation
5942 # will change the size.
5943 if ( $revObject ) {
5944 $this->mRevisionSize = $revObject->getSize();
5945 } else {
5946 $this->mRevisionSize = $this->mInputSize;
5947 }
5948 }
5949 return $this->mRevisionSize;
5950 }
5951
5957 public function setDefaultSort( $sort ) {
5958 $this->mDefaultSort = $sort;
5959 $this->mOutput->setProperty( 'defaultsort', $sort );
5960 }
5961
5972 public function getDefaultSort() {
5973 if ( $this->mDefaultSort !== false ) {
5974 return $this->mDefaultSort;
5975 } else {
5976 return '';
5977 }
5978 }
5979
5986 public function getCustomDefaultSort() {
5987 return $this->mDefaultSort;
5988 }
5989
5990 private static function getSectionNameFromStrippedText( $text ) {
5991 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
5992 $text = Sanitizer::decodeCharReferences( $text );
5993 $text = self::normalizeSectionName( $text );
5994 return $text;
5995 }
5996
5997 private static function makeAnchor( $sectionName ) {
5998 return '#' . Sanitizer::escapeIdForLink( $sectionName );
5999 }
6000
6001 private function makeLegacyAnchor( $sectionName ) {
6002 $fragmentMode = $this->svcOptions->get( 'FragmentMode' );
6003 if ( isset( $fragmentMode[1] ) && $fragmentMode[1] === 'legacy' ) {
6004 // ForAttribute() and ForLink() are the same for legacy encoding
6005 $id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
6006 } else {
6007 $id = Sanitizer::escapeIdForLink( $sectionName );
6008 }
6009
6010 return "#$id";
6011 }
6012
6021 public function guessSectionNameFromWikiText( $text ) {
6022 # Strip out wikitext links(they break the anchor)
6023 $text = $this->stripSectionName( $text );
6024 $sectionName = self::getSectionNameFromStrippedText( $text );
6025 return self::makeAnchor( $sectionName );
6026 }
6027
6037 public function guessLegacySectionNameFromWikiText( $text ) {
6038 # Strip out wikitext links(they break the anchor)
6039 $text = $this->stripSectionName( $text );
6040 $sectionName = self::getSectionNameFromStrippedText( $text );
6041 return $this->makeLegacyAnchor( $sectionName );
6042 }
6043
6049 public static function guessSectionNameFromStrippedText( $text ) {
6050 $sectionName = self::getSectionNameFromStrippedText( $text );
6051 return self::makeAnchor( $sectionName );
6052 }
6053
6060 private static function normalizeSectionName( $text ) {
6061 # T90902: ensure the same normalization is applied for IDs as to links
6063 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6064 '@phan-var MediaWikiTitleCodec $titleParser';
6065 try {
6066
6067 $parts = $titleParser->splitTitleString( "#$text" );
6068 } catch ( MalformedTitleException $ex ) {
6069 return $text;
6070 }
6071 return $parts['fragment'];
6072 }
6073
6088 public function stripSectionName( $text ) {
6089 # Strip internal link markup
6090 $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6091 $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6092
6093 # Strip external link markup
6094 # @todo FIXME: Not tolerant to blank link text
6095 # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6096 # on how many empty links there are on the page - need to figure that out.
6097 $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6098
6099 # Parse wikitext quotes (italics & bold)
6100 $text = $this->doQuotes( $text );
6101
6102 # Strip HTML tags
6103 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6104 return $text;
6105 }
6106
6117 private function fuzzTestSrvus( $text, Title $title, ParserOptions $options,
6118 $outputType = self::OT_HTML
6119 ) {
6120 $magicScopeVariable = $this->lock();
6121 $this->startParse( $title, $options, $outputType, true );
6122
6123 $text = $this->replaceVariables( $text );
6124 $text = $this->mStripState->unstripBoth( $text );
6125 $text = Sanitizer::removeHTMLtags( $text );
6126 return $text;
6127 }
6128
6135 private function fuzzTestPst( $text, Title $title, ParserOptions $options ) {
6136 return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
6137 }
6138
6145 private function fuzzTestPreprocess( $text, Title $title, ParserOptions $options ) {
6146 return $this->fuzzTestSrvus( $text, $title, $options, self::OT_PREPROCESS );
6147 }
6148
6166 public function markerSkipCallback( $s, callable $callback ) {
6167 $i = 0;
6168 $out = '';
6169 while ( $i < strlen( $s ) ) {
6170 $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6171 if ( $markerStart === false ) {
6172 $out .= call_user_func( $callback, substr( $s, $i ) );
6173 break;
6174 } else {
6175 $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6176 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6177 if ( $markerEnd === false ) {
6178 $out .= substr( $s, $markerStart );
6179 break;
6180 } else {
6181 $markerEnd += strlen( self::MARKER_SUFFIX );
6182 $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6183 $i = $markerEnd;
6184 }
6185 }
6186 }
6187 return $out;
6188 }
6189
6196 public function killMarkers( $text ) {
6197 return $this->mStripState->killMarkers( $text );
6198 }
6199
6210 public static function parseWidthParam( $value, $parseHeight = true ) {
6211 $parsedWidthParam = [];
6212 if ( $value === '' ) {
6213 return $parsedWidthParam;
6214 }
6215 $m = [];
6216 # (T15500) In both cases (width/height and width only),
6217 # permit trailing "px" for backward compatibility.
6218 if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6219 $width = intval( $m[1] );
6220 $height = intval( $m[2] );
6221 $parsedWidthParam['width'] = $width;
6222 $parsedWidthParam['height'] = $height;
6223 } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6224 $width = intval( $value );
6225 $parsedWidthParam['width'] = $width;
6226 }
6227 return $parsedWidthParam;
6228 }
6229
6239 protected function lock() {
6240 if ( $this->mInParse ) {
6241 throw new MWException( "Parser state cleared while parsing. "
6242 . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6243 }
6244
6245 // Save the backtrace when locking, so that if some code tries locking again,
6246 // we can print the lock owner's backtrace for easier debugging
6247 $e = new Exception;
6248 $this->mInParse = $e->getTraceAsString();
6249
6250 $recursiveCheck = new ScopedCallback( function () {
6251 $this->mInParse = false;
6252 } );
6253
6254 return $recursiveCheck;
6255 }
6256
6267 public static function stripOuterParagraph( $html ) {
6268 $m = [];
6269 if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && strpos( $m[1], '</p>' ) === false ) {
6270 $html = $m[1];
6271 }
6272
6273 return $html;
6274 }
6275
6286 public function getFreshParser() {
6287 if ( $this->mInParse ) {
6288 return $this->factory->create();
6289 } else {
6290 return $this;
6291 }
6292 }
6293
6301 public function enableOOUI() {
6302 wfDeprecated( __METHOD__, '1.35' );
6303 OutputPage::setupOOUI();
6304 $this->mOutput->setEnableOOUI( true );
6305 }
6306
6313 private function setOutputFlag( string $flag, string $reason ): void {
6314 $this->mOutput->setFlag( $flag );
6315 $name = $this->getTitle()->getPrefixedText();
6316 $this->logger->debug( __METHOD__ . ": set $flag flag on '$name'; $reason" );
6317 }
6318}
getUser()
$wgNoFollowNsExceptions
Namespaces in which $wgNoFollowLinks doesn't apply.
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
$wgNoFollowDomainExceptions
If this is set to an array of domains, external links to these domain names (or any subdomains) will ...
wfUrlProtocolsWithoutProtRel()
Like wfUrlProtocols(), but excludes '//' from the protocol list.
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...
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
wfMatchesDomainList( $url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
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 $function is deprecated.
$wgTitle
Definition Setup.php:799
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:85
static doBlockLevels( $text, $lineStart)
Make lists from lines starting with ':', '*', '#', etc.
static expand(Parser $parser, string $id, int $ts, NamespaceInfo $nsInfo, ServiceOptions $svcOptions, LoggerInterface $logger)
Expand the magic variable given by $index.
static register( $parser)
static register( $parser)
WebRequest clone which takes values from a provided array.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition Language.php:41
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition Linker.php:788
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition Linker.php:1701
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition Linker.php:243
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1491
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition Linker.php:164
static tocIndent()
Add another level to the Table of Contents.
Definition Linker.php:1675
static makeImageLink(Parser $parser, LinkTarget $title, $file, $frameParams=[], $handlerParams=[], $time=false, $query="", $widthOption=null)
Given parameters derived from [[Image:Foo|options...]], generate the HTML that that syntax inserts in...
Definition Linker.php:299
static splitTrail( $trail)
Split a link trail, return the "inside" portion and the remainder of the trail as a two-element array...
Definition Linker.php:1833
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:856
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition Linker.php:1812
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
Definition Linker.php:1686
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:1737
static tocLineEnd()
End a Table Of Contents line.
Definition Linker.php:1725
MediaWiki exception.
static tidy( $text)
Interface with Remex tidy.
Definition MWTidy.php:42
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.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
ArrayAccess implementation that supports deprecating access to certain properties.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
An interface for creating language converters.
A service that provides utilities to do with language names and codes.
Factory to create LinkRender objects.
Class that generates HTML links for pages.
MediaWikiServices is the service locator for the application scope of MediaWiki.
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.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Set options of the Parser.
getPreSaveTransform()
Transform wiki markup when saving the page?
getUser()
Current user.
getDisableTitleConversion()
Whether title conversion should be disabled.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:85
addTrackingCategory( $msg)
Definition Parser.php:4056
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:1228
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:1124
handleDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores,...
Definition Parser.php:4003
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:5886
static normalizeUrlComponent( $component, $unsafe)
Definition Parser.php:2301
extensionSubstitution(array $params, PPFrame $frame)
Return the text to be used for a given extension tag.
Definition Parser.php:3880
array $mTplDomCache
Definition Parser.php:246
bool string $mInParse
Recursive call protection.
Definition Parser.php:311
handleInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
Definition Parser.php:2391
const TOC_END
Definition Parser.php:150
setDefaultSort( $sort)
Mutator for $mDefaultSort.
Definition Parser.php:5957
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6267
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition Parser.php:2243
getContentLanguageConverter()
Shorthand for getting a Language Converter for Content language.
Definition Parser.php:1616
getPreloadText( $text, Title $title, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition Parser.php:961
MagicWordFactory $magicWordFactory
Definition Parser.php:322
__clone()
Allow extensions to clean up when the parser is cloned.
Definition Parser.php:498
ParserOutput $mOutput
Definition Parser.php:204
maybeMakeExternalImage( $url)
make an image if it's allowed, either through the global option, through the exception,...
Definition Parser.php:2324
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:4704
makeKnownLinkHolder(Title $nt, $text='', $trail='', $prefix='')
Render a forced-blue link inline; protect against double expansion of URLs if we're in a mode that pr...
Definition Parser.php:2708
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition Parser.php:1185
LinkRenderer $mLinkRenderer
Definition Parser.php:319
$mHighestExpansionDepth
Definition Parser.php:233
magicLinkCallback(array $m)
Definition Parser.php:1741
HookRunner $hookRunner
Definition Parser.php:361
fetchCurrentRevisionOfTitle(Title $title)
Fetch the current revision of a given title.
Definition Parser.php:3430
getCustomDefaultSort()
Accessor for $mDefaultSort Unlike getDefaultSort(), will return false if none is set.
Definition Parser.php:5986
static getSectionNameFromStrippedText( $text)
Definition Parser.php:5990
ParserOptions null $mOptions
Definition Parser.php:261
stripAltText( $caption, $holders)
Definition Parser.php:5447
getOptions()
Definition Parser.php:1069
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition Parser.php:4667
getStripState()
Get the StripState.
Definition Parser.php:1300
preSaveTransform( $text, Title $title, User $user, ParserOptions $options, $clearState=true)
Transform wiki markup when saving a page by doing "\\r\\n" -> "\\n" conversion, substituting signatur...
Definition Parser.php:4477
pstPass2( $text, User $user)
Pre-save transform helper function.
Definition Parser.php:4517
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:5913
getUserSig(User $user, $nickname=false, $fancySig=null)
Fetch the user's signature text, if any, and normalize to validated, ready-to-insert wikitext.
Definition Parser.php:4593
static statelessFetchRevision(Title $title, $parser=false)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
Definition Parser.php:3498
replaceVariables( $text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition Parser.php:2867
getFunctionSynonyms()
Definition Parser.php:5533
makeImage(Title $title, $options, $holders=false)
Parse image options text and use it to make an image.
Definition Parser.php:5195
armorLinks( $text)
Insert a NOPARSE hacky thing into any inline links in a chunk that's going to go through further pars...
Definition Parser.php:2732
Language $contLang
Definition Parser.php:325
$mHeadings
Definition Parser.php:237
setFunctionTagHook( $tag, callable $callback, $flags)
Create a tag function, e.g.
Definition Parser.php:4915
const PTD_FOR_INCLUSION
Definition Parser.php:117
setTitle(Title $t=null)
Set the context title.
Definition Parser.php:991
static guessSectionNameFromStrippedText( $text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
Definition Parser.php:6049
$mGeneratedPPNodeCount
Definition Parser.php:231
interwikiTransclude(Title $title, $action)
Transclude an interwiki link.
Definition Parser.php:3758
getFlatSectionInfo( $text)
Get an array of preprocessor section information.
Definition Parser.php:5750
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:2918
LinkHolderArray $mLinkHolders
Definition Parser.php:216
$mFunctionTagHooks
Definition Parser.php:158
$mRevisionId
Definition Parser.php:277
Title null $mTitle
Since 1.34, leaving mTitle uninitialized or setting mTitle to null is deprecated.
Definition Parser.php:270
getRevisionSize()
Get the size of the revision.
Definition Parser.php:5936
const TOC_START
Definition Parser.php:149
extractSections( $text, $sectionId, $mode, $newText='')
Break wikitext input into sections, and either pull or replace some particular section's text.
Definition Parser.php:5575
BadFileLookup $badFileLookup
Definition Parser.php:355
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition Parser.php:6088
$mDefaultSort
Definition Parser.php:234
preprocess( $text, ?Title $title, ParserOptions $options, $revid=null, $frame=false)
Expand templates and variables in the text, producing valid, static wikitext.
Definition Parser.php:917
$mTagHooks
Definition Parser.php:154
getOutputType()
Accessor for the output type.
Definition Parser.php:1028
static normalizeSectionName( $text)
Apply the same normalization as code making links to this section would.
Definition Parser.php:6060
makeFreeExternalLink( $url, $numPostProto)
Make a free external link, given a user-supplied URL.
Definition Parser.php:1820
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition Parser.php:942
$mFunctionHooks
Definition Parser.php:156
$mShowToc
Definition Parser.php:243
setUser(?User $user)
Set the current user.
Definition Parser.php:982
getImageParams( $handler)
Definition Parser.php:5141
internalParseHalfParsed( $text, $isMain=true, $linestart=true)
Helper function for parse() that transforms half-parsed HTML into fully parsed HTML.
Definition Parser.php:1654
getUrlProtocols()
Definition Parser.php:5542
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:4863
$mRevisionSize
Definition Parser.php:283
bool $mFirstCall
Whether firstCallInit still needs to be called.
Definition Parser.php:169
getHookRunner()
Get a HookRunner for calling core hooks.
Definition Parser.php:1641
getTitle()
Definition Parser.php:1007
handleHeadings( $text)
Parse headers and return html.
Definition Parser.php:1892
fetchTemplate(Title $title)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3572
braceSubstitution(array $piece, PPFrame $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
Definition Parser.php:2941
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way,...
Definition Parser.php:91
MagicWordArray $mSubstWords
Definition Parser.php:181
$mUrlProtocols
Definition Parser.php:190
MapCacheLRU null $currentRevisionCache
Definition Parser.php:303
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition Parser.php:1167
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
Definition Parser.php:2212
setOutputType( $ot)
Mutator for the output type.
Definition Parser.php:1036
array $mConf
Definition Parser.php:187
makeLegacyAnchor( $sectionName)
Definition Parser.php:6001
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
Definition Parser.php:1112
StripState $mStripState
Definition Parser.php:211
callParserFunction(PPFrame $frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
Definition Parser.php:3295
makeLimitReport()
Set the limit report data in the current ParserOutput, and return the limit report HTML comment.
Definition Parser.php:721
fetchFileAndTitle(Title $title, array $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3710
ServiceOptions $svcOptions
This is called $svcOptions instead of $options like elsewhere to avoid confusion with $mOptions,...
Definition Parser.php:343
parseExtensionTagAsTopLevelDoc( $text)
Needed by Parsoid/PHP to ensure all the hooks for extensions are run in the right order.
Definition Parser.php:899
isCurrentRevisionOfTitleCached(Title $title)
Definition Parser.php:3481
$mInputSize
Definition Parser.php:285
Title(Title $x=null)
Accessor/mutator for the Title object.
Definition Parser.php:1018
MagicWordArray $mVariables
Definition Parser.php:176
getTemplateDom(Title $title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
Definition Parser.php:3384
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1291
$mOutputType
Definition Parser.php:271
$mImageParamsMagicArray
Definition Parser.php:162
$mAutonumber
Definition Parser.php:205
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition Parser.php:4797
expandMagicVariable( $index, $frame=false)
Return value of a magic variable (like PAGENAME)
Definition Parser.php:2759
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
Definition Parser.php:1205
getTags()
Accessor.
Definition Parser.php:5521
HookContainer $hookContainer
Definition Parser.php:358
fuzzTestPreprocess( $text, Title $title, ParserOptions $options)
Definition Parser.php:6145
array $mLangLinkLanguages
Array with the language name of each language link (i.e.
Definition Parser.php:295
LinkRendererFactory $linkRendererFactory
Definition Parser.php:346
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition Parser.php:848
$mRevisionUser
Definition Parser.php:281
handleInternalLinks( $text)
Process [[ ]] wikilinks.
Definition Parser.php:2381
getTargetLanguageConverter()
Shorthand for getting a Language Converter for Target language.
Definition Parser.php:1605
$mTplRedirCache
Definition Parser.php:235
$mFunctionSynonyms
Definition Parser.php:157
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
Definition Parser.php:5701
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition Parser.php:1536
$mRevisionTimestamp
Definition Parser.php:279
static statelessFetchRevisionRecord(Title $title, $parser=null)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
Definition Parser.php:3515
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition Parser.php:5717
replaceLinkHoldersPrivate(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition Parser.php:4950
LanguageConverterFactory $languageConverterFactory
Definition Parser.php:328
incrementIncludeSize( $type, $size)
Increment an include size counter.
Definition Parser.php:3977
User $mUser
Definition Parser.php:252
handleExternalLinks( $text)
Replace external links (REL)
Definition Parser.php:2110
RevisionRecord null $mRevisionRecordObject
Definition Parser.php:288
getRevisionObject()
Get the revision object for $this->mRevisionId.
Definition Parser.php:5809
doBlockLevels( $text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
Definition Parser.php:2746
$mPPNodeCount
Definition Parser.php:226
getDefaultSort()
Accessor for $mDefaultSort Will use the empty string if none is set.
Definition Parser.php:5972
const MARKER_PREFIX
Definition Parser.php:146
replaceLinkHoldersText( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
Definition Parser.php:4961
setOutputFlag(string $flag, string $reason)
Sets the flag on the parser output but also does some debug logging.
Definition Parser.php:6313
getHookContainer()
Get a HookContainer capable of returning metadata about hooks or running extension hooks.
Definition Parser.php:1629
getFunctionHooks()
Get all registered function hook identifiers.
Definition Parser.php:4899
fetchCurrentRevisionRecordOfTitle(Title $title)
Fetch the current revision of a given title as a RevisionRecord.
Definition Parser.php:3452
startExternalParse(?Title $title, 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:4719
transformMsg( $text, ParserOptions $options, Title $title=null)
Wrapper for preprocess()
Definition Parser.php:4753
$mMarkerIndex
Definition Parser.php:164
const EXT_LINK_ADDR
Definition Parser.php:107
handleTables( $text)
Parse the wiki syntax used to render tables.
Definition Parser.php:1326
static getExternalLinkRel( $url=false, LinkTarget $title=null)
Get the rel attribute for a particular external link.
Definition Parser.php:2190
$mExpensiveFunctionCount
Definition Parser.php:241
getContentLanguage()
Get the content language that this Parser is using.
Definition Parser.php:1195
Preprocessor $mPreprocessor
Definition Parser.php:197
$mDoubleUnderscores
Definition Parser.php:239
getOutput()
Definition Parser.php:1062
$mVarCache
Definition Parser.php:160
Options( $x=null)
Accessor/mutator for the ParserOptions object.
Definition Parser.php:1089
$mImageParams
Definition Parser.php:161
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:874
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:6021
clearTagHooks()
Remove all tag hooks.
Definition Parser.php:4814
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:5798
setOptions(ParserOptions $options)
Mutator for the ParserOptions object.
Definition Parser.php:1078
static parseWidthParam( $value, $parseHeight=true)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6210
validateSig( $text)
Check that the user's signature contains no bad XML.
Definition Parser.php:4653
getPreprocessor()
Get a preprocessor object.
Definition Parser.php:1154
parseLinkParameter( $value)
Parse the value of 'link' parameter in image syntax ([[File:Foo.jpg|link=<value>]]).
Definition Parser.php:5417
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
Definition Parser.php:6037
const EXT_LINK_URL_CLASS
Definition Parser.php:103
OutputType( $x=null)
Accessor/mutator for the output type.
Definition Parser.php:1054
LoggerInterface $logger
Definition Parser.php:352
initializeVariables()
Initialize the magic variables (like CURRENTMONTHNAME) and substitution modifiers.
Definition Parser.php:2812
$mIncludeSizes
Definition Parser.php:224
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition Parser.php:484
resetOutput()
Reset the ParserOutput.
Definition Parser.php:591
setLinkID( $id)
Definition Parser.php:1104
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise.
Definition Parser.php:1142
doQuotes( $text)
Helper function for handleAllQuotes()
Definition Parser.php:1927
startParse(?Title $title, ParserOptions $options, $outputType, $clearState=true)
Definition Parser.php:4734
$mExtLinkBracketedRegex
Definition Parser.php:190
__construct( $svcOptions=null, MagicWordFactory $magicWordFactory=null, Language $contLang=null, ParserFactory $factory=null, $urlProtocols=null, SpecialPageFactory $spFactory=null, $linkRendererFactory=null, $nsInfo=null, $logger=null, BadFileLookup $badFileLookup=null, LanguageConverterFactory $languageConverterFactory=null, HookContainer $hookContainer=null)
Constructing parsers directly is deprecated! Use a ParserFactory.
Definition Parser.php:406
finalizeHeadings( $text, $origText, $isMain=true)
This function accomplishes several tasks: 1) Auto-number headings if that option is enabled 2) Add an...
Definition Parser.php:4075
const EXT_IMAGE_REGEX
Definition Parser.php:110
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition Parser.php:4939
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:1313
handleMagicLinks( $text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
Definition Parser.php:1710
NamespaceInfo $nsInfo
Definition Parser.php:349
incrementExpensiveFunctionCount()
Increment the expensive function count.
Definition Parser.php:3991
static makeAnchor( $sectionName)
Definition Parser.php:5997
const SPACE_NOT_NL
Definition Parser.php:114
int $mLinkID
Definition Parser.php:222
handleAllQuotes( $text)
Replace single quotes with HTML markup.
Definition Parser.php:1909
SpecialPageFactory $specialPageFactory
Definition Parser.php:334
fetchFileNoRegister(Title $title, array $options=[])
Helper function for fetchFileAndTitle.
Definition Parser.php:3735
firstCallInit()
Do various kinds of initialisation on the first call of the parser.
Definition Parser.php:523
preprocessToDom( $text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition Parser.php:2842
$mForceTocPosition
Definition Parser.php:244
markerSkipCallback( $s, callable $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:6166
clearState()
Clear Parser state.
Definition Parser.php:541
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:6196
ParserFactory $factory
Definition Parser.php:331
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
Definition Parser.php:6301
nextLinkID()
Definition Parser.php:1097
getRevisionRecordObject()
Get the revision record object for $this->mRevisionId.
Definition Parser.php:5832
attributeStripCallback(&$text, $frame=false)
Callback from the Sanitizer for expanding items found in HTML attribute values, so they can be safely...
Definition Parser.php:5509
argSubstitution(array $piece, PPFrame $frame)
Triple brace replacement – used for template arguments.
Definition Parser.php:3827
fuzzTestPst( $text, Title $title, ParserOptions $options)
Definition Parser.php:6135
renderImageGallery( $text, array $params)
Renders an image gallery from a text with one line per image.
Definition Parser.php:4979
fuzzTestSrvus( $text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
Strip/replaceVariables/unstrip for preprocessor regression testing.
Definition Parser.php:6117
lock()
Lock the current instance of the parser.
Definition Parser.php:6239
parse( $text, Title $title, ParserOptions $options, $linestart=true, $clearState=true, $revid=null)
Convert wikitext to HTML Do not call this function recursively.
Definition Parser.php:613
$mStripList
Definition Parser.php:159
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
Definition Parser.php:6286
fetchTemplateAndTitle(Title $title)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3527
SectionProfiler $mProfiler
Definition Parser.php:314
static statelessFetchTemplate( $title, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
Definition Parser.php:3586
Differences from DOM schema:
Variant of the Message class.
Group all the pieces relevant to the context of a request into one instance @newable.
setTitle(Title $title=null)
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:42
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1859
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2150
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:541
getBoolOption( $oname)
Get the user's current setting for a given option, as a boolean value.
Definition User.php:2697
getOption( $oname, $defaultOverride=null, $ignoreHidden=false)
Get the user's current setting for a given option.
Definition User.php:2665
isAnon()
Get whether the user is anonymous.
Definition User.php:3087
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 =>