相关工程部署可参考docsify(一)https://blog.csdn.net/u598975767/article/details/122576505docsify(二)https://blog.csdn.net/u598975767/article/details/122746132
先说一下改动前docsify自带的关键词检索功能:
1. 支持多个关键词同时检索,各关键词以空格、\等符号分开。此功能bug较多,会导致检索结果混乱。
2. 检索结果链接到落地页后,不支持高亮显示,不直观。
3. 原来的检索是基于markdown文件源码的搜索,导致源代码中的html标签、markdown标签等也会被命中。例如搜索“h1”,在检索的导航面板中会显示很多匹配到的文件,点击该文件导航后,打开的文件内容中并没有"h1"字符。(因为匹配到的只是源码字符串,而此时源码已经被浏览器解析了),再说了看个文档,也没人想去搜索其源代码里的字符串吧~~~
4. 输入特殊字符,如<、>、?、#、/、等等,会导致搜索结果混乱,因为这些特殊字符在代码解析过程中会出现转义、混淆等一系列问题。
再列一下新增的功能,以及问题修复:
1. 禁用纯特殊字符的检索;
2. 检索时忽略html源码、markdown源码等,只对文本检索;
3. 禁用多关键词检索;
4. 检索结果导航到具体页面时,支持高亮显示。
下面上代码喽:
相关文件三个:
search.js 源文件 做了部分调整,已添加中文注释;
hightlight.js 新增文件 重要逻辑已添加中文注释;
index.html 入口文件 仅添加了关键词高亮的样式和对highlight.js文件的引入。
search.js
(function () {
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) {
//放开此方法 会导致搜索结果的content中出现html原生标签
return string
var entityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
'\'': ''',
'/': '/'
};
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 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) {
slug = router.toURL(path, { id: slugify(token.text) });
index[slug] = { slug: slug, title: token.text, body: '' };
} else {
if (!slug) {
return
}
if (!index[slug]) {
index[slug] = { slug: slug, title: '', body: '' };
} else if (index[slug].body) {
index[slug].body += '\n' + (token.text || '');
} else {
index[slug].body = token.text;
}
}
});
slugify.clear();
return index
}
/**
* 此方法 用途是去掉字符串中的所有html原生代码 只保留真实内容 用作搜索匹配
* @param {*} str
* @returns
*/
function getText(str) {
return str ? $("<span>" + str + "</span>").text() : ''
}
/**
* @param {String} query
* @returns {Array}
*/
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();
//禁止检索多个关键字 此功能放开需要重新调整 main内容高亮的逻辑 hightlight.js
//var keywords = query.split(/[\s\-,\\/]+/);
var keywords = [query]
if (keywords.length !== 1) {
keywords = [].concat(query, keywords);
}
var loop = function (i) {
var post = data[i];
var isMatch = false;
var resultStr = '';
var postTitle = post.title && post.title.trim();
var postContent = post.body && post.body.trim();
var postUrl = post.slug || '';
if (postTitle && postContent) {
keywords.forEach(function (keyword) {
var regEx = new RegExp(
keyword.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'),
'gi'
);
var indexTitle = -1;
var indexContent = -1;
//这里调用getText 是为了忽略字符串中html源代码
if (postTitle) {
postTitle = getText(postTitle)
indexTitle = postTitle.search(regEx);
}
if (postContent) {
// 屏蔽markdown中的链接
postContent = getText(postContent).replace(/(\]\([^\)]*\))/g, ']')
// indexContent = postContent.search(regEx);
indexContent = postContent.search(regEx);
}
if (indexTitle < 0 && indexContent < 0) {
isMatch = false;
} else {
isMatch = true;
if (indexContent < 0) {
indexContent = 0;
}
var start = 0;
var end = 0;
//显示前10个字符 + keywords + 后60个字符 如果在开始位 keywords +后70个字符
start = indexContent < 11 ? 0 : indexContent - 10;
end = start === 0 ? 70 : indexContent + keyword.length + 60;
if (end > postContent.length) {
end = postContent.length;
}
var matchContent =
'...' +
escapeHtml(postContent)
.substring(start, end)
.replace(regEx, ("<em class=\"search-keyword\">" + keyword + "</em>")) +
'...';
postTitle = postTitle.replace(regEx, ("<em class=\"search-keyword\">" + keyword + "</em>"))
resultStr += matchContent;
}
});
if (isMatch) {
var matchingPost = {
title: escapeHtml(postTitle),
content: resultStr,
url: postUrl
};
matchingResults.push(matchingPost);
}
}
};
for (var i = 0; i < data.length; i++) loop(i);
return matchingResults
}
function init$1(config, vm) {
var isAuto = config.paths === 'auto';
var expireKey = resolveExpireKey(config.namespace);
var indexKey = resolveIndexKey(config.namespace);
var isExpired = localStorage.getItem(expireKey) < Date.now();
INDEXS = JSON.parse(localStorage.getItem(indexKey)) || {};
if (isExpired) {
INDEXS = {};
} else if (!isAuto) {
return
}
var paths = isAuto ? getAllPaths(vm.router) : config.paths;
var len = paths.length;
var count = 0;
var temp = []
paths.forEach(function (path) {
if (INDEXS[path]) {
return count++
}
temp.push(new Promise((resolve, reject) => {
Docsify.get(vm.router.getFile(path), false, vm.config.requestHeaders).then(function (reslut) {
resolve({ path, reslut })
}, reject)
}))
});
//待所有请求均完成以后再执行回调 否则后续INDEXS读取不完整
Promise.all(temp).then(result => {
result.forEach(function (res) {
INDEXS[res.path] = genIndex(res.path, res.reslut, vm.router, config.depth);
len === ++count && saveData(config.maxAge, expireKey, indexKey);
})
}).catch((error) => {
console.log(error)
})
}
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}\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 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 + "\" />\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 reg = /^[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘',。、]+$/
// 禁止检索纯特殊字符
if (value && reg.test(value)) {
return false
}
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 + "&s=" + value + "\">\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
Docsify.dom.on(
$search,
'click',
function (e) { return e.target.tagName !== 'A' && 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(opts, vm, isAuto) {
var keywords = vm.router.parse().query.s;
updateOptions(opts);
style();
tpl(keywords);
bindEvents();
keywords && setTimeout(function (_) {
doSearch(keywords);
}, 500);
}
function update(opts, vm) {
updateOptions(opts);
updatePlaceholder(opts.placeholder, vm.route.path);
updateNoData(opts.noData, vm.route.path);
}
var CONFIG = {
placeholder: 'Type to search',
noData: 'No Results!',
paths: 'auto',
depth: 2,
maxAge: 86400000, // 1 day
hideOtherSidebarContent: false,
namespace: 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;
}
var isAuto = CONFIG.paths === 'auto';
hook.mounted(function (_) {
init(CONFIG, vm, isAuto);
!isAuto && init$1(CONFIG, vm);
});
hook.doneEach(function (_) {
update(CONFIG, vm);
isAuto && init$1(CONFIG, vm);
});
};
$docsify.plugins = [].concat(install, $docsify.plugins);
}());
highlight.js
(function () {
var timer = null
//页面初始化完成
$(document).ready(function () {
//监听 #main 内容发生改变
$("#main").on('DOMNodeInserted', setHighlight)
})
/**
* 在主内容(#main)中设置高亮
* 这里timeout做防抖
*/
function setHighlight() {
timer && clearTimeout(timer)
timer = setTimeout(function () {
var search = getSearch().s
if (search) {
//去掉上次匹配关键词的样式
$("#main .search-keyword").removeClass("search-keyword")
var newHtml = getHtmlStr($("#main").html(), search)
//先解绑dom更新监听 待设置高亮完成后再重新注册监听 否则会进入死循环
newHtml && $("#main").off('DOMNodeInserted').html(newHtml).on('DOMNodeInserted', setHighlight)
}
}, 200)
}
/**
* 忽略命中html标签内的属性
* 分别对<、>的位置进行判断 区分关键字是否出现在html标签中间,
* 如果是 跳过 不替换
* 如果不是 替换高亮展示
*/
function getHtmlStr(htmlStr, search) {
//用关键字 区分大小写 分割字符串
var reg = new RegExp(search, 'i')
var tempList = htmlStr.split(reg)
var newHtml = tempList.shift()
var newHtmlIndex = newHtml === undefined ? 0 : newHtml.length
tempList.map(temp => {
var strLastLeft = newHtml.lastIndexOf("<")
var strLastRight = newHtml.lastIndexOf(">")
if (strLastRight < strLastLeft) {//最后的标签未闭合
newHtml = newHtml + search + temp
} else {
// 获取原字符串中的关键字 为了区分大小写显示 添加高亮样式
newHtml = newHtml + "<em class='search-keyword'>" + htmlStr.substr(newHtmlIndex, search.length) + "</em>" + temp
}
//计算原关键字在原字符串中的位置
newHtmlIndex = newHtmlIndex + search.length + temp.length
})
return newHtml
}
/**
* 获取搜索关键字
*/
function getSearch() {
let hash = location.hash
let search = hash && hash.indexOf("?") > -1 && hash.substring(hash.indexOf("?") + 1)
let searchObj = {}
if (search) {
search.split("&").map(key_value => {
let temp = key_value.split("=")
searchObj[temp[0]] = decodeURI(temp[1])
})
}
return searchObj
}
}())
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文档中心</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<!-- <link rel="stylesheet" href="vendor/themes/vue.css"> -->
<link rel="stylesheet" href="vendor/themes/theme-simple.css">
<link rel="stylesheet" href="vendor/themes/theme-custom.css">
<link rel="stylesheet" href="vendor/themes/docsify-copy-code.css">
<link rel="stylesheet" href="vendor/themes/prism-tomorrow.css">
<style>
.sidebar-nav>ul>li>ul>li>ul>li>a:before {
content: ""
}
.sidebar-nav>ul>li>ul>li>ul>li.active>a:before {
content: ""
}
em.search-keyword {
background: #f7e3a7;
font-style: normal;
padding: 2px;
border-radius: 2px;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="docsify.config.js"></script>
<script src="vendor/docsify.js"></script>
<script src="vendor/plugins/docsify-themeable.js"></script>
<script src="vendor/plugins/zoom-image.min.js"></script>
<script src="vendor/plugins/search.js"></script>
<script src="vendor/plugins/docsify-copy-code.min.js"></script>
<script src="vendor/plugins/docsify-pagination.min.js"></script>
<!--页面页签插件-->
<script src="vendor/plugins/docsify-tabs.js"></script>
<!-- Alerts样式插件 -->
<script src="vendor/plugins/docsify-plugin-flexible-alerts.min.js"></script>
<script src="vendor/jquery.js"></script>
<script src="vendor/plugins/hightlight.js"></script>
</body>
</html>
不到之处,烦请指正~~