Rigol-DG2052-Function-Gener.../docs/searchlib.js

371 lines
15 KiB
JavaScript

// Wrapper around lunr index searching system for pydoctor API objects
// and function to format search results items into rederable HTML elements.
// This file is meant to be used as a library for the pydoctor search bar (search.js) as well as
// provide a hackable inferface to integrate API docs searching into other platforms, i.e. provide a
// "Search in API docs" option from Read The Docs search page.
// Depends on ajax.js, bundled with pydoctor.
// Other required ressources like lunr.js, searchindex.json and all-documents.html are passed as URL
// to functions. This makes the code reusable outside of pydoctor build directory.
// Implementation note: Searches are designed to be launched synchronously, if lunrSearch() is called sucessively (while already running),
// old promise will never resolves and the searhc worker will be restarted.
// Hacky way to make the worker code inline with the rest of the source file handling the search.
// Worker message params are the following:
// - query: string
// - indexJSONData: dict
// - defaultFields: list of strings
// - autoWildcard: boolean
let _lunrWorkerCode = `
// The lunr.js code will be inserted here.
onmessage = (message) => {
if (!message.data.query) {
throw new Error('No search query provided.');
}
if (!message.data.indexJSONData) {
throw new Error('No index data provided.');
}
if (!message.data.defaultFields) {
throw new Error('No default fields provided.');
}
if (!message.data.hasOwnProperty('autoWildcard')){
throw new Error('No value for auto wildcard provided.');
}
// Create index
let index = lunr.Index.load(message.data.indexJSONData);
// Declare query function building
function _queryfn(_query){ // _query is the Query object
// Edit the parsed query clauses that are applicable for all fields (default) in order
// to remove the field 'kind' from the clause since this it's only useful when specifically requested.
var parser = new lunr.QueryParser(message.data.query, _query)
parser.parse()
var hasTraillingWildcard = false;
_query.clauses.forEach(clause => {
if (clause.fields == _query.allFields){
// we change the query fields when they are applicable to all fields
// to a list of predefined fields because we might include additional filters (like kind:)
// which should not be matched by default.
clause.fields = message.data.defaultFields;
}
// clause.wildcard is actually always NONE due to https://github.com/olivernn/lunr.js/issues/495
// But this works...
if (clause.term.slice(-1) == '*'){
// we want to avoid the auto wildcard system only if a trailling wildcard is already added
// not if a leading wildcard exists
hasTraillingWildcard = true
}
});
// Auto wilcard feature, see issue https://github.com/twisted/pydoctor/issues/648
var new_clauses = [];
if ((message.data.autoWildcard == true) && (hasTraillingWildcard == false)){
_query.clauses.forEach(clause => {
// Setting clause.wildcard is useless.
// But this works...
let new_clause = {...clause}
new_clause.term = new_clause.term + '*'
clause.boost = 2
new_clause.boost = 0
new_clauses.push(new_clause)
});
}
new_clauses.forEach(clause => {
_query.clauses.push(clause)
});
console.log('Parsed query:')
console.dir(_query.clauses)
}
// Launch the search
let results = index.query(_queryfn)
// Post message with results
postMessage({'results':results});
};
`;
// Adapted from https://stackoverflow.com/a/44137284
// Override worker methods to detect termination and count message posting and restart() method.
// This allows some optimizations since the worker doesn't need to be restarted when it hasn't been used.
function _makeWorkerSmart(workerURL) {
// make normal worker
var worker = new Worker(workerURL);
// assume that it's running from the start
worker.terminated = false;
worker.postMessageCount = 0;
// count the number of times postMessage() is called
worker.postMessage = function() {
this.postMessageCount = this.postMessageCount + 1;
// normal post message
return Worker.prototype.postMessage.apply(this, arguments);
}
// sets terminated to true
worker.terminate = function() {
if (this.terminated===true){return;}
this.terminated = true;
// normal terminate
return Worker.prototype.terminate.apply(this, arguments);
}
// creates NEW WORKER with the same URL as itself, terminate worker first.
worker.restart = function() {
this.terminate();
return _makeWorkerSmart(workerURL);
}
return worker;
}
var _searchWorker = null
/**
* The searchEventsEnv Document variable let's let caller register a event listener "searchStarted" for sending
* a signal when the search actually starts, could be up to 0.2 or 0.3 secs ater user finished typing.
*/
let searchEventsEnv = document.implementation.createHTMLDocument(
'This is a document to popagate search related events, we avoid using "document" for performance reasons.');
// there is a difference in abortSearch() vs restartSearchWorker().
// abortSearch() triggers a abortSearch event, which have a effect on searches that are not yet running in workers.
// whereas restartSearchWorker() which kills the worker if it's in use, but does not abort search that is not yet posted to the worker.
function abortSearch(){
searchEventsEnv.dispatchEvent(new CustomEvent('abortSearch', {}));
}
// Kills and restarts search worker (if needed).
function restartSearchWorker() {
var w = _searchWorker;
if (w!=null){
if (w.postMessageCount>0){
// the worker has been used, it has to be restarted
// TODO: Actually it needs to be restarted only if it's running a search right now.
// Otherwise we can reuse the same worker, but that's not a very big deal in this context.
w = w.restart();
}
// Else, the worker has never been used, it can be returned as is.
// This can happens when typing fast with a very large index JSON to load.
}
_searchWorker = w;
}
function _getWorkerPromise(lunJsSourceCode){ // -> Promise of a fresh worker to run a query.
let promise = new Promise((resolve, reject) => {
// Do the search business, wrap the process inside an inline Worker.
// This is a hack such that the UI can refresh during the search.
if (_searchWorker===null){
// Create only one blob and URL.
let lunrWorkerCode = lunJsSourceCode + _lunrWorkerCode;
let _workerBlob = new Blob([lunrWorkerCode], {type: 'text/javascript'});
let _workerObjectURL = window.URL.createObjectURL(_workerBlob);
_searchWorker = _makeWorkerSmart(_workerObjectURL)
}
else{
restartSearchWorker();
}
resolve(_searchWorker);
});
return promise
}
/**
* Launch a search and get a promise of results. One search can be lauch at a time only.
* Old promise never resolves if calling lunrSearch() again while already running.
* @param query: Query string.
* @param indexURL: URL pointing to the Lunr search index, generated by pydoctor.
* @param defaultFields: List of strings: default fields to apply to query clauses when none is specified. ["name", "names", "qname"] for instance.
* @param lunrJsURL: URL pointing to a copy of lunr.js.
* @param searchDelay: Number of miliseconds to wait before actually launching the query. This is useful to set for "search as you type" kind of search box
* because it let a chance to users to continue typing without triggering useless searches (because previous search is aborted on launching a new one).
* @param autoWildcard: Whether to automatically append wildcards to all query clauses when no wildcard is already specified. boolean.
*/
function lunrSearch(query, indexURL, defaultFields, lunrJsURL, searchDelay, autoWildcard){
// Abort ongoing search
abortSearch();
// Register abort procedure.
var _aborted = false;
searchEventsEnv.addEventListener('abortSearch', (ev) => {
_aborted = true;
searchEventsEnv.removeEventListener('abortSearch', this);
});
// Pref:
// Because this function can be called a lot of times in a very few moments,
// Actually launch search after a delay to let a chance to users to continue typing,
// which would trigger a search abort event, which would avoid wasting a worker
// for a search that is not wanted anymore.
return new Promise((_resolve, _reject) => {
setTimeout(() => {
_resolve(
_getIndexDataPromise(indexURL).then((lunrIndexData) => {
// Include lunr.js source inside the worker such that it has no dependencies.
return httpGetPromise(lunrJsURL).then((responseText) => {
// Do the search business, wrap the process inside an inline Worker.
// This is a hack such that the UI can refresh during the search.
return _getWorkerPromise(responseText).then((worker) => {
let promise = new Promise((resolve, reject) => {
worker.onmessage = (message) => {
if (!message.data.results){
reject("No data received from worker");
}
else{
console.log("Got result from worker:")
console.dir(message.data.results)
resolve(message.data.results)
}
}
worker.onerror = function(error) {
reject(error);
};
});
let _msgData = {
'query': query,
'indexJSONData': lunrIndexData,
'defaultFields': defaultFields,
'autoWildcard': autoWildcard,
}
if (!_aborted){
console.log(`Posting query "${query}" to worker:`)
console.dir(_msgData)
worker.postMessage(_msgData);
searchEventsEnv.dispatchEvent(
new CustomEvent("searchStarted", {'query':query})
);
}
return promise
});
});
})
);}, searchDelay);
});
}
/**
* @param results: list of lunr.Index~Result.
* @param allDocumentsURL: URL pointing to all-documents.html, generated by pydoctor.
* @returns: Promise of a list of HTMLElement corresponding to the all-documents.html
* list elements matching your search results.
*/
function fetchResultsData(results, allDocumentsURL){
return _getAllDocumentsPromise(allDocumentsURL).then((allDocuments) => {
// Look for results data in parsed all-documents.html
return _asyncFor(results, (result) => {
// Find the result model row data.
var dobj = allDocuments.getElementById(result.ref);
if (!dobj){
throw new Error("Cannot find document ID: " + result.ref);
}
// Return result data
return dobj;
})
})
}
/**
* Transform list item as in all-documents.html into a formatted search result row.
*/
function buildSearchResult(dobj) {
// Build one result item
var tr = document.createElement('tr'),
kindtd = document.createElement('td'),
contenttd = document.createElement('td'),
article = document.createElement('article'),
header = document.createElement('header'),
section = document.createElement('section'),
code = document.createElement('code'),
a = document.createElement('a'),
p = document.createElement('p');
p.innerHTML = dobj.querySelector('.summary').innerHTML;
a.setAttribute('href', dobj.querySelector('.url').innerHTML);
a.setAttribute('class', 'internal-link');
a.innerHTML = dobj.querySelector('.fullName').innerHTML;
let kind_value = dobj.querySelector('.kind').innerHTML;
let type_value = dobj.querySelector('.type').innerHTML;
// Adding '()' on functions and methods
if (type_value.endsWith("Function")){
a.innerHTML = a.innerHTML + '()';
}
kindtd.innerHTML = kind_value;
// Putting everything together
tr.appendChild(kindtd);
tr.appendChild(contenttd);
contenttd.appendChild(article);
article.appendChild(header);
article.appendChild(section);
header.appendChild(code);
code.appendChild(a);
section.appendChild(p);
// Set kind as the CSS class of the kind td tag
let ob_css_class = dobj.querySelector('.kind').innerHTML.toLowerCase().replace(' ', '');
kindtd.setAttribute('class', ob_css_class);
// Set private
if (dobj.querySelector('.privacy').innerHTML.includes('PRIVATE')){
tr.setAttribute('class', 'private');
}
return tr;
}
// This gives the UI the opportunity to refresh while we're iterating over a large list.
function _asyncFor(iterable, callback) { // -> Promise of List of results returned by callback
const promise_global = new Promise((resolve_global, reject_global) => {
let promises = [];
iterable.forEach((element) => {
promises.push(new Promise((resolve, _reject) => {
setTimeout(() => {
try{ resolve(callback(element)); }
catch (error){ _reject(error); }
}, 0);
}));
});
Promise.all(promises).then((results) =>{
resolve_global(results);
}).catch((err) => {
reject_global(err);
});
});
return promise_global;
}
// Cache indexes JSON data since it takes a little bit of time to load JSON into stuctured data
var _indexDataCache = {};
function _getIndexDataPromise(indexURL) { // -> Promise of a structured data for the lunr Index.
if (!_indexDataCache[indexURL]){
return httpGetPromise(indexURL).then((responseText) => {
_indexDataCache[indexURL] = JSON.parse(responseText)
return (_indexDataCache[indexURL]);
});
}
else{
return new Promise((_resolve, _reject) => {
_resolve(_indexDataCache[indexURL]);
});
}
}
// Cache Document object
var _allDocumentsCache = {};
function _getAllDocumentsPromise(allDocumentsURL) { // -> Promise of the all-documents.html Document object.
if (!_allDocumentsCache[allDocumentsURL]){
return httpGetPromise(allDocumentsURL).then((responseText) => {
let _parser = new self.DOMParser();
_allDocumentsCache[allDocumentsURL] = _parser.parseFromString(responseText, "text/html");
return (_allDocumentsCache[allDocumentsURL]);
});
}
else{
return new Promise((_resolve, _reject) => {
_resolve(_allDocumentsCache[allDocumentsURL]);
});
}
}