367 lines
13 KiB
JavaScript
367 lines
13 KiB
JavaScript
|
window.onload = function() {
|
||
|
if (document.body.contains(document.goSearch)) {
|
||
|
document.goSearch.onsubmit = function() { return goSearchNow() };
|
||
|
|
||
|
/*
|
||
|
Source:
|
||
|
- https://github.com/nextapps-de/flexsearch#index-documents-field-search
|
||
|
- https://raw.githack.com/nextapps-de/flexsearch/master/demo/autocomplete.html
|
||
|
- http://elasticlunr.com/
|
||
|
- https://github.com/getzola/zola/blob/master/docs/static/search.js
|
||
|
- https://github.com/aaranxu/adidoks/blob/main/static/js/search.js
|
||
|
*/
|
||
|
(function(){
|
||
|
function inputFocus(e) {
|
||
|
|
||
|
if (e.keyCode === 191//forward slash
|
||
|
&& document.activeElement.tagName !== "INPUT"
|
||
|
&& document.activeElement.tagName !== "TEXTAREA") {
|
||
|
e.preventDefault();
|
||
|
searchinput.focus();
|
||
|
suggestions.classList.remove('d-none');
|
||
|
}
|
||
|
|
||
|
if (e.keyCode === 27 ) {//escape
|
||
|
searchinput.blur();
|
||
|
suggestions.classList.add('d-none');
|
||
|
closeAllLists();
|
||
|
}
|
||
|
|
||
|
const focusableSuggestions= suggestions.querySelectorAll('a');
|
||
|
if (suggestions.classList.contains('d-none')
|
||
|
|| focusableSuggestions.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
const focusable= [...focusableSuggestions];
|
||
|
const index = focusable.indexOf(document.activeElement);
|
||
|
|
||
|
let nextIndex = 0;
|
||
|
|
||
|
if (e.keyCode === 38) {//up arrow
|
||
|
e.preventDefault();
|
||
|
nextIndex= index > 0 ? index-1 : 0;
|
||
|
focusableSuggestions[nextIndex].focus();
|
||
|
}
|
||
|
else if (e.keyCode === 40) {//down arrow
|
||
|
e.preventDefault();
|
||
|
nextIndex= index+1 < focusable.length ? index+1 : index;
|
||
|
focusableSuggestions[nextIndex].focus();
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
var suggestions = document.getElementById("suggestions");
|
||
|
var searchinput = document.getElementById("searchinput");
|
||
|
document.addEventListener("keydown", inputFocus);
|
||
|
document.addEventListener("click", function(event) {suggestions.contains(event.target) || suggestions.classList.add("d-none")});
|
||
|
|
||
|
var lang = document.documentElement.getAttribute("lang");
|
||
|
var langOnly = lang.substring(0, 2);
|
||
|
var baseUrl = document.querySelector("meta[name='base']").getAttribute("content");
|
||
|
if (baseUrl.slice(-1) == "/") {
|
||
|
baseUrl = baseUrl.slice(0, -1);
|
||
|
}
|
||
|
|
||
|
var index;
|
||
|
searchinput.addEventListener('input', show_results, true);
|
||
|
suggestions.addEventListener('click', accept_suggestion, true);
|
||
|
|
||
|
|
||
|
// in page results when press enter or click search icon from search box
|
||
|
function closeSearchNow() {
|
||
|
const main = document.querySelector("main");
|
||
|
main.innerHTML = window.main
|
||
|
}
|
||
|
|
||
|
function goSearchNow() {
|
||
|
const main = document.querySelector("main");
|
||
|
if (!window.main) {
|
||
|
window.main = main.innerHTML
|
||
|
};
|
||
|
var results = document.getElementById("suggestions");// suggestions div generated by search box
|
||
|
|
||
|
var ResultsClone = results.cloneNode(true);// make a clone of the results, so that we can alter it
|
||
|
ResultsClone.id = "results";// alter the id of our clone, so that we can apply different css style
|
||
|
|
||
|
var headerDiv = document.createElement("div");// create a div element
|
||
|
|
||
|
var headerContent = '<form name="closeSearch"><h2><button type="submit" title="Close Search"><i class="svgs x"></i></button> <i class="svgs search"></i> '.concat(document.getElementById("searchinput").value, "</h2></form>");// header to use at top of results page
|
||
|
|
||
|
headerDiv.innerHTML = headerContent;// document element div (headerDiv), set the inner contents to our header html (headerContent)
|
||
|
|
||
|
ResultsClone.insertBefore(headerDiv, ResultsClone.firstChild);//insert our header div at the top of the page
|
||
|
|
||
|
main.innerHTML = ResultsClone.outerHTML;//display ResultsClone.outerHTML as the page
|
||
|
results.innerHTML = "";// clear the suggestions div popup
|
||
|
document.getElementById("searchinput").value = "";// clear the search input box
|
||
|
document.body.contains(document.closeSearch) && (document.closeSearch.onsubmit = function() { closeSearchNow() })
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
/* Close search suggestion popup list */
|
||
|
function closeAllLists(elmnt) {
|
||
|
var suggestions = document.getElementById("suggestions");
|
||
|
while (suggestions.firstChild) {
|
||
|
suggestions.removeChild(suggestions.firstChild);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
async function show_results() {
|
||
|
var initIndex = async function () {
|
||
|
if (index === undefined) {
|
||
|
index = fetch(baseUrl + '/search_index.' + langOnly + '.json')
|
||
|
.then(
|
||
|
async function(response) {
|
||
|
return await elasticlunr.Index.load(await response.json());
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
let res = await index;
|
||
|
return res;
|
||
|
}
|
||
|
var value = this.value.trim();
|
||
|
var options = {
|
||
|
bool: "OR",
|
||
|
fields: {
|
||
|
title: {boost: 2},
|
||
|
body: {boost: 1},
|
||
|
}
|
||
|
};
|
||
|
//var results = index.search(value, options);
|
||
|
var results = (await initIndex()).search(value, options);
|
||
|
|
||
|
var entry, childs = suggestions.childNodes;
|
||
|
var i = 0, len = results.length;
|
||
|
var items = value.split(/\s+/);
|
||
|
suggestions.classList.remove('d-none');
|
||
|
|
||
|
results.forEach(function(page) {
|
||
|
if (page.doc.body !== '') {
|
||
|
entry = document.createElement('div');
|
||
|
|
||
|
entry.innerHTML = '<a href><span></span><span></span></a>';
|
||
|
|
||
|
a = entry.querySelector('a'),
|
||
|
t = entry.querySelector('span:first-child'),
|
||
|
d = entry.querySelector('span:nth-child(2)');
|
||
|
a.href = page.ref;
|
||
|
t.textContent = page.doc.title;
|
||
|
d.innerHTML = makeTeaser(page.doc.body, items);
|
||
|
|
||
|
suggestions.appendChild(entry);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
while(childs.length > len){
|
||
|
suggestions.removeChild(childs[i])
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
function accept_suggestion(){
|
||
|
|
||
|
while(suggestions.lastChild){
|
||
|
|
||
|
suggestions.removeChild(suggestions.lastChild);
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
// Get the string bytes from binary
|
||
|
function getByteByBinary(binaryCode) {
|
||
|
// Binary system, starts with `0b` in ES6
|
||
|
// Octal number system, starts with `0` in ES5 and starts with `0o` in ES6
|
||
|
// Hexadecimal, starts with `0x` in both ES5 and ES6
|
||
|
var byteLengthDatas = [0, 1, 2, 3, 4];
|
||
|
var len = byteLengthDatas[Math.ceil(binaryCode.length / 8)];
|
||
|
return len;
|
||
|
}
|
||
|
|
||
|
// Get the string bytes from hexadecimal
|
||
|
function getByteByHex(hexCode) {
|
||
|
return getByteByBinary(parseInt(hexCode, 16).toString(2));
|
||
|
}
|
||
|
// Get substring by bytes
|
||
|
// If using JavaScript inline substring method, it will return error codes
|
||
|
// Source: https://www.52pojie.cn/thread-1059814-1-1.html
|
||
|
function substringByByte(str, maxLength) {
|
||
|
var result = "";
|
||
|
var flag = false;
|
||
|
var len = 0;
|
||
|
var length = 0;
|
||
|
var length2 = 0;
|
||
|
for (var i = 0; i < str.length; i++) {
|
||
|
var code = str.codePointAt(i).toString(16);
|
||
|
if (code.length > 4) {
|
||
|
i++;
|
||
|
if ((i + 1) < str.length) {
|
||
|
flag = str.codePointAt(i + 1).toString(16) == "200d";
|
||
|
}
|
||
|
}
|
||
|
if (flag) {
|
||
|
len += getByteByHex(code);
|
||
|
if (i == str.length - 1) {
|
||
|
length += len;
|
||
|
if (length <= maxLength) {
|
||
|
result += str.substr(length2, i - length2 + 1);
|
||
|
} else {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
if (len != 0) {
|
||
|
length += len;
|
||
|
length += getByteByHex(code);
|
||
|
if (length <= maxLength) {
|
||
|
result += str.substr(length2, i - length2 + 1);
|
||
|
length2 = i + 1;
|
||
|
} else {
|
||
|
break
|
||
|
}
|
||
|
len = 0;
|
||
|
continue;
|
||
|
}
|
||
|
length += getByteByHex(code);
|
||
|
if (length <= maxLength) {
|
||
|
if (code.length <= 4) {
|
||
|
result += str[i]
|
||
|
} else {
|
||
|
result += str[i - 1] + str[i]
|
||
|
}
|
||
|
length2 = i + 1;
|
||
|
} else {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
/* Taken from mdbook
|
||
|
// The strategy is as follows:
|
||
|
// First, assign a value to each word in the document:
|
||
|
// Words that correspond to search terms (stemmer aware): 40
|
||
|
// Normal words: 2
|
||
|
// First word in a sentence: 8
|
||
|
// Then use a sliding window with a constant number of words and count the
|
||
|
// sum of the values of the words within the window. Then use the window that got the
|
||
|
// maximum sum. If there are multiple maximas, then get the last one.
|
||
|
// Enclose the terms in <b>.
|
||
|
*/
|
||
|
function makeTeaser(body, terms) {
|
||
|
var TERM_WEIGHT = 40;
|
||
|
var NORMAL_WORD_WEIGHT = 2;
|
||
|
var FIRST_WORD_WEIGHT = 8;
|
||
|
var TEASER_MAX_WORDS = 30;
|
||
|
|
||
|
var stemmedTerms = terms.map(function (w) {
|
||
|
return elasticlunr.stemmer(w.toLowerCase());
|
||
|
});
|
||
|
var termFound = false;
|
||
|
var index = 0;
|
||
|
var weighted = []; // contains elements of ["word", weight, index_in_document]
|
||
|
|
||
|
// split in sentences, then words
|
||
|
var sentences = body.toLowerCase().split(". ");
|
||
|
for (var i in sentences) {
|
||
|
var words = sentences[i].split(/[\s\n]/);
|
||
|
var value = FIRST_WORD_WEIGHT;
|
||
|
for (var j in words) {
|
||
|
|
||
|
var word = words[j];
|
||
|
|
||
|
if (word.length > 0) {
|
||
|
for (var k in stemmedTerms) {
|
||
|
if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {
|
||
|
value = TERM_WEIGHT;
|
||
|
termFound = true;
|
||
|
}
|
||
|
}
|
||
|
weighted.push([word, value, index]);
|
||
|
value = NORMAL_WORD_WEIGHT;
|
||
|
}
|
||
|
|
||
|
index += word.length;
|
||
|
index += 1; // ' ' or '.' if last word in sentence
|
||
|
}
|
||
|
|
||
|
index += 1; // because we split at a two-char boundary '. '
|
||
|
}
|
||
|
|
||
|
if (weighted.length === 0) {
|
||
|
if (body.length !== undefined && body.length > TEASER_MAX_WORDS * 10) {
|
||
|
return body.substring(0, TEASER_MAX_WORDS * 10) + '...';
|
||
|
} else {
|
||
|
return body;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var windowWeights = [];
|
||
|
var windowSize = Math.min(weighted.length, TEASER_MAX_WORDS);
|
||
|
// We add a window with all the weights first
|
||
|
var curSum = 0;
|
||
|
for (var i = 0; i < windowSize; i++) {
|
||
|
curSum += weighted[i][1];
|
||
|
}
|
||
|
windowWeights.push(curSum);
|
||
|
|
||
|
for (var i = 0; i < weighted.length - windowSize; i++) {
|
||
|
curSum -= weighted[i][1];
|
||
|
curSum += weighted[i + windowSize][1];
|
||
|
windowWeights.push(curSum);
|
||
|
}
|
||
|
|
||
|
// If we didn't find the term, just pick the first window
|
||
|
var maxSumIndex = 0;
|
||
|
if (termFound) {
|
||
|
var maxFound = 0;
|
||
|
// backwards
|
||
|
for (var i = windowWeights.length - 1; i >= 0; i--) {
|
||
|
if (windowWeights[i] > maxFound) {
|
||
|
maxFound = windowWeights[i];
|
||
|
maxSumIndex = i;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var teaser = [];
|
||
|
var startIndex = weighted[maxSumIndex][2];
|
||
|
for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {
|
||
|
var word = weighted[i];
|
||
|
if (startIndex < word[2]) {
|
||
|
// missing text from index to start of `word`
|
||
|
teaser.push(body.substring(startIndex, word[2]));
|
||
|
startIndex = word[2];
|
||
|
}
|
||
|
|
||
|
// add <em/> around search terms
|
||
|
if (word[1] === TERM_WEIGHT) {
|
||
|
teaser.push("<b>");
|
||
|
}
|
||
|
|
||
|
startIndex = word[2] + word[0].length;
|
||
|
// Check the string is ascii characters or not
|
||
|
var re = /^[\x00-\xff]+$/
|
||
|
if (word[1] !== TERM_WEIGHT && word[0].length >= 12 && !re.test(word[0])) {
|
||
|
// If the string's length is too long, it maybe a Chinese/Japance/Korean article
|
||
|
// if using substring method directly, it may occur error codes on emoji chars
|
||
|
var strBefor = body.substring(word[2], startIndex);
|
||
|
var strAfter = substringByByte(strBefor, 12);
|
||
|
teaser.push(strAfter);
|
||
|
} else {
|
||
|
teaser.push(body.substring(word[2], startIndex));
|
||
|
}
|
||
|
|
||
|
if (word[1] === TERM_WEIGHT) {
|
||
|
teaser.push("</b>");
|
||
|
}
|
||
|
}
|
||
|
teaser.push("…");
|
||
|
return teaser.join("");
|
||
|
}
|
||
|
document.goSearch.onsubmit = function() { return goSearchNow() };
|
||
|
}());
|
||
|
}
|
||
|
};
|