Hit PageUp/PageDown or'
+' C+Enter/S+C+Enter to scroll through the'
+' results.
'
+'
Search terms are all required, use'
+' “N:str” to negate a term.'
+'
Use “M:str” to match only'
+' identifiers from modules that (partially) match'
+' “str”; “M:” by'
+' itself will restrict results to bound names only.
'
+'
“L:str” is similar to'
+' “M:str”, but'
+' “str” should match the module name'
+' exactly.
'
+'
“T:str” restricts results to ones in'
+' the “str” manual (naming the'
+' directory where the manual is found).
'
+'
Entries that correspond to bindings have module links that'
+' create a query restricted to bindings in that module (using'
+' “L:”), other entries have similar links for'
+' restricting results to a specific manual (using'
+' “T:”); you can control whether manual links'
+' appear (and how) in the preferences.
'
+'
Right-clicking these links refines the current query instead of'
+' changing it (but some browsers don\'t support this).
';
// get the widgets we use
query = document.getElementById("search_box");
status_line = document.getElementById("search_status");
ctx_query_label_line = document.getElementById("ctx_query_label");
prev_page_link1 = document.getElementById("prev_page_link1");
prev_page_link2 = document.getElementById("prev_page_link2");
next_page_link1 = document.getElementById("next_page_link1");
next_page_link2 = document.getElementById("next_page_link2");
// result_links is the array of result link pairs
result_links = new Array();
n = document.getElementById("search_result");
results_container = n.parentNode;
results_container.normalize();
result_links.push(n);
AdjustResultsNum();
// get search string
var init_q = GetPageArg("q",false);
if (init_q && init_q != "") query.value = init_q;
ContextFilter();
DoSearch();
query.focus();
query.select();
}
function AdjustResultsNum() {
if (result_links.length == results_num) return;
if (results_num <= 0) results_num = 1; // expects at least one template
while (result_links.length > results_num)
results_container.removeChild(result_links.pop());
while (result_links.length < results_num) {
var n = result_links[0].cloneNode(true);
result_links.push(n);
results_container.appendChild(n);
}
}
// Terms are compared using Compare(pat,str), which returns one of several
// constants, in increasing order of success:
// - C_fail: no match
// - C_words1: all of the "subwords" in pat matched, where a word is an
// alphanumeric or a punctuation substring (for example, "foo-bar!!" has
// these words: "foo", "-", "bar", "!!"), and a match means a prefix of a
// subword in str matched one of pat's subwords
// - C_words2: same, but all of the subwords in str were matched (so both str
// and pat have the same number of subwords)
// - C_words3: same, but all matches were complete (so str is a permutation of
// pat)
// - C_match: pat matched somewhere in str
// - C_prefix: pat is a prefix of str
// - C_exact: pat is equal to str
// - C_rexact: there was a ("real") exact match, see below
// Note that the value is searched from the last one and going back, so, for
// example, if pat is a prefix of str the result is C_prefix, and no subword
// matching is attempted. A result of C_exact can be returned by some of the
// `X:' operators: its purpose is to be able to return a result that doesn't
// affect the exactness of the search (for example L:foo searches for a source
// module that is exactly `foo', but it will return C_exact which means that it
// doesn't change whether the match is considered exact or not).
var C_fail = 0, C_min = 0,
C_words1 = 1,
C_words2 = 2,
C_words3 = 3,
C_match = 4,
C_prefix = 5,
C_exact = 6,
C_rexact = 7, C_max = 7;
function Compare(pat, str) {
var i = str.indexOf(pat);
if (i < 0) return C_fail;
if (i > 0) return C_match;
if (pat.length == str.length) return C_rexact;
return C_prefix;
}
function MaxCompares(pat, strs) {
var r = C_min, c;
for (var i=0; i r) { if (c >= C_max) return c; else r = c; }
}
return r;
}
// Utilities for dealing with "subword" searches
function SortByLength(x, y) {
// use this to sort with descending length and otherwise lexicographically
var xlen = x.length, ylen = y.length;
if (xlen != ylen) return ylen - xlen;
else if (x < y) return -1;
else if (x > y) return +1;
else return 0;
}
function CompileWordCompare(term) {
var words = term.split(/\b/).sort(SortByLength);
var word_num = words.length;
var is_word = new Array(word_num);
for (var i=0; i= 0);
return function (str) {
var str_words = str.split(/\b/).sort(SortByLength);
var str_word_num = str_words.length;
if (str_word_num < word_num) return C_fail; // can't work
var r = C_words3;
for (var i=0; i 0 && !is_word[i])) {
found = j;
r = C_words2;
break;
}
}
}
}
if (found >= 0) str_words[found] = null;
else return C_fail;
}
// either C_words3 for a complete permutation, or C_words2 when all of the
// subwords in str were matched
if (word_num == str_word_num) return r;
// otherwise return C_words1 regardless of whether all of the words in term
// matched exactly words in str
else return C_words1;
};
}
// Tests:
// console.log("testing...");
// function t(x,y,e) {
// var r = CompileWordCompare(x)(y);
// if (e != r) {
// console.log('CompileWordCompare("'+x+'")("'+y+'")'+
// " => "+r+" but expected "+e);
// }
// }
// t("x-y","x-y",C_words3);
// t("x-y","y-x",C_words3);
// t("x-y","y--x",C_words2);
// t("x-y","y=-x",C_words2);
// t("xx-yy","x-y",C_fail);
// t("xx-yy","xxx-yyy",C_words2);
// t("x-y-x","xxx-yyy",C_fail);
// t("x-y-x","xxx-yyy-xxx",C_words2);
// t("x-y-x","yyy-xxx-xxx",C_words2);
// t("x-y-xx","xx-y-xxx",C_words2);
// console.log("done");
function NormalizeSpaces(str) {
// use single spaces first, then trim edge spaces
return str.replace(/\s\s*/g," ").replace(/^ /,"").replace(/ $/,"");
}
function SanitizeHTML(str) {
// Minimal protection against bad html strings
// HACK: we don't want to actually parse these things, but we do want a way
// to have tt text, so use a "{{...}}" markup for that.
return str.replace(/[&]/g, "&")
.replace(/>/g, ">")
.replace(/")
.replace(/}}/g, "");
}
function UrlToManual(url) {
return url.replace(/#.*$/, "") // remove fragment,
.replace(/\?.*$/, "") // query,
.replace(/\/[^\/]*$/, "") // filename,
.replace(/^(.*\/|>)/, ""); // and directory.
}
// Tests for matches and highlights:
// "append"
// "L:racket append"
// "L:racket" (no exact matches except for the `racket' module)
// "L:racke" (only module names that match `racke')
// "edi" and "edit" (the top few should be the same)
// Additional "hidden" operators:
// "A:{ foo bar }" -- an `and' query
// "O:{ foo bar }" -- an `or' query
// "Q:foo" -- stands for just "foo", useful for quoting Q:} inside the above
// Note: they're "hidden" because the syntax might change, and it's intended
// mostly for context queries.
function CompileTerm(term) {
var op = ((term.search(/^[NLMTQ]:/) == 0) && term.substring(0,1));
if (op) term = term.substring(2);
term = term.toLowerCase();
switch (op) {
case "N":
op = CompileTerm(term);
// return C_exact if it's not found, so it doesn't disqualify exact matches
return function(x) { return (op(x) >= C_match) ? C_fail : C_exact; };
case "L":
return function(x) {
if (!x[3]) return C_fail;
if (x[3] == "module") // rexact allowed, show partial module matches
return Compare(term,x[0]);
return (MaxCompares(term,x[3]) >= C_exact) ? C_exact : C_fail;
};
case "M":
return function(x) {
if (!x[3]) return C_fail;
if (x[3] == "module") return Compare(term,x[0]); // rexact allowed
return (MaxCompares(term,x[3]) >= C_match) ? C_exact : C_fail;
};
case "T":
return function(x) {
if (Compare(term,UrlToManual(x[1])) < C_exact) return C_fail;
else if (x[1].search(/\/index\.html$/) > 0) return C_rexact;
else return C_exact;
};
/* a case for "Q" is not needed -- same as the default case below */
default:
var compare_words = CompileWordCompare(term);
return function(x) {
var r = Compare(term,x[0]);
// only bindings can be used for rexact matches
if (r >= C_rexact) return (x[3] ? r : C_exact);
if (r > C_words3) return r;
else return compare_words(x[0]);
};
}
}
function CompileAndTerms(preds) {
return function(x) {
var r = C_max, c;
for (var i=0; i r) { if (c >= C_max) return c; else r = c; }
}
return r;
};
}
function CompileTermsR(terms, nested) {
var term, result = new Array();
while (terms.length > 0) {
term = terms.pop();
switch (term) {
case "A:{": result.push(CompileTermsR(terms, CompileAndTerms)); break;
case "O:{": result.push(CompileTermsR(terms, CompileOrTerms)); break;
default:
// "}" has terminates a compound, otherwise it's an ordinary search term
if (nested && (term == "}")) return nested(result);
else result.push(CompileTerm(term));
}
}
// all compound operators are implicitly terminated at the end
if (nested) return nested(result);
else return result;
}
function CompileTerms(terms, nested) {
terms.reverse();
return CompileTermsR(terms, nested);
}
function Id(x) {
return x;
}
var indicators =
(function() {
// construct indicator lines that look like: "--->..."
var i, j, s, a = new Array();
for (i=0; i<11; i++) {
s = "";
for (j=0; j<10; j++) {
s += (j==9 && i==10) ? "■"
: (j"+s+"");
}
return a;
}());
function MakeShowProgress() {
var orig = status_line.innerHTML;
return function(n) {
status_line.innerHTML =
orig + indicators[Math.round(10*n/search_data.length)];
};
}
function Search(data, term, is_pre, K) {
// `K' is a continuation if this run is supposed to happen in a "thread"
// false otherwise
var t = false;
function Killer() { if (t) clearTimeout(t); };
// term comes with normalized spaces (trimmed, and no double spaces)
var preds = (term=="") ? [] : CompileTerms(term.split(/ /), false);
if (preds.length == 0) {
var ret = is_pre ? [0,data] : [0,[]];
if (K) { K(ret); return Killer; }
else return ret;
}
var i = 0;
var matches = new Array(C_max-C_min);
for (i=0; i 0) {
var r, min = C_max, max = C_min;
for (var j=0; j max) max = r;
}
if (max >= C_rexact && min >= C_exact) min = C_rexact;
if (min > C_min) matches[C_max - min].push(data[i]);
fuel--; i++;
}
if (i'
+ text
+ '');
}
var search_data; // pre-filtered searchable index data
function ContextFilter() {
ctx_query = NormalizeSpaces(ctx_query);
search_data = Search(plt_search_data, ctx_query, true, false)[1];
if (ctx_query == "") {
ctx_query_label_line.innerHTML =
''
+'[set context]';
} else {
ctx_query_label_line.innerHTML =
'Context: ' + GetContextHTML()
+ ' '
+ GetContextClearerHTML('[clear')
+ '/'
+'modify]';
}
last_search_term = null;
last_search_term_raw = null;
}
var last_search_term, last_search_term_raw;
var search_results = [], first_search_result, exact_results_num;
var kill_bg_search = function(){ return; };
var search_timer = false;
function DoDelayedSearch() {
// the whole delayed search thing was done to avoid redundant searching that
// get the UI stuck, but now the search happens on a "thread" anyway, so it
// might not be needed
if (search_timer) clearTimeout(search_timer);
search_timer = setTimeout(DoSearch, type_delay);
}
function DoSearch() {
var term = query.value;
if (term == last_search_term_raw) return;
last_search_term_raw = term;
term = NormalizeSpaces(term);
if (term == last_search_term) return;
last_search_term = term;
status_line.innerHTML = "Searching " + search_data.length + " entries";
kill_bg_search();
kill_bg_search = Search(search_data, term, false,
// use a continuation to run this in the background
function(res) {
search_results = res[1];
exact_results_num = res[0];
first_search_result = 0;
status_line.innerHTML = "" + search_results.length + " entries found";
query.style.backgroundColor =
((search_results.length == 0) && (term != "")) ? "#ffe0e0" : "white";
UpdateResults();
});
}
function UncompactUrl(url) {
return url.replace(/^>/, plt_main_url);
}
function UncompactHtml(x) {
if (typeof x == "string") {
return x;
} else if (!(x instanceof Array)) {
return alert("Internal error in PLT docs");
} else if ((x.length == 2) && (typeof(x[0]) == "number")) {
return '' + UncompactHtml(x[1]) + '';
} else {
var s = "";
for (var i=0; i= search_results.length)
first_search_result = 0;
var link_args = (page_query_string && ("?"+page_query_string));
for (var i=0; i 0)) {
note = 'provided from ';
for (var j=0; j'
+ desc[j] + '';
} else if (desc == "module") {
note = 'module';
}
if (show_manuals == 2 || (show_manuals == 1 && !desc)) {
var manual = UrlToManual(res[1]),
idx = (show_manual_titles && plt_manual_ptrs[manual]);
note = (note ? (note + " ") : "");
note += 'in '
+ ''
+ ((typeof idx == "number")
? (''+UncompactHtml(search_data[idx][2])+'')
: manual)
+ '';
}
if (note)
note = ' ' + note + '';
var href = UncompactUrl(res[1]);
if (link_args) {
var hash = href.indexOf("#");
if (hash >= 0)
href = href.substring(0,hash) + link_args + href.substring(hash);
else
href = href + link_args;
}
result_links[i].innerHTML =
''
+ UncompactHtml(res[2]) + '' + (note || "");
result_links[i].style.backgroundColor =
(n < exact_results_num) ? highlight_color : background_color;
result_links[i].style.display = "block";
} else {
result_links[i].style.display = "none";
}
}
var exact = Math.min((exact_results_num - first_search_result),
results_num);
exact = (exact <= 0) ? ''
: ' ('
+ ((exact == results_num) ? 'all' : exact)
+ ' exact)';
if (search_results.length == 0) {
if (last_search_term == "") status_line.innerHTML = "";
else status_line.innerHTML = 'No matches found'
+ ((ctx_query != "")
? (' in '+GetContextHTML()
+' '+GetContextClearerHTML('[clear]'))
: '')
+ '
'
+ '(Make sure your spelling is correct'
+ (last_search_term.search(/ /)>=0 ? ', or try fewer keywords' : '')
+ ((ctx_query != "") ? ', or clear the search context' : '')
+ ')
';
} else if (search_results.length <= results_num)
status_line.innerHTML = "Showing all matches" + exact;
else
status_line.innerHTML =
"Showing "
+ (first_search_result+1) + "-"
+ Math.min(first_search_result+results_num,search_results.length)
+ exact
+ " of " + search_results.length
+ ((search_results.length==search_data.length) ? "" : " matches");
prev_page_link1.style.color = prev_page_link2.style.color =
(first_search_result-results_num >= 0) ? "black" : "#e0e0e0";
next_page_link1.style.color = next_page_link2.style.color =
(first_search_result+results_num < search_results.length)
? "black" : "#e0e0e0";
saved_status = false;
}
function HandleKeyEvent(event) {
var key = null;
if (typeof event == "string") key = event;
else if (event) {
switch (event.which || event.keyCode) {
case 13: key = (event.ctrlKey ? "C-Enter" : "Enter"); break;
case 33: key = "PgUp"; break;
case 34: key = "PgDn"; break;
}
}
// note: uses of DoSearch() below starts a background search, which
// means that the operation can still be done on the previously
// displayed results.
switch (key) {
case "Enter": // starts a search immediately
DoSearch();
return false;
case "C-Enter": // C-enter with no change scrolls down (S -> up)
if (query.value == last_search_term_raw) {
if (!event.shiftKey) first_search_result += results_num;
else if (first_search_result > 0) first_search_result -= results_num;
else first_search_result = search_results.length - results_num;
UpdateResults();
} else {
DoSearch();
}
return false;
case "PgUp":
DoSearch(); // in case we didn't update it yet
first_search_result -= results_num;
UpdateResults();
return false;
case "PgDn":
DoSearch(); // in case we didn't update it yet
if (first_search_result + results_num < search_results.length) {
first_search_result += results_num;
UpdateResults();
}
return false;
}
// this function is called via onkeydown, which is happens before the change
// is visible; so use a timer to call this after the input field is updated
setTimeout(DoDelayedSearch, 0);
return true;
}
key_handler = HandleKeyEvent;
// use this one to set the query field without jumping to the current url
// again, since some browsers will reload the whole page for that (it would be
// nice if there was an easy way to add it to the history too)
function NewQuery(node,label) {
var m, href = node.href;
if ((m = href.search(/[?]q=[^?&;]+$/)) >= 0) { // `q' cannot be empty
query.value = decodeURIComponent(href.substring(m+3));
query.focus();
DoSearch();
return false;
} else if ((m = href.search(/[?]hq=[^?&;]*$/)) >= 0) { // `hq' can
SetContextQuery(decodeURIComponent(href.substring(m+4)),
decodeURIComponent(label));
return false;
} else {
return true;
}
}
new_query = NewQuery;
// and this appends the query to the current value (it's hooked on the
// oncontextmenu handler that doesn't work everywhere, but at least in
// FF and IE)
function RefineQuery(node) {
var m = node.href.search(/[?]q=[^?&;]+$/);
if (m < 0) return true;
m = decodeURIComponent(node.href.substring(m+3));
if (query.value.indexOf(m) >= 0) return true;
else {
query.value = query.value + " " + m;
query.focus();
DoSearch();
return false;
}
}
refine_query = RefineQuery;
var panel_shown = false;
function TogglePanel(name) {
if (panel_shown) {
document.getElementById(panel_shown+"_panel").style.display = "none";
document.getElementById("close_panel").style.display = "none";
}
panel_shown = ((panel_shown != name) && name);
if (panel_shown == "prefs") {
document.getElementById("ctx_query_pref").value = ctx_query;
document.getElementById("show_manuals_pref").selectedIndex = show_manuals;
document.getElementById("show_manual_titles_pref").checked
= show_manual_titles;
document.getElementById("results_num_pref").value = results_num;
document.getElementById("type_delay_pref").value = type_delay;
document.getElementById("highlight_color_pref").value = highlight_color;
} else if (panel_shown == "contexts") {
document.getElementById("context_query_pref").value = ctx_query;
}
if (panel_shown) {
document.getElementById(panel_shown+"_panel").style.display = "block";
document.getElementById("close_panel").style.display = "block";
// This is annoying -- would be nicer to have a way to make sure
// that it's in view without scrolling if not necessary.
// document.getElementById(panel_shown+"_panel").scrollIntoView();
}
}
toggle_panel = TogglePanel;
function HidePrefs(event) {
if ((event.which || event.keyCode) == 27) {
query.focus();
TogglePanel("prefs"); // this function is called only when it's shown
}
}
hide_prefs = HidePrefs;
function SetContextQuery(inp,label) {
// can be called with the input object, or with a string
if (typeof inp != "string") inp = inp.value;
if (inp != ctx_query) {
ctx_query = inp;
SetCookie("PLT_ContextQuery", ctx_query);
label = label || "";
ctx_query_label = label;
SetCookie("PLT_ContextQueryLabel",label);
ContextFilter();
document.getElementById("ctx_query_pref").value = ctx_query;
document.getElementById("context_query_pref").value = ctx_query;
DoSearch();
}
}
set_ctx_query = SetContextQuery;
set_context_query = SetContextQuery; // a different widget, same effect
function SetShowManuals(inp) {
if (inp.selectedIndex != show_manuals) {
show_manuals = inp.selectedIndex;
SetCookie("PLT_ManualSettings", show_manuals+(show_manual_titles?10:0));
UpdateResults();
}
}
set_show_manuals = SetShowManuals;
function SetShowManualTitles(inp) {
if (inp.checked != show_manual_titles) {
show_manual_titles = inp.checked;
SetCookie("PLT_ManualSettings", show_manuals+(show_manual_titles?10:0));
UpdateResults();
}
}
set_show_manual_titles = SetShowManualTitles;
function SetResultsNum(inp) {
var n = (parseInt(inp.value.replace(/[^0-9]+/g,"")) || results_num);
inp.value = n;
if (n != results_num) {
results_num = n;
SetCookie("PLT_ResultsNum", results_num);
AdjustResultsNum();
UpdateResults();
}
}
set_results_num = SetResultsNum;
function SetTypeDelay(inp) {
var n = (parseInt(inp.value.replace(/[^0-9]+/g,"")) || type_delay);
inp.value = n;
if (n != type_delay) {
type_delay = n;
SetCookie("PLT_TypeDelay", type_delay);
}
}
set_type_delay = SetTypeDelay;
function SetHighlightColor(inp) {
var c = (inp.value.replace(/[^a-zA-Z0-9#]/g,"") || highlight_color);
inp.value = c;
if (c != highlight_color) {
highlight_color = c;
SetCookie("PLT_HighlightColor", highlight_color);
UpdateResults();
}
}
set_highlight_color = SetHighlightColor;
AddOnLoad(InitializeSearch);
})();