From a0936780bb089f119fa1e11621a92facbe9f8718 Mon Sep 17 00:00:00 2001 From: Erik Bernhardson Date: Wed, 16 Nov 2016 15:41:16 -0800 Subject: [PATCH] Extract main search result rendering from SpecialSearch Has a pre-existing problem related to the link offset of secondary inline results. Specifically the offset of secondary inline starts at the provided offset, even though there may be primary results displayed above it. Bug: T150390 Change-Id: Id1d6b357f45a2cf615d9412cc95dd597c724e8b6 --- autoload.php | 2 + includes/specials/SpecialSearch.php | 222 ++---------------- .../search/BasicSearchResultSetWidget.php | 135 +++++++++++ .../search/InterwikiSearchResultSetWidget.php | 130 ++++++++++ includes/widget/search/SearchResultWidget.php | 2 +- 5 files changed, 293 insertions(+), 198 deletions(-) create mode 100644 includes/widget/search/BasicSearchResultSetWidget.php create mode 100644 includes/widget/search/InterwikiSearchResultSetWidget.php diff --git a/autoload.php b/autoload.php index c8740ea9d9..ed3cbeb65a 100644 --- a/autoload.php +++ b/autoload.php @@ -932,7 +932,9 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Widget\\DateTimeInputWidget' => __DIR__ . '/includes/widget/DateTimeInputWidget.php', 'MediaWiki\\Widget\\NamespaceInputWidget' => __DIR__ . '/includes/widget/NamespaceInputWidget.php', 'MediaWiki\\Widget\\SearchInputWidget' => __DIR__ . '/includes/widget/SearchInputWidget.php', + 'MediaWiki\\Widget\\Search\\BasicSearchResultSetWidget' => __DIR__ . '/includes/widget/search/BasicSearchResultSetWidget.php', 'MediaWiki\\Widget\\Search\\FullSearchResultWidget' => __DIR__ . '/includes/widget/search/FullSearchResultWidget.php', + 'MediaWiki\\Widget\\Search\\InterwikiSearchResultSetWidget' => __DIR__ . '/includes/widget/search/InterwikiSearchResultSetWidget.php', 'MediaWiki\\Widget\\Search\\SearchFormWidget' => __DIR__ . '/includes/widget/search/SearchFormWidget.php', 'MediaWiki\\Widget\\Search\\SearchResultWidget' => __DIR__ . '/includes/widget/search/SearchResultWidget.php', 'MediaWiki\\Widget\\Search\\SimpleSearchResultWidget' => __DIR__ . '/includes/widget/search/SimpleSearchResultWidget.php', diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 37d86c36f7..735194eaac 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -24,6 +24,10 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\Widget\Search\BasicSearchResultSetWidget; +use MediaWiki\Widget\Search\InterwikiSearchResultSetWidget; +use MediaWiki\Widget\Search\FullSearchResultWidget; +use MediaWiki\Widget\Search\SimpleSearchResultWidget; /** * implements Special:Search - Run text & title search and display the output @@ -75,12 +79,6 @@ class SpecialSearch extends SpecialPage { */ protected $runSuggestion = true; - /** - * Names of the wikis, in format: Interwiki prefix -> caption - * @var array - */ - protected $customCaptions; - /** * Search engine configurations. * @var SearchEngineConfig @@ -327,6 +325,9 @@ class SpecialSearch extends SpecialPage { if ( $textMatches ) { $textMatchesNum = $textMatches->numRows(); $numTextMatches = $textMatches->getTotalHits(); + if ( $textMatchesNum > 0 ) { + $search->augmentSearchResults( $textMatches ); + } } $num = $titleMatchesNum + $textMatchesNum; $totalRes = $numTitleMatches + $numTextMatches; @@ -373,6 +374,8 @@ class SpecialSearch extends SpecialPage { // Show the create link ahead $this->showCreateLink( $title, $num, $titleMatches, $textMatches ); + Hooks::run( 'SpecialSearchResults', [ $term, $titleMatches, $textMatches ] ); + // If we have no results and have not already displayed an error message if ( $num === 0 && !$hasErrors ) { $out->wrapWikiMsg( "

\n$1

", [ @@ -381,50 +384,27 @@ class SpecialSearch extends SpecialPage { ] ); } - Hooks::run( 'SpecialSearchResults', [ $term, $titleMatches, $textMatches ] ); + // Although $num might be 0 there can still be secondary or inline + // results to display. + $linkRenderer = $this->getLinkRenderer(); + $mainResultWidget = new FullSearchResultWidget( $this, $linkRenderer ); + $sidebarResultWidget = new SimpleSearchResultWidget( $this, $linkRenderer ); + $sidebarResultsWidget = new InterwikiSearchResultSetWidget( + $this, + $sidebarResultWidget, + $linkRenderer, + MediaWikiServices::getInstance()->getInterwikiLookup() + ); + $widget = new BasicSearchResultSetWidget( $this, $mainResultWidget, $sidebarResultsWidget ); + + $out->addHTML( $widget->render( + $term, $this->offset, $titleMatches, $textMatches + ) ); - $out->parserOptions()->setEditSection( false ); if ( $titleMatches ) { - if ( $numTitleMatches > 0 ) { - $out->wrapWikiMsg( "==$1==\n", 'titlematches' ); - $out->addHTML( $this->showMatches( $titleMatches ) ); - } $titleMatches->free(); } - if ( $textMatches ) { - // output appropriate heading - if ( $numTextMatches > 0 && $numTitleMatches > 0 ) { - $out->addHTML( '
' ); - // if no title matches the heading is redundant - $out->wrapWikiMsg( "==$1==\n", 'textmatches' ); - } - - // show results - if ( $numTextMatches > 0 ) { - $search->augmentSearchResults( $textMatches ); - $out->addHTML( $this->showMatches( $textMatches ) ); - } - - // show secondary interwiki results if any - if ( $textMatches->hasInterwikiResults( SearchResultSet::SECONDARY_RESULTS ) ) { - $out->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults( - SearchResultSet::SECONDARY_RESULTS ), $term ) ); - } - } - - if ( $hasOtherResults ) { - foreach ( $textMatches->getInterwikiResults( SearchResultSet::INLINE_RESULTS ) - as $interwiki => $interwikiResult ) { - if ( $interwikiResult instanceof Status || $interwikiResult->numRows() == 0 ) { - // ignore bad interwikis for now - continue; - } - // TODO: wiki header - $out->addHTML( $this->showMatches( $interwikiResult, $interwiki ) ); - } - } - if ( $textMatches ) { $textMatches->free(); } @@ -449,18 +429,6 @@ class SpecialSearch extends SpecialPage { Hooks::run( 'SpecialSearchResultsAppend', [ $this, $out, $term ] ); } - /** - * Produce wiki header for interwiki results - * @param string $interwiki Interwiki name - * @param SearchResultSet $interwikiResult The result set - * @return string - */ - protected function interwikiHeader( $interwiki, $interwikiResult ) { - // TODO: we need to figure out how to name wikis correctly - $wikiMsg = $this->msg( 'search-interwiki-results-' . $interwiki )->parse(); - return "

\n$wikiMsg

"; - } - /** * Generates HTML shown to the user when we have a suggestion about a query * that might give more results than their current query. @@ -711,146 +679,6 @@ class SpecialSearch extends SpecialPage { return false; } - /** - * Show whole set of results - * - * @param SearchResultSet $matches - * @param string $interwiki Interwiki name - * - * @return string - */ - protected function showMatches( $matches, $interwiki = null ) { - global $wgContLang; - - $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); - $out = ''; - $result = $matches->next(); - $pos = $this->offset; - - if ( $result && $interwiki ) { - $out .= $this->interwikiHeader( $interwiki, $matches ); - } - - $out .= "\n"; - - // convert the whole thing to desired language variant - $out = $wgContLang->convert( $out ); - - return $out; - } - - /** - * Extract custom captions from search-interwiki-custom message - */ - protected function getCustomCaptions() { - if ( is_null( $this->customCaptions ) ) { - $this->customCaptions = []; - // format per line : - $customLines = explode( "\n", $this->msg( 'search-interwiki-custom' )->text() ); - foreach ( $customLines as $line ) { - $parts = explode( ":", $line, 2 ); - if ( count( $parts ) == 2 ) { // validate line - $this->customCaptions[$parts[0]] = $parts[1]; - } - } - } - } - - /** - * Show results from other wikis - * - * @param SearchResultSet|array $matches - * @param string $terms - * - * @return string - */ - protected function showInterwiki( $matches, $terms ) { - global $wgContLang; - - // work out custom project captions - $this->getCustomCaptions(); - - if ( !is_array( $matches ) ) { - $matches = [ $matches ]; - } - - $iwResults = []; - foreach ( $matches as $set ) { - $result = $set->next(); - while ( $result ) { - if ( !$result->isBrokenTitle() ) { - $iwResults[$result->getTitle()->getInterwiki()][] = $result; - } - $result = $set->next(); - } - } - - $out = ''; - $widget = new MediaWiki\Widget\Search\SimpleSearchResultWidget( - $this, - $this->getLinkRenderer() - ); - foreach ( $iwResults as $iwPrefix => $results ) { - $out .= $this->iwHeaderHtml( $iwPrefix, $terms ); - $out .= ""; - } - - $out = - "
" . - "
" . - $this->msg( 'search-interwiki-caption' )->escaped() . - "
" . - $out . - "
"; - - // convert the whole thing to desired language variant - return $wgContLang->convert( $out ); - } - - /** - * @param string $iwPrefix The interwiki prefix to render a header for - * @param string $terms The user-provided search terms - */ - protected function iwHeaderHtml( $iwPrefix, $terms ) { - if ( isset( $this->customCaptions[$iwPrefix] ) ) { - $caption = $this->customCaptions[$iwPrefix]; - } else { - $iwLookup = MediaWiki\MediaWikiServices::getInstance()->getInterwikiLookup(); - $interwiki = $iwLookup->fetch( $iwPrefix ); - $parsed = wfParseUrl( wfExpandUrl( $interwiki ? $interwiki->getURL() : '/' ) ); - $caption = $this->msg( 'search-interwiki-default', $parsed['host'] )->text(); - } - $searchLink = Linker::linkKnown( - Title::newFromText( "$iwPrefix:Special:Search" ), - $this->msg( 'search-interwiki-more' )->text(), - [], - [ - 'search' => $terms, - 'fulltext' => 1, - ] - ); - return - "
" . - "{$searchLink}" . - $caption . - "
"; - } - /** * @return array */ diff --git a/includes/widget/search/BasicSearchResultSetWidget.php b/includes/widget/search/BasicSearchResultSetWidget.php new file mode 100644 index 0000000000..2c31bd2a43 --- /dev/null +++ b/includes/widget/search/BasicSearchResultSetWidget.php @@ -0,0 +1,135 @@ +specialPage = $specialPage; + $this->resultWidget = $resultWidget; + $this->sidebarWidget = $sidebarWidget; + } + + /** + * @param string $term The search term to highlight + * @param int $offset The offset of the first result in the result set + * @param SearchResultSet|null $titleResultSet Results of searching only page titles + * @param SearchResultSet|null $textResultSet Results of general full text search. + * @return string HTML + */ + public function render( + $term, + $offset, + SearchResultSet $titleResultSet = null, + SearchResultSet $textResultSet = null + ) { + global $wgContLang; + + $hasTitle = $titleResultSet ? $titleResultSet->numRows() > 0 : false; + $hasText = $textResultSet ? $textResultSet->numRows() > 0 : false; + $hasSecondary = $textResultSet + ? $textResultSet->hasInterwikiResults( SearchResultSet::SECONDARY_RESULTS ) + : false; + $hasSecondaryInline = $textResultSet + ? $textResultSet->hasInterwikiResults( SearchResultSet::INLINE_RESULTS ) + : false; + + if ( !$hasTitle && !$hasText && !$hasSecondary && !$hasSecondaryInline ) { + return ''; + } + + $out = ''; + if ( $hasTitle ) { + $out .= $this->header( $this->specialPage->msg( 'titlematches' ) ) + . $this->renderResultSet( $titleResultSet, $offset ); + } + + if ( $hasText ) { + if ( $hasTitle ) { + $out .= "
" . + $this->header( $this->specialPage->msg( 'textmatches' ) ); + } + $out .= $this->renderResultSet( $textResultSet, $offset ); + } + + if ( $hasSecondaryInline ) { + $iwResults = $textResultSet->getInterwikiResults( SearchResultSet::INLINE_RESULTS ); + foreach ( $iwResults as $interwiki => $results ) { + if ( $results instanceof Status || $results->numRows() === 0 ) { + // ignore bad interwikis for now + continue; + } + $out .= + "

" . + $this->specialPage->msg( "search-interwiki-results-{$interwiki}" )->parse() . + "

"; + $out .= $this->renderResultSet( $results, $offset ); + } + } + + if ( $hasSecondary ) { + $out .= $this->sidebarWidget->render( + $term, + $textResultSet->getInterwikiResults( SearchResultSet::SECONDARY_RESULTS ) + ); + } + + // Convert the whole thing to desired language variant + // TODO: Move this up to Special:Search? + return $wgContLang->convert( $out ); + } + + /** + * Generate a headline for a section of the search results. In prior + * implementations this was rendering wikitext of '==$1==', but seems + * a waste to call the full parser to generate this tiny bit of html + * + * @param Message $msg i18n message to use as header + * @return string HTML + */ + protected function header( Message $msg ) { + return + "

" . + "" . $msg->escaped() . "" . + "

"; + } + + /** + * @param SearchResultSet $resultSet The search results to render + * @param int $offset Offset of the first result in $resultSet + * @return string HTML + */ + protected function renderResultSet( SearchResultSet $resultSet, $offset ) { + global $wgContLang; + + $terms = $wgContLang->convertForSearchResult( $resultSet->termMatches() ); + + $hits = []; + $result = $resultSet->next(); + while ( $result ) { + $hits[] .= $this->resultWidget->render( $result, $terms, $offset++ ); + $result = $resultSet->next(); + } + + return ""; + } +} diff --git a/includes/widget/search/InterwikiSearchResultSetWidget.php b/includes/widget/search/InterwikiSearchResultSetWidget.php new file mode 100644 index 0000000000..c738483562 --- /dev/null +++ b/includes/widget/search/InterwikiSearchResultSetWidget.php @@ -0,0 +1,130 @@ +specialSearch = $specialSearch; + $this->resultWidget = $resultWidget; + $this->linkRenderer = $linkRenderer; + $this->iwLookup = $iwLookup; + } + + /** + * @param string $term User provided search term + * @param SearchResultSet|SearchResultSet[] $resultSets List of interwiki + * results to render. + * @return string HTML + */ + public function render( $term, $resultSets ) { + if ( !is_array( $resultSets ) ) { + $resultSets = [ $resultSets ]; + } + + $this->loadCustomCaptions(); + + $iwResults = []; + foreach ( $resultSets as $resultSet ) { + $result = $resultSet->next(); + while ( $result ) { + if ( !$result->isBrokenTitle() ) { + $iwResults[$result->getTitle()->getInterwiki()][] = $result; + } + $result = $resultSet->next(); + } + } + + $out = ''; + foreach ( $iwResults as $iwPrefix => $results ) { + $out .= $this->headerHtml( $iwPrefix, $term ); + $out .= ""; + } + + return + "
" . + "
" . + $this->specialSearch->msg( 'search-interwiki-caption' )->text() . + '
' . + $out . + "
"; + } + + /** + * Generates an appropriate HTML header for the given interwiki prefix + * + * @param string $iwPrefix Interwiki prefix of wiki to show header for + * @param string $term User provided search term + * @return string HTML + */ + protected function headerHtml( $iwPrefix, $term ) { + if ( isset( $this->customCaptions[$iwPrefix] ) ) { + $caption = $this->customCaptions[$iwPrefix]; + } else { + $interwiki = $this->iwLookup->fetch( $iwPrefix ); + $parsed = wfParseUrl( wfExpandUrl( $interwiki ? $interwiki->getURL() : '/' ) ); + $caption = $this->specialSearch->msg( 'search-interwiki-default', $parsed['host'] )->text(); + } + $searchLink = $this->linkRenderer->makeLink( + Title::newFromText( "$iwPrefix:Special:Search" ), + $this->specialSearch->msg( 'search-interwiki-more' )->text(), + [], + [ + 'search' => $term, + 'fulltext' => 1, + ] + ); + return + "
" . + "{$searchLink}" . + $caption . + "
"; + } + + protected function loadCustomCaptions() { + if ( $this->customCaptions !== null ) { + return; + } + + $this->customCaptions = []; + $customLines = explode( "\n", $this->specialSearch->msg( 'search-interwiki-custom' )->text() ); + foreach ( $customLines as $line ) { + $parts = explode( ':', $line, 2 ); + if ( count( $parts ) === 2 ) { + $this->customCaptions[$parts[0]] = $parts[1]; + } + } + } +} diff --git a/includes/widget/search/SearchResultWidget.php b/includes/widget/search/SearchResultWidget.php index b53cd5d2d2..3fbdbef2a9 100644 --- a/includes/widget/search/SearchResultWidget.php +++ b/includes/widget/search/SearchResultWidget.php @@ -11,7 +11,7 @@ interface SearchResultWidget { /** * @param SearchResult $result The result to render * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet) - * @param int $position The result position, including offset + * @param int $position The zero indexed result position, including offset * @return string HTML */ public function render( SearchResult $result, $terms, $position ); -- 2.20.1