40 private $linkRenderer;
42 private $linkBatchFactory;
52 private $namespaceInfo;
70 private const MAX_ID_SIZE = 7;
72 private const MARKER_PREFIX =
"\x1B\"'";
97 $this->linkRenderer = $linkRenderer;
98 $this->linkBatchFactory = $linkBatchFactory;
99 $this->linkCache = $linkCache;
100 $this->repoGroup = $repoGroup;
101 $this->userLang = $userLang;
102 $this->contLang = $contLang;
103 $this->titleParser = $titleParser;
104 $this->namespaceInfo = $namespaceInfo;
105 $this->hookRunner =
new HookRunner( $hookContainer );
106 $this->linkAlwaysKnownLookup = $linkAlwaysKnownLookup;
121 $samePage =
false, $wikiId =
false, $enableSectionLinks =
true
123 return $this->preprocessInternal( $comment,
false, $selfLinkTarget,
124 $samePage, $wikiId, $enableSectionLinks );
138 $samePage =
false, $wikiId =
false, $enableSectionLinks =
true
140 return $this->preprocessInternal( $comment,
true, $selfLinkTarget,
141 $samePage, $wikiId, $enableSectionLinks );
152 $this->flushLinkBatches();
153 return preg_replace_callback(
154 '/' . self::MARKER_PREFIX .
'([0-9]{' . self::MAX_ID_SIZE .
'})/',
156 $callback = $this->links[(int)$m[1]] ??
null;
160 return '<!-- MISSING -->';
176 private function preprocessInternal( $comment, $unsafe, $selfLinkTarget, $samePage, $wikiId,
181 $comment = strtr( $comment,
"\n\x1b",
" " );
184 $comment = Sanitizer::escapeHtmlAllowEntities( $comment );
186 if ( $enableSectionLinks ) {
187 $comment = $this->doSectionLinks( $comment, $selfLinkTarget, $samePage, $wikiId );
189 return $this->doWikiLinks( $comment, $selfLinkTarget, $samePage, $wikiId );
208 private function doSectionLinks(
210 $selfLinkTarget =
null,
214 $comment = preg_replace_callback(
220 '!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!',
221 function ( $match ) use ( $selfLinkTarget, $samePage, $wikiId ) {
222 $pre = ( $match[1] ??
'' ) !==
'';
223 $section = ( $match[2] ??
'' );
224 $post = ( $match[3] ??
'' ) !==
'';
227 $this->hookRunner->onFormatAutocomments(
228 $comment, $pre, $section, $post,
229 Title::castFromLinkTarget( $selfLinkTarget ),
232 if ( $comment !==
null ) {
237 $parsedSection = $section;
239 if ( $selfLinkTarget ) {
240 $decodedSection = substr( Parser::guessSectionNameFromStrippedText(
243 str_replace( [
'[[:',
'[[',
']]' ],
'', $section )
245 if ( $decodedSection !==
'' || $section ===
'' ) {
247 $targetWithSection =
new TitleValue(
NS_MAIN,
'', $decodedSection );
249 $targetWithSection = $selfLinkTarget->createFragmentTarget( $decodedSection );
251 if ( $section ===
'' ) {
253 $linkHtml =
wfMessage(
'autocomment-top' )->inLanguage( $this->userLang )->escaped();
255 $linkHtml = $parsedSection;
257 $parsedSection = $this->makeSectionLink(
259 $this->userLang->getArrow() .
260 Html::rawElement(
'bdi', [
'dir' => $this->userLang->getDir() ], $linkHtml ),
267 # autocomment $postsep written summary ( summary)
268 $parsedSection .=
wfMessage(
'colon-separator' )->inContentLanguage()->escaped();
270 if ( $parsedSection ) {
271 $parsedSection = Html::rawElement(
'span', [
'class' =>
'autocomment' ], $parsedSection );
274 # written summary $presep autocomment (summary )
275 $parsedSection =
wfMessage(
'autocomment-prefix' )->inContentLanguage()->escaped()
281 return str_replace( [
'[',
']' ], [
'[',
']' ], $parsedSection );
300 private function makeSectionLink(
301 LinkTarget $target, $html, $wikiId, LinkTarget $contextTitle
303 if ( $wikiId !==
null && $wikiId !==
false && !$target->isExternal() ) {
304 return $this->linkRenderer->makeExternalLink(
305 WikiMap::getForeignURL(
307 $target->getNamespace() === 0
308 ? $target->getDBkey()
309 : $this->namespaceInfo->getCanonicalName( $target->getNamespace() ) .
310 ':' . $target->getDBkey(),
311 $target->getFragment()
313 new HtmlArmor( $html ),
316 } elseif ( !$target->hasFragment() && $target->getText() ===
'' && $target->getNamespace() ===
NS_MAIN ) {
320 return Html::rawElement(
'a', [
324 return $this->linkRenderer->makePreloadedLink( $target,
new HtmlArmor( $html ),
'' );
345 private function doWikiLinks( $comment, $selfLinkTarget =
null, $samePage =
false, $wikiId =
false ) {
346 return preg_replace_callback(
349 \s*+ # ignore leading whitespace, the *+ quantifier disallows backtracking
350 :? # ignore optional leading colon
351 ([^[\]|]+) # 1. link target; page names cannot include [, ] or |
354 # Stop matching at ]] without relying on backtracking.
358 ([^[]*) # 3. link trail (the text up until the next link)
360 function ( $match ) use ( $selfLinkTarget, $samePage, $wikiId ) {
362 $medians .= preg_quote(
363 $this->namespaceInfo->getCanonicalName(
NS_MEDIA ),
'/' );
365 $medians .= preg_quote(
366 $this->contLang->getNsText(
NS_MEDIA ),
370 $comment = $match[0];
373 if ( str_contains( $match[1],
'%' ) ) {
375 rawurldecode( $match[1] ),
376 [
'<' =>
'<',
'>' =>
'>' ]
381 if ( $match[2] !=
"" ) {
388 if ( preg_match(
'/^' . $medians .
'(.*)$/i', $match[1], $submatch ) ) {
390 $linkRegexp =
'/\[\[(.*?)\]\]/';
391 $linkTarget = $this->titleParser->makeTitleValueSafe(
NS_FILE, $submatch[1] );
393 $linkMarker = $this->addFileLink( $linkTarget, $text );
398 if ( isset( $match[1][0] ) && $match[1][0] ==
':' ) {
399 $match[1] = substr( $match[1], 1 );
401 if ( $match[1] !==
false && $match[1] !==
null && $match[1] !==
'' ) {
403 $this->contLang->linkTrail(),
407 $trail = $submatch[1];
411 $linkRegexp =
'/\[\[(.*?)\]\]' . preg_quote( $trail,
'/' ) .
'/';
412 [ $inside, $trail ] = Linker::splitTrail( $trail );
415 $linkTarget = Linker::normalizeSubpageLink( $selfLinkTarget, $match[1], $linkText );
418 $target = $this->titleParser->parseTitle( $linkTarget );
420 if ( $target->getText() ==
'' && !$target->isExternal()
421 && !$samePage && $selfLinkTarget
423 $target = $selfLinkTarget->createFragmentTarget( $target->getFragment() );
431 $linkMarker = $this->addPageLink(
437 $linkMarker .= $trail;
438 }
catch ( MalformedTitleException ) {
445 $comment = preg_replace(
466 private function addLinkMarker( $callback ) {
467 $nextId = count( $this->links );
468 if ( strlen( (
string)$nextId ) > self::MAX_ID_SIZE ) {
469 throw new \RuntimeException(
'Too many links in comment batch' );
471 $this->links[] = $callback;
472 return sprintf( self::MARKER_PREFIX .
"%0" . self::MAX_ID_SIZE .
'd', $nextId );
485 private function addPageLink( LinkTarget $target, $text, $wikiId, LinkTarget $contextTitle ) {
486 if ( $wikiId !==
null && $wikiId !==
false && !$target->isExternal() ) {
488 return $this->linkRenderer->makeExternalLink(
489 WikiMap::getForeignURL(
491 $target->getNamespace() === 0
492 ? $target->getDBkey()
493 : $this->namespaceInfo->getCanonicalName( $target->getNamespace() ) .
494 ':' . $target->getDBkey(),
495 $target->getFragment()
497 new HtmlArmor( $text ),
500 } elseif ( $this->linkCache->getGoodLinkID( $target ) ||
501 $this->linkAlwaysKnownLookup->isAlwaysKnown( $target )
504 return $this->linkRenderer->makeKnownLink( $target,
new HtmlArmor( $text ) );
505 } elseif ( $this->linkCache->isBadLink( $target ) ) {
507 return $this->linkRenderer->makeBrokenLink( $target,
new HtmlArmor( $text ) );
511 if ( !$this->linkBatch ) {
512 $this->linkBatch = $this->linkBatchFactory->newLinkBatch();
513 $this->linkBatch->setCaller( __METHOD__ );
515 $this->linkBatch->addObj( $target );
516 return $this->addLinkMarker(
function () use ( $target, $text ) {
517 return $this->linkRenderer->makeLink( $target,
new HtmlArmor( $text ) );
528 private function addFileLink( LinkTarget $target, $html ) {
529 $this->fileBatch[] = [
532 return $this->addLinkMarker(
function () use ( $target, $html ) {
533 return Linker::makeMediaLinkFile(
535 $this->files[$target->getDBkey()] ??
false,
544 private function flushLinkBatches() {
545 if ( $this->linkBatch ) {
546 $this->linkBatch->execute();
547 $this->linkBatch =
null;
549 if ( $this->fileBatch ) {
550 $this->files += $this->repoGroup->findFiles( $this->fileBatch );
551 $this->fileBatch = [];