SeekquarrySeekquarry - Yioop Repo - Seekquarry

/ src / scripts / suggest.js⤓
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  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 - 2026s
 * @filesource
 */
/*
 * SUGGEST_VERSION_NO is part of the name the history of queries is kept
 * under in localStorage. Changing it when the kept format changes makes
 * each browser start a new history rather than read the old one.
 */
SUGGEST_VERSION_NO = 0;
/*
 * KeyCodes holds the codes of the arrow keys that move through the list
 * of suggestions
 */
KeyCodes = new Object();
KeyCodes.UP_ARROW = 38;
KeyCodes.DOWN_ARROW = 40;
/*
 * MAX_DISPLAY is how many suggestions the dropdown lists, and so how many
 * rows tall it is
 */
MAX_DISPLAY = 6;
/*
 * MIN_SPELL_CHECK_WIDTH is the longest query, in characters, that
 * spellCheck offers a correction for
 */
MIN_SPELL_CHECK_WIDTH = 40;
/*
 * MIN_CORRECTED_LENGTH is the fewest characters a word of a query must have
 * for correctSpelling to change it. Nearly every string of one or two
 * letters is one edit away from some dictionary word, so a shorter word is
 * left as typed.
 */
MIN_CORRECTED_LENGTH = 3;
/*
 * FONT_HEIGHT is the height in pixels of one row of the dropdown
 */
FONT_HEIGHT = 24;
/*
 * END_OF_TERM_MARKER is the key that ends a word in a trie (a tree with
 * one letter on each branch). loadFiles sets it to the marker the loaded
 * trie names.
 */
END_OF_TERM_MARKER = " ";
corrected_query = "";
/**
 * processSubmit is called when the search form is sent. It adds the query
 * to the history of queries kept in localStorage, which later suggestions
 * list first.
 */
function processSubmit()
{
    updateLocalStorage();
}
/**
 * resetSuggest hides the dropdown of suggestions, empties the query field
 * and takes the focus off the page element that has it, so an on-screen
 * keyboard closes. The search form calls it when it is reset.
 *
 * @return {boolean} false, so the form's own reset does not also run
 */
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;
}
/**
 * onTypeTerm is called when a key is let go in the query field. For an
 * arrow key it moves the selected suggestion up or down. For any other key
 * it lists suggestions for the last word typed: matching queries from the
 * history kept in localStorage first, then the most frequent completions
 * from the dictionary trie, and shows the dropdown under the search box.
 *
 * @param Event event the key event
 * @param Element text_field the query field
 */
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;
            }
        }
        if (local_suggest) {
            search_list = "";
        }
        count = local_count;
        // Now search the actual dictionary trie
        found_terms = [];
        termSuggest(dictionary, input_term);
        /* The dictionary keeps names such as England capitalized, so they
           are looked for under the capitalized prefix too */
        if (locale == 'en-US' && input_term.length > 0) {
            termSuggest(dictionary, input_term.charAt(0).toUpperCase() +
                input_term.slice(1).toLowerCase(), false);
        }
        search_list += completionsList(MAX_DISPLAY - count);
        // 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'>&nbsp;</span></li>";
        }
        if (count < 1) {
            search_list = "";
        }
        suggest_dropdown.scrollTop =  0;
        suggest_results.innerHTML = search_list;
        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";
            placeDropdown(suggest_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;
        }
    }
}
/**
 * correctSpelling gives the dictionary word most likely wanted for word. A
 * word with a digit in it, such as 6 or 2021, or with fewer than
 * MIN_CORRECTED_LENGTH characters, is kept as typed. So is a word the
 * dictionary holds with a frequency of at least 25. Else the most frequent
 * word one edit away (see edits1) is taken, when it is more frequent than
 * word by a large enough factor. A candidate the dictionary does not hold
 * counts as frequency 0.
 *
 * @param String word the word to correct
 * @return String the corrected word, or "" when none is found
 */
function correctSpelling(word)
{
    if (word.length < MIN_CORRECTED_LENGTH || /\d/.test(word)) {
        return 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++) {
        curr_prob = 0;
        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;
}
/**
 * edits1 gives the strings one edit from word: one letter left out, two
 * letters next to each other swapped, one letter changed, or one letter
 * added, using the letters of the global alpha.
 *
 * @param String word the word to edit
 * @return Array the strings one edit away, each once
 */
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++;
        }
    }
    return [...new Set(deletes.concat(transposes, replaces, inserts))];
}
/**
 * known gives the strings of words_ip that the dictionary trie holds as
 * words.
 *
 * @param Array words_ip strings to look up
 * @return Array the strings that are dictionary 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;
}
/**
 * updateLocalStorage adds the query in the query field to the history of
 * queries in localStorage, counting how often each was sent. The history
 * is kept as a trie of the queries, then "@@", then the counts as JSON.
 */
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));
}
/**
 * sortLocalTerms gives the queries of the history, most sent first,
 * each as its count, a "*" and the query.
 *
 * @return Array the queries of the history with their counts
 */
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;
}
/**
 * termFrequencyComparison compares two entries of sortLocalTerms, each a
 * count, a "*" and a query, by their counts.
 *
 * @param String a the first entry
 * @param String b the second entry
 * @return number 0 if the counts are the same, negative if b's is larger,
 *     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);
}
/**
 * setSelectedTerm marks the dropdown's row numbered pos as selected or
 * not, and when title_flag is given also puts its query in the query field.
 *
 * @param int pos number of the row in the dropdown, from 0
 * @param String class_value "selected" or "unselected", the CSS class to
 *      give it
 * @param bool title_flag if given, the row's query is put in the query
 *      field
 */
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;
}

/**
 * termDblClick runs a search for a dropdown row that was double clicked:
 * it puts the row's query in the query field, hides the dropdown and sends
 * the search form.
 *
 * @param String term the row's query
 * @param String termid id of the row's span
 */
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();
}
/**
 * getTrieTerms finds all the words in trie_array, the part of a trie under
 * parent_word. A dictionary word not in the history is added to
 * found_terms with its frequency. While local_terms_present is true, the
 * trie is the history's, and each word is added to search_list_array.
 * Each is kept with the words typed before it in the query, and with its
 * part past the typed prefix in bold.
 *
 * @param Array trie_array part of a trie, as nested objects keyed by
 *      encoded letters
 * @param String parent_word the letters on the path to trie_array
 * @param String highlighted_word parent_word with a <b> tag after the
 *      typed prefix
 */
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>";
                    found_terms.push({terms: search_terms,
                        highlighted: highlighted_terms,
                        frequency: parseInt(trie_array[key])});
                    //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;
                }
            }
        }
    }
}
/**
 * completionsList makes the list items for the completions getTrieTerms
 * put in found_terms, highest frequency first, at most room of them.
 * Completions that differ only in case, as one found under a capitalized
 * prefix may, are listed once, as the one of higher frequency. Each item's
 * span is numbered from count, which grows by one for each item made.
 *
 * @param int room most items to make
 * @return String the list items
 */
function completionsList(room)
{
    let items = "";
    let listed = {};
    found_terms.sort((first, second) => second.frequency - first.frequency);
    for (let i = 0; i < found_terms.length && room > 0; i++) {
        let found = found_terms[i];
        let lower_terms = found.terms.toLowerCase();
        if (listed[lower_terms]) {
            continue;
        }
        listed[lower_terms] = true;
        items += "<li><span id='term" + count + "' class='unselected' " +
            "onclick='setSelectedTerm(this.id.substr(4), \"selected\", " +
            "true)' title='" + found.terms + "' " +
            "onmouseover='setSelectedTerm(this.id.substr(4), \"selected\")' " +
            "onmouseout='setSelectedTerm(this.id.substr(4), " +
            "\"unselected\")' ondblclick='termDblClick(\"" + found.terms +
            "\", this.id)'>" + found.highlighted + "</span></li>";
        count++;
        room--;
    }
    return items;
}
/**
 * placeDropdown puts the dropdown of suggestions just under the search
 * box, with the box's left edge and width, when the page's style fixes it
 * on the screen, as on a page of results. The landing page places it in
 * the flow of the page itself, so it is left alone there.
 *
 * @param Element suggest_dropdown the element holding the list
 */
function placeDropdown(suggest_dropdown)
{
    let query_field = elt("query-field");
    if (!query_field || !window.getComputedStyle ||
        window.getComputedStyle(suggest_dropdown).position != "fixed") {
        return;
    }
    let box = query_field.closest(".search-box-inner") || query_field;
    let box_place = box.getBoundingClientRect();
    suggest_dropdown.style.top = box_place.bottom + "px";
    suggest_dropdown.style.left = box_place.left + "px";
    suggest_dropdown.style.boxSizing = "border-box";
    suggest_dropdown.style.width = box_place.width + "px";
}
/**
 * exist gives the part of trie_array under the letters of term.
 *
 * @param Array trie_array a trie, as nested objects keyed by encoded
 *      letters
 * @param String term the letters to follow
 * @return mixed the part of the trie under term, or false if the trie has
 *      no such path
 */
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;
}
/**
 * termSuggest finds the completions of term in trie_array with
 * getTrieTerms. For en-US the term is lowercased first, unless lowercase
 * is false.
 *
 * @param Array trie_array a trie, as nested objects keyed by encoded
 *      letters
 * @param String term the prefix to complete
 * @param bool lowercase whether to lowercase term for en-US
 * @return bool false if there is no trie or no term
 */
function termSuggest(trie_array, term, lowercase = true)
{
    last_word = false;
    // For US english ignore the case
    if (locale == 'en-US' && lowercase) {
        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;
}
/**
 * getUnicodeCharAndNextOffset gives the Unicode character that starts at
 * offset i of str, which may take two UTF-16 code units, and the offset of
 * its last unit
 *
 * 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];
}
/**
 * loadFiles asks the server for the dictionary trie of the page's language
 * when the page loads. Once it arrives, it is kept in dictionary, and
 * spellCheck runs if the language's letters are known.
 */
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();
        }
    }
}
/**
 * spellCheck offers a corrected query on a page of results, under the
 * search box, when correctSpelling changes a word of a query of at most
 * MIN_SPELL_CHECK_WIDTH characters. It offers none for a query with a
 * meta word, such as site:, or one sent more than five times.
 */
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 query = elt("query-field").value;
        if (query == "") {
            return;
        }
        let ret_array;
        let ret_word;
        let locale_ver = locale + '_' + SUGGEST_VERSION_NO;
        if (typeof localStorage != '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;
        }
        if (query.length > MIN_SPELL_CHECK_WIDTH) {
            return;
        }
        let term_array = query.split(" ");
        let num_terms = term_array.length;
        for (let i = 0; i < num_terms; i++) {
            ret_word = correctSpelling(term_array[i].toLowerCase());
            if (ret_word.trim(" ") == "") {
                corrected_query += term_array[i] + " ";
            } else {
                corrected_query += ret_word + " ";
            }
        }
        corrected_query = corrected_query.trim();
        if (corrected_query.toLowerCase() != query.toLowerCase().trim()) {
            spell_link = "?q=" + encodeURIComponent(corrected_query);
            if (logged_in) {
                spell_link = "?" + csrf_name + "=" +
                    encodeURIComponent(csrf_token) + "&q=" +
                    encodeURIComponent(corrected_query);
            }
            /* The query comes from the page's address, so it is put in
               the page as text, never as markup. */
            let label = ce("b");
            let link = ce("a");
            link.rel = "nofollow";
            link.href = spell_link;
            link.textContent = corrected_query;
            label.textContent = local_strings.spell + ": ";
            label.appendChild(link);
            corrected_spell.innerHTML = "";
            corrected_spell.appendChild(label);
            setDisplay('spell-check', true);
        }
    }
}
if (tag("body")[0]) {
    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);
    }
}