PageSourceSearch

https://matplotlib.org/stable/_static/searchtools.js

js matplotlib.org collected 2026-09-24 07:16:47 UTC 22,464 bytes, 693 lines download raw bytes

1/*
2 * Sphinx JavaScript utilities for the full-text search.
3 */
4"use strict";
5
6/**
7 * Simple result scoring code.
8 */
9if (typeof Scorer === "undefined") {
10  var Scorer = {
11    // Implement the following function to further tweak the score for each result
12    // The function takes a result array [docname, title, anchor, descr, score, filename]
13    // and returns the new score.
14    /*
15    score: result => {
16      const [docname, title, anchor, descr, score, filename, kind] = result
17      return score
18    },
19    */
20
21    // query matches the full name of an object
22    objNameMatch: 11,
23    // or matches in the last dotted part of the object name
24    objPartialMatch: 6,
25    // Additive scores depending on the priority of the object
26    objPrio: {
27      0: 15, // used to be importantResults
28      1: 5, // used to be objectResults
29      2: -5, // used to be unimportantResults
30    },
31    //  Used when the priority is not in the mapping.
32    objPrioDefault: 0,
33
34    // query found in title
35    title: 15,
36    partialTitle: 7,
37    // query found in terms
38    term: 5,
39    partialTerm: 2,
40  };
41}
42
43// Global search result kind enum, used by themes to style search results.
44// prettier-ignore
45class SearchResultKind {
46  static get index() { return "index"; }
47  static get object() { return "object"; }
48  static get text() { return "text"; }
49  static get title() { return "title"; }
50}
51
52const _removeChildren = (element) => {
53  while (element && element.lastChild) element.removeChild(element.lastChild);
54};
55
56/**
57 * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
58 */
59const _escapeRegExp = (string) =>
60  string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
61
62const _escapeHTML = (text) => {
63  return text
64    .replaceAll("&", "&")
65    .replaceAll("<", "&lt;")
66    .replaceAll(">", "&gt;")
67    .replaceAll('"', "&quot;")
68    .replaceAll("'", "&apos;");
69};
70
71const _displayItem = (item, searchTerms, highlightTerms) => {
72  const docBuilder = DOCUMENTATION_OPTIONS.BUILDER;
73  const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX;
74  const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX;
75  const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY;
76  const contentRoot = document.documentElement.dataset.content_root;
77
78  const [docName, title, anchor, descr, score, _filename, kind] = item;
79
80  let listItem = document.createElement("li");
81  // Add a class representing the item's type:
82  // can be used by a theme's CSS selector for styling
83  // See SearchResultKind for the class names.
84  listItem.classList.add(`kind-${kind}`);
85  let requestUrl;
86  let linkUrl;
87  if (docBuilder === "dirhtml") {
88    // dirhtml builder
89    let dirname = docName + "/";
90    if (dirname.match(/\/index\/$/))
91      dirname = dirname.substring(0, dirname.length - 6);
92    else if (dirname === "index/") dirname = "";
93    requestUrl = contentRoot + dirname;
94    linkUrl = requestUrl;
95  } else {
96    // normal html builders
97    requestUrl = contentRoot + docName + docFileSuffix;
98    linkUrl = docName + docLinkSuffix;
99  }
100  let linkEl = listItem.appendChild(document.createElement("a"));
101  linkEl.href = linkUrl + anchor;
102  linkEl.dataset.score = score;
103  linkEl.innerHTML = _escapeHTML(title);
104  if (descr) {
105    listItem.appendChild(document.createElement("span")).innerHTML =
106      ` (${_escapeHTML(descr)})`;
107    // highlight search terms in the description
108    if (SPHINX_HIGHLIGHT_ENABLED)
109      // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js
110      highlightTerms.forEach((term) =>
111        _highlightText(listItem, term, "highlighted"),
112      );
113  } else if (showSearchSummary)
114    fetch(requestUrl)
115      .then((responseData) => responseData.text())
116      .then((data) => {
117        if (data)
118          listItem.appendChild(
119            Search.makeSearchSummary(data, searchTerms, anchor),
120          );
121        // highlight search terms in the summary
122        if (SPHINX_HIGHLIGHT_ENABLED)
123          // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js
124          highlightTerms.forEach((term) =>
125            _highlightText(listItem, term, "highlighted"),
126          );
127      });
128  Search.output.appendChild(listItem);
129};
130const _finishSearch = (resultCount) => {
131  Search.stopPulse();
132  Search.title.innerText = _("Search Results");
133  if (!resultCount)
134    Search.status.innerText = Documentation.gettext(
135      "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories.",
136    );
137  else
138    Search.status.innerText = Documentation.ngettext(
139      "Search finished, found one page matching the search query.",
140      "Search finished, found ${resultCount} pages matching the search query.",
141      resultCount,
142    ).replace("${resultCount}", resultCount);
143};
144const _displayNextItem = (
145  results,
146  resultCount,
147  searchTerms,
148  highlightTerms,
149) => {
150  // results left, load the summary and display it
151  // this is intended to be dynamic (don't sub resultsCount)
152  if (results.length) {
153    _displayItem(results.pop(), searchTerms, highlightTerms);
154    setTimeout(
155      () => _displayNextItem(results, resultCount, searchTerms, highlightTerms),
156      5,
157    );
158  }
159  // search finished, update title and status message
160  else _finishSearch(resultCount);
161};
162// Helper function used by query() to order search results.
163// Each input is an array of [docname, title, anchor, descr, score, filename, kind].
164// Order the results by score (in opposite order of appearance, since the
165// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically.
166const _orderResultsByScoreThenName = (a, b) => {
167  const leftScore = a[4];
168  const rightScore = b[4];
169  if (leftScore === rightScore) {
170    // same score: sort alphabetically
171    const leftTitle = a[1].toLowerCase();
172    const rightTitle = b[1].toLowerCase();
173    if (leftTitle === rightTitle) return 0;
174    return leftTitle > rightTitle ? -1 : 1; // inverted is intentional
175  }
176  return leftScore > rightScore ? 1 : -1;
177};
178
179/**
180 * Default splitQuery function. Can be overridden in ``sphinx.search`` with a
181 * custom function per language.
182 *
183 * The regular expression works by splitting the string on consecutive characters
184 * that are not Unicode letters, numbers, underscores, or emoji characters.
185 * This is the same as ``\W+`` in Python, preserving the surrogate pair area.
186 */
187if (typeof splitQuery === "undefined") {
188  var splitQuery = (query) =>
189    query
190      .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu)
191      .filter((term) => term); // remove remaining empty strings
192}
193
194/**
195 * Search Module
196 */
197const Search = {
198  _index: null,
199  _queued_query: null,
200  _pulse_status: -1,
201
202  htmlToText: (htmlString, anchor) => {
203    const htmlElement = new DOMParser().parseFromString(
204      htmlString,
205      "text/html",
206    );
207    for (const removalQuery of [".headerlink", "script", "style"]) {
208      htmlElement.querySelectorAll(removalQuery).forEach((el) => {
209        el.remove();
210      });
211    }
212    if (anchor) {
213      const anchorContent = htmlElement.querySelector(
214        `[role="main"] ${anchor}`,
215      );
216      if (anchorContent) return anchorContent.textContent;
217
218      console.warn(
219        `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.`,
220      );
221    }
222
223    // if anchor not specified or not found, fall back to main content
224    const docContent = htmlElement.querySelector('[role="main"]');
225    if (docContent) return docContent.textContent;
226
227    console.warn(
228      "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template.",
229    );
230    return "";
231  },
232
233  init: () => {
234    const query = new URLSearchParams(window.location.search).get("q");
235    document
236      .querySelectorAll('input[name="q"]')
237      .forEach((el) => (el.value = query));
238    if (query) Search.performSearch(query);
239  },
240
241  loadIndex: (url) =>
242    (document.body.appendChild(document.createElement("script")).src = url),
243
244  setIndex: (index) => {
245    Search._index = index;
246    if (Search._queued_query !== null) {
247      const query = Search._queued_query;
248      Search._queued_query = null;
249      Search.query(query);
250    }
251  },
252
253  hasIndex: () => Search._index !== null,
254
255  deferQuery: (query) => (Search._queued_query = query),
256
257  stopPulse: () => (Search._pulse_status = -1),
258
259  startPulse: () => {
260    if (Search._pulse_status >= 0) return;
261
262    const pulse = () => {
263      Search._pulse_status = (Search._pulse_status + 1) % 4;
264      Search.dots.innerText = ".".repeat(Search._pulse_status);
265      if (Search._pulse_status >= 0) window.setTimeout(pulse, 500);
266    };
267    pulse();
268  },
269
270  /**
271   * perform a search for something (or wait until index is loaded)
272   */
273  performSearch: (query) => {
274    // create the required interface elements
275    const searchText = document.createElement("h2");
276    searchText.textContent = _("Searching");
277    const searchSummary = document.createElement("p");
278    searchSummary.classList.add("search-summary");
279    searchSummary.innerText = "";
280    const searchList = document.createElement("ul");
281    searchList.setAttribute("role", "list");
282    searchList.classList.add("search");
283
284    const out = document.getElementById("search-results");
285    Search.title = out.appendChild(searchText);
286    Search.dots = Search.title.appendChild(document.createElement("span"));
287    Search.status = out.appendChild(searchSummary);
288    Search.output = out.appendChild(searchList);
289
290    const searchProgress = document.getElementById("search-progress");
291    // Some themes don't use the search progress node
292    if (searchProgress) {
293      searchProgress.innerText = _("Preparing search...");
294    }
295    Search.startPulse();
296
297    // index already loaded, the browser was quick!
298    if (Search.hasIndex()) Search.query(query);
299    else Search.deferQuery(query);
300  },
301
302  _parseQuery: (query) => {
303    // stem the search terms and add them to the correct list
304    const stemmer = new Stemmer();
305    const searchTerms = new Set();
306    const excludedTerms = new Set();
307    const highlightTerms = new Set();
308    const objectTerms = new Set(splitQuery(query.toLowerCase().trim()));
309    splitQuery(query.trim()).forEach((queryTerm) => {
310      const queryTermLower = queryTerm.toLowerCase();
311
312      // maybe skip this "word"
313      // stopwords set is from language_data.js
314      if (stopwords.has(queryTermLower) || queryTerm.match(/^\d+$/)) return;
315
316      // stem the word
317      let word = stemmer.stemWord(queryTermLower);
318      // select the correct list
319      if (word[0] === "-") excludedTerms.add(word.substr(1));
320      else {
321        searchTerms.add(word);
322        highlightTerms.add(queryTermLower);
323      }
324    });
325
326    if (SPHINX_HIGHLIGHT_ENABLED) {
327      // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js
328      localStorage.setItem(
329        "sphinx_highlight_terms",
330        [...highlightTerms].join(" "),
331      );
332    }
333
334    // console.debug("SEARCH: searching for:");
335    // console.info("required: ", [...searchTerms]);
336    // console.info("excluded: ", [...excludedTerms]);
337
338    return [query, searchTerms, excludedTerms, highlightTerms, objectTerms];
339  },
340
341  /**
342   * execute search (requires search index to be loaded)
343   */
344  _performSearch: (
345    query,
346    searchTerms,
347    excludedTerms,
348    highlightTerms,
349    objectTerms,
350  ) => {
351    const filenames = Search._index.filenames;
352    const docNames = Search._index.docnames;
353    const titles = Search._index.titles;
354    const allTitles = Search._index.alltitles;
355    const indexEntries = Search._index.indexentries;
356
357    // Collect multiple result groups to be sorted separately and then ordered.
358    // Each is an array of [docname, title, anchor, descr, score, filename, kind].
359    const normalResults = [];
360    const nonMainIndexResults = [];
361
362    _removeChildren(document.getElementById("search-progress"));
363
364    const queryLower = query.toLowerCase().trim();
365    for (const [title, foundTitles] of Object.entries(allTitles)) {
366      if (
367        title.toLowerCase().trim().includes(queryLower)
368        && queryLower.length >= title.length / 2
369      ) {
370        for (const [file, id] of foundTitles) {
371          const score = Math.round(
372            (Scorer.title * queryLower.length) / title.length,
373          );
374          const boost = titles[file] === title ? 1 : 0; // add a boost for document titles
375          normalResults.push([
376            docNames[file],
377            titles[file] !== title ? `${titles[file]} > ${title}` : title,
378            id !== null ? "#" + id : "",
379            null,
380            score + boost,
381            filenames[file],
382            SearchResultKind.title,
383          ]);
384        }
385      }
386    }
387
388    // search for explicit entries in index directives
389    for (const [entry, foundEntries] of Object.entries(indexEntries)) {
390      if (entry.includes(queryLower) && queryLower.length >= entry.length / 2) {
391        for (const [file, id, isMain] of foundEntries) {
392          const score = Math.round((100 * queryLower.length) / entry.length);
393          const result = [
394            docNames[file],
395            titles[file],
396            id ? "#" + id : "",
397            null,
398            score,
399            filenames[file],
400            SearchResultKind.index,
401          ];
402          if (isMain) {
403            normalResults.push(result);
404          } else {
405            nonMainIndexResults.push(result);
406          }
407        }
408      }
409    }
410
411    // lookup as object
412    objectTerms.forEach((term) =>
413      normalResults.push(...Search.performObjectSearch(term, objectTerms)),
414    );
415
416    // lookup as search terms in fulltext
417    normalResults.push(
418      ...Search.performTermsSearch(searchTerms, excludedTerms),
419    );
420
421    // let the scorer override scores with a custom scoring function
422    if (Scorer.score) {
423      normalResults.forEach((item) => (item[4] = Scorer.score(item)));
424      nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item)));
425    }
426
427    // Sort each group of results by score and then alphabetically by name.
428    normalResults.sort(_orderResultsByScoreThenName);
429    nonMainIndexResults.sort(_orderResultsByScoreThenName);
430
431    // Combine the result groups in (reverse) order.
432    // Non-main index entries are typically arbitrary cross-references,
433    // so display them after other results.
434    let results = [...nonMainIndexResults, ...normalResults];
435
436    // remove duplicate search results
437    // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept
438    let seen = new Set();
439    results = results.reverse().reduce((acc, result) => {
440      let resultStr = result
441        .slice(0, 4)
442        .concat([result[5]])
443        .map((v) => String(v))
444        .join(",");
445      if (!seen.has(resultStr)) {
446        acc.push(result);
447        seen.add(resultStr);
448      }
449      return acc;
450    }, []);
451
452    return results.reverse();
453  },
454
455  query: (query) => {
456    const [
457      searchQuery,
458      searchTerms,
459      excludedTerms,
460      highlightTerms,
461      objectTerms,
462    ] = Search._parseQuery(query);
463    const results = Search._performSearch(
464      searchQuery,
465      searchTerms,
466      excludedTerms,
467      highlightTerms,
468      objectTerms,
469    );
470
471    // for debugging
472    //Search.lastresults = results.slice();  // a copy
473    // console.info("search results:", Search.lastresults);
474
475    // print the results
476    _displayNextItem(results, results.length, searchTerms, highlightTerms);
477  },
478
479  /**
480   * search for object names
481   */
482  performObjectSearch: (object, objectTerms) => {
483    const filenames = Search._index.filenames;
484    const docNames = Search._index.docnames;
485    const objects = Search._index.objects;
486    const objNames = Search._index.objnames;
487    const titles = Search._index.titles;
488
489    const results = [];
490
491    const objectSearchCallback = (prefix, match) => {
492      const name = match[4];
493      const fullname = (prefix ? prefix + "." : "") + name;
494      const fullnameLower = fullname.toLowerCase();
495      if (fullnameLower.indexOf(object) < 0) return;
496
497      let score = 0;
498      const parts = fullnameLower.split(".");
499
500      // check for different match types: exact matches of full name or
501      // "last name" (i.e. last dotted part)
502      if (fullnameLower === object || parts.slice(-1)[0] === object)
503        score += Scorer.objNameMatch;
504      else if (parts.slice(-1)[0].indexOf(object) > -1)
505        score += Scorer.objPartialMatch; // matches in last name
506
507      const objName = objNames[match[1]][2];
508      const title = titles[match[0]];
509
510      // If more than one term searched for, we require other words to be
511      // found in the name/title/description
512      const otherTerms = new Set(objectTerms);
513      otherTerms.delete(object);
514      if (otherTerms.size > 0) {
515        const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase();
516        if (
517          [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0)
518        )
519          return;
520      }
521
522      let anchor = match[3];
523      if (anchor === "") anchor = fullname;
524      else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname;
525
526      const descr = objName + _(", in ") + title;
527
528      // add custom score for some objects according to scorer
529      if (Scorer.objPrio.hasOwnProperty(match[2]))
530        score += Scorer.objPrio[match[2]];
531      else score += Scorer.objPrioDefault;
532
533      results.push([
534        docNames[match[0]],
535        fullname,
536        "#" + anchor,
537        descr,
538        score,
539        filenames[match[0]],
540        SearchResultKind.object,
541      ]);
542    };
543    Object.keys(objects).forEach((prefix) =>
544      objects[prefix].forEach((array) => objectSearchCallback(prefix, array)),
545    );
546    return results;
547  },
548
549  /**
550   * search for full-text terms in the index
551   */
552  performTermsSearch: (searchTerms, excludedTerms) => {
553    // prepare search
554    const terms = Search._index.terms;
555    const titleTerms = Search._index.titleterms;
556    const filenames = Search._index.filenames;
557    const docNames = Search._index.docnames;
558    const titles = Search._index.titles;
559
560    const scoreMap = new Map();
561    const fileMap = new Map();
562
563    // perform the search on the required terms
564    searchTerms.forEach((word) => {
565      const files = [];
566      // find documents, if any, containing the query word in their text/title term indices
567      // use Object.hasOwnProperty to avoid mismatching against prototype properties
568      const arr = [
569        {
570          files: terms.hasOwnProperty(word) ? terms[word] : undefined,
571          score: Scorer.term,
572        },
573        {
574          files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined,
575          score: Scorer.title,
576        },
577      ];
578      // add support for partial matches
579      if (word.length > 2) {
580        const escapedWord = _escapeRegExp(word);
581        if (!terms.hasOwnProperty(word)) {
582          Object.keys(terms).forEach((term) => {
583            if (term.match(escapedWord))
584              arr.push({ files: terms[term], score: Scorer.partialTerm });
585          });
586        }
587        if (!titleTerms.hasOwnProperty(word)) {
588          Object.keys(titleTerms).forEach((term) => {
589            if (term.match(escapedWord))
590              arr.push({ files: titleTerms[term], score: Scorer.partialTitle });
591          });
592        }
593      }
594
595      // no match but word was a required one
596      if (arr.every((record) => record.files === undefined)) return;
597
598      // found search word in contents
599      arr.forEach((record) => {
600        if (record.files === undefined) return;
601
602        let recordFiles = record.files;
603        if (recordFiles.length === undefined) recordFiles = [recordFiles];
604        files.push(...recordFiles);
605
606        // set score for the word in each file
607        recordFiles.forEach((file) => {
608          if (!scoreMap.has(file)) scoreMap.set(file, new Map());
609          const fileScores = scoreMap.get(file);
610          fileScores.set(word, record.score);
611        });
612      });
613
614      // create the mapping
615      files.forEach((file) => {
616        if (!fileMap.has(file)) fileMap.set(file, [word]);
617        else if (fileMap.get(file).indexOf(word) === -1)
618          fileMap.get(file).push(word);
619      });
620    });
621
622    // now check if the files don't contain excluded terms
623    const results = [];
624    for (const [file, wordList] of fileMap) {
625      // check if all requirements are matched
626
627      // as search terms with length < 3 are discarded
628      const filteredTermCount = [...searchTerms].filter(
629        (term) => term.length > 2,
630      ).length;
631      if (
632        wordList.length !== searchTerms.size
633        && wordList.length !== filteredTermCount
634      )
635        continue;
636
637      // ensure that none of the excluded terms is in the search result
638      if (
639        [...excludedTerms].some(
640          (term) =>
641            terms[term] === file
642            || titleTerms[term] === file
643            || (terms[term] || []).includes(file)
644            || (titleTerms[term] || []).includes(file),
645        )
646      )
647        break;
648
649      // select one (max) score for the file.
650      const score = Math.max(...wordList.map((w) => scoreMap.get(file).get(w)));
651      // add result to the result list
652      results.push([
653        docNames[file],
654        titles[file],
655        "",
656        null,
657        score,
658        filenames[file],
659        SearchResultKind.text,
660      ]);
661    }
662    return results;
663  },
664
665  /**
666   * helper function to return a node containing the
667   * search summary for a given text. keywords is a list
668   * of stemmed words.
669   */
670  makeSearchSummary: (htmlText, keywords, anchor) => {
671    const text = Search.htmlToText(htmlText, anchor);
672    if (text === "") return null;
673
674    const textLower = text.toLowerCase();
675    const actualStartPosition = [...keywords]
676      .map((k) => textLower.indexOf(k.toLowerCase()))
677      .filter((i) => i > -1)
678      .slice(-1)[0];
679    const startWithContext = Math.max(actualStartPosition - 120, 0);
680
681    const top = startWithContext === 0 ? "" : "...";
682    const tail = startWithContext + 240 < text.length ? "..." : "";
683
684    let summary = document.createElement("p");
685    summary.classList.add("context");
686    summary.textContent =
687      top + text.substr(startWithContext, 240).trim() + tail;
688
689    return summary;
690  },
691};
692
693_ready(Search.init);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.