viewgit/inc/functions.php:22 Function utf8_encode() is deprecated [8192]
/** * SeekQuarry/Yioop -- * Open Source Pure PHP Search Engine, Crawler, and Indexer * * Copyright (C) 2009 - 2023 Chris Pollett chris@pollett.org * * LICENSE: * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * * END LICENSE * * @author Sandhya Vissapragada, Chris Pollett * @license https://www.gnu.org/licenses/ GPL3 * @link https://www.seekquarry.com/ * @copyright 2009 - 2023s * @filesource */ /* * Update the version number manually when ever * suggest.js undergoes changes */ SUGGEST_VERSION_NO = 0; /* * Constants for key codes will handle */ KeyCodes = new Object(); KeyCodes.UP_ARROW = 38; KeyCodes.DOWN_ARROW = 40; /* * Maximum number of search terms to display */ MAX_DISPLAY = 6; /* * Maximum number of characters in query to do spellsheck for */ MIN_SPELL_CHECK_WIDTH = 40; /* * Height of a search term in pixels */ FONT_HEIGHT = 24; /* * Used to delimit the end of a term in a trie. * The value below is the default define. Might be set * what the trie object loaded says in loadTrie */ END_OF_TERM_MARKER = " "; /* * Process to follow once onsubmit event is fired * * @param None * @return None */ corrected_query = ""; function processSubmit() { updateLocalStorage(); } /* * */ function resetSuggest() { let suggest_dropdown = elt("suggest-dropdown"); suggest_dropdown.className = ""; suggest_dropdown.style.height = "0"; suggest_dropdown.style.visibility = "hidden"; elt('query-field').value = ""; document.activeElement.blur(); return false; } /* * Steps to follow every time a key is up from the user end * Handles up/dowm arrow keys * * @param Event event current event * @return String text_field Current value from the search box * */ function onTypeTerm(event, text_field) { let key_code_pressed; let term_array; let input_term = text_field.value.trim(); let suggest_results = elt("suggest-results"); let suggest_dropdown = elt("suggest-dropdown"); let query = elt("query-field").value; let scroll_pos = 0; let tmp_pos = 0; let local_count = 0; locale_terms = new Object(); local_terms_present = false; local_suggest = true; search_list_array = new Object(); scroll_horz = false; out_query = false; if (typeof transliterate == 'function') { out_query = transliterate(query); } if (out_query && out_query.length > 0) { input_term = out_query; } //To find the length of an associative array Object.size = function(obj) { let size = 0, key; for (key in obj) { if (obj.hasOwnProperty(key)) size++; } return size; }; concat_term = ""; if (window.event) { // IE8 and earlier key_code_pressed = event.keyCode; } else if (event.which) { // IE9/Firefox/Chrome/Opera/Safari key_code_pressed = event.which; } term_array = input_term.split(" "); concat_array = input_term.split(" ", term_array.length - 1); if (input_term != "") { for (let i = 0; i < concat_array.length; i++) { concat_term += concat_array[i] + " "; } concat_term = concat_term.trim(); } input_term = term_array[term_array.length - 1].trim(" "); // behavior if typing keys other than up or down (notice call termSuggest) if (key_code_pressed != KeyCodes.DOWN_ARROW && key_code_pressed != KeyCodes.UP_ARROW) { search_list = ""; // First search the local storage to fetch the suggestions if (typeof localStorage != 'undefined') { let locale_ver = locale+'_' + SUGGEST_VERSION_NO; if (localStorage[locale_ver] == null) { localStorage.clear(); count = 0; } else if (localStorage[locale_ver] != null) { split_str = localStorage[locale_ver].split("@@"); locale_terms = JSON.parse(split_str[1]); local_dict =JSON.parse(localStorage[locale_ver].split("@@", 1)); if (local_dict != null) { local_terms_present = true; termSuggest(local_dict, input_term); local_terms_present = false; } let sorted_local = sortLocalTerms(); if (Object.size(search_list_array) > 0) { search_list = ""; for (let i = 0; i < sorted_local.length ; i++) { let split_array = sorted_local[i].split('*'); if (search_list_array[split_array[1]] != null) { search_split = search_list_array[split_array[1]].split("_"); search_list += "<li><span id='term" +local_count+ "' class='unselected' onclick='" + "setSelectedTerm(\"" + local_count + "\", \"selected\", true)' " + "title='" + search_split[0] + "' " + "onmouseover='setSelectedTerm(\"" + local_count + "\",\"selected\")'" + "onmouseout='setSelectedTerm(\""+ local_count+"\",\"unselected\")'" + "ondblclick='termDblClick(\"" + search_split[0]+ "\",this.id)' >" + search_split[1] + "</span></li>"; local_count++; } } } local_suggest = false; } } count = local_count; // Now search the actual dictionary trie termSuggest(dictionary, input_term); // insert nbsp of the number of suggestions are less than MAX_DISPLAY short_max = MAX_DISPLAY - count; for (let i = 0; i < short_max; i++) { search_list += "<li><span class='unselected'> </span></li>"; } if (count < 1) { search_list = ""; } suggest_dropdown.scrollTop = 0; suggest_results.innerHTML = search_list; //sort the list let count_tmp=0; Array.from(suggest_results.getElementsByTagName("LI")) .sort((a, b) => b.getAttribute("data-frequency") -a.getAttribute("data-frequency")) .forEach(li => { li.firstChild.id=("term"+count_tmp); count_tmp+=1; suggest_results.appendChild(li); }); cursor_pos = -1; num_items = count; if (num_items == 0 || search_list == "") { suggest_dropdown.className = ""; suggest_dropdown.style.height = "0"; suggest_dropdown.style.visibility = "hidden"; suggest_results.style.visibility = "hidden"; } else { suggest_dropdown.className = "dropdown"; suggest_results.style.visibility = "visible"; suggest_dropdown.style.visibility = "visible"; suggest_dropdown.style.height = (FONT_HEIGHT * MAX_DISPLAY) + "px"; if (scroll_horz) { suggest_dropdown.style.overflowX = "scroll"; } else { suggest_dropdown.style.overflowX = "hidden"; } } } // behavior on up down arrows if (suggest_results.style.visibility == "visible") { if (key_code_pressed == KeyCodes.DOWN_ARROW) { if (cursor_pos < 0) { cursor_pos = 0; setSelectedTerm(cursor_pos, "selected", true); } else { if (cursor_pos < num_items - 1) { setSelectedTerm(cursor_pos, "unselected"); cursor_pos++; } setSelectedTerm(cursor_pos, "selected", true); } scroll_count = 1; scroll_pos = (cursor_pos - MAX_DISPLAY >= 0) ? (cursor_pos - MAX_DISPLAY + 1) : 0; suggest_dropdown.scrollTop = scroll_pos * FONT_HEIGHT; } else if (key_code_pressed == KeyCodes.UP_ARROW) { if (cursor_pos < 0) { cursor_pos = 0; setSelectedTerm(cursor_pos, "selected", true); } else { if (cursor_pos > 0) { setSelectedTerm(cursor_pos, "unselected"); cursor_pos--; } setSelectedTerm(cursor_pos, "selected", true); } scroll_pos = (cursor_pos - MAX_DISPLAY + scroll_count >= 0) ? (cursor_pos - MAX_DISPLAY + scroll_count) : 0; scroll_count = (MAX_DISPLAY > scroll_count) ? scroll_count + 1: MAX_DISPLAY; suggest_dropdown.scrollTop = scroll_pos * FONT_HEIGHT; } } } /* * To correct the spelling of the query words * * @param String word Input word * @return String corrected_word Corrected word */ function correctSpelling(word) { let prob = 0; let correct_threshold = 25; let trie_subtree = exist(dictionary, word); if (trie_subtree != false) { prob = parseInt(trie_subtree[END_OF_TERM_MARKER]); if (prob >= correct_threshold) { return word; } } let curr_prob = 0; let candidates = known(edits1(word)); candidates.push(word); let corrected_word = ""; // Use the frequencies to get the best match for (let i = 0; i < candidates.length; i++) { trie_subtree = exist(dictionary, candidates[i]); if (trie_subtree != false) { curr_prob = parseInt(trie_subtree[END_OF_TERM_MARKER]); } if (curr_prob > correct_threshold * prob) { correct_threshold = 1; prob = curr_prob; corrected_word = candidates[i]; } } return corrected_word; } /* * Gets the candidates for the spell correction with edit * distance 1 * * @param String word Input word * @return Array set Words with edit distance - 1 */ function edits1(word) { let splits = new Object(); let deletes = new Array(); let transposes =new Array(); let replaces = new Array(); let inserts = new Array(); let j = 0; splits[""] = word; for (let i = 0; i < word.length; i++) { splits[word.substring(0, i + 1)] = word.substring(i+1, word.length); } // Deletes for (key in splits) { if (splits[key] != "") { deletes[j] = key + splits[key].substring(1); j++; } } // Transposes j = 0; for (key in splits) { if (splits[key].length > 1) { transposes[j] = key + splits[key].substring(1, 2) + splits[key].substring(0,1) + splits[key].substring(2); j++; } } // Replaces j = 0; for (key in splits) { if (splits[key] != "") { for (let i = 0;i < alpha.length; i++) { replaces[j] = key + alpha.substring(i,i+1) + splits[key].substring(1); j++; } } } // Inserts j = 0; for (key in splits) { for (let i=0; i < alpha.length; i++) { inserts[j] = key + alpha.substring(i, i + 1) + splits[key]; j++; } } let set = deletes.concat(transposes).concat(replaces).concat(inserts).unique(); return set; } Array.prototype.unique = function() { let a = this.concat(); for (let i = 0; i<a.length; ++i) { for (let j=i+1; j < a.length; ++j) { if (a[i] === a[j]) a.splice(j, 1); } } return a; } /* * To get the set of words which are known from the dictionary * * @param Array words_ip array of words * @return Array known_words array of known words */ function known(words_ip) { let known_words = new Array(); let j=0; let ret_array; for (let i = 0; i < words_ip.length; i++) { ret_array = exist(dictionary, words_ip[i]); if (ret_array[END_OF_TERM_MARKER] != null) { known_words[j] = words_ip[i]; j++; } } return known_words; } /* * To update the local storage with the previous query terms and * create a trie on those terms * */ function updateLocalStorage() { let trie_to_store; trie_storage = {}; let store_term = elt("query-field").value; let freq, k = 0; let sorted_locale_terms = new Array(); if (localStorage) { if (locale_terms && locale_terms[store_term] == null) { locale_terms[store_term] = 1; } else { freq = parseInt(locale_terms[store_term]); freq++; locale_terms[store_term] = freq; } for (let key in locale_terms) { sorted_locale_terms[k] = key; k++; } sorted_locale_terms.sort(); // Build the trie for (let i=0; i<sorted_locale_terms.length; i++) { let trie_word = sorted_locale_terms[i]; let letters = trie_word.split(""); let cur = trie_storage; for (let j=0; j < letters.length; j++) { let letter = encode(letters[j]); let pos = cur[ letter ]; if (pos == null) { if (j === letters.length - 1) { cur = cur[ letter ] = {'$' : '$'}; } else { cur = cur[ letter ] = {}; } } else if (pos === 0) { cur = cur[ letter ] = { '$' : '$' }; } else { cur = cur[ letter ]; } } } } trie_to_store = JSON.stringify(trie_storage); localStorage.setItem(locale + '_' + SUGGEST_VERSION_NO, trie_to_store + "@@" + JSON.stringify(locale_terms)); } /* * Sort the local storage words based of number of times they are queried * * @return Array local storage words */ function sortLocalTerms() { let local_storage_array = new Array(); if (Object.size(locale_terms) > 0) { let j = 0; for (let key in locale_terms) { local_storage_array[j] = locale_terms[key] + "*"+ key; j++; } } local_storage_array.sort(termFrequencyComparison); local_storage_array.reverse(); return local_storage_array; } /* * Callback used by a sort call in sortLocalTerms to compare two * string where before the * in the string is a term and after is a frequency * * @param String a in format described above * @param String b in format described above * @return number 0 - if same frequency, negative if b has larger frequency, * positive otherwise */ function termFrequencyComparison(a, b) { let split_array1 = a.split('*'); let split_array2 = b.split('*'); let val1 = parseInt(split_array1[0]); let val2 = parseInt(split_array2[0]); return (val1 - val2); } /* * To select an suggest value while up/down arrow keys are being used * and place in the search box * * @param int pos index in the list items of suggest terms * @param String class_value value for CSS class attribute for that list item */ function setSelectedTerm(pos, class_value, title_flag) { let query_field_object = elt("query-field"); if (typeof title_flag != 'undefined') { query_field_object.value = elt("term" + pos).title; } elt("term" + pos).className = class_value; } /* * To selects a term from the suggest dropdownlist and performs as search * * @param String term locwhat was clicked on */ function termDblClick(term, termid) { let results_dropdown = elt("suggest-results"); let query_field_object = elt("query-field"); query_field_object.value = term; results_dropdown.innerHTML = ""; elt("suggest-dropdown").style.display = "none"; elt("search-form").submit(); } /* * Fetch words from the Trie and add to seachList with <li> </li> tags * * @param Array trie_array contains all search terms * @param String parent_word the prefix want to find sub-term for in trie * @param String highlighted_word parent_word, root_word + "<b>" + rest of * parent */ function getTrieTerms(trie_array, parent_word, highlighted_word) { let search_terms; let highlighted_terms; if (trie_array != null) { for (key in trie_array) { if (key != END_OF_TERM_MARKER ) { getTrieTerms(trie_array[key], parent_word + key, highlighted_word + key); } else { if ( (locale_terms[decode(parent_word)] == null && local_terms_present == false)) { search_terms = concat_term.trim() + " " + decode(parent_word); search_terms = search_terms.trim(); highlighted_terms = concat_term.trim() + " " + decode(highlighted_word) + "</b>"; search_list += "<li data-frequency="+trie_array[key]+ "><span class='unselected' onclick='setSelectedTerm(" + "this.id.substr(4), \"selected\", true)' " + "title='" + search_terms + "' " + "onmouseover='setSelectedTerm(this.id.substr(4),"+ "\"selected\")'" + "onmouseout='setSelectedTerm(this.id.substr(4),"+ "\"unselected\")'" + "ondblclick='termDblClick(\""+search_terms +"\",this.id)'"+ ">" + highlighted_terms + "</span></li>"; count++; //handle long suggests phrases with horizontal scrollbar if (search_terms.length * 24 > 1200 && !scroll_horz) scroll_horz = true; } else if (local_terms_present == true) { search_terms = concat_term.trim() + " " + decode(parent_word); search_terms = search_terms.trim(); highlighted_terms = concat_term.trim() + " " + decode(highlighted_word) + "</b>"; search_list_array[decode(parent_word)] = search_terms + "_" +highlighted_terms; //handle long suggests phrases with horizontal scrollbar if (search_terms.length * 24 > 1200 && !scroll_horz) scroll_horz = true; } } } } } /* * Returns the sub trie_array under term in * trie_array. If term does not exist in the trie_array * returns false * * @param String term what to look up * @return Array trie_array sub trie_array under term */ function exist(trie_array, term) { if (trie_array == null) { return false; } for (let i = 0; i < term.length; i++) { tmp = getUnicodeCharAndNextOffset(term, i); if (tmp == false) return false; next_char = tmp[0]; i = tmp[1]; enc_char = encode(next_char); trie_array = trie_array[enc_char]; if (trie_array == null) { return false; } } return trie_array; } /* * Entry point to find word completions/suggestions. Finds the portion of * trie_array beneath term. Then using this subtrie get the first six entries. * Six is specified in get values. * * @param Array trie_array - a nested array represent a trie * @param String term - what to look up suggestions for * @sideeffect global Array search_list has list of first six entries */ function termSuggest(trie_array, term) { last_word = false; if (local_suggest == true) { count = 0; search_list = ""; } // For US english ignore the case if (locale == 'en-US') { term = term.toLowerCase(); } let tmp; if (trie_array == null) { return false; } if ((term.length) > 0) { trie_array = exist(trie_array, term); } else { return false; } getTrieTerms(trie_array, term, term + "<b>"); } /* wrappers to save typing */ function decode(str) { str = str.replace(/\+/g, '%20'); return decodeURIComponent(str); } /* wrappers to save typing */ function encode(str) { str = encodeURIComponent(str); str = str.replace(/\'/g, '%27'); // encodeURIComponent doesn't convert return str; } /* * Extract next Unicode Char beginning at offset i in str returns Array * with this character and the next offset * * This is based on code found at: * https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects * /String/charAt * * @param String str what to get the next character out of * @param int i current offset into str * @return Array pair of Unicode character beginning at i and the next offset */ function getUnicodeCharAndNextOffset(str, i) { let code = str.charCodeAt(i); if (isNaN(code)) { return ''; } if (code < 0xD800 || code > 0xDFFF) { return [str.charAt(i), i]; } if (0xD800 <= code && code <= 0xDBFF) { if (str.length <= i + 1) { return false; } let next = str.charCodeAt(i + 1); if (0xDC00 > next || next > 0xDFFF) { return false; } return [str.charAt(i) + str.charAt(i + 1), i + 1]; } if (i === 0) { return false; } let prev = str.charCodeAt(i-1); if (0xD800 > prev || prev > 0xDBFF) { return false; } return [str.charAt(i + 1), i + 1]; } /* * Load the Trie during the launch of website * Trie's are represented using nested arrays. */ function loadFiles() { let request = makeRequest(); let trie_loc = ""; if (request) { request.onreadystatechange = function() { if (request.readyState == 4 && request.status == 200 && request.responseText != "") { trie = JSON.parse(request.responseText); dictionary = trie["trie_array"]; END_OF_TERM_MARKER = trie["end_marker"]; if (typeof alpha != 'undefined') spellCheck(); } END_OF_TERM_MARKER = (typeof END_OF_TERM_MARKER == 'undefined') ? ' ' : END_OF_TERM_MARKER; } locale = document.documentElement.lang; if (locale) { let redirects = document.getElementsByTagName( "html")[0].getAttribute("data-redirect"); let base_url = document.getElementsByTagName( "html")[0].getAttribute("data-base"); if (redirects && base_url) { trie_loc = base_url + "wd/suggest/" + locale; } else { trie_loc = (base_url) ? base_url : "./"; trie_loc += "?c=resource&a=suggest&locale=" + locale; } request.open("GET", trie_loc, true); request.send(); } } } /* * To process spell correction */ function spellCheck() { let reference_node; let spell_link; let csrf_token; if (document.getElementsByClassName) { reference_node = document.getElementsByClassName("search-results")[0]; } if (reference_node) { let corrected_spell = elt("spell-check"); /* corrected_spell might not be present if WORD_SUGGEST off or on a trending or chart page */ if (!corrected_spell) { return; } let logged_in = (elt("csrf-token") !== null); if (logged_in) { csrf_token = elt("csrf-token").value; } let its_value = elt("its-value").value; let query = elt("query-field").value; if (query == "") { return; } let ret_array; let ret_word; if (typeof localStorage != 'undefined') { let locale_ver = locale + '_' + SUGGEST_VERSION_NO; } if (typeof locale_ver != 'undefined' && localStorage[locale_ver] != null) { split_str = localStorage[locale_ver].split("@@"); locale_terms = JSON.parse(split_str[1]); if (locale_terms[query] > 5) { return; // search for a lot so don't suggest } } // avoid suggesting if query contains meta words if (query.indexOf(":") !== -1) { return; } let term_array = query.split(" "); let num_terms = term_array.length; for (let i = 0; i < num_terms; i++) { ret_word = ""; ret_word = correctSpelling(term_array[i].toLowerCase()); if (ret_word.trim(" ") == "") { corrected_query += term_array[i] + " "; } else { corrected_query += ret_word + " "; } } if (query.length > MIN_SPELL_CHECK_WIDTH) { return; } if (corrected_query.trim() != query.toLowerCase().trim()) { if (logged_in) { let token_name = csrf_name; spell_link = "?" + token_name + "=" + csrf_token + "&q=" + corrected_query; } else { spell_link = "?q=" + corrected_query; } setDisplay('spell-check', true); corrected_spell.innerHTML = "<b>" + local_strings.spell +": <a rel='nofollow' href='" + spell_link + "'>" + corrected_query + "</a></b>"; } } } tag("body")[0].onload = loadFiles; let ip_field = elt("query-field"); if (ip_field != null) { ip_field.onpaste = function(e) { setTimeout(function(){ onTypeTerm(e,ip_field); }, 0); } ip_field.oncut = function(e) { setTimeout(function(){ onTypeTerm(e,ip_field); }, 0); } }