39 private $linkRenderer;
41 private $linkBatchFactory;
51 private $namespaceInfo;
68 private const MAX_ID_SIZE = 7;
70 private const MARKER_PREFIX =
"\x1B\"'";
94 $this->linkRenderer = $linkRenderer;
95 $this->linkBatchFactory = $linkBatchFactory;
96 $this->linkCache = $linkCache;
97 $this->repoGroup = $repoGroup;
98 $this->userLang = $userLang;
99 $this->contLang = $contLang;
100 $this->titleParser = $titleParser;
101 $this->namespaceInfo = $namespaceInfo;
102 $this->hookRunner =
new HookRunner( $hookContainer );
117 $samePage =
false, $wikiId =
false, $enableSectionLinks =
true
119 return $this->preprocessInternal( $comment,
false, $selfLinkTarget,
120 $samePage, $wikiId, $enableSectionLinks );
134 $samePage =
false, $wikiId =
false, $enableSectionLinks =
true
136 return $this->preprocessInternal( $comment,
true, $selfLinkTarget,
137 $samePage, $wikiId, $enableSectionLinks );
148 $this->flushLinkBatches();
149 return preg_replace_callback(
150 '/' . self::MARKER_PREFIX .
'([0-9]{' . self::MAX_ID_SIZE .
'})/',
152 $callback = $this->links[(int)$m[1]] ??
null;
156 return '<!-- MISSING -->';
172 private function preprocessInternal( $comment, $unsafe, $selfLinkTarget, $samePage, $wikiId,
177 $comment = strtr( $comment,
"\n\x1b",
" " );
180 $comment = Sanitizer::escapeHtmlAllowEntities( $comment );
182 if ( $enableSectionLinks ) {
183 $comment = $this->doSectionLinks( $comment, $selfLinkTarget, $samePage, $wikiId );
185 return $this->doWikiLinks( $comment, $selfLinkTarget, $samePage, $wikiId );
204 private function doSectionLinks(
206 $selfLinkTarget =
null,
210 $comment = preg_replace_callback(
216 '!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!',
217 function ( $match ) use ( $selfLinkTarget, $samePage, $wikiId ) {
218 $pre = ( $match[1] ??
'' ) !==
'';
219 $section = ( $match[2] ??
'' );
220 $post = ( $match[3] ??
'' ) !==
'';
223 $this->hookRunner->onFormatAutocomments(
224 $comment, $pre, $section, $post,
225 Title::castFromLinkTarget( $selfLinkTarget ),
228 if ( $comment !==
null ) {
233 $parsedSection = $section;
235 if ( $selfLinkTarget ) {
236 $decodedSection = substr( Parser::guessSectionNameFromStrippedText(
239 str_replace( [
'[[:',
'[[',
']]' ],
'', $section )
241 if ( $decodedSection !==
'' || $section ===
'' ) {
243 $targetWithSection =
new TitleValue(
NS_MAIN,
'', $decodedSection );
245 $targetWithSection = $selfLinkTarget->createFragmentTarget( $decodedSection );
247 if ( $section ===
'' ) {
249 $linkHtml =
wfMessage(
'autocomment-top' )->inLanguage( $this->userLang )->escaped();
251 $linkHtml = $parsedSection;
253 $parsedSection = $this->makeSectionLink(
255 $this->userLang->getArrow() .
256 Html::rawElement(
'bdi', [
'dir' => $this->userLang->getDir() ], $linkHtml ),
263 # autocomment $postsep written summary ( summary)
264 $parsedSection .=
wfMessage(
'colon-separator' )->inContentLanguage()->escaped();
266 if ( $parsedSection ) {
267 $parsedSection = Html::rawElement(
'span', [
'class' =>
'autocomment' ], $parsedSection );
270 # written summary $presep autocomment (summary )
271 $parsedSection =
wfMessage(
'autocomment-prefix' )->inContentLanguage()->escaped()
277 return str_replace( [
'[',
']' ], [
'[',
']' ], $parsedSection );
296 private function makeSectionLink(
297 LinkTarget $target, $text, $wikiId, LinkTarget $contextTitle
299 if ( $wikiId !==
null && $wikiId !==
false && !$target->isExternal() ) {
300 return $this->linkRenderer->makeExternalLink(
301 WikiMap::getForeignURL(
303 $target->getNamespace() === 0
304 ? $target->getDBkey()
305 : $this->namespaceInfo->getCanonicalName( $target->getNamespace() ) .
306 ':' . $target->getDBkey(),
307 $target->getFragment()
309 new HtmlArmor( $text ),
313 return $this->linkRenderer->makePreloadedLink( $target,
new HtmlArmor( $text ),
'' );
334 private function doWikiLinks( $comment, $selfLinkTarget =
null, $samePage =
false, $wikiId =
false ) {
335 return preg_replace_callback(
338 \s*+ # ignore leading whitespace, the *+ quantifier disallows backtracking
339 :? # ignore optional leading colon
340 ([^[\]|]+) # 1. link target; page names cannot include [, ] or |
343 # Stop matching at ]] without relying on backtracking.
347 ([^[]*) # 3. link trail (the text up until the next link)
349 function ( $match ) use ( $selfLinkTarget, $samePage, $wikiId ) {
351 $medians .= preg_quote(
352 $this->namespaceInfo->getCanonicalName(
NS_MEDIA ),
'/' );
354 $medians .= preg_quote(
355 $this->contLang->getNsText(
NS_MEDIA ),
359 $comment = $match[0];
362 if ( str_contains( $match[1],
'%' ) ) {
364 rawurldecode( $match[1] ),
365 [
'<' =>
'<',
'>' =>
'>' ]
370 if ( $match[2] !=
"" ) {
377 if ( preg_match(
'/^' . $medians .
'(.*)$/i', $match[1], $submatch ) ) {
379 $linkRegexp =
'/\[\[(.*?)\]\]/';
380 $linkTarget = $this->titleParser->makeTitleValueSafe(
NS_FILE, $submatch[1] );
382 $linkMarker = $this->addFileLink( $linkTarget, $text );
387 if ( isset( $match[1][0] ) && $match[1][0] ==
':' ) {
388 $match[1] = substr( $match[1], 1 );
390 if ( $match[1] !==
false && $match[1] !==
null && $match[1] !==
'' ) {
392 $this->contLang->linkTrail(),
396 $trail = $submatch[1];
400 $linkRegexp =
'/\[\[(.*?)\]\]' . preg_quote( $trail,
'/' ) .
'/';
401 [ $inside, $trail ] = Linker::splitTrail( $trail );
404 $linkTarget = Linker::normalizeSubpageLink( $selfLinkTarget, $match[1], $linkText );
407 $target = $this->titleParser->parseTitle( $linkTarget );
409 if ( $target->getText() ==
'' && !$target->isExternal()
410 && !$samePage && $selfLinkTarget
412 $target = $selfLinkTarget->createFragmentTarget( $target->getFragment() );
420 $linkMarker = $this->addPageLink(
426 $linkMarker .= $trail;
427 }
catch ( MalformedTitleException ) {
434 $comment = preg_replace(
455 private function addLinkMarker( $callback ) {
456 $nextId = count( $this->links );
457 if ( strlen( (
string)$nextId ) > self::MAX_ID_SIZE ) {
458 throw new \RuntimeException(
'Too many links in comment batch' );
460 $this->links[] = $callback;
461 return sprintf( self::MARKER_PREFIX .
"%0" . self::MAX_ID_SIZE .
'd', $nextId );
474 private function addPageLink( LinkTarget $target, $text, $wikiId, LinkTarget $contextTitle ) {
475 if ( $wikiId !==
null && $wikiId !==
false && !$target->isExternal() ) {
477 return $this->linkRenderer->makeExternalLink(
478 WikiMap::getForeignURL(
480 $target->getNamespace() === 0
481 ? $target->getDBkey()
482 : $this->namespaceInfo->getCanonicalName( $target->getNamespace() ) .
483 ':' . $target->getDBkey(),
484 $target->getFragment()
486 new HtmlArmor( $text ),
489 } elseif ( $this->linkCache->getGoodLinkID( $target ) ||
490 Title::newFromLinkTarget( $target )->isAlwaysKnown()
493 return $this->linkRenderer->makeKnownLink( $target,
new HtmlArmor( $text ) );
494 } elseif ( $this->linkCache->isBadLink( $target ) ) {
496 return $this->linkRenderer->makeBrokenLink( $target,
new HtmlArmor( $text ) );
500 if ( !$this->linkBatch ) {
501 $this->linkBatch = $this->linkBatchFactory->newLinkBatch();
502 $this->linkBatch->setCaller( __METHOD__ );
504 $this->linkBatch->addObj( $target );
505 return $this->addLinkMarker(
function () use ( $target, $text ) {
506 return $this->linkRenderer->makeLink( $target,
new HtmlArmor( $text ) );
517 private function addFileLink( LinkTarget $target, $html ) {
518 $this->fileBatch[] = [
521 return $this->addLinkMarker(
function () use ( $target, $html ) {
522 return Linker::makeMediaLinkFile(
524 $this->files[$target->getDBkey()] ??
false,
533 private function flushLinkBatches() {
534 if ( $this->linkBatch ) {
535 $this->linkBatch->execute();
536 $this->linkBatch =
null;
538 if ( $this->fileBatch ) {
539 $this->files += $this->repoGroup->findFiles( $this->fileBatch );
540 $this->fileBatch = [];