| // 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; | |
| } |