mods.shadowfacts.net/js/main.js

219 lines
6.8 KiB
JavaScript

let searchData;
function getSearchData() {
if (searchData) {
return new Promise((resolve, reject) => {
resolve(searchData);
});
} else {
return fetch("/search.json")
.then(res => res.json());
}
}
function matcher(q, cb) {
getSearchData()
.then(data => {
cb(Object.keys(data)
.map(fuzzy_match.bind(null, q))
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(res => {
const formatted = res[3];
const url = data[res[2]];
return `<a href="${url}">${formatted}</a>`;
}));
})
}
$(document).ready(() => {
$("[data-toggle='tooltip']").tooltip();
$("#search").typeahead({
hint: true,
highlight: true,
minLength: 1
}, {
name: "search",
source: matcher
});
// let dropdownShown = false;
// $("#search-results-trigger").on("shown.bs.dropdown", () => {
// console.log("shown");
// dropdownShown = true;
// });
// $("#search-results-trigger").on("hidden.bs.dropdown", () => {
// console.log("hidden");
// dropdownShown = false;
// });
// $("#search").on("input", () => {
// const results = $("#search-results");
// const q = $("#search").val().trim();
// if (q.length == 0) {
// if (dropdownShown) {
// $("#search-results-trigger").dropdown("toggle");
// $("#search").focus();
// }
// // results.hide();
// return;
// } else {
// if (!dropdownShown) {
// $("#search-results-trigger").dropdown("toggle");
// $("#search").focus();
// }
// // results.show();
// }
// getSearchData()
// .then(data => {
// results.html(Object.keys(data)
// .map(fuzzy_match.bind(null, q))
// .sort((a, b) => b[1] - a[1])
// .slice(0, 5)
// .map(res => {
// const formatted = res[3];
// const url = data[res[2]];
// return `<a href="${url}" class="dropdown-item">${formatted}</a>`;
// })
// .join(""));
// });
// });
});
// Source: https://github.com/forrestthewoods/lib_fts
// Returns [bool, score, str, formattedStr]
// bool: true if each character in pattern is found sequentially within str
// score: integer; higher is better match. Value has no intrinsic meaning. Range varies with pattern.
// Can only compare scores with same search pattern.
// formattedStr: input str with matched characters marked in <b> tags. Delete if unwanted.
function fuzzy_match(pattern, str) {
// Score consts
var adjacency_bonus = 5; // bonus for adjacent matches
var separator_bonus = 10; // bonus if match occurs after a separator
var camel_bonus = 10; // bonus if match is uppercase and prev is lower
var leading_letter_penalty = -3; // penalty applied for every letter in str before the first match
var max_leading_letter_penalty = -9; // maximum penalty for leading letters
var unmatched_letter_penalty = -1; // penalty for every letter that doesn't matter
// Loop variables
var score = 0;
var patternIdx = 0;
var patternLength = pattern.length;
var strIdx = 0;
var strLength = str.length;
var prevMatched = false;
var prevLower = false;
var prevSeparator = true; // true so if first letter match gets separator bonus
// Use "best" matched letter if multiple string letters match the pattern
var bestLetter = null;
var bestLower = null;
var bestLetterIdx = null;
var bestLetterScore = 0;
var matchedIndices = [];
// Loop over strings
while (strIdx != strLength) {
var patternChar = patternIdx != patternLength ? pattern.charAt(patternIdx) : null;
var strChar = str.charAt(strIdx);
var patternLower = patternChar != null ? patternChar.toLowerCase() : null;
var strLower = strChar.toLowerCase();
var strUpper = strChar.toUpperCase();
var nextMatch = patternChar && patternLower == strLower;
var rematch = bestLetter && bestLower == strLower;
var advanced = nextMatch && bestLetter;
var patternRepeat = bestLetter && patternChar && bestLower == patternLower;
if (advanced || patternRepeat) {
score += bestLetterScore;
matchedIndices.push(bestLetterIdx);
bestLetter = null;
bestLower = null;
bestLetterIdx = null;
bestLetterScore = 0;
}
if (nextMatch || rematch) {
var newScore = 0;
// Apply penalty for each letter before the first pattern match
// Note: std::max because penalties are negative values. So max is smallest penalty.
if (patternIdx == 0) {
var penalty = Math.max(strIdx * leading_letter_penalty, max_leading_letter_penalty);
score += penalty;
}
// Apply bonus for consecutive bonuses
if (prevMatched)
newScore += adjacency_bonus;
// Apply bonus for matches after a separator
if (prevSeparator)
newScore += separator_bonus;
// Apply bonus across camel case boundaries. Includes "clever" isLetter check.
if (prevLower && strChar == strUpper && strLower != strUpper)
newScore += camel_bonus;
// Update patter index IFF the next pattern letter was matched
if (nextMatch)
++patternIdx;
// Update best letter in str which may be for a "next" letter or a "rematch"
if (newScore >= bestLetterScore) {
// Apply penalty for now skipped letter
if (bestLetter != null)
score += unmatched_letter_penalty;
bestLetter = strChar;
bestLower = bestLetter.toLowerCase();
bestLetterIdx = strIdx;
bestLetterScore = newScore;
}
prevMatched = true;
}
else {
// Append unmatch characters
formattedStr += strChar;
score += unmatched_letter_penalty;
prevMatched = false;
}
// Includes "clever" isLetter check.
prevLower = strChar == strLower && strLower != strUpper;
prevSeparator = strChar == '_' || strChar == ' ';
++strIdx;
}
// Apply score for last match
if (bestLetter) {
score += bestLetterScore;
matchedIndices.push(bestLetterIdx);
}
// Finish out formatted string after last pattern matched
// Build formated string based on matched letters
var formattedStr = "";
var lastIdx = 0;
for (var i = 0; i < matchedIndices.length; ++i) {
var idx = matchedIndices[i];
formattedStr += str.substr(lastIdx, idx - lastIdx) + "<b>" + str.charAt(idx) + "</b>";
lastIdx = idx + 1;
}
formattedStr += str.substr(lastIdx, str.length - lastIdx);
var matched = patternIdx == patternLength;
return [matched, score, str, formattedStr];
}