Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
searchtools.js620 linesDownload Raw Back to _static
1/*2 * searchtools.js3 * ~~~~~~~~~~~~~~~~4 *5 * Sphinx JavaScript utilities for the full-text search.6 *7 * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.8 * :license: BSD, see LICENSE for details.9 *10 */11"use strict";12 13/**14 * Simple result scoring code.15 */16if (typeof Scorer === "undefined") {17  var Scorer = {18    // Implement the following function to further tweak the score for each result19    // The function takes a result array [docname, title, anchor, descr, score, filename]20    // and returns the new score.21    /*22    score: result => {23      const [docname, title, anchor, descr, score, filename] = result24      return score25    },26    */27 28    // query matches the full name of an object29    objNameMatch: 11,30    // or matches in the last dotted part of the object name31    objPartialMatch: 6,32    // Additive scores depending on the priority of the object33    objPrio: {34      0: 15, // used to be importantResults35      1: 5, // used to be objectResults36      2: -5, // used to be unimportantResults37    },38    //  Used when the priority is not in the mapping.39    objPrioDefault: 0,40 41    // query found in title42    title: 15,43    partialTitle: 7,44    // query found in terms45    term: 5,46    partialTerm: 2,47  };48}49 50const _removeChildren = (element) => {51  while (element && element.lastChild) element.removeChild(element.lastChild);52};53 54/**55 * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping56 */57const _escapeRegExp = (string) =>58  string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string59 60const _displayItem = (item, searchTerms, highlightTerms) => {61  const docBuilder = DOCUMENTATION_OPTIONS.BUILDER;62  const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX;63  const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX;64  const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY;65  const contentRoot = document.documentElement.dataset.content_root;66 67  const [docName, title, anchor, descr, score, _filename] = item;68 69  let listItem = document.createElement("li");70  let requestUrl;71  let linkUrl;72  if (docBuilder === "dirhtml") {73    // dirhtml builder74    let dirname = docName + "/";75    if (dirname.match(/\/index\/$/))76      dirname = dirname.substring(0, dirname.length - 6);77    else if (dirname === "index/") dirname = "";78    requestUrl = contentRoot + dirname;79    linkUrl = requestUrl;80  } else {81    // normal html builders82    requestUrl = contentRoot + docName + docFileSuffix;83    linkUrl = docName + docLinkSuffix;84  }85  let linkEl = listItem.appendChild(document.createElement("a"));86  linkEl.href = linkUrl + anchor;87  linkEl.dataset.score = score;88  linkEl.innerHTML = title;89  if (descr) {90    listItem.appendChild(document.createElement("span")).innerHTML =91      " (" + descr + ")";92    // highlight search terms in the description93    if (SPHINX_HIGHLIGHT_ENABLED)  // set in sphinx_highlight.js94      highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted"));95  }96  else if (showSearchSummary)97    fetch(requestUrl)98      .then((responseData) => responseData.text())99      .then((data) => {100        if (data)101          listItem.appendChild(102            Search.makeSearchSummary(data, searchTerms, anchor)103          );104        // highlight search terms in the summary105        if (SPHINX_HIGHLIGHT_ENABLED)  // set in sphinx_highlight.js106          highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted"));107      });108  Search.output.appendChild(listItem);109};110const _finishSearch = (resultCount) => {111  Search.stopPulse();112  Search.title.innerText = _("Search Results");113  if (!resultCount)114    Search.status.innerText = Documentation.gettext(115      "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories."116    );117  else118    Search.status.innerText = _(119      "Search finished, found ${resultCount} page(s) matching the search query."120    ).replace('${resultCount}', resultCount);121};122const _displayNextItem = (123  results,124  resultCount,125  searchTerms,126  highlightTerms,127) => {128  // results left, load the summary and display it129  // this is intended to be dynamic (don't sub resultsCount)130  if (results.length) {131    _displayItem(results.pop(), searchTerms, highlightTerms);132    setTimeout(133      () => _displayNextItem(results, resultCount, searchTerms, highlightTerms),134      5135    );136  }137  // search finished, update title and status message138  else _finishSearch(resultCount);139};140// Helper function used by query() to order search results.141// Each input is an array of [docname, title, anchor, descr, score, filename].142// Order the results by score (in opposite order of appearance, since the143// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically.144const _orderResultsByScoreThenName = (a, b) => {145  const leftScore = a[4];146  const rightScore = b[4];147  if (leftScore === rightScore) {148    // same score: sort alphabetically149    const leftTitle = a[1].toLowerCase();150    const rightTitle = b[1].toLowerCase();151    if (leftTitle === rightTitle) return 0;152    return leftTitle > rightTitle ? -1 : 1; // inverted is intentional153  }154  return leftScore > rightScore ? 1 : -1;155};156 157/**158 * Default splitQuery function. Can be overridden in ``sphinx.search`` with a159 * custom function per language.160 *161 * The regular expression works by splitting the string on consecutive characters162 * that are not Unicode letters, numbers, underscores, or emoji characters.163 * This is the same as ``\W+`` in Python, preserving the surrogate pair area.164 */165if (typeof splitQuery === "undefined") {166  var splitQuery = (query) => query167      .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu)168      .filter(term => term)  // remove remaining empty strings169}170 171/**172 * Search Module173 */174const Search = {175  _index: null,176  _queued_query: null,177  _pulse_status: -1,178 179  htmlToText: (htmlString, anchor) => {180    const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html');181    for (const removalQuery of [".headerlinks", "script", "style"]) {182      htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() });183    }184    if (anchor) {185      const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`);186      if (anchorContent) return anchorContent.textContent;187 188      console.warn(189        `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.`190      );191    }192 193    // if anchor not specified or not found, fall back to main content194    const docContent = htmlElement.querySelector('[role="main"]');195    if (docContent) return docContent.textContent;196 197    console.warn(198      "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template."199    );200    return "";201  },202 203  init: () => {204    const query = new URLSearchParams(window.location.search).get("q");205    document206      .querySelectorAll('input[name="q"]')207      .forEach((el) => (el.value = query));208    if (query) Search.performSearch(query);209  },210 211  loadIndex: (url) =>212    (document.body.appendChild(document.createElement("script")).src = url),213 214  setIndex: (index) => {215    Search._index = index;216    if (Search._queued_query !== null) {217      const query = Search._queued_query;218      Search._queued_query = null;219      Search.query(query);220    }221  },222 223  hasIndex: () => Search._index !== null,224 225  deferQuery: (query) => (Search._queued_query = query),226 227  stopPulse: () => (Search._pulse_status = -1),228 229  startPulse: () => {230    if (Search._pulse_status >= 0) return;231 232    const pulse = () => {233      Search._pulse_status = (Search._pulse_status + 1) % 4;234      Search.dots.innerText = ".".repeat(Search._pulse_status);235      if (Search._pulse_status >= 0) window.setTimeout(pulse, 500);236    };237    pulse();238  },239 240  /**241   * perform a search for something (or wait until index is loaded)242   */243  performSearch: (query) => {244    // create the required interface elements245    const searchText = document.createElement("h2");246    searchText.textContent = _("Searching");247    const searchSummary = document.createElement("p");248    searchSummary.classList.add("search-summary");249    searchSummary.innerText = "";250    const searchList = document.createElement("ul");251    searchList.classList.add("search");252 253    const out = document.getElementById("search-results");254    Search.title = out.appendChild(searchText);255    Search.dots = Search.title.appendChild(document.createElement("span"));256    Search.status = out.appendChild(searchSummary);257    Search.output = out.appendChild(searchList);258 259    const searchProgress = document.getElementById("search-progress");260    // Some themes don't use the search progress node261    if (searchProgress) {262      searchProgress.innerText = _("Preparing search...");263    }264    Search.startPulse();265 266    // index already loaded, the browser was quick!267    if (Search.hasIndex()) Search.query(query);268    else Search.deferQuery(query);269  },270 271  _parseQuery: (query) => {272    // stem the search terms and add them to the correct list273    const stemmer = new Stemmer();274    const searchTerms = new Set();275    const excludedTerms = new Set();276    const highlightTerms = new Set();277    const objectTerms = new Set(splitQuery(query.toLowerCase().trim()));278    splitQuery(query.trim()).forEach((queryTerm) => {279      const queryTermLower = queryTerm.toLowerCase();280 281      // maybe skip this "word"282      // stopwords array is from language_data.js283      if (284        stopwords.indexOf(queryTermLower) !== -1 ||285        queryTerm.match(/^\d+$/)286      )287        return;288 289      // stem the word290      let word = stemmer.stemWord(queryTermLower);291      // select the correct list292      if (word[0] === "-") excludedTerms.add(word.substr(1));293      else {294        searchTerms.add(word);295        highlightTerms.add(queryTermLower);296      }297    });298 299    if (SPHINX_HIGHLIGHT_ENABLED) {  // set in sphinx_highlight.js300      localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" "))301    }302 303    // console.debug("SEARCH: searching for:");304    // console.info("required: ", [...searchTerms]);305    // console.info("excluded: ", [...excludedTerms]);306 307    return [query, searchTerms, excludedTerms, highlightTerms, objectTerms];308  },309 310  /**311   * execute search (requires search index to be loaded)312   */313  _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => {314    const filenames = Search._index.filenames;315    const docNames = Search._index.docnames;316    const titles = Search._index.titles;317    const allTitles = Search._index.alltitles;318    const indexEntries = Search._index.indexentries;319 320    // Collect multiple result groups to be sorted separately and then ordered.321    // Each is an array of [docname, title, anchor, descr, score, filename].322    const normalResults = [];323    const nonMainIndexResults = [];324 325    _removeChildren(document.getElementById("search-progress"));326 327    const queryLower = query.toLowerCase().trim();328    for (const [title, foundTitles] of Object.entries(allTitles)) {329      if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) {330        for (const [file, id] of foundTitles) {331          let score = Math.round(100 * queryLower.length / title.length)332          normalResults.push([333            docNames[file],334            titles[file] !== title ? `${titles[file]} > ${title}` : title,335            id !== null ? "#" + id : "",336            null,337            score,338            filenames[file],339          ]);340        }341      }342    }343 344    // search for explicit entries in index directives345    for (const [entry, foundEntries] of Object.entries(indexEntries)) {346      if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) {347        for (const [file, id, isMain] of foundEntries) {348          const score = Math.round(100 * queryLower.length / entry.length);349          const result = [350            docNames[file],351            titles[file],352            id ? "#" + id : "",353            null,354            score,355            filenames[file],356          ];357          if (isMain) {358            normalResults.push(result);359          } else {360            nonMainIndexResults.push(result);361          }362        }363      }364    }365 366    // lookup as object367    objectTerms.forEach((term) =>368      normalResults.push(...Search.performObjectSearch(term, objectTerms))369    );370 371    // lookup as search terms in fulltext372    normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms));373 374    // let the scorer override scores with a custom scoring function375    if (Scorer.score) {376      normalResults.forEach((item) => (item[4] = Scorer.score(item)));377      nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item)));378    }379 380    // Sort each group of results by score and then alphabetically by name.381    normalResults.sort(_orderResultsByScoreThenName);382    nonMainIndexResults.sort(_orderResultsByScoreThenName);383 384    // Combine the result groups in (reverse) order.385    // Non-main index entries are typically arbitrary cross-references,386    // so display them after other results.387    let results = [...nonMainIndexResults, ...normalResults];388 389    // remove duplicate search results390    // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept391    let seen = new Set();392    results = results.reverse().reduce((acc, result) => {393      let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(',');394      if (!seen.has(resultStr)) {395        acc.push(result);396        seen.add(resultStr);397      }398      return acc;399    }, []);400 401    return results.reverse();402  },403 404  query: (query) => {405    const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query);406    const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms);407 408    // for debugging409    //Search.lastresults = results.slice();  // a copy410    // console.info("search results:", Search.lastresults);411 412    // print the results413    _displayNextItem(results, results.length, searchTerms, highlightTerms);414  },415 416  /**417   * search for object names418   */419  performObjectSearch: (object, objectTerms) => {420    const filenames = Search._index.filenames;421    const docNames = Search._index.docnames;422    const objects = Search._index.objects;423    const objNames = Search._index.objnames;424    const titles = Search._index.titles;425 426    const results = [];427 428    const objectSearchCallback = (prefix, match) => {429      const name = match[4]430      const fullname = (prefix ? prefix + "." : "") + name;431      const fullnameLower = fullname.toLowerCase();432      if (fullnameLower.indexOf(object) < 0) return;433 434      let score = 0;435      const parts = fullnameLower.split(".");436 437      // check for different match types: exact matches of full name or438      // "last name" (i.e. last dotted part)439      if (fullnameLower === object || parts.slice(-1)[0] === object)440        score += Scorer.objNameMatch;441      else if (parts.slice(-1)[0].indexOf(object) > -1)442        score += Scorer.objPartialMatch; // matches in last name443 444      const objName = objNames[match[1]][2];445      const title = titles[match[0]];446 447      // If more than one term searched for, we require other words to be448      // found in the name/title/description449      const otherTerms = new Set(objectTerms);450      otherTerms.delete(object);451      if (otherTerms.size > 0) {452        const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase();453        if (454          [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0)455        )456          return;457      }458 459      let anchor = match[3];460      if (anchor === "") anchor = fullname;461      else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname;462 463      const descr = objName + _(", in ") + title;464 465      // add custom score for some objects according to scorer466      if (Scorer.objPrio.hasOwnProperty(match[2]))467        score += Scorer.objPrio[match[2]];468      else score += Scorer.objPrioDefault;469 470      results.push([471        docNames[match[0]],472        fullname,473        "#" + anchor,474        descr,475        score,476        filenames[match[0]],477      ]);478    };479    Object.keys(objects).forEach((prefix) =>480      objects[prefix].forEach((array) =>481        objectSearchCallback(prefix, array)482      )483    );484    return results;485  },486 487  /**488   * search for full-text terms in the index489   */490  performTermsSearch: (searchTerms, excludedTerms) => {491    // prepare search492    const terms = Search._index.terms;493    const titleTerms = Search._index.titleterms;494    const filenames = Search._index.filenames;495    const docNames = Search._index.docnames;496    const titles = Search._index.titles;497 498    const scoreMap = new Map();499    const fileMap = new Map();500 501    // perform the search on the required terms502    searchTerms.forEach((word) => {503      const files = [];504      const arr = [505        { files: terms[word], score: Scorer.term },506        { files: titleTerms[word], score: Scorer.title },507      ];508      // add support for partial matches509      if (word.length > 2) {510        const escapedWord = _escapeRegExp(word);511        if (!terms.hasOwnProperty(word)) {512          Object.keys(terms).forEach((term) => {513            if (term.match(escapedWord))514              arr.push({ files: terms[term], score: Scorer.partialTerm });515          });516        }517        if (!titleTerms.hasOwnProperty(word)) {518          Object.keys(titleTerms).forEach((term) => {519            if (term.match(escapedWord))520              arr.push({ files: titleTerms[term], score: Scorer.partialTitle });521          });522        }523      }524 525      // no match but word was a required one526      if (arr.every((record) => record.files === undefined)) return;527 528      // found search word in contents529      arr.forEach((record) => {530        if (record.files === undefined) return;531 532        let recordFiles = record.files;533        if (recordFiles.length === undefined) recordFiles = [recordFiles];534        files.push(...recordFiles);535 536        // set score for the word in each file537        recordFiles.forEach((file) => {538          if (!scoreMap.has(file)) scoreMap.set(file, {});539          scoreMap.get(file)[word] = record.score;540        });541      });542 543      // create the mapping544      files.forEach((file) => {545        if (!fileMap.has(file)) fileMap.set(file, [word]);546        else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word);547      });548    });549 550    // now check if the files don't contain excluded terms551    const results = [];552    for (const [file, wordList] of fileMap) {553      // check if all requirements are matched554 555      // as search terms with length < 3 are discarded556      const filteredTermCount = [...searchTerms].filter(557        (term) => term.length > 2558      ).length;559      if (560        wordList.length !== searchTerms.size &&561        wordList.length !== filteredTermCount562      )563        continue;564 565      // ensure that none of the excluded terms is in the search result566      if (567        [...excludedTerms].some(568          (term) =>569            terms[term] === file ||570            titleTerms[term] === file ||571            (terms[term] || []).includes(file) ||572            (titleTerms[term] || []).includes(file)573        )574      )575        break;576 577      // select one (max) score for the file.578      const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w]));579      // add result to the result list580      results.push([581        docNames[file],582        titles[file],583        "",584        null,585        score,586        filenames[file],587      ]);588    }589    return results;590  },591 592  /**593   * helper function to return a node containing the594   * search summary for a given text. keywords is a list595   * of stemmed words.596   */597  makeSearchSummary: (htmlText, keywords, anchor) => {598    const text = Search.htmlToText(htmlText, anchor);599    if (text === "") return null;600 601    const textLower = text.toLowerCase();602    const actualStartPosition = [...keywords]603      .map((k) => textLower.indexOf(k.toLowerCase()))604      .filter((i) => i > -1)605      .slice(-1)[0];606    const startWithContext = Math.max(actualStartPosition - 120, 0);607 608    const top = startWithContext === 0 ? "" : "...";609    const tail = startWithContext + 240 < text.length ? "..." : "";610 611    let summary = document.createElement("p");612    summary.classList.add("context");613    summary.textContent = top + text.substr(startWithContext, 240).trim() + tail;614 615    return summary;616  },617};618 619_ready(Search.init);620 
codekingpro/portable-devtools · Team Ai