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

This commit is contained in:
Armin Ronacher 2008-09-10 09:11:56 +00:00
parent b4ec549f04
commit 3debdc2c2a
5 changed files with 185 additions and 273 deletions

View File

@ -34,6 +34,7 @@ from sphinx.latexwriter import LaTeXWriter
from sphinx.environment import BuildEnvironment, NoUri from sphinx.environment import BuildEnvironment, NoUri
from sphinx.highlighting import PygmentsBridge from sphinx.highlighting import PygmentsBridge
from sphinx.util.console import bold, purple, darkgreen from sphinx.util.console import bold, purple, darkgreen
from sphinx.search import js_index
# side effect: registers roles and directives # side effect: registers roles and directives
from sphinx import roles from sphinx import roles
@ -338,10 +339,10 @@ class StandaloneHTMLBuilder(Builder):
name = 'html' name = 'html'
copysource = True copysource = True
out_suffix = '.html' out_suffix = '.html'
indexer_format = json indexer_format = js_index
supported_image_types = ['image/svg+xml', 'image/png', 'image/gif', supported_image_types = ['image/svg+xml', 'image/png', 'image/gif',
'image/jpeg'] 'image/jpeg']
searchindex_filename = 'searchindex.json' searchindex_filename = 'searchindex.js'
add_header_links = True add_header_links = True
add_definition_links = True add_definition_links = True

View File

@ -10,6 +10,7 @@
""" """
import re import re
import cPickle as pickle import cPickle as pickle
from cStringIO import StringIO
from docutils.nodes import Text, NodeVisitor from docutils.nodes import Text, NodeVisitor
@ -20,6 +21,37 @@ from sphinx.util import json
word_re = re.compile(r'\w+(?u)') 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): class Stemmer(PorterStemmer):
""" """
All those porter stemmer implementations look hideous. All those porter stemmer implementations look hideous.

View File

@ -94,11 +94,9 @@ jQuery.fn.highlightText = function(text, className) {
var Documentation = { var Documentation = {
init : function() { init : function() {
/* this.addContextElements(); -- now done statically */
this.fixFirefoxAnchorBug(); this.fixFirefoxAnchorBug();
this.highlightSearchWords(); this.highlightSearchWords();
this.initModIndex(); this.initModIndex();
this.initComments();
}, },
/** /**
@ -108,6 +106,8 @@ var Documentation = {
PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; }, PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; },
LOCALE : 'unknown', 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) { gettext : function(string) {
var translated = Documentation.TRANSLATIONS[string]; var translated = Documentation.TRANSLATIONS[string];
if (typeof translated == 'undefined') if (typeof translated == 'undefined')
@ -133,14 +133,12 @@ var Documentation = {
* add context elements like header anchor links * add context elements like header anchor links
*/ */
addContextElements : function() { addContextElements : function() {
for (var i = 1; i <= 6; i++) { $('div[@id] > :header:first').each(function() {
$('h' + i + '[@id]').each(function() {
$('<a class="headerlink">\u00B6</a>'). $('<a class="headerlink">\u00B6</a>').
attr('href', '#' + this.id). attr('href', '#' + this.id).
attr('title', _('Permalink to this headline')). attr('title', _('Permalink to this headline')).
appendTo(this); appendTo(this);
}); });
}
$('dt[@id]').each(function() { $('dt[@id]').each(function() {
$('<a class="headerlink">\u00B6</a>'). $('<a class="headerlink">\u00B6</a>').
attr('href', '#' + this.id). attr('href', '#' + this.id).
@ -196,37 +194,6 @@ var Documentation = {
} }
}, },
/**
* 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 * helper function to hide the search marks again
*/ */
@ -235,22 +202,6 @@ var Documentation = {
$('span.highlight').removeClass('highlight'); $('span.highlight').removeClass('highlight');
}, },
/**
* 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 * make the url absolute
*/ */
@ -270,108 +221,7 @@ var Documentation = {
}); });
var url = parts.join('/'); var url = parts.join('/');
return path.substring(url.lastIndexOf('/') + 1, path.length - 1); 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 // quick alias for translations

View File

@ -224,6 +224,10 @@ var PorterStemmer = function() {
*/ */
var Search = { var Search = {
_index : null,
_queued_query : null,
_pulse_status : -1,
init : function() { init : function() {
var params = $.getQueryParameters(); var params = $.getQueryParameters();
if (params.q) { if (params.q) {
@ -234,33 +238,68 @@ var Search = {
}, },
/** /**
* perform a search for something * Sets the index
*/ */
performSearch : function(query) { setIndex : function(index) {
// create the required interface elements var q;
var out = $('#search-results'); this._index = index;
var title = $('<h2>' + _('Searching') + '</h2>').appendTo(out); if ((q = this._queued_query) !== null) {
var dots = $('<span></span>').appendTo(title); this._queued_query = null;
var status = $('<p style="display: none"></p>').appendTo(out); Search.query(q);
var output = $('<ul class="search"/>').appendTo(out); }
$('#search-progress').text(_('Getting search index...')); },
// spawn a background runner for updating the dots hasIndex : function() {
// until the search has finished return self._index !== null;
var pulseStatus = 0; },
deferQuery : function(query) {
this._queued_query = query;
},
stopPulse : function() {
this._pulse_status = 0;
},
startPulse : function() {
if (this._pulse_status >= 0)
return;
function pulse() { function pulse() {
pulseStatus = (pulseStatus + 1) % 4; Search._pulse_status = (Search._pulse_status + 1) % 4;
var dotString = ''; var dotString = '';
for (var i = 0; i < pulseStatus; i++) { for (var i = 0; i < Search._pulse_status; i++) {
dotString += '.'; dotString += '.';
} }
dots.text(dotString); Search.dots.text(dotString);
if (pulseStatus > -1) { if (Search._pulse_status > -1) {
window.setTimeout(pulse, 500); window.setTimeout(pulse, 500);
} }
}; };
pulse(); 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 // stem the searchwords and add them to the
// correct list // correct list
var stemmer = new PorterStemmer(); var stemmer = new PorterStemmer();
@ -291,36 +330,29 @@ var Search = {
console.info('required: ', searchwords); console.info('required: ', searchwords);
console.info('excluded: ', excluded); console.info('excluded: ', excluded);
// fetch searchindex and perform search
$.getJSON('searchindex.json', function(data) {
// prepare search // prepare search
var filenames = data[0]; var filenames = this._index[0];
var titles = data[1] var titles = this._index[1];
var words = data[2]; var words = this._index[2];
var fileMap = {}; var fileMap = {};
var files = null; var files = null;
$('#search-progress').empty();
$('#search-progress').empty()
// perform the search on the required words // perform the search on the required words
for (var i = 0; i < searchwords.length; i++) { for (var i = 0; i < searchwords.length; i++) {
var word = searchwords[i]; var word = searchwords[i];
// no match but word was a required one // no match but word was a required one
if ((files = words[word]) == null) { if ((files = words[word]) == null)
break; break;
}
// create the mapping // create the mapping
for (var j = 0; j < files.length; j++) { for (var j = 0; j < files.length; j++) {
var file = files[j]; var file = files[j];
if (file in fileMap) { if (file in fileMap)
fileMap[file].push(word); fileMap[file].push(word);
} else
else {
fileMap[file] = [word]; fileMap[file] = [word];
} }
} }
}
// now check if the files are in the correct // now check if the files are in the correct
// areas and if the don't contain excluded words // areas and if the don't contain excluded words
@ -329,9 +361,9 @@ var Search = {
var valid = true; var valid = true;
// check if all requirements are matched // check if all requirements are matched
if (fileMap[file].length != searchwords.length) { if (fileMap[file].length != searchwords.length)
continue; continue;
}
// ensure that none of the excluded words is in the // ensure that none of the excluded words is in the
// search result. // search result.
for (var i = 0; i < excluded.length; i++) { for (var i = 0; i < excluded.length; i++) {
@ -343,14 +375,13 @@ var Search = {
// if we have still a valid result we can add it // if we have still a valid result we can add it
// to the result list // to the result list
if (valid) { if (valid)
results.push([filenames[file], titles[file]]); results.push([filenames[file], titles[file]]);
} }
}
// delete unused variables in order to not waste // delete unused variables in order to not waste
// memory until list is retrieved completely // memory until list is retrieved completely
delete filenames, titles, words, data; delete filenames, titles, words;
// now sort the results by title // now sort the results by title
results.sort(function(a, b) { results.sort(function(a, b) {
@ -372,7 +403,7 @@ var Search = {
highlightstring).html(item[1])); highlightstring).html(item[1]));
$.get('_sources/' + item[0] + '.txt', function(data) { $.get('_sources/' + item[0] + '.txt', function(data) {
listItem.append($.makeSearchSummary(data, searchwords, hlwords)); listItem.append($.makeSearchSummary(data, searchwords, hlwords));
output.append(listItem); Search.output.append(listItem);
listItem.slideDown(10, function() { listItem.slideDown(10, function() {
displayNextItem(); displayNextItem();
}); });
@ -380,23 +411,21 @@ var Search = {
} }
// search finished, update title and status message // search finished, update title and status message
else { else {
pulseStatus = -1; Search.stopPulse();
title.text(_('Search Results')); Search.title.text(_('Search Results'));
if (!resultCount) { 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.')); 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.'));
} }
else { else {
status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount)); Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));
} }
status.fadeIn(500); Search.status.fadeIn(500);
} }
} }
displayNextItem(); displayNextItem();
});
} }
} }
$(document).ready(function() { $(document).ready(function() {
Search.init(); Search.init();
}); });

View File

@ -1,6 +1,6 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% set title = _('Search') %} {% set title = _('Search') %}
{% set script_files = script_files + ['_static/searchtools.js'] %} {% set script_files = script_files + ['_static/searchtools.js', 'searchindex.js'] %}
{% block body %} {% block body %}
<h1 id="search-documentation">{{ _('Search') }}</h1> <h1 id="search-documentation">{{ _('Search') }}</h1>
<p> <p>