bookshelf-doc/static/declaration-data.js

238 lines
6.7 KiB
JavaScript
Raw Normal View History

2022-02-22 04:40:14 +00:00
/**
* This module is a wrapper that facilitates manipulating the declaration data.
2022-02-22 07:01:14 +00:00
*
2022-02-22 04:40:14 +00:00
* Please see {@link DeclarationDataCenter} for more information.
*/
import { SITE_ROOT } from "./site-root.js";
const CACHE_DB_NAME = "declaration-data";
const CACHE_DB_VERSION = 1;
2022-02-22 04:40:14 +00:00
/**
* The DeclarationDataCenter is used for declaration searching.
2022-02-22 07:01:14 +00:00
*
2022-02-22 04:40:14 +00:00
* For usage, see the {@link init} and {@link search} methods.
*/
export class DeclarationDataCenter {
/**
2022-02-22 07:01:14 +00:00
* The declaration data. Users should not interact directly with this field.
*
2022-02-22 04:40:14 +00:00
* *NOTE:* This is not made private to support legacy browsers.
*/
2022-02-22 07:01:14 +00:00
declarationData = null;
2022-02-22 04:40:14 +00:00
/**
* Used to implement the singleton, in case we need to fetch data mutiple times in the same page.
*/
static singleton = null;
/**
* Construct a DeclarationDataCenter with given data.
2022-02-22 07:01:14 +00:00
*
2022-02-22 04:40:14 +00:00
* Please use {@link DeclarationDataCenter.init} instead, which automates the data fetching process.
* @param {*} declarationData
*/
constructor(declarationData) {
this.declarationData = declarationData;
}
/**
* The actual constructor of DeclarationDataCenter
* @returns {Promise<DeclarationDataCenter>}
*/
static async init() {
if (!DeclarationDataCenter.singleton) {
const timestampUrl = new URL(
`${SITE_ROOT}declaration-data.timestamp`,
window.location
);
2022-02-22 04:40:14 +00:00
const dataUrl = new URL(
`${SITE_ROOT}declaration-data.bmp`,
window.location
);
const timestampRes = await fetch(timestampUrl);
const timestamp = await timestampRes.text();
// try to use cache first
let store = await getDeclarationStore();
const data = await fetchCachedDeclarationData(store, timestamp);
if (data) {
// if data is defined, use the cached one.
DeclarationDataCenter.singleton = new DeclarationDataCenter(data);
} else {
// undefined. then fetch the data from the server.
const dataRes = await fetch(dataUrl);
const dataJson = await dataRes.json();
// the data is a map of name (original case) to declaration data.
const data = new Map(
dataJson.map(({ name, doc, link, source: sourceLink }) => [
2022-02-22 07:01:14 +00:00
name,
{
name,
lowerName: name.toLowerCase(),
lowerDoc: doc.toLowerCase(),
link,
sourceLink,
},
])
);
// get store again in case it's inactive
let store = await getDeclarationStore();
await cacheDeclarationData(store, timestamp, data);
DeclarationDataCenter.singleton = new DeclarationDataCenter(data);
}
2022-02-22 04:40:14 +00:00
}
return DeclarationDataCenter.singleton;
}
/**
* Search for a declaration.
* @returns {Array<any>}
*/
search(pattern, strict = false) {
if (!pattern) {
return [];
}
2022-02-22 07:01:14 +00:00
if (strict) {
let decl = this.declarationData.get(pattern);
return decl ? [decl] : [];
} else {
return getMatches(this.declarationData, pattern);
}
2022-02-22 04:40:14 +00:00
}
}
function isSeparater(char) {
return char === "." || char === "_";
}
2022-02-22 07:01:14 +00:00
// HACK: the fuzzy matching is quite hacky
function matchCaseSensitive(declName, lowerDeclName, pattern) {
2022-02-22 04:40:14 +00:00
let i = 0,
j = 0,
err = 0,
lastMatch = 0;
while (i < declName.length && j < pattern.length) {
2022-02-22 07:01:14 +00:00
if (pattern[j] === declName[i] || pattern[j] === lowerDeclName[i]) {
2022-02-22 04:40:14 +00:00
err += (isSeparater(pattern[j]) ? 0.125 : 1) * (i - lastMatch);
if (pattern[j] !== declName[i]) err += 0.5;
lastMatch = i + 1;
j++;
} else if (isSeparater(declName[i])) {
err += 0.125 * (i + 1 - lastMatch);
lastMatch = i + 1;
}
i++;
}
err += 0.125 * (declName.length - lastMatch);
if (j === pattern.length) {
return err;
}
}
function getMatches(declarations, pattern, maxResults = 30) {
const lowerPats = pattern.toLowerCase().split(/\s/g);
const patNoSpaces = pattern.replace(/\s/g, "");
const results = [];
2022-02-22 07:01:14 +00:00
for (const {
name,
lowerName,
lowerDoc,
link,
sourceLink,
} of declarations.values()) {
let err = matchCaseSensitive(name, lowerName, patNoSpaces);
2022-02-22 04:40:14 +00:00
// match all words as substrings of docstring
if (
2022-02-22 07:01:14 +00:00
err >= 3 &&
2022-02-22 04:40:14 +00:00
pattern.length > 3 &&
lowerPats.every((l) => lowerDoc.indexOf(l) != -1)
) {
err = 3;
}
if (err !== undefined) {
2022-02-22 07:01:14 +00:00
results.push({ name, err, lowerName, lowerDoc, link, sourceLink });
2022-02-22 04:40:14 +00:00
}
}
return results.sort(({ err: a }, { err: b }) => a - b).slice(0, maxResults);
}
// TODO: refactor the indexedDB part to be more robust
/**
* Get the indexedDB database, automatically initialized.
* @returns {Promise<IDBObjectStore>}
*/
function getDeclarationStore() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(CACHE_DB_NAME, CACHE_DB_VERSION);
request.onerror = function (event) {
reject(
new Error(
`fail to open indexedDB ${CACHE_DB_NAME} of version ${CACHE_DB_VERSION}`
)
);
};
request.onupgradeneeded = function (event) {
let db = event.target.result;
// We only need to store one object, so no key path or increment is needed.
let objectStore = db.createObjectStore("declaration");
objectStore.transaction.oncomplete = function (event) {
resolve(objectStore);
};
};
request.onsuccess = function (event) {
resolve(
event.target.result
.transaction("declaration", "readwrite")
.objectStore("declaration")
);
};
});
}
/**
* Store data in indexedDB object store.
* @param {IDBObjectStore} store
* @param {string} timestamp
* @param {Map<string, any>} data
*/
function cacheDeclarationData(store, timestamp, data) {
return new Promise((resolve, reject) => {
let clearRequest = store.clear();
clearRequest.onsuccess = function (event) {
let addRequest = store.add(data, timestamp);
addRequest.onsuccess = function (event) {
resolve();
};
addRequest.onerror = function (event) {
reject(new Error(`fail to store declaration data`));
};
};
clearRequest.onerror = function (event) {
reject(new Error("fail to clear object store"));
};
});
}
/**
* Retrieve data from indexedDB database.
* @param {IDBObjectStore} store
* @param {string} timestamp
* @returns {Promise<Map<string, any>|undefined>}
*/
async function fetchCachedDeclarationData(store, timestamp) {
return new Promise((resolve, reject) => {
let transactionRequest = store.get(timestamp);
transactionRequest.onsuccess = function (event) {
resolve(event.result);
};
transactionRequest.onerror = function (event) {
reject(new Error(`fail to store declaration data`));
};
});
}