Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 213 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
| FullSearchResultWidget | |
0.00% |
0 / 213 |
|
0.00% |
0 / 14 |
3422 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
| render | |
0.00% |
0 / 58 |
|
0.00% |
0 / 1 |
110 | |||
| generateMainLinkHtml | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
12 | |||
| generateAltTitleHtml | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| generateRedirectHtml | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| generateSectionHtml | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| generateCategoryHtml | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| generateSizeHtml | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
| generateFileHtml | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
90 | |||
| getThumbnail | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
| generateThumbnailHtml | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
72 | |||
| transformThumbnail | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
| generateThumbnailPlaceholderHtml | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
| buildMeta | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace MediaWiki\Search\SearchWidgets; |
| 4 | |
| 5 | use MediaWiki\Category\Category; |
| 6 | use MediaWiki\FileRepo\File\File; |
| 7 | use MediaWiki\FileRepo\RepoGroup; |
| 8 | use MediaWiki\HookContainer\HookContainer; |
| 9 | use MediaWiki\HookContainer\HookRunner; |
| 10 | use MediaWiki\Html\Html; |
| 11 | use MediaWiki\Linker\LinkRenderer; |
| 12 | use MediaWiki\MainConfigNames; |
| 13 | use MediaWiki\Media\MediaTransformOutput; |
| 14 | use MediaWiki\Media\ThumbnailImage; |
| 15 | use MediaWiki\Message\Message; |
| 16 | use MediaWiki\Parser\Sanitizer; |
| 17 | use MediaWiki\Search\Entity\SearchResultThumbnail; |
| 18 | use MediaWiki\Search\SearchResult; |
| 19 | use MediaWiki\Search\SearchResultThumbnailProvider; |
| 20 | use MediaWiki\Specials\SpecialSearch; |
| 21 | use MediaWiki\Title\Title; |
| 22 | use MediaWiki\User\Options\UserOptionsManager; |
| 23 | use Wikimedia\HtmlArmor\HtmlArmor; |
| 24 | |
| 25 | /** |
| 26 | * Renders a 'full' multi-line search result with metadata. |
| 27 | * |
| 28 | * The Title |
| 29 | * some *highlighted* *text* about the search result |
| 30 | * 5 KiB (651 words) - 12:40, 6 Aug 2016 |
| 31 | */ |
| 32 | class FullSearchResultWidget implements SearchResultWidget { |
| 33 | |
| 34 | public const THUMBNAIL_SIZE = 90; |
| 35 | |
| 36 | protected SpecialSearch $specialPage; |
| 37 | protected LinkRenderer $linkRenderer; |
| 38 | private HookRunner $hookRunner; |
| 39 | private RepoGroup $repoGroup; |
| 40 | private SearchResultThumbnailProvider $thumbnailProvider; |
| 41 | private UserOptionsManager $userOptionsManager; |
| 42 | |
| 43 | /** @var string */ |
| 44 | private $thumbnailPlaceholderHtml; |
| 45 | |
| 46 | public function __construct( |
| 47 | SpecialSearch $specialPage, |
| 48 | LinkRenderer $linkRenderer, |
| 49 | HookContainer $hookContainer, |
| 50 | RepoGroup $repoGroup, |
| 51 | SearchResultThumbnailProvider $thumbnailProvider, |
| 52 | UserOptionsManager $userOptionsManager |
| 53 | ) { |
| 54 | $this->specialPage = $specialPage; |
| 55 | $this->linkRenderer = $linkRenderer; |
| 56 | $this->hookRunner = new HookRunner( $hookContainer ); |
| 57 | $this->repoGroup = $repoGroup; |
| 58 | $this->thumbnailProvider = $thumbnailProvider; |
| 59 | $this->userOptionsManager = $userOptionsManager; |
| 60 | } |
| 61 | |
| 62 | /** |
| 63 | * @param SearchResult $result The result to render |
| 64 | * @param int $position The result position, including offset |
| 65 | * @return string HTML |
| 66 | */ |
| 67 | public function render( SearchResult $result, $position ) { |
| 68 | // If the page doesn't *exist*... our search index is out of date. |
| 69 | // The least confusing at this point is to drop the result. |
| 70 | // You may get less results, but... on well. :P |
| 71 | if ( $result->isBrokenTitle() || $result->isMissingRevision() ) { |
| 72 | return ''; |
| 73 | } |
| 74 | |
| 75 | $link = $this->generateMainLinkHtml( $result, $position ); |
| 76 | // If page content is not readable, just return ths title. |
| 77 | // This is not quite safe, but better than showing excerpts from |
| 78 | // non-readable pages. Note that hiding the entry entirely would |
| 79 | // screw up paging (really?). |
| 80 | if ( !$this->specialPage->getAuthority()->definitelyCan( 'read', $result->getTitle() ) ) { |
| 81 | return Html::rawElement( 'li', [], $link ); |
| 82 | } |
| 83 | |
| 84 | $redirect = $this->generateRedirectHtml( $result ); |
| 85 | $section = $this->generateSectionHtml( $result ); |
| 86 | $category = $this->generateCategoryHtml( $result ); |
| 87 | $date = htmlspecialchars( |
| 88 | $this->specialPage->getLanguage()->userTimeAndDate( |
| 89 | $result->getTimestamp(), |
| 90 | $this->specialPage->getUser() |
| 91 | ) |
| 92 | ); |
| 93 | [ $file, $desc, $thumb ] = $this->generateFileHtml( $result ); |
| 94 | $snippet = $result->getTextSnippet(); |
| 95 | if ( $snippet ) { |
| 96 | $snippetWithEllipsis = $snippet . $this->specialPage->msg( 'ellipsis' ); |
| 97 | $extract = Html::rawElement( 'div', [ 'class' => 'searchresult' ], $snippetWithEllipsis ); |
| 98 | } else { |
| 99 | $extract = ''; |
| 100 | } |
| 101 | |
| 102 | if ( $result->getTitle() && $result->getTitle()->getNamespace() !== NS_FILE ) { |
| 103 | // If no file, then the description is about size |
| 104 | $desc = $this->generateSizeHtml( $result ); |
| 105 | |
| 106 | // Let hooks do their own final construction if desired. |
| 107 | // FIXME: Not sure why this is only for results without thumbnails, |
| 108 | // but keeping it as-is for now to prevent breaking hook consumers. |
| 109 | $html = null; |
| 110 | $score = ''; |
| 111 | $related = ''; |
| 112 | // TODO: remove this instanceof and always pass [], let implementers do the cast if |
| 113 | // they want to be SearchDatabase specific |
| 114 | $terms = $result instanceof \MediaWiki\Search\SqlSearchResult ? $result->getTermMatches() : []; |
| 115 | if ( !$this->hookRunner->onShowSearchHit( $this->specialPage, $result, |
| 116 | $terms, $link, $redirect, $section, $extract, $score, |
| 117 | // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args |
| 118 | $desc, $date, $related, $html ) |
| 119 | ) { |
| 120 | return $html; |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | // All the pieces have been collected. Now generate the final HTML |
| 125 | $joined = "{$link} {$redirect} {$category} {$section} {$file}"; |
| 126 | $meta = $this->buildMeta( $desc, $date ); |
| 127 | |
| 128 | // Text portion of the search result |
| 129 | $html = Html::rawElement( |
| 130 | 'div', |
| 131 | [ 'class' => 'mw-search-result-heading' ], |
| 132 | $joined |
| 133 | ); |
| 134 | $html .= $extract . ' ' . $meta; |
| 135 | |
| 136 | // If the result has a thumbnail, place it next to the text block |
| 137 | if ( $thumb ) { |
| 138 | $gridCells = Html::rawElement( |
| 139 | 'div', |
| 140 | [ 'class' => 'searchResultImage-thumbnail' ], |
| 141 | $thumb |
| 142 | ) . Html::rawElement( |
| 143 | 'div', |
| 144 | [ 'class' => 'searchResultImage-text' ], |
| 145 | $html |
| 146 | ); |
| 147 | $html = Html::rawElement( |
| 148 | 'div', |
| 149 | [ 'class' => 'searchResultImage' ], |
| 150 | $gridCells |
| 151 | ); |
| 152 | } |
| 153 | |
| 154 | return Html::rawElement( |
| 155 | 'li', |
| 156 | [ 'class' => [ 'mw-search-result', 'mw-search-result-ns-' . $result->getTitle()->getNamespace() ] ], |
| 157 | $html |
| 158 | ); |
| 159 | } |
| 160 | |
| 161 | /** |
| 162 | * Generates HTML for the primary call to action. It is |
| 163 | * typically the article title, but the search engine can |
| 164 | * return an exact snippet to use (typically the article |
| 165 | * title with highlighted words). |
| 166 | * |
| 167 | * @param SearchResult $result |
| 168 | * @param int $position |
| 169 | * @return string HTML |
| 170 | */ |
| 171 | protected function generateMainLinkHtml( SearchResult $result, $position ) { |
| 172 | $snippet = $result->getTitleSnippet(); |
| 173 | if ( $snippet === '' ) { |
| 174 | $snippet = null; |
| 175 | } else { |
| 176 | $snippet = new HtmlArmor( $snippet ); |
| 177 | } |
| 178 | |
| 179 | // clone to prevent hook from changing the title stored inside $result |
| 180 | $title = clone $result->getTitle(); |
| 181 | $query = []; |
| 182 | |
| 183 | $attributes = [ 'data-serp-pos' => $position ]; |
| 184 | $this->hookRunner->onShowSearchHitTitle( $title, $snippet, $result, |
| 185 | $result instanceof \MediaWiki\Search\SqlSearchResult ? $result->getTermMatches() : [], |
| 186 | // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args |
| 187 | $this->specialPage, $query, $attributes ); |
| 188 | |
| 189 | $link = $this->linkRenderer->makeLink( |
| 190 | $title, |
| 191 | $snippet, |
| 192 | $attributes, |
| 193 | $query |
| 194 | ); |
| 195 | |
| 196 | return $link; |
| 197 | } |
| 198 | |
| 199 | /** |
| 200 | * Generates an alternate title link, such as (redirect from <a>Foo</a>). |
| 201 | * |
| 202 | * @param string $msgKey i18n message used to wrap title |
| 203 | * @param Title|null $title The title to link to, or null to generate |
| 204 | * the message without a link. In that case $text must be non-null. |
| 205 | * @param string|null $text The text snippet to display, or null |
| 206 | * to use the title |
| 207 | * @return string HTML |
| 208 | */ |
| 209 | protected function generateAltTitleHtml( $msgKey, ?Title $title, $text ) { |
| 210 | $inner = $title === null |
| 211 | ? $text |
| 212 | : $this->linkRenderer->makeLink( $title, $text ? new HtmlArmor( $text ) : null ); |
| 213 | |
| 214 | return "<span class='searchalttitle'>" . |
| 215 | $this->specialPage->msg( $msgKey )->rawParams( $inner )->parse() |
| 216 | . "</span>"; |
| 217 | } |
| 218 | |
| 219 | /** |
| 220 | * @param SearchResult $result |
| 221 | * @return string HTML |
| 222 | */ |
| 223 | protected function generateRedirectHtml( SearchResult $result ) { |
| 224 | $title = $result->getRedirectTitle(); |
| 225 | return $title === null |
| 226 | ? '' |
| 227 | : $this->generateAltTitleHtml( 'search-redirect', $title, $result->getRedirectSnippet() ); |
| 228 | } |
| 229 | |
| 230 | /** |
| 231 | * @param SearchResult $result |
| 232 | * @return string HTML |
| 233 | */ |
| 234 | protected function generateSectionHtml( SearchResult $result ) { |
| 235 | $title = $result->getSectionTitle(); |
| 236 | return $title === null |
| 237 | ? '' |
| 238 | : $this->generateAltTitleHtml( 'search-section', $title, $result->getSectionSnippet() ); |
| 239 | } |
| 240 | |
| 241 | /** |
| 242 | * @param SearchResult $result |
| 243 | * @return string HTML |
| 244 | */ |
| 245 | protected function generateCategoryHtml( SearchResult $result ) { |
| 246 | $snippet = $result->getCategorySnippet(); |
| 247 | return $snippet |
| 248 | ? $this->generateAltTitleHtml( 'search-category', null, $snippet ) |
| 249 | : ''; |
| 250 | } |
| 251 | |
| 252 | /** |
| 253 | * @param SearchResult $result |
| 254 | * @return string HTML |
| 255 | */ |
| 256 | protected function generateSizeHtml( SearchResult $result ) { |
| 257 | $title = $result->getTitle(); |
| 258 | if ( $title->getNamespace() === NS_CATEGORY ) { |
| 259 | $cat = Category::newFromTitle( $title ); |
| 260 | return $this->specialPage->msg( 'search-result-category-size' ) |
| 261 | ->numParams( $cat->getMemberCount(), $cat->getSubcatCount(), $cat->getFileCount() ) |
| 262 | ->escaped(); |
| 263 | // TODO: This is a bit odd...but requires changing the i18n message to fix |
| 264 | } elseif ( $result->getByteSize() !== null || $result->getWordCount() > 0 ) { |
| 265 | return $this->specialPage->msg( 'search-result-size' ) |
| 266 | ->sizeParams( $result->getByteSize() ) |
| 267 | ->numParams( $result->getWordCount() ) |
| 268 | ->escaped(); |
| 269 | } |
| 270 | |
| 271 | return ''; |
| 272 | } |
| 273 | |
| 274 | /** |
| 275 | * @param SearchResult $result |
| 276 | * @return array Three element array containing the main file html, |
| 277 | * a text description of the file, and finally the thumbnail html. |
| 278 | * If no thumbnail is available the second and third will be null. |
| 279 | */ |
| 280 | protected function generateFileHtml( SearchResult $result ) { |
| 281 | $title = $result->getTitle(); |
| 282 | // don't assume that result is a valid title; e.g. could be an interwiki link target |
| 283 | if ( $title === null || !$title->canExist() ) { |
| 284 | return [ '', null, null ]; |
| 285 | } |
| 286 | |
| 287 | $html = ''; |
| 288 | if ( $result->isFileMatch() ) { |
| 289 | $html = Html::element( 'span', |
| 290 | [ 'class' => 'searchalttitle' ], |
| 291 | $this->specialPage->msg( 'search-file-match' )->text() |
| 292 | ); |
| 293 | } |
| 294 | |
| 295 | $allowExtraThumbsFromRequest = $this->specialPage->getRequest()->getVal( 'search-thumbnail-extra-namespaces' ); |
| 296 | $allowExtraThumbsFromPreference = $this->userOptionsManager->getOption( |
| 297 | $this->specialPage->getUser(), |
| 298 | 'search-thumbnail-extra-namespaces' |
| 299 | ); |
| 300 | $allowExtraThumbs = (bool)( $allowExtraThumbsFromRequest ?? $allowExtraThumbsFromPreference ); |
| 301 | if ( !$allowExtraThumbs && $title->getNamespace() !== NS_FILE ) { |
| 302 | return [ $html, null, null ]; |
| 303 | } |
| 304 | |
| 305 | $thumbnail = $this->getThumbnail( $result, self::THUMBNAIL_SIZE ); |
| 306 | $thumbnailName = $thumbnail ? $thumbnail->getName() : null; |
| 307 | if ( !$thumbnailName ) { |
| 308 | return [ $html, null, $this->generateThumbnailHtml( $result ) ]; |
| 309 | } |
| 310 | |
| 311 | $img = $this->repoGroup->findFile( $thumbnailName ); |
| 312 | if ( !$img ) { |
| 313 | return [ $html, null, $this->generateThumbnailHtml( $result ) ]; |
| 314 | } |
| 315 | |
| 316 | // File::getShortDesc() is documented to return HTML, but many handlers used to incorrectly |
| 317 | // return plain text (T395834), so sanitize it in case the same bug is present in extensions. |
| 318 | $unsafeShortDesc = $img->getShortDesc( $this->specialPage->getLanguage() ); |
| 319 | $shortDesc = Sanitizer::removeSomeTags( $unsafeShortDesc ); |
| 320 | |
| 321 | return [ |
| 322 | $html, |
| 323 | $this->specialPage->msg( 'parentheses' )->rawParams( $shortDesc )->escaped(), |
| 324 | $this->generateThumbnailHtml( $result, $thumbnail ) |
| 325 | ]; |
| 326 | } |
| 327 | |
| 328 | /** |
| 329 | * @param SearchResult $result |
| 330 | * @param int $size |
| 331 | * @return SearchResultThumbnail|null |
| 332 | */ |
| 333 | private function getThumbnail( SearchResult $result, int $size ): ?SearchResultThumbnail { |
| 334 | $title = $result->getTitle(); |
| 335 | // don't assume that result is a valid title; e.g. could be an interwiki link target |
| 336 | if ( $title === null || !$title->canExist() ) { |
| 337 | return null; |
| 338 | } |
| 339 | |
| 340 | $thumbnails = $this->thumbnailProvider->getThumbnails( [ $title->getArticleID() => $title ], $size ); |
| 341 | |
| 342 | return $thumbnails[ $title->getArticleID() ] ?? null; |
| 343 | } |
| 344 | |
| 345 | /** |
| 346 | * @param SearchResult $result |
| 347 | * @param SearchResultThumbnail|null $thumbnail |
| 348 | * @return string|null |
| 349 | */ |
| 350 | private function generateThumbnailHtml( SearchResult $result, ?SearchResultThumbnail $thumbnail = null ): ?string { |
| 351 | $title = $result->getTitle(); |
| 352 | // don't assume that result is a valid title; e.g. could be an interwiki link target |
| 353 | if ( $title === null || !$title->canExist() ) { |
| 354 | return null; |
| 355 | } |
| 356 | |
| 357 | $thumbnailNamespaces = $this->specialPage->getConfig()->get( MainConfigNames::ThumbnailNamespaces ); |
| 358 | if ( !$title->inNamespaces( $thumbnailNamespaces ) ) { |
| 359 | return null; |
| 360 | } |
| 361 | |
| 362 | $thumbnailName = $thumbnail?->getName(); |
| 363 | if ( $thumbnailName === null ) { |
| 364 | return $this->generateThumbnailPlaceholderHtml(); |
| 365 | } |
| 366 | |
| 367 | $img = $this->repoGroup->findFile( $thumbnailName ); |
| 368 | if ( !$img ) { |
| 369 | return $this->generateThumbnailPlaceholderHtml(); |
| 370 | } |
| 371 | |
| 372 | $thumb = $this->transformThumbnail( $img, $thumbnail ); |
| 373 | if ( $thumb ) { |
| 374 | if ( $title->getNamespace() === NS_FILE ) { |
| 375 | // don't use a custom link, just use traditional thumbnail HTML |
| 376 | // @phan-suppress-next-line SecurityCheck-DoubleEscaped |
| 377 | return $thumb->toHtml( [ |
| 378 | 'desc-link' => true, |
| 379 | 'loading' => 'lazy', |
| 380 | 'alt' => $this->specialPage->msg( 'search-thumbnail-alt', $title->getPrefixedText() ), |
| 381 | ] ); |
| 382 | } |
| 383 | |
| 384 | // thumbnails for non-file results should link to the relevant title |
| 385 | // @phan-suppress-next-line SecurityCheck-DoubleEscaped |
| 386 | return $thumb->toHtml( [ |
| 387 | 'desc-link' => true, |
| 388 | 'custom-title-link' => $title, |
| 389 | 'loading' => 'lazy', |
| 390 | 'alt' => $this->specialPage->msg( 'search-thumbnail-alt', $title->getPrefixedText() ), |
| 391 | ] ); |
| 392 | } |
| 393 | |
| 394 | return $this->generateThumbnailPlaceholderHtml(); |
| 395 | } |
| 396 | |
| 397 | /** |
| 398 | * @param File $img |
| 399 | * @param SearchResultThumbnail $thumbnail |
| 400 | * @return ThumbnailImage|MediaTransformOutput|bool False on failure |
| 401 | */ |
| 402 | private function transformThumbnail( File $img, SearchResultThumbnail $thumbnail ) { |
| 403 | $optimalThumbnailWidth = $thumbnail->getWidth(); |
| 404 | |
| 405 | // $thumb will have rescaled to fit within a <$size>x<$size> bounding |
| 406 | // box, but we want it to cover a full square (at the cost of losing |
| 407 | // some of the edges) |
| 408 | // instead of the largest side matching up with $size, we want the |
| 409 | // smallest size to match (or exceed) $size |
| 410 | $thumbnailMaxDimension = max( $thumbnail->getWidth(), $thumbnail->getHeight() ); |
| 411 | $thumbnailMinDimension = min( $thumbnail->getWidth(), $thumbnail->getHeight() ); |
| 412 | $rescaleCoefficient = $thumbnailMinDimension |
| 413 | ? $thumbnailMaxDimension / $thumbnailMinDimension : 1; |
| 414 | |
| 415 | // we'll only deal with width from now on since conventions for |
| 416 | // standard sizes have formed around width; height will simply |
| 417 | // follow according to aspect ratio |
| 418 | $rescaledWidth = (int)round( $rescaleCoefficient * $thumbnail->getWidth() ); |
| 419 | |
| 420 | // we'll also be looking at $wgThumbLimits to ensure that we pick |
| 421 | // from within the predefined list of sizes |
| 422 | // NOTE: only do this when there is a difference in the rescaled |
| 423 | // size vs the original thumbnail size - some media types are |
| 424 | // different and thumb limits don't matter (e.g. for audio, the |
| 425 | // player must remain at the size we want, regardless of whether or |
| 426 | // not it fits the thumb limits, which in this case are irrelevant) |
| 427 | if ( $rescaledWidth !== $thumbnail->getWidth() ) { |
| 428 | $thumbLimits = $this->specialPage->getConfig()->get( MainConfigNames::ThumbLimits ); |
| 429 | $largerThumbLimits = array_filter( |
| 430 | $thumbLimits, |
| 431 | static function ( $limit ) use ( $rescaledWidth ) { |
| 432 | return $limit >= $rescaledWidth; |
| 433 | } |
| 434 | ); |
| 435 | $optimalThumbnailWidth = $largerThumbLimits ? min( $largerThumbLimits ) : max( $thumbLimits ); |
| 436 | } |
| 437 | |
| 438 | return $img->transform( [ 'width' => $optimalThumbnailWidth ] ); |
| 439 | } |
| 440 | |
| 441 | private function generateThumbnailPlaceholderHtml(): string { |
| 442 | if ( $this->thumbnailPlaceholderHtml ) { |
| 443 | return $this->thumbnailPlaceholderHtml; |
| 444 | } |
| 445 | |
| 446 | $path = MW_INSTALL_PATH . '/resources/lib/ooui/themes/wikimediaui/images/icons/imageLayoutFrameless.svg'; |
| 447 | $this->thumbnailPlaceholderHtml = Html::rawElement( |
| 448 | 'div', |
| 449 | [ |
| 450 | 'class' => 'searchResultImage-thumbnail-placeholder', |
| 451 | 'aria-hidden' => 'true', |
| 452 | ], |
| 453 | file_get_contents( $path ) |
| 454 | ); |
| 455 | return $this->thumbnailPlaceholderHtml; |
| 456 | } |
| 457 | |
| 458 | /** |
| 459 | * @param string $desc HTML description of result, ex: size in bytes, or empty string |
| 460 | * @param string $date HTML representation of last edit date, or empty string |
| 461 | * @return string HTML A div combining $desc and $date with a separator in a <div>. |
| 462 | * If either is missing only one will be represented. If both are missing an empty |
| 463 | * string will be returned. |
| 464 | */ |
| 465 | protected function buildMeta( $desc, $date ) { |
| 466 | if ( $desc && $date ) { |
| 467 | $meta = $this->specialPage->msg( 'search-result-meta-separator', |
| 468 | Message::rawParam( $desc ), |
| 469 | Message::rawParam( $date ) |
| 470 | )->escaped(); |
| 471 | } elseif ( $desc ) { |
| 472 | $meta = $desc; |
| 473 | } elseif ( $date ) { |
| 474 | $meta = $date; |
| 475 | } else { |
| 476 | return ''; |
| 477 | } |
| 478 | |
| 479 | return "<div class='mw-search-result-data'>{$meta}</div>"; |
| 480 | } |
| 481 | } |