Ajax suggestions:
authorRobert Stojnić <rainman@users.mediawiki.org>
Tue, 15 Apr 2008 23:06:28 +0000 (23:06 +0000)
committerRobert Stojnić <rainman@users.mediawiki.org>
Tue, 15 Apr 2008 23:06:28 +0000 (23:06 +0000)
* check in a new ajax suggestion engine (mwsuggest.js) which uses
  OpenSearch to fetch results (by default via API), this should
  deprecated the old ajaxsearch thingy
* extend PrefixSearchBackend hook to accept multiple namespaces for
  future lucene use (default implementation however can still
  process only one)
* Added to preferences, also a feature to turn it on/off for every
  input (disabled atm until I work out browser issues completely)
* WMF wikis probably won't be using API to fetch results, but a
  custom php wrapper that just forwards the request to appropriate
  lucene daemon, added support for that

SpecialSearch:
* moved stuff out of SpecialSearch to SearchEngine, like snippet
  highlighting and such
* support for additional interwiki results, e.g. title matches
  from other projects shown in a separate box on the right
* todo: interwiki box doesn't have standard prev/next links to
  avoid clutter and unintuitive interface
* support for related articles

15 files changed:
docs/hooks.txt
includes/DefaultSettings.php
includes/OutputPage.php
includes/PrefixSearch.php
includes/SearchEngine.php
includes/Skin.php
includes/SpecialPreferences.php
includes/SpecialSearch.php
includes/api/ApiOpenSearch.php
languages/messages/MessagesEn.php
maintenance/language/messages.inc
opensearch_desc.php
skins/common/mwsuggest.js [new file with mode: 0644]
skins/common/shared.css
skins/monobook/main.css

index f6a9edc..06de421 100644 (file)
@@ -886,7 +886,7 @@ $form : PreferencesForm object
 
 'PrefixSearchBackend': Override the title prefix search used for OpenSearch and
 AJAX search suggestions. Put results into &$results outparam and return false.
-$ns : int namespace key to search in
+$ns : array of int namespace keys to search in
 $search : search term (not guaranteed to be conveniently normalized)
 $limit : maximum number of results to return
 &$results : out param: array of page names (strings)
index cf5206f..07391f4 100644 (file)
@@ -1597,6 +1597,32 @@ $wgDisableCounters = false;
 
 $wgDisableTextSearch = false;
 $wgDisableSearchContext = false;
+
+/**
+ * Template for OpenSearch suggestions, defaults to API action=opensearch
+ * 
+ * Sites with heavy load would tipically have these point to a custom
+ * PHP wrapper to avoid firing up mediawiki for every keystroke
+ * 
+ * Placeholders: {searchTerms}
+ * 
+ */
+$wgOpenSearchTemplate = false;
+
+/**
+ * Enable suggestions while typing in search boxes 
+ * (results are passed around in OpenSearch format) 
+ */
+$wgEnableMWSuggest = false;
+
+/**
+ *  Template for internal MediaWiki suggestion engine, defaults to API action=opensearch
+ *  
+ *  Placeholders: {searchTerms}, {namespaces}, {dbname}
+ *  
+ */
+$wgMWSuggestTemplate = false;
+
 /**
  * If you've disabled search semi-permanently, this also disables updates to the
  * table. If you ever re-enable, be sure to rebuild the search table.
index c8c3bab..e915b7a 100644 (file)
@@ -670,7 +670,7 @@ class OutputPage {
                global $wgUser, $wgOutputEncoding, $wgRequest;
                global $wgContLanguageCode, $wgDebugRedirects, $wgMimeType;
                global $wgJsMimeType, $wgStylePath, $wgUseAjax, $wgAjaxSearch, $wgAjaxWatch;
-               global $wgServer, $wgStyleVersion;
+               global $wgServer, $wgStyleVersion, $wgEnableMWSuggest;
 
                if( $this->mDoNothing ){
                        return;
@@ -772,10 +772,13 @@ class OutputPage {
                        if( $wgAjaxWatch && $wgUser->isLoggedIn() ) {
                                $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxwatch.js?$wgStyleVersion\"></script>\n" );
                        }
+                       
+                       if ( $wgEnableMWSuggest && !$wgUser->getOption( 'disablesuggest', false ) ){
+                               $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/mwsuggest.js?$wgStyleVersion\"></script>\n" );                                  
+                       }
                }
 
 
-
                # Buffer output; final headers may depend on later processing
                ob_start();
 
index 1a43afe..a3ff05e 100644 (file)
@@ -5,19 +5,23 @@ class PrefixSearch {
         * Do a prefix search of titles and return a list of matching page names.
         * @param string $search
         * @param int $limit
+        * @param array $namespaces - used if query is not explicitely prefixed
         * @return array of strings
         */
-       public static function titleSearch( $search, $limit ) {
+       public static function titleSearch( $search, $limit, $namespaces=array() ) {
                $search = trim( $search );
                if( $search == '' ) {
                        return array(); // Return empty result
                }
-
+               $namespaces = self::validateNamespaces( $namespaces );
+               
                $title = Title::newFromText( $search );
                if( $title && $title->getInterwiki() == '' ) {
-                       $ns = $title->getNamespace();
+                       $ns = array($title->getNamespace());
+                       if($ns[0] == NS_MAIN) 
+                               $ns = $namespaces; // no explicit prefix, use default namespaces
                        return self::searchBackend(
-                               $title->getNamespace(), $title->getText(), $limit );
+                               $ns, $title->getText(), $limit );
                }
 
                // Is this a namespace prefix?
@@ -26,29 +30,32 @@ class PrefixSearch {
                        && $title->getNamespace() != NS_MAIN
                        && $title->getInterwiki() == '' ) {
                        return self::searchBackend(
-                               $title->getNamespace(), '', $limit );
+                               array($title->getNamespace()), '', $limit );
                }
-
-               return self::searchBackend( 0, $search, $limit );
+                               
+               return self::searchBackend( $namespaces, $search, $limit );
        }
 
 
        /**
         * Do a prefix search of titles and return a list of matching page names.
+        * @param array $namespaces
         * @param string $search
         * @param int $limit
         * @return array of strings
         */
-       protected static function searchBackend( $ns, $search, $limit ) {
-               if( $ns == NS_MEDIA ) {
-                       $ns = NS_IMAGE;
-               } elseif( $ns == NS_SPECIAL ) {
-                       return self::specialSearch( $search, $limit );
+       protected static function searchBackend( $namespaces, $search, $limit ) {
+               if( count($namespaces) == 1 ){
+                       $ns = $namespaces[0];
+                       if( $ns == NS_MEDIA ) {
+                               $namespaces = array(NS_IMAGE);
+                       } elseif( $ns == NS_SPECIAL ) {
+                               return self::specialSearch( $search, $limit );
+                       }
                }
-
                $srchres = array();
-               if( wfRunHooks( 'PrefixSearchBackend', array( $ns, $search, $limit, &$srchres ) ) ) {
-                       return self::defaultSearchBackend( $ns, $search, $limit );
+               if( wfRunHooks( 'PrefixSearchBackend', array( $namespaces, $search, $limit, &$srchres ) ) ) {
+                       return self::defaultSearchBackend( $namespaces, $search, $limit );
                }
                return $srchres;
        }
@@ -91,18 +98,22 @@ class PrefixSearch {
         * Unless overridden by PrefixSearchBackend hook...
         * This is case-sensitive except the first letter (per $wgCapitalLinks)
         *
-        * @param int $ns Namespace to search in
+        * @param array $namespaces Namespaces to search in
         * @param string $search term
         * @param int $limit max number of items to return
         * @return array of title strings
         */
-       protected static function defaultSearchBackend( $ns, $search, $limit ) {
+       protected static function defaultSearchBackend( $namespaces, $search, $limit ) {
                global $wgCapitalLinks, $wgContLang;
 
                if( $wgCapitalLinks ) {
                        $search = $wgContLang->ucfirst( $search );
                }
 
+               $ns = array_shift($namespaces); // support only one namespace
+               if( in_array(NS_MAIN,$namespaces))
+                       $ns = NS_MAIN; // if searching on many always default to main 
+               
                // Prepare nested request
                $req = new FauxRequest(array (
                        'action' => 'query',
@@ -129,5 +140,25 @@ class PrefixSearch {
 
                return $srchres;
        }
-
+       
+       /**
+        * Validate an array of numerical namespace indexes
+        * 
+        * @param array $namespaces
+        */
+       protected static function validateNamespaces($namespaces){
+               global $wgContLang;
+               $validNamespaces = $wgContLang->getNamespaces();
+               if( is_array($namespaces) && count($namespaces)>0 ){
+                       $valid = array();
+                       foreach ($namespaces as $ns){
+                               if( is_numeric($ns) && array_key_exists($ns, $validNamespaces) )
+                                       $valid[] = $ns;
+                       }
+                       if( count($valid) > 0 )
+                               return $valid;
+               }
+               
+               return array( NS_MAIN );
+       }
 }
index 4d8909c..00cb185 100644 (file)
@@ -35,7 +35,7 @@ class SearchEngine {
        function searchTitle( $term ) {
                return null;
        }
-
+       
        /**
         * If an exact title match can be find, or a very slightly close match,
         * return the title. If no match, returns NULL.
@@ -222,6 +222,50 @@ class SearchEngine {
                }
                return $arr;
        }
+       
+       /**
+        * Extract default namespaces to search from the given user's
+        * settings, returning a list of index numbers.
+        *
+        * @param User $user
+        * @return array
+        * @static 
+        */
+       public static function userNamespaces( &$user ) {
+               $arr = array();
+               foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+                       if( $user->getOption( 'searchNs' . $ns ) ) {
+                               $arr[] = $ns;
+                       }
+               }
+               return $arr;
+       }
+       
+       /**
+        * Find snippet highlight settings for a given user
+        *
+        * @param User $user
+        * @return array contextlines, contextchars 
+        * @static
+        */
+       public static function userHighlightPrefs( &$user ){
+               //$contextlines = $user->getOption( 'contextlines',  5 );
+               $contextlines = 2; // Hardcode this. Old defaults sucked. :)
+               $contextchars = $user->getOption( 'contextchars', 50 );
+               return array($contextlines, $contextchars);
+       }
+       
+       /**
+        * An array of namespaces indexes to be searched by default
+        * 
+        * @return array 
+        * @static
+        */
+       public static function defaultNamespaces(){
+               global $wgNamespacesToBeSearchedDefault;
+               
+               return array_keys($wgNamespacesToBeSearchedDefault, true);
+       }
 
        /**
         * Return a 'cleaned up' search string
@@ -281,6 +325,37 @@ class SearchEngine {
        function updateTitle( $id, $title ) {
                // no-op
        }
+       
+       /**
+        * Get OpenSearch suggestion template
+        * 
+        * @return string
+        * @static 
+        */
+       public static function getOpenSearchTemplate() {
+               global $wgOpenSearchTemplate, $wgServer, $wgScriptPath;
+               if($wgOpenSearchTemplate)               
+                       return $wgOpenSearchTemplate;
+               else{ 
+                       $ns = implode(',',SearchEngine::defaultNamespaces());
+                       if(!$ns) $ns = "0";
+                       return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace='.$ns;
+               }
+       }
+       
+       /**
+        * Get internal MediaWiki Suggest template 
+        * 
+        * @return string
+        * @static
+        */
+       public static function getMWSuggestTemplate() {
+               global $wgMWSuggestTemplate, $wgServer, $wgScriptPath;
+               if($wgMWSuggestTemplate)                
+                       return $wgMWSuggestTemplate;
+               else 
+                       return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace={namespaces}';
+       }
 }
 
 
@@ -352,6 +427,35 @@ class SearchResultSet {
        function getSuggestionSnippet(){
                return '';
        }
+       
+       /**
+        * Return information about how and from where the results were fetched,
+        * should be useful for diagnostics and debugging 
+        *
+        * @return string
+        */
+       function getInfo() {
+               return null;
+       }
+       
+       /**
+        * Return a result set of hits on other (multiple) wikis associated with this one
+        *
+        * @return SearchResultSet
+        */
+       function getInterwikiResults() {
+               return null;
+       }
+       
+       /**
+        * Check if there are results on other wikis
+        *
+        * @return boolean
+        */
+       function hasInterwikiResults() {
+               return $this->getInterwikiResults() != null;
+       }
+       
 
        /**
         * Fetches next search result, or false.
@@ -388,6 +492,32 @@ class SearchResult {
 
        function SearchResult( $row ) {
                $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title );
+               if( !is_null($this->mTitle) )
+                       $this->mRevision = Revision::newFromTitle( $this->mTitle );
+       }
+       
+       /**
+        * Check if this is result points to an invalid title
+        *
+        * @return boolean
+        * @access public
+        */
+       function isBrokenTitle(){
+               if( is_null($this->mTitle) )
+                       return true;
+               return false;
+       }
+       
+       /**
+        * Check if target page is missing, happens when index is out of date
+        * 
+        * @return boolean
+        * @access public
+        */
+       function isMissingRevision(){
+               if( !$this->mRevision )
+                       return true;
+               return false;
        }
 
        /**
@@ -406,23 +536,93 @@ class SearchResult {
        }
 
        /**
-        * @return string highlighted text snippet, null if not supported
+        * Lazy initialization of article text from DB
         */
-       function getTextSnippet(){
-               return null;
+       protected function initText(){
+               if( !isset($this->mText) ){
+                       $this->mText = $this->mRevision->getText();
+               }
        }
 
        /**
+        * @param array $terms terms to highlight
+        * @return string highlighted text snippet, null (and not '') if not supported 
+        */
+       function getTextSnippet($terms){
+               global $wgUser;
+               $this->initText();
+               list($contextlines,$contextchars) = SearchEngine::userHighlightPrefs($wgUser);
+               return $this->extractText( $this->mText, $terms, $contextlines, $contextchars);                 
+       }
+       
+       /**
+        * Default implementation of snippet extraction
+        *
+        * @param string $text
+        * @param array $terms
+        * @param int $contextlines
+        * @param int $contextchars
+        * @return string
+        */
+       protected function extractText( $text, $terms, $contextlines, $contextchars ) {
+               global $wgLang, $wgContLang;
+               $fname = __METHOD__;
+       
+               $lines = explode( "\n", $text );
+               
+               $terms = implode( '|', $terms );
+               $max = intval( $contextchars ) + 1;
+               $pat1 = "/(.*)($terms)(.{0,$max})/i";
+
+               $lineno = 0;
+
+               $extract = "";
+               wfProfileIn( "$fname-extract" );
+               foreach ( $lines as $line ) {
+                       if ( 0 == $contextlines ) {
+                               break;
+                       }
+                       ++$lineno;
+                       $m = array();
+                       if ( ! preg_match( $pat1, $line, $m ) ) {
+                               continue;
+                       }
+                       --$contextlines;
+                       $pre = $wgContLang->truncate( $m[1], -$contextchars, ' ... ' );
+
+                       if ( count( $m ) < 3 ) {
+                               $post = '';
+                       } else {
+                               $post = $wgContLang->truncate( $m[3], $contextchars, ' ... ' );
+                       }
+
+                       $found = $m[2];
+
+                       $line = htmlspecialchars( $pre . $found . $post );
+                       $pat2 = '/(' . $terms . ")/i";
+                       $line = preg_replace( $pat2,
+                         "<span class='searchmatch'>\\1</span>", $line );
+
+                       $extract .= "${line}\n";
+               }
+               wfProfileOut( "$fname-extract" );
+               
+               return $extract;
+       }
+       
+       /**
+        * @param array $terms terms to highlight
         * @return string highlighted title, '' if not supported
         */
-       function getTitleSnippet(){
+       function getTitleSnippet($terms){
                return '';
        }
 
        /**
+        * @param array $terms terms to highlight
         * @return string highlighted redirect name (redirect to this page), '' if none or not supported
         */
-       function getRedirectSnippet(){
+       function getRedirectSnippet($terms){
                return '';
        }
 
@@ -448,24 +648,40 @@ class SearchResult {
        }
 
        /**
-        * @return string timestamp, null if not supported
+        * @return string timestamp
         */
        function getTimestamp(){
-               return null;
+               return $this->mRevision->getTimestamp();
        }
 
        /**
-        * @return int number of words, null if not supported
+        * @return int number of words
         */
        function getWordCount(){
-               return null;
+               $this->initText();
+               return str_word_count( $this->mText );
        }
 
        /**
-        * @return int size in bytes, null if not supported
+        * @return int size in bytes
         */
        function getByteSize(){
-               return null;
+               $this->initText();
+               return strlen( $this->mText );
+       }
+       
+       /**
+        * @return boolean if hit has related articles
+        */
+       function hasRelated(){
+               return false;
+       }
+       
+       /**
+        * @return interwiki prefix of the title (return iw even if title is broken)
+        */
+       function getInterwikiPrefix(){
+               return '';
        }
 }
 
index 1cc1c3f..5e324bb 100644 (file)
@@ -300,6 +300,7 @@ class Skin extends Linker {
                global $wgUseAjax, $wgAjaxWatch;
                global $wgVersion, $wgEnableAPI, $wgEnableWriteAPI;
                global $wgRestrictionTypes, $wgLivePreview;
+               global $wgMWSuggestTemplate, $wgDBname, $wgEnableMWSuggest;
 
                $ns = $wgTitle->getNamespace();
                $nsname = isset( $wgCanonicalNamespaceNames[ $ns ] ) ? $wgCanonicalNamespaceNames[ $ns ] : $wgTitle->getNsText();
@@ -331,6 +332,13 @@ class Skin extends Linker {
                        'wgEnableAPI' => $wgEnableAPI,
                        'wgEnableWriteAPI' => $wgEnableWriteAPI,
                );
+               
+               if( $wgUseAjax && $wgEnableMWSuggest && !$wgUser->getOption( 'disablesuggest', false )){
+                       $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate();
+                       $vars['wgDBname'] = $wgDBname;
+                       $vars['wgSearchNamespaces'] = SearchEngine::userNamespaces( $wgUser );
+                       $vars['wgMWSuggestMessages'] = array( wfMsg('search-mwsuggest-enabled'), wfMsg('search-mwsuggest-disabled'));
+               }
 
                foreach( $wgRestrictionTypes as $type )
                        $vars['wgRestriction' . ucfirst( $type )] = $wgTitle->getRestrictions( $type );
index 4b0f3d0..293db0e 100644 (file)
@@ -66,6 +66,7 @@ class PreferencesForm {
                $this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' );
                $this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' );
                $this->mUseAjaxSearch = $request->getCheck( 'wpUseAjaxSearch' );
+               $this->mDisableMWSuggest = $request->getCheck( 'wpDisableMWSuggest' );
 
                $this->mSaveprefs = $request->getCheck( 'wpSaveprefs' ) &&
                        $this->mPosted &&
@@ -288,6 +289,7 @@ class PreferencesForm {
                $wgUser->setOption( 'underline', $this->validateInt($this->mUnderline, 0, 2) );
                $wgUser->setOption( 'watchlistdays', $this->validateFloat( $this->mWatchlistDays, 0, 7 ) );
                $wgUser->setOption( 'ajaxsearch', $this->mUseAjaxSearch );
+               $wgUser->setOption( 'disablesuggest', $this->mDisableMWSuggest );
 
                # Set search namespace options
                foreach( $this->mSearchNs as $i => $value ) {
@@ -400,6 +402,7 @@ class PreferencesForm {
                $this->mUnderline = $wgUser->getOption( 'underline' );
                $this->mWatchlistDays = $wgUser->getOption( 'watchlistdays' );
                $this->mUseAjaxSearch = $wgUser->getBoolOption( 'ajaxsearch' );
+               $this->mDisableMWSuggest = $wgUser->getBoolOption( 'disablesuggest' );
 
                $togs = User::getToggles();
                foreach ( $togs as $tname ) {
@@ -517,7 +520,7 @@ class PreferencesForm {
                global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress;
                global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication;
                global $wgContLanguageCode, $wgDefaultSkin, $wgSkipSkins, $wgAuth;
-               global $wgEmailConfirmToEdit, $wgAjaxSearch;
+               global $wgEmailConfirmToEdit, $wgAjaxSearch, $wgEnableMWSuggest;
 
                $wgOut->setPageTitle( wfMsg( 'preferences' ) );
                $wgOut->setArticleRelated( false );
@@ -980,8 +983,13 @@ class PreferencesForm {
                                wfLabel( wfMsg( 'useajaxsearch' ), 'wpUseAjaxSearch' ),
                                wfCheck( 'wpUseAjaxSearch', $this->mUseAjaxSearch, array( 'id' => 'wpUseAjaxSearch' ) )
                        ) : '';
+               $mwsuggest = $wgEnableMWSuggest ?
+                       $this->addRow(
+                               wfLabel( wfMsg( 'mwsuggest-disable' ), 'wpDisableMWSuggest' ),
+                               wfCheck( 'wpDisableMWSuggest', $this->mDisableMWSuggest, array( 'id' => 'wpDisableMWSuggest' ) )
+                       ) : '';
                $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'searchresultshead' ) . '</legend><table>' .
-                       $ajaxsearch .
+                       $ajaxsearch .                   
                        $this->addRow(
                                wfLabel( wfMsg( 'resultsperpage' ), 'wpSearch' ),
                                wfInput( 'wpSearch', 4, $this->mSearch, array( 'id' => 'wpSearch' ) )
@@ -994,6 +1002,7 @@ class PreferencesForm {
                                wfLabel( wfMsg( 'contextchars' ), 'wpSearchChars' ),
                                wfInput( 'wpSearchChars', 4, $this->mSearchChars, array( 'id' => 'wpSearchChars' ) )
                        ) .
+                       $mwsuggest.
                "</table><fieldset><legend>" . wfMsg( 'defaultns' ) . "</legend>$ps</fieldset></fieldset>" );
 
                # Misc
index 85726ce..dd9c9c5 100644 (file)
@@ -32,10 +32,10 @@ function wfSpecialSearch( $par = '' ) {
 
        $search = str_replace( "\n", " ", $wgRequest->getText( 'search', $par ) );
        $searchPage = new SpecialSearch( $wgRequest, $wgUser );
-       if( $wgRequest->getVal( 'fulltext' ) ||
-               !is_null( $wgRequest->getVal( 'offset' ) ) ||
-               !is_null ($wgRequest->getVal( 'searchx' ) ) ) {
-               $searchPage->showResults( $search );
+       if( $wgRequest->getVal( 'fulltext' ) 
+               || !is_null( $wgRequest->getVal( 'offset' )) 
+               || !is_null( $wgRequest->getVal( 'searchx' ))) {
+               $searchPage->showResults( $search, 'search' );
        } else {
                $searchPage->goResult( $search );
        }
@@ -60,7 +60,7 @@ class SpecialSearch {
 
                $this->namespaces = $this->powerSearch( $request );
                if( empty( $this->namespaces ) ) {
-                       $this->namespaces = $this->userNamespaces( $user );
+                       $this->namespaces = SearchEngine::userNamespaces( $user );
                }
 
                $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false;
@@ -118,10 +118,11 @@ class SpecialSearch {
        function showResults( $term ) {
                $fname = 'SpecialSearch::showResults';
                wfProfileIn( $fname );
+               global $wgOut, $wgUser;
+               $sk = $wgUser->getSkin();
 
                $this->setupPage( $term );
 
-               global $wgOut;
                $wgOut->addWikiMsg( 'searchresulttext' );
 
                if( '' === trim( $term ) ) {
@@ -175,19 +176,24 @@ class SpecialSearch {
                        wfProfileOut( $fname );
                        return;
                }
+               
                $textMatches = $search->searchText( $rewritten );
 
-               // did you mean...
+               // did you mean... suggestions
                if($textMatches && $textMatches->hasSuggestion()){
-                       global $wgScript;
-                       $fulltext = htmlspecialchars(wfMsg('search'));
-                       $suggestLink = '<a href="'.$wgScript.'?title=Special:Search&amp;search='.
-                               urlencode($textMatches->getSuggestionQuery()).'&amp;fulltext='.$fulltext.'">'
-                               .$textMatches->getSuggestionSnippet().'</a>';
+                       $st = SpecialPage::getTitleFor( 'Search' );                     
+                       $stParams = wfArrayToCGI( array( 
+                                       'search'        => $textMatches->getSuggestionQuery(), 
+                                       'fulltext'      => wfMsg('search')),
+                                       $this->powerSearchOptions());
+                                       
+                       $suggestLink = '<a href="'.$st->escapeLocalURL($stParams).'">'.
+                                       $textMatches->getSuggestionSnippet().'</a>';
+                                       
                        $wgOut->addHTML('<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>');
                }
 
-
+               // show number of results
                $num = ( $titleMatches ? $titleMatches->numRows() : 0 )
                        + ( $textMatches ? $textMatches->numRows() : 0);
                $totalNum = 0;
@@ -197,7 +203,8 @@ class SpecialSearch {
                        $totalNum += $textMatches->getTotalHits();
                if ( $num > 0 ) {
                        if ( $totalNum > 0 ){
-                               $top = wfMsgExt('showingresultstotal',array( 'parseinline' ), $this->offset+1, $this->offset+$num, $totalNum);
+                               $top = wfMsgExt('showingresultstotal', array( 'parseinline' ), 
+                                       $this->offset+1, $this->offset+$num, $totalNum );
                        } elseif ( $num >= $this->limit ) {
                                $top = wfShowingResults( $this->offset, $this->limit );
                        } else {
@@ -206,6 +213,7 @@ class SpecialSearch {
                        $wgOut->addHTML( "<p>{$top}</p>\n" );
                }
 
+               // prev/next links
                if( $num || $this->offset ) {
                        $prevnext = wfViewPrevNext( $this->offset, $this->limit,
                                SpecialPage::getTitleFor( 'Search' ),
@@ -230,16 +238,23 @@ class SpecialSearch {
                }
 
                if( $textMatches ) {
+                       // output appropriate heading
                        if( $textMatches->numRows() ) {
                                if($titleMatches)
                                        $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' );
                                else // if no title matches the heading is redundant
-                                       $wgOut->addHTML("<hr/>");
-                               $wgOut->addHTML( $this->showMatches( $textMatches ) );
+                                       $wgOut->addHTML("<hr/>");                                                               
                        } elseif( $num == 0 ) {
                                # Don't show the 'no text matches' if we received title matches
                                $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' );
                        }
+                       // show interwiki results if any
+                       if( $textMatches->hasInterwikiResults() )
+                               $wgOut->addHtml( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ));
+                       // show results
+                       if( $textMatches->numRows() )
+                               $wgOut->addHTML( $this->showMatches( $textMatches ) );
+                               
                        $textMatches->free();
                }
 
@@ -255,38 +270,20 @@ class SpecialSearch {
 
        #------------------------------------------------------------------
        # Private methods below this line
-
+       
        /**
         *
         */
        function setupPage( $term ) {
                global $wgOut;
                if( !empty( $term ) )
-                       $wgOut->setPageTitle( wfMsg( 'searchresults' ) );
+                       $wgOut->setPageTitle( wfMsg( 'searchresults' ) );                       
                $subtitlemsg = ( Title::newFromText( $term ) ? 'searchsubtitle' : 'searchsubtitleinvalid' );
                $wgOut->setSubtitle( $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ) );
                $wgOut->setArticleRelated( false );
                $wgOut->setRobotpolicy( 'noindex,nofollow' );
        }
 
-       /**
-        * Extract default namespaces to search from the given user's
-        * settings, returning a list of index numbers.
-        *
-        * @param User $user
-        * @return array
-        * @private
-        */
-       function userNamespaces( &$user ) {
-               $arr = array();
-               foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
-                       if( $user->getOption( 'searchNs' . $ns ) ) {
-                               $arr[] = $ns;
-                       }
-               }
-               return $arr;
-       }
-
        /**
         * Extract "power search" namespace settings from the request object,
         * returning a list of index numbers to search.
@@ -319,22 +316,27 @@ class SpecialSearch {
                return $opt;
        }
 
-
-
        /**
+        * Show whole set of results 
+        * 
         * @param SearchResultSet $matches
-        * @param string $terms partial regexp for highlighting terms
         */
        function showMatches( &$matches ) {
                $fname = 'SpecialSearch::showMatches';
                wfProfileIn( $fname );
 
                global $wgContLang;
-               $tm = $wgContLang->convertForSearchResult( $matches->termMatches() );
-               $terms = implode( '|', $tm );
-
+               $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
+
+               $out = "";
+               
+               $infoLine = $matches->getInfo();
+               if( !is_null($infoLine) )
+                       $out .= "\n<!-- {$infoLine} -->\n";
+                       
+               
                $off = $this->offset + 1;
-               $out = "<ul start='{$off}' class='mw-search-results'>\n";
+               $out .= "<ul start='{$off}' class='mw-search-results'>\n";
 
                while( $result = $matches->next() ) {
                        $out .= $this->showHit( $result, $terms );
@@ -351,25 +353,22 @@ class SpecialSearch {
        /**
         * Format a single hit result
         * @param SearchResult $result
-        * @param string $terms partial regexp for highlighting terms
+        * @param array $terms terms to highlight
         */
        function showHit( $result, $terms ) {
                $fname = 'SpecialSearch::showHit';
                wfProfileIn( $fname );
                global $wgUser, $wgContLang, $wgLang;
-
-               $t = $result->getTitle();
-               if( is_null( $t ) ) {
+               
+               if( $result->isBrokenTitle() ) {
                        wfProfileOut( $fname );
                        return "<!-- Broken link in search result -->\n";
                }
+               
+               $t = $result->getTitle();
                $sk = $wgUser->getSkin();
 
-               //$contextlines = $wgUser->getOption( 'contextlines',  5 );
-               $contextlines = 2; // Hardcode this. Old defaults sucked. :)
-               $contextchars = $wgUser->getOption( 'contextchars', 50 );
-
-               $link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet());
+               $link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
 
                //If page content is not readable, just return the title.
                //This is not quite safe, but better than showing excerpts from non-readable pages
@@ -378,15 +377,34 @@ class SpecialSearch {
                        return "<li>{$link}</li>\n";
                }
 
-               $revision = Revision::newFromTitle( $t );
                // If the page doesn't *exist*... our search index is out of date.
                // The least confusing at this point is to drop the result.
                // You may get less results, but... oh well. :P
-               if( !$revision ) {
+               if( $result->isMissingRevision() ) {
                        return "<!-- missing page " .
                                htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
                }
 
+               // format redirects / relevant sections
+               $redirectTitle = $result->getRedirectTitle();
+               $redirectText = $result->getRedirectSnippet($terms);
+               $sectionTitle = $result->getSectionTitle();
+               $sectionText = $result->getSectionSnippet($terms);
+               $redirect = '';
+               if( !is_null($redirectTitle) )
+                       $redirect = "<span class='searchalttitle'>"
+                               .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
+                               ."</span>";
+               $section = '';
+               if( !is_null($sectionTitle) )
+                       $section = "<span class='searchalttitle'>" 
+                               .wfMsg('search-section', $sk->makeKnownLinkObj( $sectionTitle, $sectionText))
+                               ."</span>";
+
+               // format text extract
+               $extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>";
+               
+               // format score
                if( is_null( $result->getScore() ) ) {
                        // Search engine doesn't report scoring info
                        $score = '';
@@ -396,51 +414,27 @@ class SpecialSearch {
                                . ' - ';
                }
 
-               // try to fetch everything from the search engine backend
-               // then fill-in what couldn't be fetched
-               $extract = $result->getTextSnippet();
+               // format description
                $byteSize = $result->getByteSize();
                $wordCount = $result->getWordCount();
                $timestamp = $result->getTimestamp();
-               $redirectTitle = $result->getRedirectTitle();
-               $redirectText = $result->getRedirectSnippet();
-               $sectionTitle = $result->getSectionTitle();
-               $sectionText = $result->getSectionSnippet();
-
-               // fallback
-               if( is_null($extract) || is_null($wordCount) || is_null($byteSize) ){
-                       $text = $revision->getText();
-                       if( is_null($extract) )
-                               $extract = $this->extractText( $text, $terms, $contextlines, $contextchars );
-                       if( is_null($byteSize) )
-                               $byteSize = strlen( $text );
-                       if( is_null($wordCount) )
-                               $wordCount = str_word_count( $text );
-               }
-               if( is_null($timestamp) ){
-                       $timestamp = $revision->getTimestamp();
-               }
-
-               // format description
                $size = wfMsgExt( 'search-result-size', array( 'parsemag', 'escape' ),
                        $sk->formatSize( $byteSize ),
                        $wordCount );
                $date = $wgLang->timeanddate( $timestamp );
 
-               // format redirects / sections
-               $redirect = '';
-               if( !is_null($redirectTitle) )
-                       $redirect = "<span class='searchalttitle'>"
-                               .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
-                               ."</span>";
-               $section = '';
-               if( !is_null($sectionTitle) )
-                       $section = "<span class='searchalttitle'>"
-                               .wfMsg('search-section', $sk->makeKnownLinkObj( $sectionTitle, $sectionText))
-                               ."</span>";
-               // wrap extract
-               $extract = "<div class='searchresult'>".$extract."</div>";
-
+               // link to related articles if supported
+               $related = '';
+               if( $result->hasRelated() ){
+                       $st = SpecialPage::getTitleFor( 'Search' );
+                       $stParams = wfArrayToCGI( $this->powerSearchOptions(),
+                               array('search'    => wfMsg('searchrelated').':'.$t->getPrefixedText(),
+                                     'fulltext'  => wfMsg('search') ));
+                       
+                       $related = ' -- <a href="'.$st->escapeLocalURL($stParams).'">'. 
+                               wfMsg('search-relatedarticle').'</a>';
+               }
+                               
                // Include a thumbnail for media files...
                if( $t->getNamespace() == NS_IMAGE ) {
                        $img = wfFindFile( $t );
@@ -462,7 +456,7 @@ class SpecialSearch {
                                                '<td valign="top">' .
                                                $link .
                                                $extract .
-                                               "<div class='mw-search-result-data'>{$score}{$desc} - {$date}</div>" .
+                                               "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" .
                                                '</td>' .
                                                '</tr>' .
                                                '</table>' .
@@ -473,55 +467,109 @@ class SpecialSearch {
 
                wfProfileOut( $fname );
                return "<li>{$link} {$redirect} {$section} {$extract}\n" .
-                       "<div class='mw-search-result-data'>{$score}{$size} - {$date}</div>" .
+                       "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
                        "</li>\n";
 
        }
 
-       private function extractText( $text, $terms, $contextlines, $contextchars ) {
-               global $wgLang, $wgContLang;
-               $fname = __METHOD__;
-
-               $lines = explode( "\n", $text );
-
-               $max = intval( $contextchars ) + 1;
-               $pat1 = "/(.*)($terms)(.{0,$max})/i";
-
-               $lineno = 0;
-
-               $extract = "";
-               wfProfileIn( "$fname-extract" );
-               foreach ( $lines as $line ) {
-                       if ( 0 == $contextlines ) {
-                               break;
-                       }
-                       ++$lineno;
-                       $m = array();
-                       if ( ! preg_match( $pat1, $line, $m ) ) {
-                               continue;
-                       }
-                       --$contextlines;
-                       $pre = $wgContLang->truncate( $m[1], -$contextchars, ' ... ' );
+       /**
+        * Show results from other wikis
+        * 
+        * @param SearchResultSet $matches
+        */
+       function showInterwiki( &$matches, $query ) {
+               $fname = 'SpecialSearch::showInterwiki';
+               wfProfileIn( $fname );
 
-                       if ( count( $m ) < 3 ) {
-                               $post = '';
-                       } else {
-                               $post = $wgContLang->truncate( $m[3], $contextchars, ' ... ' );
-                       }
+               global $wgContLang;
+               $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
 
-                       $found = $m[2];
+               $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".wfMsg('search-interwiki-caption')."</div>\n";             
+               $off = $this->offset + 1;
+               $out .= "<ul start='{$off}' class='mw-search-iwresults'>\n";
+
+               // work out custom project captions
+               $customCaptions = array();
+               $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption>
+               foreach($customLines as $line){
+                       $parts = explode(":",$line,2);
+                       if(count($parts) == 2) // validate line
+                               $customCaptions[$parts[0]] = $parts[1]; 
+               }
+               
+               
+               $prev = null;
+               while( $result = $matches->next() ) {
+                       $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
+                       $prev = $result->getInterwikiPrefix();
+               }
+               // FIXME: should support paging in a non-confusing way (not sure how though, maybe via ajax)..
+               $out .= "</ul></div>\n";
 
-                       $line = htmlspecialchars( $pre . $found . $post );
-                       $pat2 = '/(' . $terms . ")/i";
-                       $line = preg_replace( $pat2,
-                         "<span class='searchmatch'>\\1</span>", $line );
+               // convert the whole thing to desired language variant
+               global $wgContLang;
+               $out = $wgContLang->convert( $out );
+               wfProfileOut( $fname );
+               return $out;
+       }
+       
+       /**
+        * Show single interwiki link
+        *
+        * @param SearchResult $result
+        * @param string $lastInterwiki
+        * @param array $terms
+        * @param string $query 
+        * @param array $customCaptions iw prefix -> caption
+        */
+       function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions){
+               $fname = 'SpecialSearch::showInterwikiHit';
+               wfProfileIn( $fname );
+               global $wgUser, $wgContLang, $wgLang;
+               
+               if( $result->isBrokenTitle() ) {
+                       wfProfileOut( $fname );
+                       return "<!-- Broken link in search result -->\n";
+               }
+               
+               $t = $result->getTitle();
+               $sk = $wgUser->getSkin();
+               
+               $link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
+                               
+               // format redirect if any
+               $redirectTitle = $result->getRedirectTitle();
+               $redirectText = $result->getRedirectSnippet($terms);
+               $redirect = '';
+               if( !is_null($redirectTitle) )
+                       $redirect = "<span class='searchalttitle'>"
+                               .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
+                               ."</span>";
 
-                       $extract .= "${line}\n";
+               $out = "";
+               // display project name 
+               if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()){
+                       if( key_exists($t->getInterwiki(),$customCaptions) )
+                               // captions from 'search-interwiki-custom'
+                               $caption = $customCaptions[$t->getInterwiki()];
+                       else{
+                               // default is to show the hostname of the other wiki which might suck 
+                               // if there are many wikis on one hostname
+                               $parsed = parse_url($t->getFullURL());
+                               $caption = wfMsg('search-interwiki-default', $parsed['host']); 
+                       }               
+                       // "more results" link (special page stuff could be localized, but we might not know target lang)
+                       $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");                        
+                       $searchLink = $sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'),
+                               wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search'))); 
+                       $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>{$searchLink}</span>{$caption}</div>\n<ul>";
                }
-               wfProfileOut( "$fname-extract" );
 
-               return $extract;
+               $out .= "<li>{$link} {$redirect}</li>\n"; 
+               wfProfileOut( $fname );
+               return $out;
        }
+       
 
        /**
         * Generates the power search box at bottom of [[Special:Search]]
@@ -575,7 +623,7 @@ class SpecialSearch {
                        'action' => $wgScript
                ));
                $out .= Xml::hidden( 'title', 'Special:Search' );
-               $out .= Xml::input( 'search', 50, $term ) . ' ';
+               $out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . ' ';
                foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
                        if( in_array( $ns, $this->namespaces ) ) {
                                $out .= Xml::hidden( "ns{$ns}", '1' );
index ecad643..390d42e 100644 (file)
@@ -45,11 +45,12 @@ class ApiOpenSearch extends ApiBase {
                $params = $this->extractRequestParams();
                $search = $params['search'];
                $limit = $params['limit'];
-
+               $namespaces = $params['namespace'];
+               
                // Open search results may be stored for a very long time
                $this->getMain()->setCacheMaxAge(1200);
 
-               $srchres = PrefixSearch::titleSearch( $search, $limit );
+               $srchres = PrefixSearch::titleSearch( $search, $limit, $namespaces );
 
                // Set top level elements
                $result = $this->getResult();
@@ -66,14 +67,20 @@ class ApiOpenSearch extends ApiBase {
                                ApiBase :: PARAM_MIN => 1,
                                ApiBase :: PARAM_MAX => 100,
                                ApiBase :: PARAM_MAX2 => 100
-                       )
+                       ),
+                       'namespace' => array(
+                               ApiBase :: PARAM_DFLT => NS_MAIN,
+                               ApiBase :: PARAM_TYPE => 'namespace',
+                               ApiBase :: PARAM_ISMULTI => true
+                       ),
                );
        }
 
        public function getParamDescription() {
                return array (
                        'search' => 'Search string',
-                       'limit' => 'Maximum amount of results to return'
+                       'limit' => 'Maximum amount of results to return',
+                       'namespace' => 'Namespaces to search',
                );
        }
 
index 52ed781..161631c 100644 (file)
@@ -1297,7 +1297,16 @@ You can [[:\$1|create this page]].",
 'search-redirect'       => '(redirect $1)',
 'search-section'        => '(section $1)',
 'search-suggest'        => 'Did you mean: $1',
+'search-interwiki-caption' => 'Sister projects',
+'search-interwiki-default' => "$1 results:",
+'search-interwiki-custom'  => '', # do not translate or duplicate this message to other languages
+'search-interwiki-more'    => '(more)',
+'search-mwsuggest-enabled' => 'with suggestions',
+'search-mwsuggest-disabled'=> 'no suggestions',
+'search-relatedarticle'    => 'Related',
+'mwsuggest-disable'     => 'Disable AJAX suggestions',  
 'searchall'             => 'all',
+'searchrelated'         => 'related',
 'showingresults'        => "Showing below up to {{PLURAL:$1|'''1''' result|'''$1''' results}} starting with #'''$2'''.",
 'showingresultsnum'     => "Showing below {{PLURAL:$3|'''1''' result|'''$3''' results}} starting with #'''$2'''.",
 'showingresultstotal'   => "Showing below results '''$1 - $2''' of '''$3'''",
index 50eb9ab..d3d9634 100644 (file)
@@ -701,6 +701,15 @@ $wgMessageStructure = array(
                'search-redirect',
                'search-section',
                'search-suggest',
+               'search-interwiki-caption',
+               'search-interwiki-default',
+               'search-interwiki-custom',
+               'search-interwiki-more',
+               'search-mwsuggest-enabled',
+               'search-mwsuggest-disabled',    
+               'search-relatedarticle',
+               'mwsuggest-disable',
+               'searchrelated',        
                'searchall',
                'showingresults',
                'showingresultsnum',
index a94e3e9..82e74b2 100644 (file)
@@ -16,7 +16,7 @@ $favicon = htmlspecialchars( wfExpandUrl( $wgFavicon ) );
 $title = SpecialPage::getTitleFor( 'Search' );
 $template = $title->escapeFullURL( 'search={searchTerms}' );
 
-$suggest = htmlspecialchars($wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}');
+$suggest = htmlspecialchars(SearchEngine::getOpenSearchTemplate() );
 
 
 $response = $wgRequest->response();
diff --git a/skins/common/mwsuggest.js b/skins/common/mwsuggest.js
new file mode 100644 (file)
index 0000000..244538b
--- /dev/null
@@ -0,0 +1,762 @@
+/*
+ * OpenSearch ajax suggestion engine for MediaWiki
+ * 
+ * uses core MediaWiki open search support to fetch suggestions
+ * and show them below search boxes and other inputs
+ *
+ * by Robert Stojnic (April 2008)
+ */
+// search_box_id -> Results object 
+var os_map = {};
+// cached data, url -> json_text
+var os_cache = {};
+// global variables for suggest_keypress
+var os_cur_keypressed = 0;
+var os_last_keypress = 0;
+var os_keypressed_count = 0;
+// type: Timer
+var os_timer = null;
+// tie mousedown/up events
+var os_mouse_pressed = false;
+var os_mouse_num = -1;
+// if true, the last change was made by mouse (and not keyboard)
+var os_mouse_moved = false;
+// delay between keypress and suggestion (in ms)
+var os_search_timeout = 250;
+// these pairs of inputs/forms will be autoloaded at startup
+var os_autoload_inputs = new Array('searchInput', 'powerSearchText', 'searchText');
+var os_autoload_forms = new Array('searchform', 'powersearch', 'search' );
+// if we stopped the service
+var os_is_stopped = false;
+// max lines to show in suggest table
+var os_max_lines_per_suggest = 7;
+// if we are about to focus the searchbox for the first time
+var os_first_focus = true;
+
+/** Timeout timer class that will fetch the results */ 
+function os_Timer(id,r,query){
+       this.id = id;
+       this.r = r;
+       this.query = query;     
+}
+
+/** Property class for single search box */
+function os_Results(name, formname){   
+       this.searchform = formname; // id of the searchform
+       this.searchbox = name; // id of the searchbox
+       this.container = name+"Suggest"; // div that holds results
+       this.resultTable = name+"Result"; // id base for the result table (+num = table row)
+       this.resultText = name+"ResultText"; // id base for the spans within result tables (+num)
+       this.toggle = name+"Toggle"; // div that has the toggle (enable/disable) link
+       this.query = null; // last processed query
+       this.results = null;  // parsed titles
+       this.resultCount = 0; // number of results
+       this.original = null; // query that user entered 
+       this.selected = -1; // which result is selected
+       this.containerCount = 0; // number of results visible in container 
+       this.containerRow = 0; // height of result field in the container
+       this.containerTotal = 0; // total height of the container will all results
+       this.visible = false; // if container is visible
+}
+
+/** Hide results div */
+function os_hideResults(r){
+       var c = document.getElementById(r.container);
+       if(c != null)
+               c.style.visibility = "hidden";
+       r.visible = false;
+       r.selected = -1;
+}
+
+/** Show results div */
+function os_showResults(r){
+       if(os_is_stopped)
+               return;
+       os_fitContainer(r);
+       var c = document.getElementById(r.container);
+       r.selected = -1;
+       if(c != null){
+               c.scrollTop = 0;
+               c.style.visibility = "visible";
+               r.visible = true;
+       }       
+}
+
+function os_operaWidthFix(x){
+       // TODO: better css2 incompatibility detection here
+       if(is_opera || is_khtml || navigator.userAgent.toLowerCase().indexOf('firefox/1')!=-1){
+               return x - 30; // opera&konqueror & old firefox don't understand overflow-x, estimate scrollbar width
+       }       
+       return x;
+}
+
+function os_encodeQuery(value){
+  if (encodeURIComponent) {
+    return encodeURIComponent(value);
+  }
+  if(escape) {
+    return escape(value);
+  }
+}
+function os_decodeValue(value){
+  if (decodeURIComponent) {
+    return decodeURIComponent(value);
+  } 
+  if(unescape){
+       return unescape(value);
+  }
+}
+
+/** Brower-dependent functions to find window inner size, and scroll status */
+function f_clientWidth() {
+       return f_filterResults (
+               window.innerWidth ? window.innerWidth : 0,
+               document.documentElement ? document.documentElement.clientWidth : 0,
+               document.body ? document.body.clientWidth : 0
+       );
+}
+function f_clientHeight() {
+       return f_filterResults (
+               window.innerHeight ? window.innerHeight : 0,
+               document.documentElement ? document.documentElement.clientHeight : 0,
+               document.body ? document.body.clientHeight : 0
+       );
+}
+function f_scrollLeft() {
+       return f_filterResults (
+               window.pageXOffset ? window.pageXOffset : 0,
+               document.documentElement ? document.documentElement.scrollLeft : 0,
+               document.body ? document.body.scrollLeft : 0
+       );
+}
+function f_scrollTop() {
+       return f_filterResults (
+               window.pageYOffset ? window.pageYOffset : 0,
+               document.documentElement ? document.documentElement.scrollTop : 0,
+               document.body ? document.body.scrollTop : 0
+       );
+}
+function f_filterResults(n_win, n_docel, n_body) {
+       var n_result = n_win ? n_win : 0;
+       if (n_docel && (!n_result || (n_result > n_docel)))
+               n_result = n_docel;
+       return n_body && (!n_result || (n_result > n_body)) ? n_body : n_result;
+}
+
+/** Get the height available for the results container */
+function os_availableHeight(r){
+       var absTop = document.getElementById(r.container).style.top;
+       var px = absTop.lastIndexOf("px");
+       if(px > 0)
+               absTop = absTop.substring(0,px);
+       return f_clientHeight() - (absTop - f_scrollTop());
+}
+
+
+/** Get element absolute position {left,top} */
+function os_getElementPosition(elemID){
+       var offsetTrail = document.getElementById(elemID);
+       var offsetLeft = 0;
+       var offsetTop = 0;
+       while (offsetTrail){
+               offsetLeft += offsetTrail.offsetLeft;
+               offsetTop += offsetTrail.offsetTop;
+               offsetTrail = offsetTrail.offsetParent;
+       }
+       if (navigator.userAgent.indexOf('Mac') != -1 && typeof document.body.leftMargin != 'undefined'){
+               offsetLeft += document.body.leftMargin;
+               offsetTop += document.body.topMargin;
+       }
+       return {left:offsetLeft,top:offsetTop};
+}
+
+/** Create the container div that will hold the suggested titles */
+function os_createContainer(r){
+       var c = document.createElement("div");
+       var s = document.getElementById(r.searchbox);
+       var pos = os_getElementPosition(r.searchbox);   
+       var left = pos.left;
+       var top = pos.top + s.offsetHeight;
+       var body = document.getElementById("globalWrapper");
+       c.className = "os-suggest";
+       c.setAttribute("id", r.container);      
+       body.appendChild(c); 
+       
+       // dynamically generated style params   
+       // IE workaround, cannot explicitely set "style" attribute
+       c = document.getElementById(r.container);
+       c.style.top = top+"px";
+       c.style.left = left+"px";
+       c.style.width = s.offsetWidth+"px";
+       
+       // mouse event handlers
+       c.onmouseover = function(event) { os_eventMouseover(r.searchbox, event); };
+       c.onmousemove = function(event) { os_eventMousemove(r.searchbox, event); };
+       c.onmousedown = function(event) { return os_eventMousedown(r.searchbox, event); };
+       c.onmouseup = function(event) { os_eventMouseup(r.searchbox, event); };
+       return c;
+}
+
+/** change container height to fit to screen */
+function os_fitContainer(r){   
+       var c = document.getElementById(r.container);
+       var h = os_availableHeight(r) - 20;
+       var inc = r.containerRow;
+       h = parseInt(h/inc) * inc;
+       if(h < (2 * inc) && r.resultCount > 1) // min: two results
+               h = 2 * inc;    
+       if((h/inc) > os_max_lines_per_suggest )
+               h = inc * os_max_lines_per_suggest;
+       if(h < r.containerTotal){
+               c.style.height = h +"px";
+               r.containerCount = parseInt(Math.round(h/inc));
+       } else{
+               c.style.height = r.containerTotal+"px";
+               r.containerCount = r.resultCount;
+       }
+}
+/** If some entries are longer than the box, replace text with "..." */
+function os_trimResultText(r){
+       var w = document.getElementById(r.container).offsetWidth;
+       if(r.containerCount < r.resultCount){           
+               w -= 20; // give 20px for scrollbar             
+       } else
+               w = os_operaWidthFix(w);
+       if(w < 10)
+               return;
+       for(var i=0;i<r.resultCount;i++){
+               var e = document.getElementById(r.resultText+i);
+               var replace = 1;
+               var lastW = e.offsetWidth+1;
+               var iteration = 0;
+               var changedText = false;
+               while(e.offsetWidth > w && (e.offsetWidth < lastW || iteration<2)){
+                       changedText = true;
+                       lastW = e.offsetWidth;
+                       var l = e.innerHTML;                    
+                       e.innerHTML = l.substring(0,l.length-replace)+"...";
+                       iteration++;
+                       replace = 4; // how many chars to replace
+               }
+               if(changedText){
+                       // show hint for trimmed titles
+                       document.getElementById(r.resultTable+i).setAttribute("title",r.results[i]);
+               }
+       }
+}
+
+/** Handles data from XMLHttpRequest, and updates the suggest results */
+function os_updateResults(r, query, text, cacheKey){    
+       os_cache[cacheKey] = text;
+       r.query = query;
+       r.original = query;
+       if(text == ""){
+               r.results = null;
+               r.resultCount = 0;
+               os_hideResults(r);
+       } else{         
+               try {
+                       var p = eval('('+text+')'); // simple json parse, could do a safer one
+                       if(p.length<2 || p[1].length == 0){
+                               r.results = null;
+                               r.resultCount = 0;
+                               os_hideResults(r);
+                               return;
+                       }               
+                       var c = document.getElementById(r.container);
+                       if(c == null)
+                               c = os_createContainer(r);                      
+                       c.innerHTML = os_createResultTable(r,p[1]);
+                       // init container table sizes
+                       var t = document.getElementById(r.resultTable);         
+                       r.containerTotal = t.offsetHeight;      
+                       r.containerRow = t.offsetHeight / r.resultCount;
+                       os_trimResultText(r);                           
+                       os_showResults(r);
+               } catch(e){
+                       // bad response from server or such
+                       os_hideResults(r);                      
+                       os_cache[cacheKey] = null;
+               }
+       }       
+}
+
+/** Create the result table to be placed in the container div */
+function os_createResultTable(r, results){
+       var c = document.getElementById(r.container);
+       var width = os_operaWidthFix(c.offsetWidth);    
+       var html = "<table class=\"os-suggest-results\" id=\""+r.resultTable+"\" style=\"width: "+width+"px;\">";
+       r.results = new Array();
+       r.resultCount = results.length;
+       for(i=0;i<results.length;i++){
+               var title = os_decodeValue(results[i]);
+               r.results[i] = title;
+               html += "<tr><td class=\"os-suggest-result\" id=\""+r.resultTable+i+"\"><span id=\""+r.resultText+i+"\">"+title+"</span></td></tr>";
+       }
+       html+="</table>"
+       return html;
+}
+
+/** Fetch namespaces from checkboxes or hidden fields in the search form,
+    if none defined use wgSearchNamespaces global */
+function os_getNamespaces(r){  
+       var namespaces = "";
+       var elements = document.forms[r.searchform].elements;
+       for(i=0; i < elements.length; i++){
+               var name = elements[i].name;
+               if(typeof name != 'undefined' && name.length > 2 
+               && name[0]=='n' && name[1]=='s' 
+               && ((elements[i].type=='checkbox' && elements[i].checked) 
+                       || (elements[i].type=='hidden' && elements[i].value=="1")) ){
+                       if(namespaces!="")
+                               namespaces+="|";
+                       namespaces+=name.substring(2);
+               }
+       }
+       if(namespaces == "")
+               namespaces = wgSearchNamespaces.join("|");
+       return namespaces;
+}
+
+/** Update results if user hasn't already typed something else */
+function os_updateIfRelevant(r, query, text, cacheKey){
+       var t = document.getElementById(r.searchbox);
+       if(t != null && t.value == query){ // check if response is still relevant                                       
+               os_updateResults(r, query, text, cacheKey);
+       }
+       r.query = query;
+}
+
+/** Fetch results after some timeout */
+function os_delayedFetch(){
+       if(os_timer == null)
+               return;
+       var r = os_timer.r;
+       var query = os_timer.query;
+       os_timer = null;
+       var path = wgMWSuggestTemplate.replace("{namespaces}",os_getNamespaces(r))
+                                                                 .replace("{dbname}",wgDBname)
+                                                                 .replace("{searchTerms}",os_encodeQuery(query));
+       
+       // try to get from cache, if not fetch using ajax
+       var cached = os_cache[path];
+       if(cached != null){
+               os_updateIfRelevant(r, query, cached, path);
+       } else{                                                                   
+               var xmlhttp = sajax_init_object();
+               if(xmlhttp){
+                       try {                   
+                               xmlhttp.open("GET", path, true);
+                               xmlhttp.onreadystatechange=function(){
+                               if (xmlhttp.readyState==4 && typeof os_updateIfRelevant == 'function') {                                
+                                       os_updateIfRelevant(r, query, xmlhttp.responseText, path);
+                               }
+                       };
+                       xmlhttp.send(null);             
+               } catch (e) {
+                               if (window.location.hostname == "localhost") {
+                                       alert("Your browser blocks XMLHttpRequest to 'localhost', try using a real hostname for development/testing.");
+                               }
+                               throw e;
+                       }
+               }
+       }
+}
+
+/** Init timed update via os_delayedUpdate() */
+function os_fetchResults(r, query, timeout){
+       if(query == ""){
+               os_hideResults(r);
+               return;
+       } else if(query == r.query)
+               return; // no change
+       
+       os_is_stopped = false; // make sure we're running
+       
+       /* var cacheKey = wgDBname+":"+query; 
+       var cached = os_cache[cacheKey];
+       if(cached != null){
+               os_updateResults(r,wgDBname,query,cached);
+               return;
+       } */
+       
+       // cancel any pending fetches
+       if(os_timer != null && os_timer.id != null)
+               clearTimeout(os_timer.id);
+       // schedule delayed fetching of results 
+       if(timeout != 0){
+               os_timer = new os_Timer(setTimeout("os_delayedFetch()",timeout),r,query);
+       } else{         
+               os_timer = new os_Timer(null,r,query);
+               os_delayedFetch(); // do it now!
+       }
+
+}
+/** Change the highlighted row (i.e. suggestion), from position cur to next */
+function os_changeHighlight(r, cur, next, updateSearchBox){
+       if (next >= r.resultCount)
+               next = r.resultCount-1;
+       if (next < -1)
+               next = -1;   
+       r.selected = next;
+       if (cur == next)
+       return; // nothing to do.
+    
+    if(cur >= 0){
+       var curRow = document.getElementById(r.resultTable + cur);
+       if(curRow != null)
+               curRow.className = "os-suggest-result";
+    }
+    var newText;
+    if(next >= 0){
+       var nextRow = document.getElementById(r.resultTable + next);
+       if(nextRow != null)
+               nextRow.className = "os-suggest-result-hl";
+       newText = r.results[next];
+    } else
+       newText = r.original;
+       
+    // adjust the scrollbar if any
+    if(r.containerCount < r.resultCount){
+       var c = document.getElementById(r.container);
+       var vStart = c.scrollTop / r.containerRow;
+       var vEnd = vStart + r.containerCount;
+       if(next < vStart)
+               c.scrollTop = next * r.containerRow;
+       else if(next >= vEnd)
+               c.scrollTop = (next - r.containerCount + 1) * r.containerRow;
+    }
+       
+    // update the contents of the search box
+    if(updateSearchBox){
+       os_updateSearchQuery(r,newText);        
+    }
+}
+
+function os_updateSearchQuery(r,newText){
+       document.getElementById(r.searchbox).value = newText;
+    r.query = newText;
+}
+
+/** Find event target */
+function os_getTarget(e){
+       if (!e) var e = window.event;
+       if (e.target) return e.target;
+       else if (e.srcElement) return e.srcElement;
+       else return null;
+}
+
+
+
+/********************
+ *  Keyboard events 
+ ********************/ 
+
+/** Event handler that will fetch results on keyup */
+function os_eventKeyup(e){
+       var targ = os_getTarget(e);
+       var r = os_map[targ.id];
+       if(r == null)
+               return; // not our event
+               
+       // some browsers won't generate keypressed for arrow keys, catch it 
+       if(os_keypressed_count == 0){
+               os_processKey(r,os_cur_keypressed,targ);
+       }
+       var query = targ.value;
+       os_fetchResults(r,query,os_search_timeout);
+}
+
+/** catch arrows up/down and escape to hide the suggestions */
+function os_processKey(r,keypressed,targ){
+       if (keypressed == 40){ // Arrow Down
+       if (r.visible) {                
+               os_changeHighlight(r, r.selected, r.selected+1, true);                  
+       } else if(os_timer == null){
+               // user wants to get suggestions now
+               r.query = "";
+                       os_fetchResults(r,targ.value,0);
+       }
+       } else if (keypressed == 38){ // Arrow Up
+               if (r.visible){
+                       os_changeHighlight(r, r.selected, r.selected-1, true);
+               }
+       } else if(keypressed == 27){ // Escape
+               document.getElementById(r.searchbox).value = r.original;
+               r.query = r.original;
+               os_hideResults(r);
+       } else if(r.query != document.getElementById(r.searchbox).value){
+               // os_hideResults(r); // don't show old suggestions
+       }
+}
+
+/** When keys is held down use a timer to output regular events */
+function os_eventKeypress(e){  
+       var targ = os_getTarget(e);
+       var r = os_map[targ.id];
+       if(r == null)
+               return; // not our event
+       
+       var keypressed = os_cur_keypressed;
+       if(keypressed == 38 || keypressed == 40){
+               var d = new Date()
+               var now = d.getTime();
+               if(now - os_last_keypress < 120){
+                       os_last_keypress = now;
+                       return;
+               }
+       }
+       
+       os_keypressed_count++;
+       os_processKey(r,keypressed,targ);
+}
+
+/** Catch the key code (Firefox bug)  */
+function os_eventKeydown(e){
+       var targ = os_getTarget(e);
+       var r = os_map[targ.id];
+       if(r == null)
+               return; // not our event
+               
+       os_mouse_moved = false;
+               
+       if(os_first_focus){
+               // firefox bug, focus&defocus to make autocomplete=off valid
+               targ.blur(); targ.focus();
+               os_first_focus = false;
+       }
+
+       os_cur_keypressed = (window.Event) ? e.which : e.keyCode;
+       os_last_keypress = 0;
+       os_keypressed_count = 0;
+}
+
+/** Event: loss of focus of input box */
+function os_eventBlur(e){      
+       if(os_first_focus)
+               return; // we are focusing/defocusing
+       var targ = os_getTarget(e);
+       var r = os_map[targ.id];
+       if(r == null)
+               return; // not our event
+       if(!os_mouse_pressed)   
+               os_hideResults(r);
+}
+
+/** Event: focus (catch only when stopped) */
+function os_eventFocus(e){     
+       if(os_first_focus)
+               return; // we are focusing/defocusing
+}
+
+
+
+/********************
+ *  Mouse events 
+ ********************/ 
+
+/** Mouse over the container */
+function os_eventMouseover(srcId, e){
+       var targ = os_getTarget(e);     
+       var r = os_map[srcId];
+       if(r == null || !os_mouse_moved)
+               return; // not our event
+       var num = os_getNumberSuffix(targ.id);
+       if(num >= 0)
+               os_changeHighlight(r,r.selected,num,false);
+                                       
+}
+
+/* Get row where the event occured (from its id) */
+function os_getNumberSuffix(id){
+       var num = id.substring(id.length-2);
+       if( ! (num.charAt(0) >= '0' && num.charAt(0) <= '9') )
+               num = num.substring(1);
+       if(os_isNumber(num))
+               return parseInt(num);
+       else
+               return -1;
+}
+
+/** Save mouse move as last action */
+function os_eventMousemove(srcId, e){
+       os_mouse_moved = true;
+}
+
+/** Mouse button held down, register possible click  */
+function os_eventMousedown(srcId, e){
+       var targ = os_getTarget(e);
+       var r = os_map[srcId];
+       if(r == null)
+               return; // not our event
+       var num = os_getNumberSuffix(targ.id);
+       
+       os_mouse_pressed = true;
+       if(num >= 0){
+               os_mouse_num = num;
+               // os_updateSearchQuery(r,r.results[num]);
+       }
+       // keep the focus on the search field
+       document.getElementById(r.searchbox).focus();
+       
+       return false; // prevents selection
+}
+
+/** Mouse button released, check for click on some row */
+function os_eventMouseup(srcId, e){
+       var targ = os_getTarget(e);
+       var r = os_map[srcId];
+       if(r == null)
+               return; // not our event
+       var num = os_getNumberSuffix(targ.id);
+               
+       if(num >= 0 && os_mouse_num == num){
+               os_updateSearchQuery(r,r.results[num]);
+               os_hideResults(r);
+               document.getElementById(r.searchform).submit();
+       }
+       os_mouse_pressed = false;
+       // keep the focus on the search field
+       document.getElementById(r.searchbox).focus();
+}
+
+/** Check if x is a valid integer */
+function os_isNumber(x){
+       if(x == "" || isNaN(x))
+               return false;
+       for(var i=0;i<x.length;i++){
+               var c = x.charAt(i);
+               if( ! (c >= '0' && c <= '9') )
+                       return false;
+       }
+       return true;
+}
+
+
+/** When the form is submitted hide everything, cancel updates... */
+function os_eventOnsubmit(e){
+       var targ = os_getTarget(e);
+
+       os_is_stopped = true;
+       // kill timed requests
+       if(os_timer != null && os_timer.id != null){
+               clearTimeout(os_timer.id);
+               os_timer = null;
+       }
+       // Hide all suggestions
+       for(i=0;i<os_autoload_inputs.length;i++){
+               var r = os_map[os_autoload_inputs[i]];
+               if(r != null){
+                       var b = document.getElementById(r.searchform);
+                       if(b != null && b == targ){ 
+                               // set query value so the handler won't try to fetch additional results
+                               r.query = document.getElementById(r.searchbox).value;
+                       }                       
+                       os_hideResults(r);
+               }
+       }
+       return true;
+}
+
+/** Init Result objects and event handlers */
+function os_initHandlers(name, formname, element){
+       var r = new os_Results(name, formname); 
+       // event handler
+       element.onkeyup = function(event) { os_eventKeyup(event); };
+       element.onkeydown = function(event) { os_eventKeydown(event); };
+       element.onkeypress = function(event) { os_eventKeypress(event); };
+       element.onblur = function(event) { os_eventBlur(event); };
+       element.onfocus = function(event) { os_eventFocus(event); };
+       element.setAttribute("autocomplete","off");
+       // stopping handler
+       document.getElementById(formname).onsubmit = function(event){ return os_eventOnsubmit(event); };
+       os_map[name] = r; 
+       // toggle link
+       if(document.getElementById(r.toggle) == null){
+               // TODO: disable this while we figure out a way for this to work in all browsers 
+               /* if(name=='searchInput'){
+                       // special case: place above the main search box
+                       var t = os_createToggle(r,"os-suggest-toggle");
+                       var searchBody = document.getElementById('searchBody');
+                       var first = searchBody.parentNode.firstChild.nextSibling.appendChild(t);
+               } else{
+                       // default: place below search box to the right
+                       var t = os_createToggle(r,"os-suggest-toggle-def");
+                       var top = element.offsetTop + element.offsetHeight;
+                       var left = element.offsetLeft + element.offsetWidth;
+                       t.style.position = "absolute";
+                       t.style.top = top + "px";
+                       t.style.left = left + "px";
+                       element.parentNode.appendChild(t);
+                       // only now width gets calculated, shift right
+                       left -= t.offsetWidth;
+                       t.style.left = left + "px";
+                       t.style.visibility = "visible";
+               } */
+       }
+       
+}
+
+/** Return the span element that contains the toggle link */
+function os_createToggle(r,className){
+       var t = document.createElement("span");
+       t.className = className;
+       t.setAttribute("id", r.toggle);
+       var link = document.createElement("a");
+       link.setAttribute("href","javascript:void(0);");
+       link.onclick = function(){ os_toggle(r.searchbox,r.searchform) };
+       var msg = document.createTextNode(wgMWSuggestMessages[0]);
+       link.appendChild(msg);
+       t.appendChild(link);
+       return t;       
+}
+
+/** Call when user clicks on some of the toggle links */
+function os_toggle(inputId,formName){
+       r = os_map[inputId];
+       var msg = '';
+       if(r == null){
+               os_enableSuggestionsOn(inputId,formName);
+               r = os_map[inputId];
+               msg = wgMWSuggestMessages[0];           
+       } else{
+               os_disableSuggestionsOn(inputId,formName);
+               msg = wgMWSuggestMessages[1];
+       }
+       // change message
+       var link = document.getElementById(r.toggle).firstChild;
+       link.replaceChild(document.createTextNode(msg),link.firstChild);
+}
+
+/** Call this to enable suggestions on input (id=inputId), on a form (name=formName) */
+function os_enableSuggestionsOn(inputId, formName){
+       os_initHandlers( inputId, formName, document.getElementById(inputId) );
+}
+
+/** Call this to disable suggestios on input box (id=inputId) */
+function os_disableSuggestionsOn(inputId){
+       r = os_map[inputId];
+       if(r != null){
+               // cancel/hide results
+               os_timer = null;
+               os_hideResults(r);
+               // turn autocomplete on !
+               document.getElementById(inputId).setAttribute("autocomplete","on");
+               // remove descriptor    
+               os_map[inputId] = null;
+       }
+}
+
+/** Initialization, call upon page onload */
+function os_MWSuggestInit() {
+       for(i=0;i<os_autoload_inputs.length;i++){
+               var id = os_autoload_inputs[i];
+               var form = os_autoload_forms[i];
+               element = document.getElementById( id );
+               if(element != null)
+                       os_initHandlers(id,form,element);
+       }       
+}
+
+hookEvent("load", os_MWSuggestInit);
index 938ae35..f4302b0 100644 (file)
@@ -91,11 +91,44 @@ p.mw-ipb-conveniencelinks, p.mw-filedelete-editreasons, p.mw-delete-editreasons
 }
 
 /* Search results */
+div.searchresult {
+       font-size: 95%;
+       width:38em;
+}
 .mw-search-results li {
        padding-bottom: 1em;
 }
 .mw-search-result-data {
        color: green;
+       font-size: 97%;
+}
+
+div#mw-search-interwiki {
+       float: right;
+       width: 18em;
+       border-style: solid;
+       border-color: #AAAAAA;
+       border-width: 1px;
+       margin-top: 2ex;
+}
+
+div#mw-search-interwiki li {
+       font-size: 95%;
+}
+
+.mw-search-interwiki-more {
+       float: right;
+       font-size: 90%;
+}
+
+span.searchalttitle {
+       font-size: 95%;
+}
+
+div.searchdidyoumean {
+       font-size: 127%;
+       padding-bottom:1ex;
+       padding-top:1ex; 
 }
 
 /*
@@ -109,6 +142,53 @@ table.mw-userrights-groups * td,table.mw-userrights-groups * th {
        padding-right: 1.5em;
 }
 
+/* 
+ * OpenSearch ajax suggestions
+ */
+.os-suggest {
+       overflow: auto; 
+       overflow-x: hidden; 
+       position: absolute;
+       top: 0px;
+       left: 0px;
+       width: 0px;
+       background-color: white; 
+       border-style: solid;
+       border-color: #AAAAAA;
+       border-width: 1px;
+       z-index:99; 
+       visibility:hidden; 
+       font-size:95%;  
+}
+
+table.os-suggest-results {
+       font-size: 95%;
+       cursor: pointer; 
+       border: 0; 
+}
+
+td.os-suggest-result, td.os-suggest-result-hl {
+       align: left; 
+       white-space: nowrap;
+       background-color: white; 
+}
+td.os-suggest-result-hl {
+       background-color: #4C59A6; 
+       color: white;
+}
+.os-suggest-toggle {
+       position: relative; 
+       left: 1ex;
+       font-size: 65%;
+}
+.os-suggest-toggle-def {
+       position: absolute;
+       top: 0px;
+       left: 0px;
+       font-size: 65%;
+       visibility: hidden;
+}
+
 /* Page history styling */
 /* the auto-generated edit comments */
 .autocomment { color: gray; }
index 03fb31a..9a45c6f 100644 (file)
@@ -1167,7 +1167,7 @@ div.patrollink {
        font-size: 75%;
        text-align: right;
 }
-span.newpage, span.minor, span.searchmatch, span.bot {
+span.newpage, span.minor, span.bot {
        font-weight: bold;
 }
 span.unpatrolled {
@@ -1175,24 +1175,6 @@ span.unpatrolled {
        color: red;
 }
 
-span.searchmatch {
-       font-size: 95%; 
-}
-div.searchresult {
-       font-size: 95%;
-       width:38em;
-}
-
-span.searchalttitle {
-       font-size: 95%;
-}
-
-div.searchdidyoumean {
-       font-size: 127%;
-       padding-bottom:1ex;
-       padding-top:1ex; 
-}
-
 .sharedUploadNotice {
        font-style: italic;
 }
@@ -1562,3 +1544,25 @@ div#mw-recreate-deleted-warn ul li {
        vertical-align: middle;
        font-size: 90%;
 }
+
+/** Special:Search stuff */
+div#mw-search-interwiki-caption {
+       text-align: center;
+       font-weight: bold;
+       font-size: 95%;
+}
+
+.mw-search-interwiki-project {
+       font-size: 97%;
+       text-align: left;
+       padding-left: 0.2em;
+       padding-right: 0.15em;
+       padding-bottom: 0.2em;
+       padding-top: 0.15em;
+       background: #cae8ff;
+}
+
+span.searchmatch {
+       font-weight: bold;
+}
+