blob: a9c1357037980c78d4c173fafebd69e8c14acf2f [file] [log] [blame]
// Global doc schema manager.
// Not in the vue observer.
// import {fetch} from 'whatwg-fetch';
// import levenshtein from 'js-levenshtein';
// stringSimilarity has better result than levenshtein when handling typo
import { stringSimilarity } from 'string-similarity-js';
import {getDocJSONPVarNname} from './shared';
// import stripHtml from 'string-strip-html';
// import Fuse from 'fuse.js';
let baseUrl;
let cdnRoot;
let rootName;
let docVersion;
// Cached data.
let outlineFetcher;
let outline;
let descStorage = {};
let pageOutlines;
let outlineNodesMap = {};
let allNodesPaths = [];
function fetchDocContentJS(basename) {
return new Promise(function (resolve, reject) {
const varname = getDocJSONPVarNname(basename);
const url = `${cdnRoot}/${basename}?${docVersion}`;
const script = document.createElement('script');
script.async = true;
script.onload = function () {
if (window[varname]) {
resolve(window[varname]);
}
else {
reject(`Load failed. ${varname}`);
}
};
script.src = url;
document.body.appendChild(script);
});
}
function stripHtml(str) {
// Simple and fast regexp html replacer
// string-strip-html is toooo slow.
return str.replace(/<[^>]*>?/gm, '');
}
/**
* Convert path to element id.
*/
export function convertPathToId(path) {
return 'doc-content-' + path
.replace(/[\. <>]/g, '-');
}
/**
* Get doc json async
*/
export function getOutlineAsync() {
if (!outlineFetcher) {
throw new Error('Preload json with url first');
}
return outlineFetcher;
}
function processOutlines(json) {
outline = json;
pageOutlines = {};
function joinPath(a, b, connector) {
return a ? (a + connector + b) : b;
}
function processNode(node, parentNode) {
if (!node.type) {
node.type = typeof node.default;
}
if (!(node.type instanceof Array)) {
node.type = [node.type];
}
// Normalize to any
for (let i = 0; i < node.type.length; i++) {
if (node.type[i] === '*') {
node.type[i] = 'any';
}
}
if (node.arrayItemType) {
node.path = joinPath(parentNode.path, node.arrayItemType, '-');
}
else {
node.path = joinPath(parentNode.path, node.prop, '.');
}
if (node.children) {
// Ignore option.series, option.dataZoom, option.visualMap
if (node.path.indexOf('.') < 0 && !node.children[0].arrayItemType) {
pageOutlines[node.path] = node;
}
for (let i = 0; i < node.children.length; i++) {
processNode(node.children[i], node);
}
}
outlineNodesMap[node.path] = node;
}
for (let i = 0; i < json.children.length; i++) {
processNode(json.children[i], {});
}
json.isRoot = true;
allNodesPaths = Object.keys(outlineNodesMap);
return json;
}
/**
* Preload doc json
*/
export function preload(_baseUrl, _cdnRoot, _rootName, _docVersion) {
baseUrl = _baseUrl;
cdnRoot = _cdnRoot;
rootName = _rootName;
docVersion = _docVersion || '1';
if (!outlineFetcher) {
outlineFetcher = fetchDocContentJS(`${rootName}-outline.js`)
.then(_json => processOutlines(_json));
}
return outlineFetcher;
}
/**
* Get outline of page
*/
export function getPageOutlineAsync(targetPath) {
let pagePath = targetPath.split('.')[0];
return getOutlineAsync().then(() => {
return pageOutlines[pagePath]
// Use top outline for `option.color`, etc.
|| getOutlineAsync();
});
}
function createIndexer(map, pagePath) {
let list = [];
for (let path in map) {
list.push({
path: pagePath ? (pagePath + '.' + path) : path,
content: map[path].desc,
text: stripHtml(map[path].desc)
});
}
return {
search(query) {
let results = [];
// TODO 常用词汇联想
let tokens = query.split(/[ +,]/).filter(t => !!t).map(token => {
// Case insensitive
return new RegExp(token, 'i');
});
if (!tokens.length) {
return results;
}
for (let k = 0; k < list.length; k++) {
let searched = true;
for (let i = 0; i < tokens.length; i++) {
if (!tokens[i].test(list[k].text) && !tokens[i].test(list[k].path)) {
searched = false;
break;
}
}
if (searched) {
results.push(list[k]);
}
}
return results;
}
};
}
function ensurePageDescStorage(targetPath) {
if (!pageOutlines) {
throw new Error('Outline data is not loaded.');
}
let pagePath = targetPath.split('.')[0];
// Configuration like `option.color`, `option.backgroundColor` is in the `option` page.
// Configuration like `option.series-bar` is in the `option.series-bar` page.
let partionKey = !pageOutlines[pagePath] || !targetPath
? rootName
: rootName + '.' + pagePath;
if (!descStorage[partionKey]) {
let fetcher = fetchDocContentJS(`${partionKey}.js`);
descStorage[partionKey] = {
fetcher
};
fetcher.then(map => {
descStorage[partionKey].indexer = createIndexer(map, pagePath);
});
}
return descStorage[partionKey];
}
/**
* Get all description of page.
*/
export function getPageTotalDescAsync(targetPath) {
return ensurePageDescStorage(targetPath).fetcher;
}
// TODO Cancel
/**
* Do loading page desc files and searching progressively
*/
export function searchAllAsync(queryString, onAdd) {
return getOutlineAsync().then(() => {
return new Promise(resolve => {
let asyncCount = 0;
function decreaseAsyncCount() {
asyncCount--;
if (!asyncCount) {
resolve();
}
}
function searchInPage(pagePath) {
let obj = ensurePageDescStorage(pagePath);
if (obj.indexer) {
onAdd(obj.indexer.search(queryString));
}
else {
asyncCount++;
obj.fetcher.then(() => {
onAdd(obj.indexer.search(queryString));
decreaseAsyncCount();
}).catch(e => {
decreaseAsyncCount();
});
}
}
// Search in root page.
searchInPage('');
for (let pagePath in pageOutlines) {
searchInPage(pagePath);
}
if (!asyncCount) {
resolve();
}
});
});
}
let querySearchScores;
/**
* Search outline with given query. Return list of nodes
*/
export function searchOutlineAsync(queryString, numberLimit = 50) {
return getOutlineAsync().then(() => {
let lists = [];
for (let i = 0; i < allNodesPaths.length; i++) {
if (lists.length >= numberLimit) {
return lists;
}
let p = allNodesPaths[i];
if (p.indexOf(queryString) >= 0) {
lists.push(getOutlineNode(p));
}
}
if (lists.length < numberLimit) {
if (!querySearchScores) {
querySearchScores = new Uint8Array(allNodesPaths.length);
}
let matchScoreCount = 0;
for (let i = 0; i < allNodesPaths.length; i++) {
querySearchScores[i] = stringSimilarity(allNodesPaths[i], queryString) * 255;
if (querySearchScores[i] > 50) {
matchScoreCount++;
}
}
let picked = {};
let safeCount = 0;
let safeProtect = 200;
while (lists.length < numberLimit && matchScoreCount > 0) {
let maxScore = 0;
let maxIndex;
for (let i = 0; i < querySearchScores.length; i++) {
if (querySearchScores[i] > maxScore && !picked[i]) {
maxIndex = i;
maxScore = querySearchScores[i];
}
}
if (maxScore > 50) { // Threshold
picked[maxIndex] = true;
lists.push(getOutlineNode(allNodesPaths[maxIndex]));
matchScoreCount--;
}
safeCount++;
if (safeCount > safeProtect) {
break;
}
}
}
return lists;
});
}
export function getOutlineNode(path) {
return outlineNodesMap[path];
}
export function getDefaultPage(wrongPath) {
if (!wrongPath) {
return Object.keys(pageOutlines)[0];
}
// Compatitable with series-line.data[i]
if (getOutlineNode(wrongPath.replace('[i]', ''))) {
return wrongPath.replace('[i]', '');
}
// Compatitable with series.data. Convert to series-line.data
let wrongPathParts = wrongPath.split('.');
let correctedPath = wrongPathParts.map(pathItem => {
let itemNode = getOutlineNode(pathItem);
let firstChild = itemNode && itemNode.children && itemNode && itemNode.children[0];
if (firstChild && firstChild.arrayItemType) {
return pathItem + '-' + firstChild.arrayItemType;
}
return pathItem;
});
if (getOutlineNode(correctedPath.join('.'))) {
return correctedPath.join('.');
}
// Else find the nearest
let mostLikeKey;
let maxSimilarity = -Infinity;
for (let i = 0; i < allNodesPaths.length; i++) {
let p = allNodesPaths[i];
let similarity = stringSimilarity(wrongPath, p);
if (similarity > maxSimilarity) {
maxSimilarity = similarity;
mostLikeKey = p;
}
}
return mostLikeKey;
}