diff --git a/javascript/_result.js b/javascript/_result.js index c90b4e6..4c71405 100644 --- a/javascript/_result.js +++ b/javascript/_result.js @@ -30,7 +30,9 @@ class AutocompleteResult { meta = null; hash = null; sortKey = null; + // uFuzzy specific highlightedText = null; + matchSource = null; // Constructor constructor(text, type) { diff --git a/javascript/_uFuzzy.js b/javascript/_uFuzzy.js index bfd8a83..ed14a06 100644 --- a/javascript/_uFuzzy.js +++ b/javascript/_uFuzzy.js @@ -47,13 +47,28 @@ class TacFuzzy { static #infoThresh = Infinity; // Make sure we are always getting info, performance isn't that bad for the default tag sets static #usePrefixCache = false; static #tacFuzzyOpts = { - intraIns: Infinity, + intraIns: 10, interIns: Infinity, intraChars: "[\\w\\-']", // Alphanumeric, hyphen, underscore & apostrophe + interChars: "[^\\s,|<>\\[\\]:]", // Everything except tag separators + interLft: 1, // loose + sort: (info, haystack, needle) => { return info["idx"].map((v, i) => i); } + } + static #tacFuzzyOptsUnicode = { + intraIns: 10, + interIns: Infinity, + unicode: true, + interSplit: "[^\\p{L}\\d']+", + intraSplit: "\\p{Ll}\\p{Lu}", + intraBound: "\\p{L}\\d|\\d\\p{L}|\\p{Ll}\\p{Lu}", + intraChars: "[\\p{L}\\d']", + interChars: "[^\\s,|<>\\[\\]:]", // Everything except tag separators + intraContr: "'\\p{L}{1,2}\\b", interLft: 1, // loose sort: (info, haystack, needle) => { return info["idx"].map((v, i) => i); } } static #u = new this.#uFuzzy(this.#tacFuzzyOpts); + static #uUnicode = new this.#uFuzzy(this.#tacFuzzyOptsUnicode); // Prefilter function to reduce search scope (from uFuzzy demo) static #prefixCache = []; static #prefilter = (haystack, needle) => { @@ -99,16 +114,19 @@ class TacFuzzy { * @param {String} needle - The search term (tagword) * @returns A list of uFuzzy search results */ - static search = (haystack, needle) => { + static search = (haystack, needle, unicode = false) => { let preFiltered = this.#usePrefixCache ? this.#prefilter(haystack, needle) : null; - let [idxs, info, order] = this.#u.search(haystack, needle, this.#oooPermute, this.#infoThresh, preFiltered); + let [idxs, info, order] = unicode + ? this.#uUnicode.search(haystack, needle, this.#oooPermute, this.#infoThresh, preFiltered) + : this.#u.search(haystack, needle, this.#oooPermute, this.#infoThresh, preFiltered); if (idxs != null) { if (info != null) { this.toStr = oi => { let hi = info.idx[oi]; - return this.#uFuzzy.highlight(haystack[hi], info.ranges[oi]); + let mark = (part, matched) => matched ? '' + part + '' : part; + return this.#uFuzzy.highlight(haystack[hi], info.ranges[oi], mark); }; return order.map(oi => [info.idx[oi], oi]) } diff --git a/javascript/tagAutocomplete.js b/javascript/tagAutocomplete.js index f72c76a..f3c0cda 100644 --- a/javascript/tagAutocomplete.js +++ b/javascript/tagAutocomplete.js @@ -13,6 +13,7 @@ "--live-translation-color-1": ["lightskyblue", "#2d89ef"], "--live-translation-color-2": ["palegoldenrod", "#eb5700"], "--live-translation-color-3": ["darkseagreen", "darkgreen"], + "--match-filter": ["brightness(1.2) drop-shadow(1px 1px 6px black)", "brightness(0.8)"] } const browserVars = { "--results-overflow-y": { @@ -90,6 +91,9 @@ const autocompleteCSS = ` content: "✨"; margin-right: 2px; } + .acMatchHighlight { + filter: var(--match-filter); + } .acWikiLink { padding: 0.5rem; margin: -0.5rem 0 -0.5rem -0.5rem; @@ -713,9 +717,27 @@ function addResultsToList(textArea, results, tagword, resetList) { displayText += `[${translations.get(result.text)}]`; // Print search term bolded in result - //itemText.innerHTML = displayText.replace(tagword, `${tagword}`); - itemText.innerHTML = result.highlightedText || displayText.replace(new RegExp(escapeRegExp(tagword), "ig"), "$&"); - //itemText.innerHTML = displayText.replace(/<mark>(.*)<\/mark>/g, "$1") + if (result.highlightedText) { + switch (result.matchSource) { + case "base": + itemText.innerHTML = result.highlightedText; + break; + case "alias": + let aliases = result.highlightedText.split(","); + let matchingAlias = aliases.find(a => a.includes("")); + itemText.innerHTML = matchingAlias + " ➝ " + result.text; + break; + case "translation": + console.error("Translation highlighting not implemented yet for aliases") + itemText.innerHTML = `${result.text}[${result.highlightedText}]` + break; + default: + itemText.innerHTML = displayText; + break; + } + } else { + itemText.innerHTML = displayText; + } const splitTypes = [ResultType.wildcardFile, ResultType.yamlWildcard] if (splitTypes.includes(result.type) && itemText.innerHTML.includes("/")) { @@ -1151,6 +1173,8 @@ async function autocomplete(textArea, prompt, fixedTag = null) { if (!fuzMatchSet.has(idx)) { const result = new AutocompleteResult(allTags[idx][0], ResultType.tag); result.highlightedText = TacFuzzy.toStr(orderIdx); + result.matchSource = "base" + result.category = allTags[idx][1]; result.count = allTags[idx][2]; result.aliases = allTags[idx][3]; @@ -1165,6 +1189,8 @@ async function autocomplete(textArea, prompt, fixedTag = null) { if (!fuzMatchSet.has(idx)) { const result = new AutocompleteResult(allTags[idx][0], ResultType.tag) result.highlightedText = TacFuzzy.toStr(orderIdx); + result.matchSource = "alias" + result.category = allTags[idx][1]; result.count = allTags[idx][2]; result.aliases = allTags[idx][3]; @@ -1179,6 +1205,8 @@ async function autocomplete(textArea, prompt, fixedTag = null) { if (!extraFuzMatchSet.has(idx)) { const result = new AutocompleteResult(extras[idx][0], ResultType.extra) result.highlightedText = TacFuzzy.toStr(orderIdx); + result.matchSource = "base" + result.category = extras[idx][1] || 0; // If no category is given, use 0 as the default result.meta = extras[idx][2] || "Custom tag"; result.aliases = extras[idx][3] || ""; @@ -1193,6 +1221,8 @@ async function autocomplete(textArea, prompt, fixedTag = null) { if (!extraFuzMatchSet.has(idx)) { const result = new AutocompleteResult(extras[idx][0], ResultType.extra) result.highlightedText = TacFuzzy.toStr(orderIdx); + result.matchSource = "alias" + result.category = extras[idx][1] || 0; // If no category is given, use 0 as the default result.meta = extras[idx][2] || "Custom tag"; result.aliases = extras[idx][3] || ""; @@ -1200,7 +1230,36 @@ async function autocomplete(textArea, prompt, fixedTag = null) { extraFuzMatchSet.add(idx); } }); - const transFuzResult = TacFuzzy.search([...translations.keys()].filter(x => !!x), tagword) + const transFuzResult = TacFuzzy.search([...translations.keys()].filter(x => !!x), tagword, true) // Unicode search here, slower but needed for non-latin translations + transFuzResult.forEach(pair => { + const idx = pair[0]; + const orderIdx = pair[1]; + if (!transFuzMatchSet.has(idx)) { + const translationKey = [...translations.keys()][idx]; + const tagForTranslation = allTags.find(t => t[0] === translationKey || t[3]?.split(",").some(a => a === translationKey)); + const extraForTranslation = extras.find(e => e[0] === translationKey || e[3]?.split(",").some(a => a === translationKey)); + + const result = new AutocompleteResult(tagForTranslation[0] || extraForTranslation[0], ResultType.tag) + result.highlightedText = TacFuzzy.toStr(orderIdx); + // TODO: translations can't differentiate between tag and alias here yet + result.matchSource = "translation"; + + result.category = tagForTranslation[1] || extraForTranslation[1] || 0; + + if (tagForTranslation) + result.count = tagForTranslation[2] || 0; + else if (extraForTranslation) + result.meta = extraForTranslation[2] || "Custom tag"; + + result.aliases = tagForTranslation[3] || extraForTranslation[3] || ""; + + if (tagForTranslation) { + tagOut.push(result); + } else if (extraForTranslation) { + extraOut.push(result); + } + } + }); // Append results for each set results = results.concat([...extraOut]).concat([...tagOut]);