search.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. (function () {
  2. /**
  3. * Converts a colon formatted string to a object with properties.
  4. *
  5. * This is process a provided string and look for any tokens in the format
  6. * of `:name[=value]` and then convert it to a object and return.
  7. * An example of this is ':include :type=code :fragment=demo' is taken and
  8. * then converted to:
  9. *
  10. * ```
  11. * {
  12. * include: '',
  13. * type: 'code',
  14. * fragment: 'demo'
  15. * }
  16. * ```
  17. *
  18. * @param {string} str The string to parse.
  19. *
  20. * @return {object} The original string and parsed object, { str, config }.
  21. */
  22. function getAndRemoveConfig(str) {
  23. if ( str === void 0 ) str = '';
  24. var config = {};
  25. if (str) {
  26. str = str
  27. .replace(/^('|")/, '')
  28. .replace(/('|")$/, '')
  29. .replace(/(?:^|\s):([\w-]+:?)=?([\w-%]+)?/g, function (m, key, value) {
  30. if (key.indexOf(':') === -1) {
  31. config[key] = (value && value.replace(/"/g, '')) || true;
  32. return '';
  33. }
  34. return m;
  35. })
  36. .trim();
  37. }
  38. return { str: str, config: config };
  39. }
  40. /* eslint-disable no-unused-vars */
  41. var INDEXS = {};
  42. var LOCAL_STORAGE = {
  43. EXPIRE_KEY: 'docsify.search.expires',
  44. INDEX_KEY: 'docsify.search.index',
  45. };
  46. function resolveExpireKey(namespace) {
  47. return namespace
  48. ? ((LOCAL_STORAGE.EXPIRE_KEY) + "/" + namespace)
  49. : LOCAL_STORAGE.EXPIRE_KEY;
  50. }
  51. function resolveIndexKey(namespace) {
  52. return namespace
  53. ? ((LOCAL_STORAGE.INDEX_KEY) + "/" + namespace)
  54. : LOCAL_STORAGE.INDEX_KEY;
  55. }
  56. function escapeHtml(string) {
  57. var entityMap = {
  58. '&': '&',
  59. '<': '&lt;',
  60. '>': '&gt;',
  61. '"': '&quot;',
  62. "'": '&#39;',
  63. };
  64. return String(string).replace(/[&<>"']/g, function (s) { return entityMap[s]; });
  65. }
  66. function getAllPaths(router) {
  67. var paths = [];
  68. Docsify.dom
  69. .findAll('.sidebar-nav a:not(.section-link):not([data-nosearch])')
  70. .forEach(function (node) {
  71. var href = node.href;
  72. var originHref = node.getAttribute('href');
  73. var path = router.parse(href).path;
  74. if (
  75. path &&
  76. paths.indexOf(path) === -1 &&
  77. !Docsify.util.isAbsolutePath(originHref)
  78. ) {
  79. paths.push(path);
  80. }
  81. });
  82. return paths;
  83. }
  84. function getTableData(token) {
  85. if (!token.text && token.type === 'table' && token.cells) {
  86. token.cells.unshift(token.header);
  87. token.text = token.cells
  88. .map(function(rows) {
  89. return rows.join(' | ');
  90. })
  91. .join(' |\n ');
  92. }
  93. return token.text;
  94. }
  95. function getListData(token) {
  96. if (!token.text && token.type === 'list') {
  97. token.text = token.raw;
  98. }
  99. return token.text;
  100. }
  101. function saveData(maxAge, expireKey, indexKey) {
  102. localStorage.setItem(expireKey, Date.now() + maxAge);
  103. localStorage.setItem(indexKey, JSON.stringify(INDEXS));
  104. }
  105. function genIndex(path, content, router, depth) {
  106. if ( content === void 0 ) content = '';
  107. var tokens = window.marked.lexer(content);
  108. var slugify = window.Docsify.slugify;
  109. var index = {};
  110. var slug;
  111. var title = '';
  112. tokens.forEach(function(token, tokenIndex) {
  113. if (token.type === 'heading' && token.depth <= depth) {
  114. var ref = getAndRemoveConfig(token.text);
  115. var str = ref.str;
  116. var config = ref.config;
  117. if (config.id) {
  118. slug = router.toURL(path, { id: slugify(config.id) });
  119. } else {
  120. slug = router.toURL(path, { id: slugify(escapeHtml(token.text)) });
  121. }
  122. if (str) {
  123. title = str
  124. .replace(/<!-- {docsify-ignore} -->/, '')
  125. .replace(/{docsify-ignore}/, '')
  126. .replace(/<!-- {docsify-ignore-all} -->/, '')
  127. .replace(/{docsify-ignore-all}/, '')
  128. .trim();
  129. }
  130. index[slug] = { slug: slug, title: title, body: '' };
  131. } else {
  132. if (tokenIndex === 0) {
  133. slug = router.toURL(path);
  134. index[slug] = {
  135. slug: slug,
  136. title: path !== '/' ? path.slice(1) : 'Home Page',
  137. body: token.text || '',
  138. };
  139. }
  140. if (!slug) {
  141. return;
  142. }
  143. if (!index[slug]) {
  144. index[slug] = { slug: slug, title: '', body: '' };
  145. } else if (index[slug].body) {
  146. token.text = getTableData(token);
  147. token.text = getListData(token);
  148. index[slug].body += '\n' + (token.text || '');
  149. } else {
  150. token.text = getTableData(token);
  151. token.text = getListData(token);
  152. index[slug].body = index[slug].body
  153. ? index[slug].body + token.text
  154. : token.text;
  155. }
  156. }
  157. });
  158. slugify.clear();
  159. return index;
  160. }
  161. function ignoreDiacriticalMarks(keyword) {
  162. if (keyword && keyword.normalize) {
  163. return keyword.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  164. }
  165. return keyword;
  166. }
  167. /**
  168. * @param {String} query Search query
  169. * @returns {Array} Array of results
  170. */
  171. function search(query) {
  172. var matchingResults = [];
  173. var data = [];
  174. Object.keys(INDEXS).forEach(function (key) {
  175. data = data.concat(Object.keys(INDEXS[key]).map(function (page) { return INDEXS[key][page]; }));
  176. });
  177. query = query.trim();
  178. var keywords = query.split(/[\s\-,\\/]+/);
  179. if (keywords.length !== 1) {
  180. keywords = [].concat(query, keywords);
  181. }
  182. var loop = function ( i ) {
  183. var post = data[i];
  184. var matchesScore = 0;
  185. var resultStr = '';
  186. var handlePostTitle = '';
  187. var handlePostContent = '';
  188. var postTitle = post.title && post.title.trim();
  189. var postContent = post.body && post.body.trim();
  190. var postUrl = post.slug || '';
  191. if (postTitle) {
  192. keywords.forEach(function (keyword) {
  193. // From https://github.com/sindresorhus/escape-string-regexp
  194. var regEx = new RegExp(
  195. escapeHtml(ignoreDiacriticalMarks(keyword)).replace(
  196. /[|\\{}()[\]^$+*?.]/g,
  197. '\\$&'
  198. ),
  199. 'gi'
  200. );
  201. var indexTitle = -1;
  202. var indexContent = -1;
  203. handlePostTitle = postTitle
  204. ? escapeHtml(ignoreDiacriticalMarks(postTitle))
  205. : postTitle;
  206. handlePostContent = postContent
  207. ? escapeHtml(ignoreDiacriticalMarks(postContent))
  208. : postContent;
  209. indexTitle = postTitle ? handlePostTitle.search(regEx) : -1;
  210. indexContent = postContent ? handlePostContent.search(regEx) : -1;
  211. if (indexTitle >= 0 || indexContent >= 0) {
  212. matchesScore += indexTitle >= 0 ? 3 : indexContent >= 0 ? 2 : 0;
  213. if (indexContent < 0) {
  214. indexContent = 0;
  215. }
  216. var start = 0;
  217. var end = 0;
  218. start = indexContent < 11 ? 0 : indexContent - 10;
  219. end = start === 0 ? 70 : indexContent + keyword.length + 60;
  220. if (postContent && end > postContent.length) {
  221. end = postContent.length;
  222. }
  223. var matchContent =
  224. '...' +
  225. handlePostContent
  226. .substring(start, end)
  227. .replace(
  228. regEx,
  229. function (word) { return ("<em class=\"search-keyword\">" + word + "</em>"); }
  230. ) +
  231. '...';
  232. resultStr += matchContent;
  233. }
  234. });
  235. if (matchesScore > 0) {
  236. var matchingPost = {
  237. title: handlePostTitle,
  238. content: postContent ? resultStr : '',
  239. url: postUrl,
  240. score: matchesScore,
  241. };
  242. matchingResults.push(matchingPost);
  243. }
  244. }
  245. };
  246. for (var i = 0; i < data.length; i++) loop( i );
  247. return matchingResults.sort(function (r1, r2) { return r2.score - r1.score; });
  248. }
  249. function init(config, vm) {
  250. var isAuto = config.paths === 'auto';
  251. var paths = isAuto ? getAllPaths(vm.router) : config.paths;
  252. var namespaceSuffix = '';
  253. // only in auto mode
  254. if (paths.length && isAuto && config.pathNamespaces) {
  255. var path = paths[0];
  256. if (Array.isArray(config.pathNamespaces)) {
  257. namespaceSuffix =
  258. config.pathNamespaces.filter(
  259. function (prefix) { return path.slice(0, prefix.length) === prefix; }
  260. )[0] || namespaceSuffix;
  261. } else if (config.pathNamespaces instanceof RegExp) {
  262. var matches = path.match(config.pathNamespaces);
  263. if (matches) {
  264. namespaceSuffix = matches[0];
  265. }
  266. }
  267. var isExistHome = paths.indexOf(namespaceSuffix + '/') === -1;
  268. var isExistReadme = paths.indexOf(namespaceSuffix + '/README') === -1;
  269. if (isExistHome && isExistReadme) {
  270. paths.unshift(namespaceSuffix + '/');
  271. }
  272. } else if (paths.indexOf('/') === -1 && paths.indexOf('/README') === -1) {
  273. paths.unshift('/');
  274. }
  275. var expireKey = resolveExpireKey(config.namespace) + namespaceSuffix;
  276. var indexKey = resolveIndexKey(config.namespace) + namespaceSuffix;
  277. var isExpired = localStorage.getItem(expireKey) < Date.now();
  278. INDEXS = JSON.parse(localStorage.getItem(indexKey));
  279. if (isExpired) {
  280. INDEXS = {};
  281. } else if (!isAuto) {
  282. return;
  283. }
  284. var len = paths.length;
  285. var count = 0;
  286. paths.forEach(function (path) {
  287. if (INDEXS[path]) {
  288. return count++;
  289. }
  290. Docsify.get(vm.router.getFile(path), false, vm.config.requestHeaders).then(
  291. function (result) {
  292. INDEXS[path] = genIndex(path, result, vm.router, config.depth);
  293. len === ++count && saveData(config.maxAge, expireKey, indexKey);
  294. }
  295. );
  296. });
  297. }
  298. /* eslint-disable no-unused-vars */
  299. var NO_DATA_TEXT = '';
  300. var options;
  301. function style() {
  302. 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}";
  303. Docsify.dom.style(code);
  304. }
  305. function tpl(defaultValue) {
  306. if ( defaultValue === void 0 ) defaultValue = '';
  307. 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>";
  308. var el = Docsify.dom.create('div', html);
  309. var aside = Docsify.dom.find('aside');
  310. Docsify.dom.toggleClass(el, 'search');
  311. Docsify.dom.before(aside, el);
  312. }
  313. function doSearch(value) {
  314. var $search = Docsify.dom.find('div.search');
  315. var $panel = Docsify.dom.find($search, '.results-panel');
  316. var $clearBtn = Docsify.dom.find($search, '.clear-button');
  317. var $sidebarNav = Docsify.dom.find('.sidebar-nav');
  318. var $appName = Docsify.dom.find('.app-name');
  319. if (!value) {
  320. $panel.classList.remove('show');
  321. $clearBtn.classList.remove('show');
  322. $panel.innerHTML = '';
  323. if (options.hideOtherSidebarContent) {
  324. $sidebarNav && $sidebarNav.classList.remove('hide');
  325. $appName && $appName.classList.remove('hide');
  326. }
  327. return;
  328. }
  329. var matchs = search(value);
  330. var html = '';
  331. matchs.forEach(function (post) {
  332. html += "<div class=\"matching-post\">\n<a href=\"" + (post.url) + "\">\n<h2>" + (post.title) + "</h2>\n<p>" + (post.content) + "</p>\n</a>\n</div>";
  333. });
  334. $panel.classList.add('show');
  335. $clearBtn.classList.add('show');
  336. $panel.innerHTML = html || ("<p class=\"empty\">" + NO_DATA_TEXT + "</p>");
  337. if (options.hideOtherSidebarContent) {
  338. $sidebarNav && $sidebarNav.classList.add('hide');
  339. $appName && $appName.classList.add('hide');
  340. }
  341. }
  342. function bindEvents() {
  343. var $search = Docsify.dom.find('div.search');
  344. var $input = Docsify.dom.find($search, 'input');
  345. var $inputWrap = Docsify.dom.find($search, '.input-wrap');
  346. var timeId;
  347. /**
  348. Prevent to Fold sidebar.
  349. When searching on the mobile end,
  350. the sidebar is collapsed when you click the INPUT box,
  351. making it impossible to search.
  352. */
  353. Docsify.dom.on(
  354. $search,
  355. 'click',
  356. function (e) { return ['A', 'H2', 'P', 'EM'].indexOf(e.target.tagName) === -1 &&
  357. e.stopPropagation(); }
  358. );
  359. Docsify.dom.on($input, 'input', function (e) {
  360. clearTimeout(timeId);
  361. timeId = setTimeout(function (_) { return doSearch(e.target.value.trim()); }, 100);
  362. });
  363. Docsify.dom.on($inputWrap, 'click', function (e) {
  364. // Click input outside
  365. if (e.target.tagName !== 'INPUT') {
  366. $input.value = '';
  367. doSearch();
  368. }
  369. });
  370. }
  371. function updatePlaceholder(text, path) {
  372. var $input = Docsify.dom.getNode('.search input[type="search"]');
  373. if (!$input) {
  374. return;
  375. }
  376. if (typeof text === 'string') {
  377. $input.placeholder = text;
  378. } else {
  379. var match = Object.keys(text).filter(function (key) { return path.indexOf(key) > -1; })[0];
  380. $input.placeholder = text[match];
  381. }
  382. }
  383. function updateNoData(text, path) {
  384. if (typeof text === 'string') {
  385. NO_DATA_TEXT = text;
  386. } else {
  387. var match = Object.keys(text).filter(function (key) { return path.indexOf(key) > -1; })[0];
  388. NO_DATA_TEXT = text[match];
  389. }
  390. }
  391. function updateOptions(opts) {
  392. options = opts;
  393. }
  394. function init$1(opts, vm) {
  395. var keywords = vm.router.parse().query.s;
  396. updateOptions(opts);
  397. style();
  398. tpl(keywords);
  399. bindEvents();
  400. keywords && setTimeout(function (_) { return doSearch(keywords); }, 500);
  401. }
  402. function update(opts, vm) {
  403. updateOptions(opts);
  404. updatePlaceholder(opts.placeholder, vm.route.path);
  405. updateNoData(opts.noData, vm.route.path);
  406. }
  407. /* eslint-disable no-unused-vars */
  408. var CONFIG = {
  409. placeholder: 'Type to search',
  410. noData: 'No Results!',
  411. paths: 'auto',
  412. depth: 2,
  413. maxAge: 86400000, // 1 day
  414. hideOtherSidebarContent: false,
  415. namespace: undefined,
  416. pathNamespaces: undefined,
  417. };
  418. var install = function(hook, vm) {
  419. var util = Docsify.util;
  420. var opts = vm.config.search || CONFIG;
  421. if (Array.isArray(opts)) {
  422. CONFIG.paths = opts;
  423. } else if (typeof opts === 'object') {
  424. CONFIG.paths = Array.isArray(opts.paths) ? opts.paths : 'auto';
  425. CONFIG.maxAge = util.isPrimitive(opts.maxAge) ? opts.maxAge : CONFIG.maxAge;
  426. CONFIG.placeholder = opts.placeholder || CONFIG.placeholder;
  427. CONFIG.noData = opts.noData || CONFIG.noData;
  428. CONFIG.depth = opts.depth || CONFIG.depth;
  429. CONFIG.hideOtherSidebarContent =
  430. opts.hideOtherSidebarContent || CONFIG.hideOtherSidebarContent;
  431. CONFIG.namespace = opts.namespace || CONFIG.namespace;
  432. CONFIG.pathNamespaces = opts.pathNamespaces || CONFIG.pathNamespaces;
  433. }
  434. var isAuto = CONFIG.paths === 'auto';
  435. hook.mounted(function (_) {
  436. init$1(CONFIG, vm);
  437. !isAuto && init(CONFIG, vm);
  438. });
  439. hook.doneEach(function (_) {
  440. update(CONFIG, vm);
  441. isAuto && init(CONFIG, vm);
  442. });
  443. };
  444. $docsify.plugins = [].concat(install, $docsify.plugins);
  445. }());