mirror of
git://git.sv.gnu.org/emacs.git
synced 2026-01-03 10:31:37 -08:00
Check in HTML version of the manual (writable on client) so that it will display automatically on the Ravenbrook server and so that we can easily include it in product releases. Copied from Perforce Change: 180338 ServerID: perforce.ravenbrook.com
560 lines
No EOL
16 KiB
JavaScript
560 lines
No EOL
16 KiB
JavaScript
/*
|
|
* searchtools.js_t
|
|
* ~~~~~~~~~~~~~~~~
|
|
*
|
|
* Sphinx JavaScript utilties for the full-text search.
|
|
*
|
|
* :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
|
|
* :license: BSD, see LICENSE for details.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* helper function to return a node containing the
|
|
* search summary for a given text. keywords is a list
|
|
* of stemmed words, hlwords is the list of normal, unstemmed
|
|
* words. the first one is used to find the occurance, the
|
|
* latter for highlighting it.
|
|
*/
|
|
|
|
jQuery.makeSearchSummary = function(text, keywords, hlwords) {
|
|
var textLower = text.toLowerCase();
|
|
var start = 0;
|
|
$.each(keywords, function() {
|
|
var i = textLower.indexOf(this.toLowerCase());
|
|
if (i > -1)
|
|
start = i;
|
|
});
|
|
start = Math.max(start - 120, 0);
|
|
var excerpt = ((start > 0) ? '...' : '') +
|
|
$.trim(text.substr(start, 240)) +
|
|
((start + 240 - text.length) ? '...' : '');
|
|
var rv = $('<div class="context"></div>').text(excerpt);
|
|
$.each(hlwords, function() {
|
|
rv = rv.highlightText(this, 'highlighted');
|
|
});
|
|
return rv;
|
|
}
|
|
|
|
|
|
/**
|
|
* Porter Stemmer
|
|
*/
|
|
var Stemmer = function() {
|
|
|
|
var step2list = {
|
|
ational: 'ate',
|
|
tional: 'tion',
|
|
enci: 'ence',
|
|
anci: 'ance',
|
|
izer: 'ize',
|
|
bli: 'ble',
|
|
alli: 'al',
|
|
entli: 'ent',
|
|
eli: 'e',
|
|
ousli: 'ous',
|
|
ization: 'ize',
|
|
ation: 'ate',
|
|
ator: 'ate',
|
|
alism: 'al',
|
|
iveness: 'ive',
|
|
fulness: 'ful',
|
|
ousness: 'ous',
|
|
aliti: 'al',
|
|
iviti: 'ive',
|
|
biliti: 'ble',
|
|
logi: 'log'
|
|
};
|
|
|
|
var step3list = {
|
|
icate: 'ic',
|
|
ative: '',
|
|
alize: 'al',
|
|
iciti: 'ic',
|
|
ical: 'ic',
|
|
ful: '',
|
|
ness: ''
|
|
};
|
|
|
|
var c = "[^aeiou]"; // consonant
|
|
var v = "[aeiouy]"; // vowel
|
|
var C = c + "[^aeiouy]*"; // consonant sequence
|
|
var V = v + "[aeiou]*"; // vowel sequence
|
|
|
|
var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0
|
|
var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1
|
|
var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1
|
|
var s_v = "^(" + C + ")?" + v; // vowel in stem
|
|
|
|
this.stemWord = function (w) {
|
|
var stem;
|
|
var suffix;
|
|
var firstch;
|
|
var origword = w;
|
|
|
|
if (w.length < 3)
|
|
return w;
|
|
|
|
var re;
|
|
var re2;
|
|
var re3;
|
|
var re4;
|
|
|
|
firstch = w.substr(0,1);
|
|
if (firstch == "y")
|
|
w = firstch.toUpperCase() + w.substr(1);
|
|
|
|
// Step 1a
|
|
re = /^(.+?)(ss|i)es$/;
|
|
re2 = /^(.+?)([^s])s$/;
|
|
|
|
if (re.test(w))
|
|
w = w.replace(re,"$1$2");
|
|
else if (re2.test(w))
|
|
w = w.replace(re2,"$1$2");
|
|
|
|
// Step 1b
|
|
re = /^(.+?)eed$/;
|
|
re2 = /^(.+?)(ed|ing)$/;
|
|
if (re.test(w)) {
|
|
var fp = re.exec(w);
|
|
re = new RegExp(mgr0);
|
|
if (re.test(fp[1])) {
|
|
re = /.$/;
|
|
w = w.replace(re,"");
|
|
}
|
|
}
|
|
else if (re2.test(w)) {
|
|
var fp = re2.exec(w);
|
|
stem = fp[1];
|
|
re2 = new RegExp(s_v);
|
|
if (re2.test(stem)) {
|
|
w = stem;
|
|
re2 = /(at|bl|iz)$/;
|
|
re3 = new RegExp("([^aeiouylsz])\\1$");
|
|
re4 = new RegExp("^" + C + v + "[^aeiouwxy]$");
|
|
if (re2.test(w))
|
|
w = w + "e";
|
|
else if (re3.test(w)) {
|
|
re = /.$/;
|
|
w = w.replace(re,"");
|
|
}
|
|
else if (re4.test(w))
|
|
w = w + "e";
|
|
}
|
|
}
|
|
|
|
// Step 1c
|
|
re = /^(.+?)y$/;
|
|
if (re.test(w)) {
|
|
var fp = re.exec(w);
|
|
stem = fp[1];
|
|
re = new RegExp(s_v);
|
|
if (re.test(stem))
|
|
w = stem + "i";
|
|
}
|
|
|
|
// Step 2
|
|
re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
|
|
if (re.test(w)) {
|
|
var fp = re.exec(w);
|
|
stem = fp[1];
|
|
suffix = fp[2];
|
|
re = new RegExp(mgr0);
|
|
if (re.test(stem))
|
|
w = stem + step2list[suffix];
|
|
}
|
|
|
|
// Step 3
|
|
re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
|
|
if (re.test(w)) {
|
|
var fp = re.exec(w);
|
|
stem = fp[1];
|
|
suffix = fp[2];
|
|
re = new RegExp(mgr0);
|
|
if (re.test(stem))
|
|
w = stem + step3list[suffix];
|
|
}
|
|
|
|
// Step 4
|
|
re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
|
|
re2 = /^(.+?)(s|t)(ion)$/;
|
|
if (re.test(w)) {
|
|
var fp = re.exec(w);
|
|
stem = fp[1];
|
|
re = new RegExp(mgr1);
|
|
if (re.test(stem))
|
|
w = stem;
|
|
}
|
|
else if (re2.test(w)) {
|
|
var fp = re2.exec(w);
|
|
stem = fp[1] + fp[2];
|
|
re2 = new RegExp(mgr1);
|
|
if (re2.test(stem))
|
|
w = stem;
|
|
}
|
|
|
|
// Step 5
|
|
re = /^(.+?)e$/;
|
|
if (re.test(w)) {
|
|
var fp = re.exec(w);
|
|
stem = fp[1];
|
|
re = new RegExp(mgr1);
|
|
re2 = new RegExp(meq1);
|
|
re3 = new RegExp("^" + C + v + "[^aeiouwxy]$");
|
|
if (re.test(stem) || (re2.test(stem) && !(re3.test(stem))))
|
|
w = stem;
|
|
}
|
|
re = /ll$/;
|
|
re2 = new RegExp(mgr1);
|
|
if (re.test(w) && re2.test(w)) {
|
|
re = /.$/;
|
|
w = w.replace(re,"");
|
|
}
|
|
|
|
// and turn initial Y back to y
|
|
if (firstch == "y")
|
|
w = firstch.toLowerCase() + w.substr(1);
|
|
return w;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Search Module
|
|
*/
|
|
var Search = {
|
|
|
|
_index : null,
|
|
_queued_query : null,
|
|
_pulse_status : -1,
|
|
|
|
init : function() {
|
|
var params = $.getQueryParameters();
|
|
if (params.q) {
|
|
var query = params.q[0];
|
|
$('input[name="q"]')[0].value = query;
|
|
this.performSearch(query);
|
|
}
|
|
},
|
|
|
|
loadIndex : function(url) {
|
|
$.ajax({type: "GET", url: url, data: null, success: null,
|
|
dataType: "script", cache: true});
|
|
},
|
|
|
|
setIndex : function(index) {
|
|
var q;
|
|
this._index = index;
|
|
if ((q = this._queued_query) !== null) {
|
|
this._queued_query = null;
|
|
Search.query(q);
|
|
}
|
|
},
|
|
|
|
hasIndex : function() {
|
|
return this._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() {
|
|
Search._pulse_status = (Search._pulse_status + 1) % 4;
|
|
var dotString = '';
|
|
for (var i = 0; i < Search._pulse_status; i++)
|
|
dotString += '.';
|
|
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.deferQuery(query);
|
|
},
|
|
|
|
query : function(query) {
|
|
var stopwords = ["and","then","into","it","as","are","in","if","for","no","there","their","was","is","be","to","that","but","they","not","such","with","by","a","on","these","of","will","this","near","the","or","at"];
|
|
|
|
// Stem the searchterms and add them to the correct list
|
|
var stemmer = new Stemmer();
|
|
var searchterms = [];
|
|
var excluded = [];
|
|
var hlterms = [];
|
|
var tmp = query.split(/\s+/);
|
|
var objectterms = [];
|
|
for (var i = 0; i < tmp.length; i++) {
|
|
if (tmp[i] != "") {
|
|
objectterms.push(tmp[i].toLowerCase());
|
|
}
|
|
|
|
if ($u.indexOf(stopwords, tmp[i]) != -1 || tmp[i].match(/^\d+$/) ||
|
|
tmp[i] == "") {
|
|
// skip this "word"
|
|
continue;
|
|
}
|
|
// stem the word
|
|
var word = stemmer.stemWord(tmp[i]).toLowerCase();
|
|
// select the correct list
|
|
if (word[0] == '-') {
|
|
var toAppend = excluded;
|
|
word = word.substr(1);
|
|
}
|
|
else {
|
|
var toAppend = searchterms;
|
|
hlterms.push(tmp[i].toLowerCase());
|
|
}
|
|
// only add if not already in the list
|
|
if (!$.contains(toAppend, word))
|
|
toAppend.push(word);
|
|
};
|
|
var highlightstring = '?highlight=' + $.urlencode(hlterms.join(" "));
|
|
|
|
// console.debug('SEARCH: searching for:');
|
|
// console.info('required: ', searchterms);
|
|
// console.info('excluded: ', excluded);
|
|
|
|
// prepare search
|
|
var filenames = this._index.filenames;
|
|
var titles = this._index.titles;
|
|
var terms = this._index.terms;
|
|
var fileMap = {};
|
|
var files = null;
|
|
// different result priorities
|
|
var importantResults = [];
|
|
var objectResults = [];
|
|
var regularResults = [];
|
|
var unimportantResults = [];
|
|
$('#search-progress').empty();
|
|
|
|
// lookup as object
|
|
for (var i = 0; i < objectterms.length; i++) {
|
|
var others = [].concat(objectterms.slice(0,i),
|
|
objectterms.slice(i+1, objectterms.length))
|
|
var results = this.performObjectSearch(objectterms[i], others);
|
|
// Assume first word is most likely to be the object,
|
|
// other words more likely to be in description.
|
|
// Therefore put matches for earlier words first.
|
|
// (Results are eventually used in reverse order).
|
|
objectResults = results[0].concat(objectResults);
|
|
importantResults = results[1].concat(importantResults);
|
|
unimportantResults = results[2].concat(unimportantResults);
|
|
}
|
|
|
|
// perform the search on the required terms
|
|
for (var i = 0; i < searchterms.length; i++) {
|
|
var word = searchterms[i];
|
|
// no match but word was a required one
|
|
if ((files = terms[word]) == null)
|
|
break;
|
|
if (files.length == undefined) {
|
|
files = [files];
|
|
}
|
|
// 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 don't contain excluded terms
|
|
for (var file in fileMap) {
|
|
var valid = true;
|
|
|
|
// check if all requirements are matched
|
|
if (fileMap[file].length != searchterms.length)
|
|
continue;
|
|
|
|
// ensure that none of the excluded terms is in the
|
|
// search result.
|
|
for (var i = 0; i < excluded.length; i++) {
|
|
if (terms[excluded[i]] == file ||
|
|
$.contains(terms[excluded[i]] || [], file)) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// if we have still a valid result we can add it
|
|
// to the result list
|
|
if (valid)
|
|
regularResults.push([filenames[file], titles[file], '', null]);
|
|
}
|
|
|
|
// delete unused variables in order to not waste
|
|
// memory until list is retrieved completely
|
|
delete filenames, titles, terms;
|
|
|
|
// now sort the regular results descending by title
|
|
regularResults.sort(function(a, b) {
|
|
var left = a[1].toLowerCase();
|
|
var right = b[1].toLowerCase();
|
|
return (left > right) ? -1 : ((left < right) ? 1 : 0);
|
|
});
|
|
|
|
// combine all results
|
|
var results = unimportantResults.concat(regularResults)
|
|
.concat(objectResults).concat(importantResults);
|
|
|
|
// 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>');
|
|
if (DOCUMENTATION_OPTIONS.FILE_SUFFIX == '') {
|
|
// dirhtml builder
|
|
var dirname = item[0] + '/';
|
|
if (dirname.match(/\/index\/$/)) {
|
|
dirname = dirname.substring(0, dirname.length-6);
|
|
} else if (dirname == 'index/') {
|
|
dirname = '';
|
|
}
|
|
listItem.append($('<a/>').attr('href',
|
|
DOCUMENTATION_OPTIONS.URL_ROOT + dirname +
|
|
highlightstring + item[2]).html(item[1]));
|
|
} else {
|
|
// normal html builders
|
|
listItem.append($('<a/>').attr('href',
|
|
item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX +
|
|
highlightstring + item[2]).html(item[1]));
|
|
}
|
|
if (item[3]) {
|
|
listItem.append($('<span> (' + item[3] + ')</span>'));
|
|
Search.output.append(listItem);
|
|
listItem.slideDown(5, function() {
|
|
displayNextItem();
|
|
});
|
|
} else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {
|
|
$.get(DOCUMENTATION_OPTIONS.URL_ROOT + '_sources/' +
|
|
item[0] + '.txt', function(data) {
|
|
if (data != '') {
|
|
listItem.append($.makeSearchSummary(data, searchterms, hlterms));
|
|
Search.output.append(listItem);
|
|
}
|
|
listItem.slideDown(5, function() {
|
|
displayNextItem();
|
|
});
|
|
}, "text");
|
|
} else {
|
|
// no source available, just display title
|
|
Search.output.append(listItem);
|
|
listItem.slideDown(5, function() {
|
|
displayNextItem();
|
|
});
|
|
}
|
|
}
|
|
// 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.'));
|
|
else
|
|
Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));
|
|
Search.status.fadeIn(500);
|
|
}
|
|
}
|
|
displayNextItem();
|
|
},
|
|
|
|
performObjectSearch : function(object, otherterms) {
|
|
var filenames = this._index.filenames;
|
|
var objects = this._index.objects;
|
|
var objnames = this._index.objnames;
|
|
var titles = this._index.titles;
|
|
|
|
var importantResults = [];
|
|
var objectResults = [];
|
|
var unimportantResults = [];
|
|
|
|
for (var prefix in objects) {
|
|
for (var name in objects[prefix]) {
|
|
var fullname = (prefix ? prefix + '.' : '') + name;
|
|
if (fullname.toLowerCase().indexOf(object) > -1) {
|
|
var match = objects[prefix][name];
|
|
var objname = objnames[match[1]][2];
|
|
var title = titles[match[0]];
|
|
// If more than one term searched for, we require other words to be
|
|
// found in the name/title/description
|
|
if (otherterms.length > 0) {
|
|
var haystack = (prefix + ' ' + name + ' ' +
|
|
objname + ' ' + title).toLowerCase();
|
|
var allfound = true;
|
|
for (var i = 0; i < otherterms.length; i++) {
|
|
if (haystack.indexOf(otherterms[i]) == -1) {
|
|
allfound = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!allfound) {
|
|
continue;
|
|
}
|
|
}
|
|
var descr = objname + _(', in ') + title;
|
|
anchor = match[3];
|
|
if (anchor == '')
|
|
anchor = fullname;
|
|
else if (anchor == '-')
|
|
anchor = objnames[match[1]][1] + '-' + fullname;
|
|
result = [filenames[match[0]], fullname, '#'+anchor, descr];
|
|
switch (match[2]) {
|
|
case 1: objectResults.push(result); break;
|
|
case 0: importantResults.push(result); break;
|
|
case 2: unimportantResults.push(result); break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// sort results descending
|
|
objectResults.sort(function(a, b) {
|
|
return (a[1] > b[1]) ? -1 : ((a[1] < b[1]) ? 1 : 0);
|
|
});
|
|
|
|
importantResults.sort(function(a, b) {
|
|
return (a[1] > b[1]) ? -1 : ((a[1] < b[1]) ? 1 : 0);
|
|
});
|
|
|
|
unimportantResults.sort(function(a, b) {
|
|
return (a[1] > b[1]) ? -1 : ((a[1] < b[1]) ? 1 : 0);
|
|
});
|
|
|
|
return [importantResults, objectResults, unimportantResults]
|
|
}
|
|
}
|
|
|
|
$(document).ready(function() {
|
|
Search.init();
|
|
}); |