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