mirror of
https://gitea.invidious.io/iv-org/shard-crystal-db.git
synced 2024-08-15 00:53:32 +00:00
1087 lines
31 KiB
JavaScript
1087 lines
31 KiB
JavaScript
|
window.CrystalDocs = (window.CrystalDocs || {});
|
|||
|
|
|||
|
CrystalDocs.base_path = (CrystalDocs.base_path || "");
|
|||
|
|
|||
|
CrystalDocs.searchIndex = (CrystalDocs.searchIndex || false);
|
|||
|
CrystalDocs.MAX_RESULTS_DISPLAY = 140;
|
|||
|
|
|||
|
CrystalDocs.runQuery = function(query) {
|
|||
|
function searchType(type, query, results) {
|
|||
|
var matches = [];
|
|||
|
var matchedFields = [];
|
|||
|
var name = type.full_name;
|
|||
|
var i = name.lastIndexOf("::");
|
|||
|
if (i > 0) {
|
|||
|
name = name.substring(i + 2);
|
|||
|
}
|
|||
|
var nameMatches = query.matches(name);
|
|||
|
if (nameMatches){
|
|||
|
matches = matches.concat(nameMatches);
|
|||
|
matchedFields.push("name");
|
|||
|
}
|
|||
|
|
|||
|
var namespaceMatches = query.matchesNamespace(type.full_name);
|
|||
|
if(namespaceMatches){
|
|||
|
matches = matches.concat(namespaceMatches);
|
|||
|
matchedFields.push("name");
|
|||
|
}
|
|||
|
|
|||
|
var docMatches = query.matches(type.doc);
|
|||
|
if(docMatches){
|
|||
|
matches = matches.concat(docMatches);
|
|||
|
matchedFields.push("doc");
|
|||
|
}
|
|||
|
if (matches.length > 0) {
|
|||
|
results.push({
|
|||
|
id: type.id,
|
|||
|
result_type: "type",
|
|||
|
kind: type.kind,
|
|||
|
name: name,
|
|||
|
full_name: type.full_name,
|
|||
|
href: type.path,
|
|||
|
summary: type.summary,
|
|||
|
matched_fields: matchedFields,
|
|||
|
matched_terms: matches
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
type.instance_methods.forEach(function(method) {
|
|||
|
searchMethod(method, type, "instance_method", query, results);
|
|||
|
})
|
|||
|
type.class_methods.forEach(function(method) {
|
|||
|
searchMethod(method, type, "class_method", query, results);
|
|||
|
})
|
|||
|
type.constructors.forEach(function(constructor) {
|
|||
|
searchMethod(constructor, type, "constructor", query, results);
|
|||
|
})
|
|||
|
type.macros.forEach(function(macro) {
|
|||
|
searchMethod(macro, type, "macro", query, results);
|
|||
|
})
|
|||
|
type.constants.forEach(function(constant){
|
|||
|
searchConstant(constant, type, query, results);
|
|||
|
});
|
|||
|
|
|||
|
type.types.forEach(function(subtype){
|
|||
|
searchType(subtype, query, results);
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
function searchMethod(method, type, kind, query, results) {
|
|||
|
var matches = [];
|
|||
|
var matchedFields = [];
|
|||
|
var nameMatches = query.matchesMethod(method.name, kind, type);
|
|||
|
if (nameMatches){
|
|||
|
matches = matches.concat(nameMatches);
|
|||
|
matchedFields.push("name");
|
|||
|
}
|
|||
|
|
|||
|
method.args.forEach(function(arg){
|
|||
|
var argMatches = query.matches(arg.external_name);
|
|||
|
if (argMatches) {
|
|||
|
matches = matches.concat(argMatches);
|
|||
|
matchedFields.push("args");
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
var docMatches = query.matches(type.doc);
|
|||
|
if(docMatches){
|
|||
|
matches = matches.concat(docMatches);
|
|||
|
matchedFields.push("doc");
|
|||
|
}
|
|||
|
|
|||
|
if (matches.length > 0) {
|
|||
|
var typeMatches = query.matches(type.full_name);
|
|||
|
if (typeMatches) {
|
|||
|
matchedFields.push("type");
|
|||
|
matches = matches.concat(typeMatches);
|
|||
|
}
|
|||
|
results.push({
|
|||
|
id: method.id,
|
|||
|
type: type.full_name,
|
|||
|
result_type: kind,
|
|||
|
name: method.name,
|
|||
|
full_name: type.full_name + "#" + method.name,
|
|||
|
args_string: method.args_string,
|
|||
|
summary: method.summary,
|
|||
|
href: type.path + "#" + method.id,
|
|||
|
matched_fields: matchedFields,
|
|||
|
matched_terms: matches
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function searchConstant(constant, type, query, results) {
|
|||
|
var matches = [];
|
|||
|
var matchedFields = [];
|
|||
|
var nameMatches = query.matches(constant.name);
|
|||
|
if (nameMatches){
|
|||
|
matches = matches.concat(nameMatches);
|
|||
|
matchedFields.push("name");
|
|||
|
}
|
|||
|
var docMatches = query.matches(constant.doc);
|
|||
|
if(docMatches){
|
|||
|
matches = matches.concat(docMatches);
|
|||
|
matchedFields.push("doc");
|
|||
|
}
|
|||
|
if (matches.length > 0) {
|
|||
|
var typeMatches = query.matches(type.full_name);
|
|||
|
if (typeMatches) {
|
|||
|
matchedFields.push("type");
|
|||
|
matches = matches.concat(typeMatches);
|
|||
|
}
|
|||
|
results.push({
|
|||
|
id: constant.id,
|
|||
|
type: type.full_name,
|
|||
|
result_type: "constant",
|
|||
|
name: constant.name,
|
|||
|
full_name: type.full_name + "#" + constant.name,
|
|||
|
value: constant.value,
|
|||
|
summary: constant.summary,
|
|||
|
href: type.path + "#" + constant.id,
|
|||
|
matched_fields: matchedFields,
|
|||
|
matched_terms: matches
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var results = [];
|
|||
|
searchType(CrystalDocs.searchIndex.program, query, results);
|
|||
|
return results;
|
|||
|
};
|
|||
|
|
|||
|
CrystalDocs.rankResults = function(results, query) {
|
|||
|
function uniqueArray(ar) {
|
|||
|
var j = {};
|
|||
|
|
|||
|
ar.forEach(function(v) {
|
|||
|
j[v + "::" + typeof v] = v;
|
|||
|
});
|
|||
|
|
|||
|
return Object.keys(j).map(function(v) {
|
|||
|
return j[v];
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
results = results.sort(function(a, b) {
|
|||
|
var matchedTermsDiff = uniqueArray(b.matched_terms).length - uniqueArray(a.matched_terms).length;
|
|||
|
var aHasDocs = b.matched_fields.includes("doc");
|
|||
|
var bHasDocs = b.matched_fields.includes("doc");
|
|||
|
|
|||
|
var aOnlyDocs = aHasDocs && a.matched_fields.length == 1;
|
|||
|
var bOnlyDocs = bHasDocs && b.matched_fields.length == 1;
|
|||
|
|
|||
|
if (a.result_type == "type" && b.result_type != "type" && !aOnlyDocs) {
|
|||
|
if(CrystalDocs.DEBUG) { console.log("a is type b not"); }
|
|||
|
return -1;
|
|||
|
} else if (b.result_type == "type" && a.result_type != "type" && !bOnlyDocs) {
|
|||
|
if(CrystalDocs.DEBUG) { console.log("b is type, a not"); }
|
|||
|
return 1;
|
|||
|
}
|
|||
|
if (a.matched_fields.includes("name")) {
|
|||
|
if (b.matched_fields.includes("name")) {
|
|||
|
var a_name = (CrystalDocs.prefixForType(a.result_type) || "") + ((a.result_type == "type") ? a.full_name : a.name);
|
|||
|
var b_name = (CrystalDocs.prefixForType(b.result_type) || "") + ((b.result_type == "type") ? b.full_name : b.name);
|
|||
|
a_name = a_name.toLowerCase();
|
|||
|
b_name = b_name.toLowerCase();
|
|||
|
for(var i = 0; i < query.normalizedTerms.length; i++) {
|
|||
|
var term = query.terms[i].replace(/^::?|::?$/, "");
|
|||
|
var a_orig_index = a_name.indexOf(term);
|
|||
|
var b_orig_index = b_name.indexOf(term);
|
|||
|
if(CrystalDocs.DEBUG) { console.log("term: " + term + " a: " + a_name + " b: " + b_name); }
|
|||
|
if(CrystalDocs.DEBUG) { console.log(a_orig_index, b_orig_index, a_orig_index - b_orig_index); }
|
|||
|
if (a_orig_index >= 0) {
|
|||
|
if (b_orig_index >= 0) {
|
|||
|
if(CrystalDocs.DEBUG) { console.log("both have exact match", a_orig_index > b_orig_index ? -1 : 1); }
|
|||
|
if(a_orig_index != b_orig_index) {
|
|||
|
if(CrystalDocs.DEBUG) { console.log("both have exact match at different positions", a_orig_index > b_orig_index ? 1 : -1); }
|
|||
|
return a_orig_index > b_orig_index ? 1 : -1;
|
|||
|
}
|
|||
|
} else {
|
|||
|
if(CrystalDocs.DEBUG) { console.log("a has exact match, b not"); }
|
|||
|
return -1;
|
|||
|
}
|
|||
|
} else if (b_orig_index >= 0) {
|
|||
|
if(CrystalDocs.DEBUG) { console.log("b has exact match, a not"); }
|
|||
|
return 1;
|
|||
|
}
|
|||
|
}
|
|||
|
} else {
|
|||
|
if(CrystalDocs.DEBUG) { console.log("a has match in name, b not"); }
|
|||
|
return -1;
|
|||
|
}
|
|||
|
} else if (
|
|||
|
!a.matched_fields.includes("name") &&
|
|||
|
b.matched_fields.includes("name")
|
|||
|
) {
|
|||
|
return 1;
|
|||
|
}
|
|||
|
|
|||
|
if (matchedTermsDiff != 0 || (aHasDocs != bHasDocs)) {
|
|||
|
if(CrystalDocs.DEBUG) { console.log("matchedTermsDiff: " + matchedTermsDiff, aHasDocs, bHasDocs); }
|
|||
|
return matchedTermsDiff;
|
|||
|
}
|
|||
|
|
|||
|
var matchedFieldsDiff = b.matched_fields.length - a.matched_fields.length;
|
|||
|
if (matchedFieldsDiff != 0) {
|
|||
|
if(CrystalDocs.DEBUG) { console.log("matched to different number of fields: " + matchedFieldsDiff); }
|
|||
|
return matchedFieldsDiff > 0 ? 1 : -1;
|
|||
|
}
|
|||
|
|
|||
|
var nameCompare = a.name.localeCompare(b.name);
|
|||
|
if(nameCompare != 0){
|
|||
|
if(CrystalDocs.DEBUG) { console.log("nameCompare resulted in: " + a.name + "<=>" + b.name + ": " + nameCompare); }
|
|||
|
return nameCompare > 0 ? 1 : -1;
|
|||
|
}
|
|||
|
|
|||
|
if(a.matched_fields.includes("args") && b.matched_fields.includes("args")) {
|
|||
|
for(var i = 0; i < query.terms.length; i++) {
|
|||
|
var term = query.terms[i];
|
|||
|
var aIndex = a.args_string.indexOf(term);
|
|||
|
var bIndex = b.args_string.indexOf(term);
|
|||
|
if(CrystalDocs.DEBUG) { console.log("index of " + term + " in args_string: " + aIndex + " - " + bIndex); }
|
|||
|
if(aIndex >= 0){
|
|||
|
if(bIndex >= 0){
|
|||
|
if(aIndex != bIndex){
|
|||
|
return aIndex > bIndex ? 1 : -1;
|
|||
|
}
|
|||
|
}else{
|
|||
|
return -1;
|
|||
|
}
|
|||
|
}else if(bIndex >= 0) {
|
|||
|
return 1;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return 0;
|
|||
|
});
|
|||
|
|
|||
|
if (results.length > 1) {
|
|||
|
// if we have more than two search terms, only include results with the most matches
|
|||
|
var bestMatchedTerms = uniqueArray(results[0].matched_terms).length;
|
|||
|
|
|||
|
results = results.filter(function(result) {
|
|||
|
return uniqueArray(result.matched_terms).length + 1 >= bestMatchedTerms;
|
|||
|
});
|
|||
|
}
|
|||
|
return results;
|
|||
|
};
|
|||
|
|
|||
|
CrystalDocs.prefixForType = function(type) {
|
|||
|
switch (type) {
|
|||
|
case "instance_method":
|
|||
|
return "#";
|
|||
|
|
|||
|
case "class_method":
|
|||
|
case "macro":
|
|||
|
case "constructor":
|
|||
|
return ".";
|
|||
|
|
|||
|
default:
|
|||
|
return false;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
CrystalDocs.displaySearchResults = function(results, query) {
|
|||
|
function sanitize(html){
|
|||
|
return html.replace(/<(?!\/?code)[^>]+>/g, "");
|
|||
|
}
|
|||
|
|
|||
|
// limit results
|
|||
|
if (results.length > CrystalDocs.MAX_RESULTS_DISPLAY) {
|
|||
|
results = results.slice(0, CrystalDocs.MAX_RESULTS_DISPLAY);
|
|||
|
}
|
|||
|
|
|||
|
var $frag = document.createDocumentFragment();
|
|||
|
var $resultsElem = document.querySelector(".search-list");
|
|||
|
$resultsElem.innerHTML = "<!--" + JSON.stringify(query) + "-->";
|
|||
|
|
|||
|
results.forEach(function(result, i) {
|
|||
|
var url = CrystalDocs.base_path + result.href;
|
|||
|
var type = false;
|
|||
|
|
|||
|
var title = query.highlight(result.result_type == "type" ? result.full_name : result.name);
|
|||
|
|
|||
|
var prefix = CrystalDocs.prefixForType(result.result_type);
|
|||
|
if (prefix) {
|
|||
|
title = "<b>" + prefix + "</b>" + title;
|
|||
|
}
|
|||
|
|
|||
|
title = "<strong>" + title + "</strong>";
|
|||
|
|
|||
|
if (result.args_string) {
|
|||
|
title +=
|
|||
|
"<span class=\"args\">" + query.highlight(result.args_string) + "</span>";
|
|||
|
}
|
|||
|
|
|||
|
$elem = document.createElement("li");
|
|||
|
$elem.className = "search-result search-result--" + result.result_type;
|
|||
|
$elem.dataset.href = url;
|
|||
|
$elem.setAttribute("title", result.full_name + " docs page");
|
|||
|
|
|||
|
var $title = document.createElement("div");
|
|||
|
$title.setAttribute("class", "search-result__title");
|
|||
|
var $titleLink = document.createElement("a");
|
|||
|
$titleLink.setAttribute("href", url);
|
|||
|
|
|||
|
$titleLink.innerHTML = title;
|
|||
|
$title.appendChild($titleLink);
|
|||
|
$elem.appendChild($title);
|
|||
|
$elem.addEventListener("click", function() {
|
|||
|
$titleLink.click();
|
|||
|
});
|
|||
|
|
|||
|
if (result.result_type !== "type") {
|
|||
|
var $type = document.createElement("div");
|
|||
|
$type.setAttribute("class", "search-result__type");
|
|||
|
$type.innerHTML = query.highlight(result.type);
|
|||
|
$elem.appendChild($type);
|
|||
|
}
|
|||
|
|
|||
|
if(result.summary){
|
|||
|
var $doc = document.createElement("div");
|
|||
|
$doc.setAttribute("class", "search-result__doc");
|
|||
|
$doc.innerHTML = query.highlight(sanitize(result.summary));
|
|||
|
$elem.appendChild($doc);
|
|||
|
}
|
|||
|
|
|||
|
$elem.appendChild(document.createComment(JSON.stringify(result)));
|
|||
|
$frag.appendChild($elem);
|
|||
|
});
|
|||
|
|
|||
|
$resultsElem.appendChild($frag);
|
|||
|
|
|||
|
CrystalDocs.toggleResultsList(true);
|
|||
|
};
|
|||
|
|
|||
|
CrystalDocs.toggleResultsList = function(visible) {
|
|||
|
if (visible) {
|
|||
|
document.querySelector(".types-list").classList.add("hidden");
|
|||
|
document.querySelector(".search-results").classList.remove("hidden");
|
|||
|
} else {
|
|||
|
document.querySelector(".types-list").classList.remove("hidden");
|
|||
|
document.querySelector(".search-results").classList.add("hidden");
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
CrystalDocs.Query = function(string) {
|
|||
|
this.original = string;
|
|||
|
this.terms = string.split(/\s+/).filter(function(word) {
|
|||
|
return CrystalDocs.Query.stripModifiers(word).length > 0;
|
|||
|
});
|
|||
|
|
|||
|
var normalized = this.terms.map(CrystalDocs.Query.normalizeTerm);
|
|||
|
this.normalizedTerms = normalized;
|
|||
|
|
|||
|
function runMatcher(field, matcher) {
|
|||
|
if (!field) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
var normalizedValue = CrystalDocs.Query.normalizeTerm(field);
|
|||
|
|
|||
|
var matches = [];
|
|||
|
normalized.forEach(function(term) {
|
|||
|
if (matcher(normalizedValue, term)) {
|
|||
|
matches.push(term);
|
|||
|
}
|
|||
|
});
|
|||
|
return matches.length > 0 ? matches : false;
|
|||
|
}
|
|||
|
|
|||
|
this.matches = function(field) {
|
|||
|
return runMatcher(field, function(normalized, term) {
|
|||
|
if (term[0] == "#" || term[0] == ".") {
|
|||
|
return false;
|
|||
|
}
|
|||
|
return normalized.indexOf(term) >= 0;
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
function namespaceMatcher(normalized, term){
|
|||
|
var i = term.indexOf(":");
|
|||
|
if(i >= 0){
|
|||
|
term = term.replace(/^::?|::?$/, "");
|
|||
|
var index = normalized.indexOf(term);
|
|||
|
if((index == 0) || (index > 0 && normalized[index-1] == ":")){
|
|||
|
return true;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
this.matchesMethod = function(name, kind, type) {
|
|||
|
return runMatcher(name, function(normalized, term) {
|
|||
|
var i = term.indexOf("#");
|
|||
|
if(i >= 0){
|
|||
|
if (kind != "instance_method") {
|
|||
|
return false;
|
|||
|
}
|
|||
|
}else{
|
|||
|
i = term.indexOf(".");
|
|||
|
if(i >= 0){
|
|||
|
if (kind != "class_method" && kind != "macro" && kind != "constructor") {
|
|||
|
return false;
|
|||
|
}
|
|||
|
}else{
|
|||
|
//neither # nor .
|
|||
|
if(term.indexOf(":") && namespaceMatcher(normalized, term)){
|
|||
|
return true;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var methodName = term;
|
|||
|
if(i >= 0){
|
|||
|
var termType = term.substring(0, i);
|
|||
|
methodName = term.substring(i+1);
|
|||
|
|
|||
|
if(termType != "") {
|
|||
|
if(CrystalDocs.Query.normalizeTerm(type.full_name).indexOf(termType) < 0){
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return normalized.indexOf(methodName) >= 0;
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
this.matchesNamespace = function(namespace){
|
|||
|
return runMatcher(namespace, namespaceMatcher);
|
|||
|
};
|
|||
|
|
|||
|
this.highlight = function(string) {
|
|||
|
if (typeof string == "undefined") {
|
|||
|
return "";
|
|||
|
}
|
|||
|
function escapeRegExp(s) {
|
|||
|
return s.replace(/[.*+?\^${}()|\[\]\\]/g, "\\$&").replace(/^[#\.:]+/, "");
|
|||
|
}
|
|||
|
return string.replace(
|
|||
|
new RegExp("(" + this.normalizedTerms.map(escapeRegExp).join("|") + ")", "gi"),
|
|||
|
"<mark>$1</mark>"
|
|||
|
);
|
|||
|
};
|
|||
|
};
|
|||
|
CrystalDocs.Query.normalizeTerm = function(term) {
|
|||
|
return term.toLowerCase();
|
|||
|
};
|
|||
|
CrystalDocs.Query.stripModifiers = function(term) {
|
|||
|
switch (term[0]) {
|
|||
|
case "#":
|
|||
|
case ".":
|
|||
|
case ":":
|
|||
|
return term.substr(1);
|
|||
|
|
|||
|
default:
|
|||
|
return term;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
CrystalDocs.search = function(string) {
|
|||
|
if(!CrystalDocs.searchIndex) {
|
|||
|
console.log("CrystalDocs search index not initialized, delaying search");
|
|||
|
|
|||
|
document.addEventListener("CrystalDocs:loaded", function listener(){
|
|||
|
document.removeEventListener("CrystalDocs:loaded", listener);
|
|||
|
CrystalDocs.search(string);
|
|||
|
});
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
document.dispatchEvent(new Event("CrystalDocs:searchStarted"));
|
|||
|
|
|||
|
var query = new CrystalDocs.Query(string);
|
|||
|
var results = CrystalDocs.runQuery(query);
|
|||
|
results = CrystalDocs.rankResults(results, query);
|
|||
|
CrystalDocs.displaySearchResults(results, query);
|
|||
|
|
|||
|
document.dispatchEvent(new Event("CrystalDocs:searchPerformed"));
|
|||
|
};
|
|||
|
|
|||
|
CrystalDocs.initializeIndex = function(data) {
|
|||
|
CrystalDocs.searchIndex = data;
|
|||
|
|
|||
|
document.dispatchEvent(new Event("CrystalDocs:loaded"));
|
|||
|
};
|
|||
|
|
|||
|
CrystalDocs.loadIndex = function() {
|
|||
|
function loadJSON(file, callback) {
|
|||
|
var xobj = new XMLHttpRequest();
|
|||
|
xobj.overrideMimeType("application/json");
|
|||
|
xobj.open("GET", file, true);
|
|||
|
xobj.onreadystatechange = function() {
|
|||
|
if (xobj.readyState == 4 && xobj.status == "200") {
|
|||
|
callback(xobj.responseText);
|
|||
|
}
|
|||
|
};
|
|||
|
xobj.send(null);
|
|||
|
}
|
|||
|
|
|||
|
function loadScript(file) {
|
|||
|
script = document.createElement("script");
|
|||
|
script.src = file;
|
|||
|
document.body.appendChild(script);
|
|||
|
}
|
|||
|
|
|||
|
function parseJSON(json) {
|
|||
|
CrystalDocs.initializeIndex(JSON.parse(json));
|
|||
|
}
|
|||
|
|
|||
|
for(var i = 0; i < document.scripts.length; i++){
|
|||
|
var script = document.scripts[i];
|
|||
|
if (script.src && script.src.indexOf("js/doc.js") >= 0) {
|
|||
|
if (script.src.indexOf("file://") == 0) {
|
|||
|
// We need to support JSONP files for the search to work on local file system.
|
|||
|
var jsonPath = script.src.replace("js/doc.js", "search-index.js");
|
|||
|
loadScript(jsonPath);
|
|||
|
return;
|
|||
|
} else {
|
|||
|
var jsonPath = script.src.replace("js/doc.js", "index.json");
|
|||
|
loadJSON(jsonPath, parseJSON);
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
console.error("Could not find location of js/doc.js");
|
|||
|
};
|
|||
|
|
|||
|
// Callback for jsonp
|
|||
|
function crystal_doc_search_index_callback(data) {
|
|||
|
CrystalDocs.initializeIndex(data);
|
|||
|
}
|
|||
|
|
|||
|
Navigator = function(sidebar, searchInput, list, leaveSearchScope){
|
|||
|
this.list = list;
|
|||
|
var self = this;
|
|||
|
|
|||
|
var performingSearch = false;
|
|||
|
|
|||
|
document.addEventListener('CrystalDocs:searchStarted', function(){
|
|||
|
performingSearch = true;
|
|||
|
});
|
|||
|
document.addEventListener('CrystalDocs:searchDebounceStarted', function(){
|
|||
|
performingSearch = true;
|
|||
|
});
|
|||
|
document.addEventListener('CrystalDocs:searchPerformed', function(){
|
|||
|
performingSearch = false;
|
|||
|
});
|
|||
|
document.addEventListener('CrystalDocs:searchDebounceStopped', function(event){
|
|||
|
performingSearch = false;
|
|||
|
});
|
|||
|
|
|||
|
function delayWhileSearching(callback) {
|
|||
|
if(performingSearch){
|
|||
|
document.addEventListener('CrystalDocs:searchPerformed', function listener(){
|
|||
|
document.removeEventListener('CrystalDocs:searchPerformed', listener);
|
|||
|
|
|||
|
// add some delay to let search results display kick in
|
|||
|
setTimeout(callback, 100);
|
|||
|
});
|
|||
|
}else{
|
|||
|
callback();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function clearMoveTimeout() {
|
|||
|
clearTimeout(self.moveTimeout);
|
|||
|
self.moveTimeout = null;
|
|||
|
}
|
|||
|
|
|||
|
function startMoveTimeout(upwards){
|
|||
|
/*if(self.moveTimeout) {
|
|||
|
clearMoveTimeout();
|
|||
|
}
|
|||
|
|
|||
|
var go = function() {
|
|||
|
if (!self.moveTimeout) return;
|
|||
|
self.move(upwards);
|
|||
|
self.moveTimout = setTimeout(go, 600);
|
|||
|
};
|
|||
|
self.moveTimeout = setTimeout(go, 800);*/
|
|||
|
}
|
|||
|
|
|||
|
function scrollCenter(element) {
|
|||
|
var rect = element.getBoundingClientRect();
|
|||
|
var middle = sidebar.clientHeight / 2;
|
|||
|
sidebar.scrollTop += rect.top + rect.height / 2 - middle;
|
|||
|
}
|
|||
|
|
|||
|
var move = this.move = function(upwards){
|
|||
|
if(!this.current){
|
|||
|
this.highlightFirst();
|
|||
|
return true;
|
|||
|
}
|
|||
|
var next = upwards ? this.current.previousElementSibling : this.current.nextElementSibling;
|
|||
|
if(next && next.classList) {
|
|||
|
this.highlight(next);
|
|||
|
scrollCenter(next);
|
|||
|
return true;
|
|||
|
}
|
|||
|
return false;
|
|||
|
};
|
|||
|
|
|||
|
this.moveRight = function(){
|
|||
|
};
|
|||
|
this.moveLeft = function(){
|
|||
|
};
|
|||
|
|
|||
|
this.highlight = function(elem) {
|
|||
|
if(!elem){
|
|||
|
return;
|
|||
|
}
|
|||
|
this.removeHighlight();
|
|||
|
|
|||
|
this.current = elem;
|
|||
|
this.current.classList.add("current");
|
|||
|
};
|
|||
|
|
|||
|
this.highlightFirst = function(){
|
|||
|
this.highlight(this.list.querySelector('li:first-child'));
|
|||
|
};
|
|||
|
|
|||
|
this.removeHighlight = function() {
|
|||
|
if(this.current){
|
|||
|
this.current.classList.remove("current");
|
|||
|
}
|
|||
|
this.current = null;
|
|||
|
}
|
|||
|
|
|||
|
this.openSelectedResult = function() {
|
|||
|
if(this.current) {
|
|||
|
this.current.click();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
this.focus = function() {
|
|||
|
searchInput.focus();
|
|||
|
searchInput.select();
|
|||
|
this.highlightFirst();
|
|||
|
}
|
|||
|
|
|||
|
function handleKeyUp(event) {
|
|||
|
switch(event.key) {
|
|||
|
case "ArrowUp":
|
|||
|
case "ArrowDown":
|
|||
|
case "i":
|
|||
|
case "j":
|
|||
|
case "k":
|
|||
|
case "l":
|
|||
|
case "c":
|
|||
|
case "h":
|
|||
|
case "t":
|
|||
|
case "n":
|
|||
|
event.stopPropagation();
|
|||
|
clearMoveTimeout();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function handleKeyDown(event) {
|
|||
|
switch(event.key) {
|
|||
|
case "Enter":
|
|||
|
event.stopPropagation();
|
|||
|
event.preventDefault();
|
|||
|
leaveSearchScope();
|
|||
|
self.openSelectedResult();
|
|||
|
break;
|
|||
|
case "Escape":
|
|||
|
event.stopPropagation();
|
|||
|
event.preventDefault();
|
|||
|
leaveSearchScope();
|
|||
|
break;
|
|||
|
case "j":
|
|||
|
case "c":
|
|||
|
case "ArrowUp":
|
|||
|
if(event.ctrlKey || event.key == "ArrowUp") {
|
|||
|
event.stopPropagation();
|
|||
|
self.move(true);
|
|||
|
startMoveTimeout(true);
|
|||
|
}
|
|||
|
break;
|
|||
|
case "k":
|
|||
|
case "h":
|
|||
|
case "ArrowDown":
|
|||
|
if(event.ctrlKey || event.key == "ArrowDown") {
|
|||
|
event.stopPropagation();
|
|||
|
self.move(false);
|
|||
|
startMoveTimeout(false);
|
|||
|
}
|
|||
|
break;
|
|||
|
case "k":
|
|||
|
case "t":
|
|||
|
case "ArrowLeft":
|
|||
|
if(event.ctrlKey || event.key == "ArrowLeft") {
|
|||
|
event.stopPropagation();
|
|||
|
self.moveLeft();
|
|||
|
}
|
|||
|
break;
|
|||
|
case "l":
|
|||
|
case "n":
|
|||
|
case "ArrowRight":
|
|||
|
if(event.ctrlKey || event.key == "ArrowRight") {
|
|||
|
event.stopPropagation();
|
|||
|
self.moveRight();
|
|||
|
}
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function handleInputKeyUp(event) {
|
|||
|
switch(event.key) {
|
|||
|
case "ArrowUp":
|
|||
|
case "ArrowDown":
|
|||
|
event.stopPropagation();
|
|||
|
event.preventDefault();
|
|||
|
clearMoveTimeout();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function handleInputKeyDown(event) {
|
|||
|
switch(event.key) {
|
|||
|
case "Enter":
|
|||
|
event.stopPropagation();
|
|||
|
event.preventDefault();
|
|||
|
delayWhileSearching(function(){
|
|||
|
self.openSelectedResult();
|
|||
|
leaveSearchScope();
|
|||
|
});
|
|||
|
break;
|
|||
|
case "Escape":
|
|||
|
event.stopPropagation();
|
|||
|
event.preventDefault();
|
|||
|
// remove focus from search input
|
|||
|
leaveSearchScope();
|
|||
|
sidebar.focus();
|
|||
|
break;
|
|||
|
case "ArrowUp":
|
|||
|
event.stopPropagation();
|
|||
|
event.preventDefault();
|
|||
|
self.move(true);
|
|||
|
startMoveTimeout(true);
|
|||
|
break;
|
|||
|
|
|||
|
case "ArrowDown":
|
|||
|
event.stopPropagation();
|
|||
|
event.preventDefault();
|
|||
|
self.move(false);
|
|||
|
startMoveTimeout(false);
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
sidebar.tabIndex = 100; // set tabIndex to enable keylistener
|
|||
|
sidebar.addEventListener('keyup', function(event) {
|
|||
|
handleKeyUp(event);
|
|||
|
});
|
|||
|
sidebar.addEventListener('keydown', function(event) {
|
|||
|
handleKeyDown(event);
|
|||
|
});
|
|||
|
searchInput.addEventListener('keydown', function(event) {
|
|||
|
handleInputKeyDown(event);
|
|||
|
});
|
|||
|
searchInput.addEventListener('keyup', function(event) {
|
|||
|
handleInputKeyUp(event);
|
|||
|
});
|
|||
|
this.move();
|
|||
|
};
|
|||
|
|
|||
|
CrystalDocs.initializeVersions = function () {
|
|||
|
function loadJSON(file, callback) {
|
|||
|
var xobj = new XMLHttpRequest();
|
|||
|
xobj.overrideMimeType("application/json");
|
|||
|
xobj.open("GET", file, true);
|
|||
|
xobj.onreadystatechange = function() {
|
|||
|
if (xobj.readyState == 4 && xobj.status == "200") {
|
|||
|
callback(xobj.responseText);
|
|||
|
}
|
|||
|
};
|
|||
|
xobj.send(null);
|
|||
|
}
|
|||
|
|
|||
|
function parseJSON(json) {
|
|||
|
CrystalDocs.loadConfig(JSON.parse(json));
|
|||
|
}
|
|||
|
|
|||
|
$elem = document.querySelector("html > head > meta[name=\"crystal_docs.json_config_url\"]")
|
|||
|
if ($elem == undefined) {
|
|||
|
return
|
|||
|
}
|
|||
|
jsonURL = $elem.getAttribute("content")
|
|||
|
if (jsonURL && jsonURL != "") {
|
|||
|
loadJSON(jsonURL, parseJSON);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
CrystalDocs.loadConfig = function (config) {
|
|||
|
var projectVersions = config["versions"]
|
|||
|
var currentVersion = document.querySelector("html > head > meta[name=\"crystal_docs.project_version\"]").getAttribute("content")
|
|||
|
|
|||
|
var currentVersionInList = projectVersions.find(function (element) {
|
|||
|
return element.name == currentVersion
|
|||
|
})
|
|||
|
|
|||
|
if (!currentVersionInList) {
|
|||
|
projectVersions.unshift({ name: currentVersion, url: '#' })
|
|||
|
}
|
|||
|
|
|||
|
$version = document.querySelector(".project-summary > .project-version")
|
|||
|
$version.innerHTML = ""
|
|||
|
|
|||
|
$select = document.createElement("select")
|
|||
|
$select.classList.add("project-versions-nav")
|
|||
|
$select.addEventListener("change", function () {
|
|||
|
window.location.href = this.value
|
|||
|
})
|
|||
|
projectVersions.forEach(function (version) {
|
|||
|
$item = document.createElement("option")
|
|||
|
$item.setAttribute("value", version.url)
|
|||
|
$item.append(document.createTextNode(version.name))
|
|||
|
|
|||
|
if (version.name == currentVersion) {
|
|||
|
$item.setAttribute("selected", true)
|
|||
|
$item.setAttribute("disabled", true)
|
|||
|
}
|
|||
|
$select.append($item)
|
|||
|
});
|
|||
|
$form = document.createElement("form")
|
|||
|
$form.setAttribute("autocomplete", "off")
|
|||
|
$form.append($select)
|
|||
|
$version.append($form)
|
|||
|
}
|
|||
|
|
|||
|
document.addEventListener("DOMContentLoaded", function () {
|
|||
|
CrystalDocs.initializeVersions()
|
|||
|
})
|
|||
|
|
|||
|
var UsageModal = function(title, content) {
|
|||
|
var $body = document.body;
|
|||
|
var self = this;
|
|||
|
var $modalBackground = document.createElement("div");
|
|||
|
$modalBackground.classList.add("modal-background");
|
|||
|
var $usageModal = document.createElement("div");
|
|||
|
$usageModal.classList.add("usage-modal");
|
|||
|
$modalBackground.appendChild($usageModal);
|
|||
|
var $title = document.createElement("h3");
|
|||
|
$title.classList.add("modal-title");
|
|||
|
$title.innerHTML = title
|
|||
|
$usageModal.appendChild($title);
|
|||
|
var $closeButton = document.createElement("span");
|
|||
|
$closeButton.classList.add("close-button");
|
|||
|
$closeButton.setAttribute("title", "Close modal");
|
|||
|
$closeButton.innerText = '×';
|
|||
|
$usageModal.appendChild($closeButton);
|
|||
|
$usageModal.insertAdjacentHTML("beforeend", content);
|
|||
|
|
|||
|
$modalBackground.addEventListener('click', function(event) {
|
|||
|
var element = event.target || event.srcElement;
|
|||
|
|
|||
|
if(element == $modalBackground) {
|
|||
|
self.hide();
|
|||
|
}
|
|||
|
});
|
|||
|
$closeButton.addEventListener('click', function(event) {
|
|||
|
self.hide();
|
|||
|
});
|
|||
|
|
|||
|
$body.insertAdjacentElement('beforeend', $modalBackground);
|
|||
|
|
|||
|
this.show = function(){
|
|||
|
$body.classList.add("js-modal-visible");
|
|||
|
};
|
|||
|
this.hide = function(){
|
|||
|
$body.classList.remove("js-modal-visible");
|
|||
|
};
|
|||
|
this.isVisible = function(){
|
|||
|
return $body.classList.contains("js-modal-visible");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
document.addEventListener('DOMContentLoaded', function() {
|
|||
|
var sessionStorage;
|
|||
|
try {
|
|||
|
sessionStorage = window.sessionStorage;
|
|||
|
} catch (e) { }
|
|||
|
if(!sessionStorage) {
|
|||
|
sessionStorage = {
|
|||
|
setItem: function() {},
|
|||
|
getItem: function() {},
|
|||
|
removeItem: function() {}
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
var repositoryName = document.querySelector('[name=repository-name]').getAttribute('content');
|
|||
|
var typesList = document.querySelector('.types-list');
|
|||
|
var searchInput = document.querySelector('.search-input');
|
|||
|
var parents = document.querySelectorAll('.types-list li.parent');
|
|||
|
|
|||
|
var scrollSidebarToOpenType = function(){
|
|||
|
var openTypes = typesList.querySelectorAll('.current');
|
|||
|
if (openTypes.length > 0) {
|
|||
|
var lastOpenType = openTypes[openTypes.length - 1];
|
|||
|
lastOpenType.scrollIntoView();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
scrollSidebarToOpenType();
|
|||
|
|
|||
|
var setPersistentSearchQuery = function(value){
|
|||
|
sessionStorage.setItem(repositoryName + '::search-input:value', value);
|
|||
|
}
|
|||
|
|
|||
|
for(var i = 0; i < parents.length; i++) {
|
|||
|
var _parent = parents[i];
|
|||
|
_parent.addEventListener('click', function(e) {
|
|||
|
e.stopPropagation();
|
|||
|
|
|||
|
if(e.target.tagName.toLowerCase() == 'li') {
|
|||
|
if(e.target.className.match(/open/)) {
|
|||
|
sessionStorage.removeItem(e.target.getAttribute('data-id'));
|
|||
|
e.target.className = e.target.className.replace(/ +open/g, '');
|
|||
|
} else {
|
|||
|
sessionStorage.setItem(e.target.getAttribute('data-id'), '1');
|
|||
|
if(e.target.className.indexOf('open') == -1) {
|
|||
|
e.target.className += ' open';
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
if(sessionStorage.getItem(_parent.getAttribute('data-id')) == '1') {
|
|||
|
_parent.className += ' open';
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var leaveSearchScope = function(){
|
|||
|
CrystalDocs.toggleResultsList(false);
|
|||
|
window.focus();
|
|||
|
}
|
|||
|
|
|||
|
var navigator = new Navigator(document.querySelector('.types-list'), searchInput, document.querySelector(".search-results"), leaveSearchScope);
|
|||
|
|
|||
|
CrystalDocs.loadIndex();
|
|||
|
var searchTimeout;
|
|||
|
var lastSearchText = false;
|
|||
|
var performSearch = function() {
|
|||
|
document.dispatchEvent(new Event("CrystalDocs:searchDebounceStarted"));
|
|||
|
|
|||
|
clearTimeout(searchTimeout);
|
|||
|
searchTimeout = setTimeout(function() {
|
|||
|
var text = searchInput.value;
|
|||
|
|
|||
|
if(text == "") {
|
|||
|
CrystalDocs.toggleResultsList(false);
|
|||
|
}else if(text == lastSearchText){
|
|||
|
document.dispatchEvent(new Event("CrystalDocs:searchDebounceStopped"));
|
|||
|
}else{
|
|||
|
CrystalDocs.search(text);
|
|||
|
navigator.highlightFirst();
|
|||
|
searchInput.focus();
|
|||
|
}
|
|||
|
lastSearchText = text;
|
|||
|
setPersistentSearchQuery(text);
|
|||
|
}, 200);
|
|||
|
};
|
|||
|
|
|||
|
if(location.hash.length > 3 && location.hash.substring(0,3) == "#q="){
|
|||
|
// allows directly linking a search query which is then executed on the client
|
|||
|
// this comes handy for establishing a custom browser search engine with https://crystal-lang.org/api/#q=%s as a search URL
|
|||
|
// TODO: Add OpenSearch description
|
|||
|
var searchQuery = location.hash.substring(3);
|
|||
|
history.pushState({searchQuery: searchQuery}, "Search for " + searchQuery, location.href.replace(/#q=.*/, ""));
|
|||
|
searchInput.value = searchQuery;
|
|||
|
document.addEventListener('CrystalDocs:loaded', performSearch);
|
|||
|
}
|
|||
|
|
|||
|
if (searchInput.value.length == 0) {
|
|||
|
var searchText = sessionStorage.getItem(repositoryName + '::search-input:value');
|
|||
|
if(searchText){
|
|||
|
searchInput.value = searchText;
|
|||
|
}
|
|||
|
}
|
|||
|
searchInput.addEventListener('keyup', performSearch);
|
|||
|
searchInput.addEventListener('input', performSearch);
|
|||
|
|
|||
|
var usageModal = new UsageModal('Keyboard Shortcuts', '' +
|
|||
|
'<ul class="usage-list">' +
|
|||
|
' <li>' +
|
|||
|
' <span class="usage-key">' +
|
|||
|
' <kbd>s</kbd>,' +
|
|||
|
' <kbd>/</kbd>' +
|
|||
|
' </span>' +
|
|||
|
' Search' +
|
|||
|
' </li>' +
|
|||
|
' <li>' +
|
|||
|
' <kbd class="usage-key">Esc</kbd>' +
|
|||
|
' Abort search / Close modal' +
|
|||
|
' </li>' +
|
|||
|
' <li>' +
|
|||
|
' <span class="usage-key">' +
|
|||
|
' <kbd>⇨</kbd>,' +
|
|||
|
' <kbd>Enter</kbd>' +
|
|||
|
' </span>' +
|
|||
|
' Open highlighted result' +
|
|||
|
' </li>' +
|
|||
|
' <li>' +
|
|||
|
' <span class="usage-key">' +
|
|||
|
' <kbd>⇧</kbd>,' +
|
|||
|
' <kbd>Ctrl+j</kbd>' +
|
|||
|
' </span>' +
|
|||
|
' Select previous result' +
|
|||
|
' </li>' +
|
|||
|
' <li>' +
|
|||
|
' <span class="usage-key">' +
|
|||
|
' <kbd>⇩</kbd>,' +
|
|||
|
' <kbd>Ctrl+k</kbd>' +
|
|||
|
' </span>' +
|
|||
|
' Select next result' +
|
|||
|
' </li>' +
|
|||
|
' <li>' +
|
|||
|
' <kbd class="usage-key">?</kbd>' +
|
|||
|
' Show usage info' +
|
|||
|
' </li>' +
|
|||
|
'</ul>'
|
|||
|
);
|
|||
|
|
|||
|
function handleShortkeys(event) {
|
|||
|
var element = event.target || event.srcElement;
|
|||
|
|
|||
|
if(element.tagName == "INPUT" || element.tagName == "TEXTAREA" || element.parentElement.tagName == "TEXTAREA"){
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
switch(event.key) {
|
|||
|
case "?":
|
|||
|
usageModal.show();
|
|||
|
break;
|
|||
|
|
|||
|
case "Escape":
|
|||
|
usageModal.hide();
|
|||
|
break;
|
|||
|
|
|||
|
case "s":
|
|||
|
case "/":
|
|||
|
if(usageModal.isVisible()) {
|
|||
|
return;
|
|||
|
}
|
|||
|
event.stopPropagation();
|
|||
|
navigator.focus();
|
|||
|
performSearch();
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
document.addEventListener('keyup', handleShortkeys);
|
|||
|
|
|||
|
var scrollToEntryFromLocationHash = function() {
|
|||
|
var hash = window.location.hash;
|
|||
|
if (hash) {
|
|||
|
var targetAnchor = decodeURI(hash.substr(1));
|
|||
|
var targetEl = document.getElementById(targetAnchor)
|
|||
|
if (targetEl) {
|
|||
|
targetEl.offsetParent.scrollTop = targetEl.offsetTop;
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
window.addEventListener("hashchange", scrollToEntryFromLocationHash, false);
|
|||
|
scrollToEntryFromLocationHash();
|
|||
|
});
|