UNPKG

docsify

Version:

A magical documentation generator.

482 lines (395 loc) 15.1 kB
(function () { /** * Converts a colon formatted string to a object with properties. * * This is process a provided string and look for any tokens in the format * of `:name[=value]` and then convert it to a object and return. * An example of this is ':include :type=code :fragment=demo' is taken and * then converted to: * * ``` * { * include: '', * type: 'code', * fragment: 'demo' * } * ``` * * @param {string} str The string to parse. * * @return {object} The original string and parsed object, { str, config }. */ function getAndRemoveConfig(str) { if ( str === void 0 ) str = ''; var config = {}; if (str) { str = str .replace(/^'/, '') .replace(/'$/, '') .replace(/(?:^|\s):([\w-]+:?)=?([\w-%]+)?/g, function (m, key, value) { if (key.indexOf(':') === -1) { config[key] = (value && value.replace(/&quot;/g, '')) || true; return ''; } return m; }) .trim(); } return { str: str, config: config }; } /* eslint-disable no-unused-vars */ var INDEXS = {}; var LOCAL_STORAGE = { EXPIRE_KEY: 'docsify.search.expires', INDEX_KEY: 'docsify.search.index', }; function resolveExpireKey(namespace) { return namespace ? ((LOCAL_STORAGE.EXPIRE_KEY) + "/" + namespace) : LOCAL_STORAGE.EXPIRE_KEY; } function resolveIndexKey(namespace) { return namespace ? ((LOCAL_STORAGE.INDEX_KEY) + "/" + namespace) : LOCAL_STORAGE.INDEX_KEY; } function escapeHtml(string) { var entityMap = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', }; return String(string).replace(/[&<>"']/g, function (s) { return entityMap[s]; }); } function getAllPaths(router) { var paths = []; Docsify.dom .findAll('.sidebar-nav a:not(.section-link):not([data-nosearch])') .forEach(function (node) { var href = node.href; var originHref = node.getAttribute('href'); var path = router.parse(href).path; if ( path && paths.indexOf(path) === -1 && !Docsify.util.isAbsolutePath(originHref) ) { paths.push(path); } }); return paths; } function getTableData(token) { if (!token.text && token.type === 'table') { token.cells.unshift(token.header); token.text = token.cells .map(function(rows) { return rows.join(' | '); }) .join(' |\n '); } return token.text; } function saveData(maxAge, expireKey, indexKey) { localStorage.setItem(expireKey, Date.now() + maxAge); localStorage.setItem(indexKey, JSON.stringify(INDEXS)); } function genIndex(path, content, router, depth) { if ( content === void 0 ) content = ''; var tokens = window.marked.lexer(content); var slugify = window.Docsify.slugify; var index = {}; var slug; tokens.forEach(function (token) { if (token.type === 'heading' && token.depth <= depth) { var ref = getAndRemoveConfig(token.text); var str = ref.str; var config = ref.config; if (config.id) { slug = router.toURL(path, { id: slugify(config.id) }); } else { slug = router.toURL(path, { id: slugify(escapeHtml(token.text)) }); } index[slug] = { slug: slug, title: str, body: '' }; } else { if (!slug) { return; } if (!index[slug]) { index[slug] = { slug: slug, title: '', body: '' }; } else if (index[slug].body) { token.text = getTableData(token); index[slug].body += '\n' + (token.text || ''); } else { token.text = getTableData(token); index[slug].body = index[slug].body ? index[slug].body + token.text : token.text; } } }); slugify.clear(); return index; } /** * @param {String} query Search query * @returns {Array} Array of results */ function search(query) { var matchingResults = []; var data = []; Object.keys(INDEXS).forEach(function (key) { data = data.concat(Object.keys(INDEXS[key]).map(function (page) { return INDEXS[key][page]; })); }); query = query.trim(); var keywords = query.split(/[\s\-,\\/]+/); if (keywords.length !== 1) { keywords = [].concat(query, keywords); } var loop = function ( i ) { var post = data[i]; var matchesScore = 0; var resultStr = ''; var postTitle = post.title && post.title.trim(); var postContent = post.body && post.body.trim(); var postUrl = post.slug || ''; if (postTitle) { keywords.forEach(function (keyword) { // From https://github.com/sindresorhus/escape-string-regexp var regEx = new RegExp( keyword.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'), 'gi' ); var indexTitle = -1; var indexContent = -1; indexTitle = postTitle ? postTitle.search(regEx) : -1; indexContent = postContent ? postContent.search(regEx) : -1; if (indexTitle >= 0 || indexContent >= 0) { matchesScore += indexTitle >= 0 ? 3 : indexContent >= 0 ? 2 : 0; if (indexContent < 0) { indexContent = 0; } var start = 0; var end = 0; start = indexContent < 11 ? 0 : indexContent - 10; end = start === 0 ? 70 : indexContent + keyword.length + 60; if (postContent && end > postContent.length) { end = postContent.length; } var matchContent = '...' + escapeHtml(postContent) .substring(start, end) .replace( regEx, function (word) { return ("<em class=\"search-keyword\">" + word + "</em>"); } ) + '...'; resultStr += matchContent; } }); if (matchesScore > 0) { var matchingPost = { title: escapeHtml(postTitle), content: postContent ? resultStr : '', url: postUrl, score: matchesScore, }; matchingResults.push(matchingPost); } } }; for (var i = 0; i < data.length; i++) loop( i ); return matchingResults.sort(function (r1, r2) { return r2.score - r1.score; }); } function init(config, vm) { var isAuto = config.paths === 'auto'; var paths = isAuto ? getAllPaths(vm.router) : config.paths; var namespaceSuffix = ''; // only in auto mode if (isAuto && config.pathNamespaces) { var path = paths[0]; if (Array.isArray(config.pathNamespaces)) { namespaceSuffix = config.pathNamespaces.find(function (prefix) { return path.startsWith(prefix); }) || namespaceSuffix; } else if (config.pathNamespaces instanceof RegExp) { var matches = path.match(config.pathNamespaces); if (matches) { namespaceSuffix = matches[0]; } } } var expireKey = resolveExpireKey(config.namespace) + namespaceSuffix; var indexKey = resolveIndexKey(config.namespace) + namespaceSuffix; var isExpired = localStorage.getItem(expireKey) < Date.now(); INDEXS = JSON.parse(localStorage.getItem(indexKey)); if (isExpired) { INDEXS = {}; } else if (!isAuto) { return; } var len = paths.length; var count = 0; paths.forEach(function (path) { if (INDEXS[path]) { return count++; } Docsify.get(vm.router.getFile(path), false, vm.config.requestHeaders).then( function (result) { INDEXS[path] = genIndex(path, result, vm.router, config.depth); len === ++count && saveData(config.maxAge, expireKey, indexKey); } ); }); } /* eslint-disable no-unused-vars */ var NO_DATA_TEXT = ''; var options; function style() { var code = "\n.sidebar {\n padding-top: 0;\n}\n\n.search {\n margin-bottom: 20px;\n padding: 6px;\n border-bottom: 1px solid #eee;\n}\n\n.search .input-wrap {\n display: flex;\n align-items: center;\n}\n\n.search .results-panel {\n display: none;\n}\n\n.search .results-panel.show {\n display: block;\n}\n\n.search input {\n outline: none;\n border: none;\n width: 100%;\n padding: 0 7px;\n line-height: 36px;\n font-size: 14px;\n border: 1px solid transparent;\n}\n\n.search input:focus {\n box-shadow: 0 0 5px var(--theme-color, #42b983);\n border: 1px solid var(--theme-color, #42b983);\n}\n\n.search input::-webkit-search-decoration,\n.search input::-webkit-search-cancel-button,\n.search input {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n.search .clear-button {\n cursor: pointer;\n width: 36px;\n text-align: right;\n display: none;\n}\n\n.search .clear-button.show {\n display: block;\n}\n\n.search .clear-button svg {\n transform: scale(.5);\n}\n\n.search h2 {\n font-size: 17px;\n margin: 10px 0;\n}\n\n.search a {\n text-decoration: none;\n color: inherit;\n}\n\n.search .matching-post {\n border-bottom: 1px solid #eee;\n}\n\n.search .matching-post:last-child {\n border-bottom: 0;\n}\n\n.search p {\n font-size: 14px;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n\n.search p.empty {\n text-align: center;\n}\n\n.app-name.hide, .sidebar-nav.hide {\n display: none;\n}"; Docsify.dom.style(code); } function tpl(defaultValue) { if ( defaultValue === void 0 ) defaultValue = ''; var html = "<div class=\"input-wrap\">\n <input type=\"search\" value=\"" + defaultValue + "\" aria-label=\"Search text\" />\n <div class=\"clear-button\">\n <svg width=\"26\" height=\"24\">\n <circle cx=\"12\" cy=\"12\" r=\"11\" fill=\"#ccc\" />\n <path stroke=\"white\" stroke-width=\"2\" d=\"M8.25,8.25,15.75,15.75\" />\n <path stroke=\"white\" stroke-width=\"2\"d=\"M8.25,15.75,15.75,8.25\" />\n </svg>\n </div>\n </div>\n <div class=\"results-panel\"></div>\n </div>"; var el = Docsify.dom.create('div', html); var aside = Docsify.dom.find('aside'); Docsify.dom.toggleClass(el, 'search'); Docsify.dom.before(aside, el); } function doSearch(value) { var $search = Docsify.dom.find('div.search'); var $panel = Docsify.dom.find($search, '.results-panel'); var $clearBtn = Docsify.dom.find($search, '.clear-button'); var $sidebarNav = Docsify.dom.find('.sidebar-nav'); var $appName = Docsify.dom.find('.app-name'); if (!value) { $panel.classList.remove('show'); $clearBtn.classList.remove('show'); $panel.innerHTML = ''; if (options.hideOtherSidebarContent) { $sidebarNav.classList.remove('hide'); $appName.classList.remove('hide'); } return; } var matchs = search(value); var html = ''; matchs.forEach(function (post) { html += "<div class=\"matching-post\">\n<a href=\"" + (post.url) + "\">\n<h2>" + (post.title) + "</h2>\n<p>" + (post.content) + "</p>\n</a>\n</div>"; }); $panel.classList.add('show'); $clearBtn.classList.add('show'); $panel.innerHTML = html || ("<p class=\"empty\">" + NO_DATA_TEXT + "</p>"); if (options.hideOtherSidebarContent) { $sidebarNav.classList.add('hide'); $appName.classList.add('hide'); } } function bindEvents() { var $search = Docsify.dom.find('div.search'); var $input = Docsify.dom.find($search, 'input'); var $inputWrap = Docsify.dom.find($search, '.input-wrap'); var timeId; /** Prevent to Fold sidebar. When searching on the mobile end, the sidebar is collapsed when you click the INPUT box, making it impossible to search. */ Docsify.dom.on( $search, 'click', function (e) { return ['A', 'H2', 'P', 'EM'].indexOf(e.target.tagName) === -1 && e.stopPropagation(); } ); Docsify.dom.on($input, 'input', function (e) { clearTimeout(timeId); timeId = setTimeout(function (_) { return doSearch(e.target.value.trim()); }, 100); }); Docsify.dom.on($inputWrap, 'click', function (e) { // Click input outside if (e.target.tagName !== 'INPUT') { $input.value = ''; doSearch(); } }); } function updatePlaceholder(text, path) { var $input = Docsify.dom.getNode('.search input[type="search"]'); if (!$input) { return; } if (typeof text === 'string') { $input.placeholder = text; } else { var match = Object.keys(text).filter(function (key) { return path.indexOf(key) > -1; })[0]; $input.placeholder = text[match]; } } function updateNoData(text, path) { if (typeof text === 'string') { NO_DATA_TEXT = text; } else { var match = Object.keys(text).filter(function (key) { return path.indexOf(key) > -1; })[0]; NO_DATA_TEXT = text[match]; } } function updateOptions(opts) { options = opts; } function init$1(opts, vm) { var keywords = vm.router.parse().query.s; updateOptions(opts); style(); tpl(keywords); bindEvents(); keywords && setTimeout(function (_) { return doSearch(keywords); }, 500); } function update(opts, vm) { updateOptions(opts); updatePlaceholder(opts.placeholder, vm.route.path); updateNoData(opts.noData, vm.route.path); } /* eslint-disable no-unused-vars */ var CONFIG = { placeholder: 'Type to search', noData: 'No Results!', paths: 'auto', depth: 2, maxAge: 86400000, // 1 day hideOtherSidebarContent: false, namespace: undefined, pathNamespaces: undefined, }; var install = function(hook, vm) { var util = Docsify.util; var opts = vm.config.search || CONFIG; if (Array.isArray(opts)) { CONFIG.paths = opts; } else if (typeof opts === 'object') { CONFIG.paths = Array.isArray(opts.paths) ? opts.paths : 'auto'; CONFIG.maxAge = util.isPrimitive(opts.maxAge) ? opts.maxAge : CONFIG.maxAge; CONFIG.placeholder = opts.placeholder || CONFIG.placeholder; CONFIG.noData = opts.noData || CONFIG.noData; CONFIG.depth = opts.depth || CONFIG.depth; CONFIG.hideOtherSidebarContent = opts.hideOtherSidebarContent || CONFIG.hideOtherSidebarContent; CONFIG.namespace = opts.namespace || CONFIG.namespace; CONFIG.pathNamespaces = opts.pathNamespaces || CONFIG.pathNamespaces; } var isAuto = CONFIG.paths === 'auto'; hook.mounted(function (_) { init$1(CONFIG, vm); !isAuto && init(CONFIG, vm); }); hook.doneEach(function (_) { update(CONFIG, vm); isAuto && init(CONFIG, vm); }); }; $docsify.plugins = [].concat(install, $docsify.plugins); }());