URI:
       searchtools.js - sphere - GPU-based 3D discrete element method algorithm with optional fluid coupling
  HTML git clone git://src.adamsgaard.dk/sphere
   DIR Log
   DIR Files
   DIR Refs
   DIR LICENSE
       ---
       searchtools.js (22464B)
       ---
            1 /*
            2  * Sphinx JavaScript utilities for the full-text search.
            3  */
            4 "use strict";
            5 
            6 /**
            7  * Simple result scoring code.
            8  */
            9 if (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
           45 class 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 
           52 const _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  */
           59 const _escapeRegExp = (string) =>
           60   string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
           61 
           62 const _escapeHTML = (text) => {
           63   return text
           64     .replaceAll("&", "&")
           65     .replaceAll("<", "&lt;")
           66     .replaceAll(">", "&gt;")
           67     .replaceAll('"', "&quot;")
           68     .replaceAll("'", "&apos;");
           69 };
           70 
           71 const _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 };
          130 const _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 };
          144 const _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.
          166 const _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  */
          187 if (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  */
          197 const 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);