[Python-checkins] r66360 - in doctools/trunk/sphinx: builder.py search.py static/doctools.js static/searchtools.js templates/search.html

armin.ronacher python-checkins at python.org
Wed Sep 10 11:11:57 CEST 2008


Author: armin.ronacher
Date: Wed Sep 10 11:11:56 2008
New Revision: 66360

Log:
Improved search system.  The search index is now a regular javascript file which should speed things up because browsers can cache it.  Removed unused code from doctools.js



Modified:
   doctools/trunk/sphinx/builder.py
   doctools/trunk/sphinx/search.py
   doctools/trunk/sphinx/static/doctools.js
   doctools/trunk/sphinx/static/searchtools.js
   doctools/trunk/sphinx/templates/search.html

Modified: doctools/trunk/sphinx/builder.py
==============================================================================
--- doctools/trunk/sphinx/builder.py	(original)
+++ doctools/trunk/sphinx/builder.py	Wed Sep 10 11:11:56 2008
@@ -34,6 +34,7 @@
 from sphinx.environment import BuildEnvironment, NoUri
 from sphinx.highlighting import PygmentsBridge
 from sphinx.util.console import bold, purple, darkgreen
+from sphinx.search import js_index
 
 # side effect: registers roles and directives
 from sphinx import roles
@@ -338,10 +339,10 @@
     name = 'html'
     copysource = True
     out_suffix = '.html'
-    indexer_format = json
+    indexer_format = js_index
     supported_image_types = ['image/svg+xml', 'image/png', 'image/gif',
                              'image/jpeg']
-    searchindex_filename = 'searchindex.json'
+    searchindex_filename = 'searchindex.js'
     add_header_links = True
     add_definition_links = True
 

Modified: doctools/trunk/sphinx/search.py
==============================================================================
--- doctools/trunk/sphinx/search.py	(original)
+++ doctools/trunk/sphinx/search.py	Wed Sep 10 11:11:56 2008
@@ -10,6 +10,7 @@
 """
 import re
 import cPickle as pickle
+from cStringIO import StringIO
 
 from docutils.nodes import Text, NodeVisitor
 
@@ -20,6 +21,37 @@
 word_re = re.compile(r'\w+(?u)')
 
 
+class _JavaScriptIndex(object):
+    """
+    The search index as javascript file that calls a function
+    on the documentation search object to register the index.
+    This serializing system does not support chaining because
+    simplejson (which it depends on) doesn't support it either.
+    """
+
+    PREFIX = 'Search.setIndex('
+    SUFFIX = ')'
+
+    def dumps(self, data):
+        return self.PREFIX + json.dumps(data) + self.SUFFIX
+
+    def loads(self, s):
+        data = s[len(self.PREFIX):-len(self.SUFFIX)]
+        if not data or not s.startswith(self.PREFIX) or not \
+           s.endswith(self.SUFFIX):
+            raise ValueError('invalid data')
+        return json.loads(data)
+
+    def dump(self, data, f):
+        f.write(self.dumps(data))
+
+    def load(self, f):
+        return self.loads(f.read())
+
+
+js_index = _JavaScriptIndex()
+
+
 class Stemmer(PorterStemmer):
     """
     All those porter stemmer implementations look hideous.

Modified: doctools/trunk/sphinx/static/doctools.js
==============================================================================
--- doctools/trunk/sphinx/static/doctools.js	(original)
+++ doctools/trunk/sphinx/static/doctools.js	Wed Sep 10 11:11:56 2008
@@ -94,11 +94,9 @@
 var Documentation = {
 
   init : function() {
-    /* this.addContextElements(); -- now done statically */
     this.fixFirefoxAnchorBug();
     this.highlightSearchWords();
     this.initModIndex();
-    this.initComments();
   },
 
   /**
@@ -108,6 +106,8 @@
   PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; },
   LOCALE : 'unknown',
 
+  // gettext and ngettext don't access this so that the functions
+  // can savely bound to a different name (_ = Documentation.gettext)
   gettext : function(string) {
     var translated = Documentation.TRANSLATIONS[string];
     if (typeof translated == 'undefined')
@@ -133,14 +133,12 @@
    * add context elements like header anchor links
    */
   addContextElements : function() {
-    for (var i = 1; i <= 6; i++) {
-      $('h' + i + '[@id]').each(function() {
-        $('<a class="headerlink">\u00B6</a>').
-        attr('href', '#' + this.id).
-        attr('title', _('Permalink to this headline')).
-        appendTo(this);
-      });
-    }
+    $('div[@id] > :header:first').each(function() {
+      $('<a class="headerlink">\u00B6</a>').
+      attr('href', '#' + this.id).
+      attr('title', _('Permalink to this headline')).
+      appendTo(this);
+    });
     $('dt[@id]').each(function() {
       $('<a class="headerlink">\u00B6</a>').
       attr('href', '#' + this.id).
@@ -197,37 +195,6 @@
   },
 
   /**
-   * init the inline comments
-   */
-  initComments : function() {
-    $('.inlinecomments div.actions').each(function() {
-      this.innerHTML += ' | ';
-      $(this).append($('<a href="#">hide comments</a>').click(function() {
-        $(this).parent().parent().toggle();
-        return false;
-      }));
-    });
-    $('.inlinecomments .comments').hide();
-    $('.inlinecomments a.bubble').each(function() {
-      $(this).click($(this).is('.emptybubble') ? function() {
-          var params = $.getQueryParameters(this.href);
-          Documentation.newComment(params.target[0]);
-          return false;
-        } : function() {
-          $('.comments', $(this).parent().parent()[0]).toggle();
-          return false;
-      });
-    });
-    $('#comments div.actions a.newcomment').click(function() {
-      Documentation.newComment();
-      return false;
-    });
-    if (document.location.hash.match(/^#comment-/))
-      $('.inlinecomments .comments ' + document.location.hash)
-        .parent().toggle();
-  },
-
-  /**
    * helper function to hide the search marks again
    */
   hideSearchWords : function() {
@@ -236,22 +203,6 @@
   },
 
   /**
-   * show the comment window for a certain id or the whole page.
-   */
-  newComment : function(id) {
-    Documentation.CommentWindow.openFor(id || '');
-  },
-
-  /**
-   * write a new comment from within a comment view box
-   */
-  newCommentFromBox : function(link) {
-    var params = $.getQueryParameters(link.href);
-    $(link).parent().parent().fadeOut('slow');
-    this.newComment(params.target);
-  },
-
-  /**
    * make the url absolute
    */
   makeURL : function(relativeURL) {
@@ -270,108 +221,7 @@
     });
     var url = parts.join('/');
     return path.substring(url.lastIndexOf('/') + 1, path.length - 1);
-  },
-
-  /**
-   * class that represents the comment window
-   */
-  CommentWindow : (function() {
-    var openWindows = {};
-
-    var Window = function(sectionID) {
-      this.url = Documentation.makeURL('@comments/' + Documentation.getCurrentURL()
-        + '/?target=' + $.urlencode(sectionID) + '&mode=ajax');
-      this.sectionID = sectionID;
-
-      this.root = $('<div class="commentwindow"></div>');
-      this.root.appendTo($('body'));
-      this.title = $('<h3>New Comment</h3>').appendTo(this.root);
-      this.body = $('<div class="form">please wait...</div>').appendTo(this.root);
-      this.resizeHandle = $('<div class="resizehandle"></div>').appendTo(this.root);
-
-      this.root.Draggable({
-        handle:       this.title[0]
-      });
-
-      this.root.css({
-        left:         window.innerWidth / 2 - $(this.root).width() / 2,
-        top:          window.scrollY + (window.innerHeight / 2 - 150)
-      });
-      this.root.fadeIn('slow');
-      this.updateView();
-    };
-
-    Window.prototype.updateView = function(data) {
-      var self = this;
-      function update(data) {
-        if (data.posted) {
-          document.location.hash = '#comment-' + data.commentID;
-          document.location.reload();
-        }
-        else {
-          self.body.html(data.body);
-          $('div.actions', self.body).append($('<input>')
-            .attr('type', 'button')
-            .attr('value', 'Close')
-            .click(function() { self.close(); })
-          );
-          $('div.actions input[@name="preview"]')
-            .attr('type', 'button')
-            .click(function() { self.submitForm($('form', self.body)[0], true); });
-          $('form', self.body).bind("submit", function() {
-            self.submitForm(this);
-            return false;
-          });
-
-          if (data.error) {
-            self.root.Highlight(1000, '#aadee1');
-            $('div.error', self.root).slideDown(500);
-          }
-        }
-      }
-
-      if (typeof data == 'undefined')
-        $.getJSON(this.url, function(json) { update(json); });
-      else
-        $.ajax({
-          url:      this.url,
-          type:     'POST',
-          dataType: 'json',
-          data:     data,
-          success:  function(json) { update(json); }
-        });
-    }
-
-    Window.prototype.getFormValue = function(name) {
-      return $('*[@name="' + name + '"]', this.body)[0].value;
-    }
-
-    Window.prototype.submitForm = function(form, previewMode) {
-      this.updateView({
-        author:         form.author.value,
-        author_mail:    form.author_mail.value,
-        title:          form.title.value,
-        comment_body:   form.comment_body.value,
-        preview:        previewMode ? 'yes' : ''
-      });
-    }
-
-    Window.prototype.close = function() {
-      var self = this;
-      delete openWindows[this.sectionID];
-      this.root.fadeOut('slow', function() {
-        self.root.remove();
-      });
-    }
-
-    Window.openFor = function(sectionID) {
-      if (sectionID in openWindows)
-        return openWindows[sectionID];
-      return new Window(sectionID);
-    }
-
-    return Window;
-  })()
+  }
 };
 
 // quick alias for translations

Modified: doctools/trunk/sphinx/static/searchtools.js
==============================================================================
--- doctools/trunk/sphinx/static/searchtools.js	(original)
+++ doctools/trunk/sphinx/static/searchtools.js	Wed Sep 10 11:11:56 2008
@@ -224,6 +224,10 @@
  */
 var Search = {
 
+    _index : null,
+    _queued_query : null,
+    _pulse_status : -1,
+
     init : function() {
         var params = $.getQueryParameters();
         if (params.q) {
@@ -234,33 +238,68 @@
     },
 
     /**
-     * perform a search for something
+     * Sets the index
      */
-    performSearch : function(query) {
-        // create the required interface elements
-        var out = $('#search-results');
-        var title = $('<h2>' + _('Searching') + '</h2>').appendTo(out);
-        var dots = $('<span></span>').appendTo(title);
-        var status = $('<p style="display: none"></p>').appendTo(out);
-        var output = $('<ul class="search"/>').appendTo(out);
-        $('#search-progress').text(_('Getting search index...'));
-
-        // spawn a background runner for updating the dots
-        // until the search has finished
-        var pulseStatus = 0;
+    setIndex : function(index) {
+        var q;
+        this._index = index;
+        if ((q = this._queued_query) !== null) {
+            this._queued_query = null;
+            Search.query(q);
+        }
+    },
+
+    hasIndex : function() {
+        return self._index !== null;
+    },
+
+    deferQuery : function(query) {
+        this._queued_query = query;
+    },
+
+    stopPulse : function() {
+        this._pulse_status = 0;
+    },
+
+    startPulse : function() {
+        if (this._pulse_status >= 0)
+            return;
         function pulse() {
-            pulseStatus = (pulseStatus + 1) % 4;
+            Search._pulse_status = (Search._pulse_status + 1) % 4;
             var dotString = '';
-            for (var i = 0; i < pulseStatus; i++) {
+            for (var i = 0; i < Search._pulse_status; i++) {
                 dotString += '.';
             }
-            dots.text(dotString);
-            if (pulseStatus > -1) {
+            Search.dots.text(dotString);
+            if (Search._pulse_status > -1) {
                 window.setTimeout(pulse, 500);
             }
         };
         pulse();
+    },
 
+    /**
+     * perform a search for something
+     */
+    performSearch : function(query) {
+        // create the required interface elements
+        this.out = $('#search-results');
+        this.title = $('<h2>' + _('Searching') + '</h2>').appendTo(this.out);
+        this.dots = $('<span></span>').appendTo(this.title);
+        this.status = $('<p style="display: none"></p>').appendTo(this.out);
+        this.output = $('<ul class="search"/>').appendTo(this.out);
+
+        $('#search-progress').text(_('Preparing search...'));
+        this.startPulse();
+
+        // index already loaded, the browser was quick!
+        if (this.hasIndex())
+            this.query(query);
+        else
+            this.setQuery(query);
+    },
+
+    query : function(query) {
         // stem the searchwords and add them to the
         // correct list
         var stemmer = new PorterStemmer();
@@ -291,112 +330,102 @@
         console.info('required: ', searchwords);
         console.info('excluded: ', excluded);
 
-        // fetch searchindex and perform search
-        $.getJSON('searchindex.json', function(data) {
+        // prepare search
+        var filenames = this._index[0];
+        var titles = this._index[1];
+        var words = this._index[2];
+        var fileMap = {};
+        var files = null;
+        $('#search-progress').empty();
+
+        // perform the search on the required words
+        for (var i = 0; i < searchwords.length; i++) {
+            var word = searchwords[i];
+            // no match but word was a required one
+            if ((files = words[word]) == null)
+                break;
+            // create the mapping
+            for (var j = 0; j < files.length; j++) {
+                var file = files[j];
+                if (file in fileMap)
+                    fileMap[file].push(word);
+                else
+                    fileMap[file] = [word];
+            }
+        }
 
-                // prepare search
-                var filenames = data[0];
-                var titles = data[1]
-                var words = data[2];
-                var fileMap = {};
-                var files = null;
-
-                $('#search-progress').empty()
-
-                // perform the search on the required words
-                for (var i = 0; i < searchwords.length; i++) {
-                    var word = searchwords[i];
-                    // no match but word was a required one
-                    if ((files = words[word]) == null) {
-                        break;
-                    }
-                    // create the mapping
-                    for (var j = 0; j < files.length; j++) {
-                        var file = files[j];
-                        if (file in fileMap) {
-                            fileMap[file].push(word);
-                        }
-                        else {
-                            fileMap[file] = [word];
-                        }
-                    }
+        // now check if the files are in the correct
+        // areas and if the don't contain excluded words
+        var results = [];
+        for (var file in fileMap) {
+            var valid = true;
+
+            // check if all requirements are matched
+            if (fileMap[file].length != searchwords.length)
+                continue;
+
+            // ensure that none of the excluded words is in the
+            // search result.
+            for (var i = 0; i < excluded.length; i++) {
+                if ($.contains(words[excluded[i]] || [], file)) {
+                    valid = false;
+                    break;
                 }
+            }
 
-                // now check if the files are in the correct
-                // areas and if the don't contain excluded words
-                var results = [];
-                for (var file in fileMap) {
-                    var valid = true;
-
-                    // check if all requirements are matched
-                    if (fileMap[file].length != searchwords.length) {
-                        continue;
-                    }
-                    // ensure that none of the excluded words is in the
-                    // search result.
-                    for (var i = 0; i < excluded.length; i++) {
-                        if ($.contains(words[excluded[i]] || [], file)) {
-                            valid = false;
-                            break;
-                        }
-                    }
-
-                    // if we have still a valid result we can add it
-                    // to the result list
-                    if (valid) {
-                        results.push([filenames[file], titles[file]]);
-                    }
-                }
+            // if we have still a valid result we can add it
+            // to the result list
+            if (valid)
+                results.push([filenames[file], titles[file]]);
+        }
 
-                // delete unused variables in order to not waste
-                // memory until list is retrieved completely
-                delete filenames, titles, words, data;
-
-                // now sort the results by title
-                results.sort(function(a, b) {
-                        var left = a[1].toLowerCase();
-                        var right = b[1].toLowerCase();
-                        return (left > right) ? -1 : ((left < right) ? 1 : 0);
+        // delete unused variables in order to not waste
+        // memory until list is retrieved completely
+        delete filenames, titles, words;
+
+        // now sort the results by title
+        results.sort(function(a, b) {
+            var left = a[1].toLowerCase();
+            var right = b[1].toLowerCase();
+            return (left > right) ? -1 : ((left < right) ? 1 : 0);
+        });
+
+        // print the results
+        var resultCount = results.length;
+        function displayNextItem() {
+            // results left, load the summary and display it
+            if (results.length) {
+                var item = results.pop();
+                var listItem = $('<li style="display:none"></li>');
+                listItem.append($('<a/>').attr(
+                    'href',
+                    item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX +
+                    highlightstring).html(item[1]));
+                $.get('_sources/' + item[0] + '.txt', function(data) {
+                    listItem.append($.makeSearchSummary(data, searchwords, hlwords));
+                    Search.output.append(listItem);
+                    listItem.slideDown(10, function() {
+                        displayNextItem();
                     });
-
-                // print the results
-                var resultCount = results.length;
-                function displayNextItem() {
-                    // results left, load the summary and display it
-                    if (results.length) {
-                        var item = results.pop();
-                        var listItem = $('<li style="display:none"></li>');
-                        listItem.append($('<a/>').attr(
-                            'href',
-                            item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX +
-                            highlightstring).html(item[1]));
-                        $.get('_sources/' + item[0] + '.txt', function(data) {
-                                listItem.append($.makeSearchSummary(data, searchwords, hlwords));
-                                output.append(listItem);
-                                listItem.slideDown(10, function() {
-                                        displayNextItem();
-                                    });
-                            });
-                    }
-                    // search finished, update title and status message
-                    else {
-                        pulseStatus = -1;
-                        title.text(_('Search Results'));
-                        if (!resultCount) {
-                            status.text(_('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.'));
-                        }
-                        else {
-                            status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));
-                        }
-                        status.fadeIn(500);
-                    }
+                });
+            }
+            // search finished, update title and status message
+            else {
+                Search.stopPulse();
+                Search.title.text(_('Search Results'));
+                if (!resultCount) {
+                    Search.status.text(_('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.'));
                 }
-                displayNextItem();
-            });
+                else {
+                    Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));
+                }
+                Search.status.fadeIn(500);
+            }
+        }
+        displayNextItem();
     }
-
 }
 
 $(document).ready(function() {
-        Search.init();
-    });
+    Search.init();
+});

Modified: doctools/trunk/sphinx/templates/search.html
==============================================================================
--- doctools/trunk/sphinx/templates/search.html	(original)
+++ doctools/trunk/sphinx/templates/search.html	Wed Sep 10 11:11:56 2008
@@ -1,6 +1,6 @@
 {% extends "layout.html" %}
 {% set title = _('Search') %}
-{% set script_files = script_files + ['_static/searchtools.js'] %}
+{% set script_files = script_files + ['_static/searchtools.js', 'searchindex.js'] %}
 {% block body %}
   <h1 id="search-documentation">{{ _('Search') }}</h1>
   <p>


More information about the Python-checkins mailing list