Extract main search result rendering from SpecialSearch
authorErik Bernhardson <ebernhardson@wikimedia.org>
Wed, 16 Nov 2016 23:41:16 +0000 (15:41 -0800)
committerErik Bernhardson <ebernhardson@wikimedia.org>
Tue, 17 Jan 2017 23:57:40 +0000 (15:57 -0800)
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
includes/specials/SpecialSearch.php
includes/widget/search/BasicSearchResultSetWidget.php [new file with mode: 0644]
includes/widget/search/InterwikiSearchResultSetWidget.php [new file with mode: 0644]
includes/widget/search/SearchResultWidget.php

index c8740ea..ed3cbeb 100644 (file)
@@ -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',
index 37d86c3..735194e 100644 (file)
  */
 
 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( "<p class=\"mw-search-nonefound\">\n$1</p>", [
@@ -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( '<div class="mw-search-visualclear"></div>' );
-                               // 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 "<p class=\"mw-search-interwiki-header mw-search-visualclear\">\n$wikiMsg</p>";
-       }
-
        /**
         * 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 .= "<ul class='mw-search-results'>\n";
-               $widget = new \MediaWiki\Widget\Search\FullSearchResultWidget(
-                       $this,
-                       $this->getLinkRenderer()
-               );
-               while ( $result ) {
-                       $out .= $widget->render( $result, $terms, $pos++ );
-                       $result = $matches->next();
-               }
-               $out .= "</ul>\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 <iwprefix>:<caption>
-                       $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 .= "<ul class='mw-search-iwresults'>";
-                       foreach ( $results as $result ) {
-                               // This makes the bold asumption interwiki results are never paginated.
-                               // That's currently true, but could change at some point?
-                               $out .= $widget->render( $result, $terms, 0 );
-                       }
-                       $out .= "</ul>";
-               }
-
-               $out =
-                       "<div id='mw-search-interwiki'>" .
-                               "<div id='mw-search-interwiki-caption'>" .
-                                       $this->msg( 'search-interwiki-caption' )->escaped() .
-                               "</div>" .
-                               $out .
-                       "</div>";
-
-               // 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
-                       "<div class='mw-search-interwiki-project'>" .
-                               "<span class='mw-search-interwiki-more'>{$searchLink}</span>" .
-                               $caption .
-                       "</div>";
-       }
-
        /**
         * @return array
         */
diff --git a/includes/widget/search/BasicSearchResultSetWidget.php b/includes/widget/search/BasicSearchResultSetWidget.php
new file mode 100644 (file)
index 0000000..2c31bd2
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+
+namespace MediaWiki\Widget\Search;
+
+use Message;
+use SearchResultSet;
+use SpecialSearch;
+use Status;
+
+/**
+ * Renders the search result area. Handles Title and Full-Text search results,
+ * along with inline and sidebar secondary (interwiki) results.
+ */
+class BasicSearchResultSetWidget {
+       /** @var SpecialSearch */
+       protected $specialPage;
+       /** @var SearchResultWidget */
+       protected $resultWidget;
+       /** @var InterwikiSearchResultSetWidget */
+       protected $sidebarWidget;
+
+       public function __construct(
+               SpecialSearch $specialPage,
+               SearchResultWidget $resultWidget,
+               InterwikiSearchResultSetWidget $sidebarWidget
+       ) {
+               $this->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 .= "<div class='mw-search-visualclear'></div>" .
+                                       $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 .=
+                                       "<p class='mw-search-interwiki-header mw-search-visualclear'>" .
+                                               $this->specialPage->msg( "search-interwiki-results-{$interwiki}" )->parse() .
+                                       "</p>";
+                               $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
+                       "<h2>" .
+                               "<span class='mw-headline'>" . $msg->escaped() . "</span>" .
+                       "</h2>";
+       }
+
+       /**
+        * @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 "<ul class='mw-search-results'>" . implode( '', $hits ) . "</ul>";
+       }
+}
diff --git a/includes/widget/search/InterwikiSearchResultSetWidget.php b/includes/widget/search/InterwikiSearchResultSetWidget.php
new file mode 100644 (file)
index 0000000..c738483
--- /dev/null
@@ -0,0 +1,130 @@
+<?php
+
+namespace MediaWiki\Widget\Search;
+
+use MediaWiki\Interwiki\InterwikiLookup;
+use MediaWiki\Linker\LinkRenderer;
+use SearchResultSet;
+use SpecialSearch;
+use Title;
+
+/**
+ * Renders one or more SearchResultSets into a sidebar grouped by
+ * interwiki prefix. Includes a per-wiki header indicating where
+ * the results are from.
+ */
+class InterwikiSearchResultSetWidget {
+       /** @var SpecialSearch */
+       protected $specialSearch;
+       /** @var SearchResultWidget */
+       protected $resultWidget;
+       /** @var string[]|null */
+       protected $customCaptions;
+       /** @var LinkRenderer */
+       protected $linkRenderer;
+       /** @var InterwikiLookup */
+       protected $iwLookup;
+
+       public function __construct(
+               SpecialSearch $specialSearch,
+               SearchResultWidget $resultWidget,
+               LinkRenderer $linkRenderer,
+               InterwikiLookup $iwLookup
+       ) {
+               $this->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 .= "<ul class='mw-search-iwresults'>";
+                       // TODO: Assumes interwiki results are never paginated
+                       $position = 0;
+                       foreach ( $results as $result ) {
+                               $out .= $this->resultWidget->render( $result, $term, $position++ );
+                       }
+                       $out .= "</ul>";
+               }
+
+               return
+                       "<div id='mw-search-interwiki'>" .
+                               "<div id='mw-search-interwiki-caption'>" .
+                                       $this->specialSearch->msg( 'search-interwiki-caption' )->text() .
+                               '</div>' .
+                               $out .
+                       "</div>";
+       }
+
+       /**
+        * 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
+                       "<div class='mw-search-interwiki-project'>" .
+                               "<span class='mw-search-interwiki-more'>{$searchLink}</span>" .
+                               $caption .
+               "</div>";
+       }
+
+       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];
+                       }
+               }
+       }
+}
index b53cd5d..3fbdbef 100644 (file)
@@ -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 );