mirror of https://github.com/IoTcat/docsify.git
parent
86594a3118
commit
079bd00395
22 changed files with 348 additions and 393 deletions
@ -1,349 +0,0 @@ |
||||
let INDEXS = {} |
||||
const CONFIG = { |
||||
placeholder: 'Type to search', |
||||
paths: 'auto', |
||||
maxAge: 86400000 // 1 day
|
||||
} |
||||
|
||||
const isObj = function (obj) { |
||||
return Object.prototype.toString.call(obj) === '[object Object]' |
||||
} |
||||
|
||||
const escapeHtml = function (string) { |
||||
const entityMap = { |
||||
'&': '&', |
||||
'<': '<', |
||||
'>': '>', |
||||
'"': '"', |
||||
"'": ''', |
||||
'/': '/' |
||||
} |
||||
|
||||
return String(string).replace(/[&<>"'\/]/g, s => entityMap[s]) |
||||
} |
||||
|
||||
/** |
||||
* find all filepath from A tag |
||||
*/ |
||||
const getAllPaths = function () { |
||||
const paths = [] |
||||
|
||||
;[].slice.call(document.querySelectorAll('a')) |
||||
.map(node => { |
||||
const href = node.href |
||||
if (/#\/[^#]*?$/.test(href)) { |
||||
const path = href.replace(/^[^#]+#/, '') |
||||
|
||||
if (paths.indexOf(path) <= 0) paths.push(path) |
||||
} |
||||
}) |
||||
|
||||
return paths |
||||
} |
||||
|
||||
/** |
||||
* return file path |
||||
*/ |
||||
const genFilePath = function (path, basePath = window.$docsify.basePath) { |
||||
let filePath = /\/$/.test(path) ? `${path}README.md` : `${path}.md` |
||||
|
||||
filePath = basePath + filePath |
||||
|
||||
return filePath.replace(/\/+/g, '/') |
||||
} |
||||
|
||||
/** |
||||
* generate index |
||||
*/ |
||||
const genIndex = function (path, content = '') { |
||||
INDEXS[path] = { slug: '', title: '', body: '' } |
||||
let slug |
||||
|
||||
content |
||||
// remove PRE and TEMPLATE tag
|
||||
.replace(/<template[^>]*?>[\s\S]+?<\/template>/g, '') |
||||
// find all html tag
|
||||
.replace(/<(\w+)([^>]*?)>([\s\S]+?)<\//g, (match, tag, attr, html) => { |
||||
// remove all html tag
|
||||
const text = html.replace(/<[^>]+>/g, '') |
||||
|
||||
// tag is headline
|
||||
if (/^h\d$/.test(tag)) { |
||||
// <h1 id="xxx"></h1>
|
||||
const id = attr.match(/id="(\S+)"/)[1] |
||||
|
||||
slug = `#/${path}#${id}`.replace(/\/+/, '/') |
||||
INDEXS[slug] = { slug, title: text, body: '' } |
||||
} else { |
||||
if (!slug) return |
||||
// other html tag
|
||||
if (!INDEXS[slug]) { |
||||
INDEXS[slug] = {} |
||||
} else { |
||||
if (INDEXS[slug].body && INDEXS[slug].body.length) { |
||||
INDEXS[slug].body += '\n' + text |
||||
} else { |
||||
INDEXS[slug].body = text |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* component |
||||
*/ |
||||
class SearchComponent { |
||||
constructor () { |
||||
if (this.rendered) return |
||||
|
||||
this.style() |
||||
|
||||
const el = document.createElement('div') |
||||
const aside = document.querySelector('aside') |
||||
|
||||
el.classList.add('search') |
||||
aside.insertBefore(el, aside.children[0]) |
||||
this.render(el) |
||||
this.rendered = true |
||||
this.bindEvent() |
||||
} |
||||
|
||||
style () { |
||||
const code = ` |
||||
.sidebar { |
||||
padding-top: 0; |
||||
} |
||||
|
||||
.search { |
||||
margin-bottom: 20px; |
||||
padding: 6px; |
||||
border-bottom: 1px solid #eee; |
||||
} |
||||
|
||||
.search .results-panel { |
||||
display: none; |
||||
} |
||||
|
||||
.search .results-panel.show { |
||||
display: block; |
||||
} |
||||
|
||||
.search input { |
||||
outline: none; |
||||
border: none; |
||||
width: 100%; |
||||
padding: 7px; |
||||
line-height: 22px; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
.search h2 { |
||||
font-size: 17px; |
||||
margin: 10px 0; |
||||
} |
||||
|
||||
.search a { |
||||
text-decoration: none; |
||||
color: inherit; |
||||
} |
||||
|
||||
.search .matching-post { |
||||
border-bottom: 1px solid #eee; |
||||
} |
||||
|
||||
.search .matching-post:last-child { |
||||
border-bottom: 0; |
||||
} |
||||
|
||||
.search p { |
||||
font-size: 14px; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
display: -webkit-box; |
||||
-webkit-line-clamp: 2; |
||||
-webkit-box-orient: vertical; |
||||
} |
||||
|
||||
.search p.empty { |
||||
text-align: center; |
||||
} |
||||
` |
||||
const style = document.createElement('style') |
||||
|
||||
style.innerHTML = code |
||||
document.head.appendChild(style) |
||||
} |
||||
|
||||
render (dom) { |
||||
dom.innerHTML = `<input type="search" placeholder="${CONFIG.placeholder}" /><div class="results-panel"></div>` |
||||
} |
||||
|
||||
bindEvent () { |
||||
const search = document.querySelector('.search') |
||||
const input = search.querySelector('.search input') |
||||
const panel = search.querySelector('.results-panel') |
||||
|
||||
search.addEventListener('click', e => e.target.tagName !== 'A' && e.stopPropagation()) |
||||
input.addEventListener('input', e => { |
||||
const target = e.target |
||||
|
||||
if (target.value.trim() !== '') { |
||||
const matchingPosts = this.search(target.value) |
||||
let html = '' |
||||
|
||||
matchingPosts.forEach(function (post, index) { |
||||
html += ` |
||||
<div class="matching-post"> |
||||
<h2><a href="${post.url}">${post.title}</a></h2> |
||||
<p>${post.content}</p> |
||||
</div> |
||||
` |
||||
}) |
||||
if (panel.classList.contains('results-panel')) { |
||||
panel.classList.add('show') |
||||
panel.innerHTML = html || '<p class="empty">No Results!</p>' |
||||
} |
||||
} else { |
||||
if (panel.classList.contains('results-panel')) { |
||||
panel.classList.remove('show') |
||||
panel.innerHTML = '' |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// From [weex website] https://weex-project.io/js/common.js
|
||||
search (keywords) { |
||||
const matchingResults = [] |
||||
const data = Object.keys(INDEXS).map(key => INDEXS[key]) |
||||
|
||||
keywords = keywords.trim().split(/[\s\-\,\\/]+/) |
||||
|
||||
for (let i = 0; i < data.length; i++) { |
||||
const post = data[i] |
||||
let isMatch = false |
||||
let resultStr = '' |
||||
const postTitle = post.title && post.title.trim() |
||||
const postContent = post.body && post.body.trim() |
||||
const postUrl = post.slug || '' |
||||
|
||||
if (postTitle !== '' && postContent !== '') { |
||||
keywords.forEach((keyword, i) => { |
||||
const regEx = new RegExp(keyword, 'gi') |
||||
let indexTitle = -1 |
||||
let indexContent = -1 |
||||
|
||||
indexTitle = postTitle.search(regEx) |
||||
indexContent = postContent.search(regEx) |
||||
|
||||
if (indexTitle < 0 && indexContent < 0) { |
||||
isMatch = false |
||||
} else { |
||||
isMatch = true |
||||
if (indexContent < 0) indexContent = 0 |
||||
|
||||
let start = 0 |
||||
let end = 0 |
||||
|
||||
start = indexContent < 11 ? 0 : indexContent - 10 |
||||
end = start === 0 ? 70 : indexContent + keyword.length + 60 |
||||
|
||||
if (end > postContent.length) end = postContent.length |
||||
|
||||
const matchContent = '...' + |
||||
postContent |
||||
.substring(start, end) |
||||
.replace(regEx, `<em class="search-keyword">${keyword}</em>`) + |
||||
'...' |
||||
|
||||
resultStr += matchContent |
||||
} |
||||
}) |
||||
|
||||
if (isMatch) { |
||||
const matchingPost = { |
||||
title: escapeHtml(postTitle), |
||||
content: resultStr, |
||||
url: postUrl |
||||
} |
||||
|
||||
matchingResults.push(matchingPost) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return matchingResults |
||||
} |
||||
} |
||||
|
||||
const searchPlugin = function () { |
||||
const isAuto = CONFIG.paths === 'auto' |
||||
const isExpired = localStorage.getItem('docsify.search.expires') < Date.now() |
||||
|
||||
INDEXS = JSON.parse(localStorage.getItem('docsify.search.index')) |
||||
|
||||
if (isExpired) { |
||||
INDEXS = {} |
||||
} else if (!isAuto) { |
||||
return |
||||
} |
||||
|
||||
let count = 0 |
||||
const paths = isAuto ? getAllPaths() : CONFIG.paths |
||||
const len = paths.length |
||||
const { load, marked, slugify } = window.Docsify.utils |
||||
const alias = window.$docsify.alias |
||||
const done = () => { |
||||
localStorage.setItem('docsify.search.expires', Date.now() + CONFIG.maxAge) |
||||
localStorage.setItem('docsify.search.index', JSON.stringify(INDEXS)) |
||||
} |
||||
|
||||
paths.forEach(path => { |
||||
if (INDEXS[path]) return count++ |
||||
let route |
||||
|
||||
// replace route
|
||||
if (alias && alias[path]) { |
||||
route = genFilePath(alias[path] || path, '') |
||||
} else { |
||||
route = genFilePath(path) |
||||
} |
||||
|
||||
load(route).then(content => { |
||||
genIndex(path, marked(content)) |
||||
slugify.clear() |
||||
count++ |
||||
|
||||
if (len === count) done() |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
const install = function () { |
||||
if (install.installed) return |
||||
install.installed = true |
||||
|
||||
const userConfig = window.$docsify.search |
||||
const isNil = window.Docsify.utils.isNil |
||||
|
||||
if (Array.isArray(userConfig)) { |
||||
CONFIG.paths = userConfig |
||||
} else if (isObj(userConfig)) { |
||||
CONFIG.paths = Array.isArray(userConfig.paths) ? userConfig.paths : 'auto' |
||||
CONFIG.maxAge = isNil(userConfig.maxAge) ? CONFIG.maxAge : userConfig.maxAge |
||||
CONFIG.placeholder = userConfig.placeholder || CONFIG.placeholder |
||||
} |
||||
|
||||
window.$docsify.plugins = [].concat(hook => { |
||||
const isAuto = CONFIG.paths === 'auto' |
||||
|
||||
hook.ready(() => { |
||||
new SearchComponent() |
||||
!isAuto && searchPlugin() |
||||
}) |
||||
isAuto && hook.doneEach(searchPlugin) |
||||
}, window.$docsify.plugins) |
||||
} |
||||
|
||||
export default install() |
@ -0,0 +1,116 @@ |
||||
import { search } from './search' |
||||
|
||||
let dom |
||||
|
||||
function style () { |
||||
const code = ` |
||||
.sidebar { |
||||
padding-top: 0; |
||||
} |
||||
|
||||
.search { |
||||
margin-bottom: 20px; |
||||
padding: 6px; |
||||
border-bottom: 1px solid #eee; |
||||
} |
||||
|
||||
.search .results-panel { |
||||
display: none; |
||||
} |
||||
|
||||
.search .results-panel.show { |
||||
display: block; |
||||
} |
||||
|
||||
.search input { |
||||
outline: none; |
||||
border: none; |
||||
width: 100%; |
||||
padding: 7px; |
||||
line-height: 22px; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
.search h2 { |
||||
font-size: 17px; |
||||
margin: 10px 0; |
||||
} |
||||
|
||||
.search a { |
||||
text-decoration: none; |
||||
color: inherit; |
||||
} |
||||
|
||||
.search .matching-post { |
||||
border-bottom: 1px solid #eee; |
||||
} |
||||
|
||||
.search .matching-post:last-child { |
||||
border-bottom: 0; |
||||
} |
||||
|
||||
.search p { |
||||
font-size: 14px; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
display: -webkit-box; |
||||
-webkit-line-clamp: 2; |
||||
-webkit-box-orient: vertical; |
||||
} |
||||
|
||||
.search p.empty { |
||||
text-align: center; |
||||
}` |
||||
const style = dom.create('style', code) |
||||
dom.appendTo(dom.head, style) |
||||
} |
||||
|
||||
function tpl (opts) { |
||||
const html = |
||||
`<input type="search" placeholder="${opts.placeholder}" />` + |
||||
'<div class="results-panel"></div>' + |
||||
'</div>' |
||||
const el = dom.create('div', html) |
||||
const aside = dom.find('aside') |
||||
|
||||
dom.toggleClass(el, 'search') |
||||
dom.before(aside, el) |
||||
} |
||||
|
||||
function bindEvents () { |
||||
const $search = dom.find('div.search') |
||||
const $input = dom.find($search, 'input') |
||||
const $panel = dom.find($search, '.results-panel') |
||||
|
||||
// Prevent to Fold sidebar
|
||||
dom.on($search, 'click', |
||||
e => e.target.tagName !== 'A' && e.stopPropagation()) |
||||
|
||||
dom.on($input, 'input', e => { |
||||
const value = e.target.value.trim() |
||||
if (!value) { |
||||
$panel.classList.remove('show') |
||||
$panel.innerHTML = '' |
||||
} |
||||
const matchs = search(value) |
||||
|
||||
let html = '' |
||||
|
||||
matchs.forEach(post => { |
||||
html += `<div class="matching-post">
|
||||
<h2><a href="${post.url}">${post.title}</a></h2> |
||||
<p>${post.content}</p> |
||||
</div>` |
||||
}) |
||||
|
||||
$panel.classList.add('show') |
||||
$panel.innerHTML = html || '<p class="empty">No Results!</p>' |
||||
}) |
||||
} |
||||
|
||||
export default function (opts) { |
||||
dom = Docsify.dom |
||||
style() |
||||
tpl(opts) |
||||
bindEvents() |
||||
} |
@ -0,0 +1,31 @@ |
||||
import initComponet from './component' |
||||
import { init as initSearch } from './search' |
||||
|
||||
const CONFIG = { |
||||
placeholder: 'Type to search', |
||||
paths: 'auto', |
||||
maxAge: 86400000 // 1 day
|
||||
} |
||||
|
||||
const install = function (hook, vm) { |
||||
const util = Docsify.util |
||||
const opts = vm.config.search |
||||
|
||||
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 |
||||
} |
||||
|
||||
const isAuto = CONFIG.paths === 'auto' |
||||
|
||||
hook.ready(_ => { |
||||
initComponet(CONFIG) |
||||
isAuto && initSearch(CONFIG, vm) |
||||
}) |
||||
!isAuto && hook.doneEach(_ => initSearch(CONFIG, vm)) |
||||
} |
||||
|
||||
window.$docsify.plugins = [].concat(install, window.$docsify.plugins) |
@ -0,0 +1,156 @@ |
||||
let INDEXS = {} |
||||
let helper |
||||
|
||||
function escapeHtml (string) { |
||||
const entityMap = { |
||||
'&': '&', |
||||
'<': '<', |
||||
'>': '>', |
||||
'"': '"', |
||||
'\'': ''', |
||||
'/': '/' |
||||
} |
||||
|
||||
return String(string).replace(/[&<>"'\/]/g, s => entityMap[s]) |
||||
} |
||||
|
||||
function getAllPaths () { |
||||
const paths = [] |
||||
|
||||
helper.dom.findAll('a') |
||||
.map(node => { |
||||
const href = node.href |
||||
const originHref = node.getAttribute('href') |
||||
const path = helper.route.parse(href).path |
||||
|
||||
if (paths.indexOf(path) === -1 && |
||||
!helper.route.isAbsolutePath(originHref)) { |
||||
paths.push(path) |
||||
} |
||||
}) |
||||
|
||||
return paths |
||||
} |
||||
|
||||
function saveData (maxAge) { |
||||
localStorage.setItem('docsify.search.expires', Date.now() + maxAge) |
||||
localStorage.setItem('docsify.search.index', JSON.stringify(INDEXS)) |
||||
} |
||||
|
||||
export function genIndex (path, content = '') { |
||||
const tokens = window.marked.lexer(content) |
||||
const toURL = Docsify.route.toURL |
||||
let slug |
||||
|
||||
tokens.forEach(token => { |
||||
if (token.type === 'heading' && token.depth === 1) { |
||||
slug = toURL(path, { id: token.text }) |
||||
INDEXS[slug] = { slug, title: token.text, body: '' } |
||||
} else { |
||||
if (!slug) return |
||||
if (!INDEXS[slug]) { |
||||
INDEXS[slug] = { slug, title: '', body: '' } |
||||
} else { |
||||
if (INDEXS[slug].body) { |
||||
INDEXS[slug].body += ('\n' + token.text) |
||||
} else { |
||||
INDEXS[slug].body = token.text |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
export function search (keywords) { |
||||
const matchingResults = [] |
||||
const data = Object.keys(INDEXS).map(key => INDEXS[key]) |
||||
|
||||
keywords = keywords.trim().split(/[\s\-\,\\/]+/) |
||||
|
||||
for (let i = 0; i < data.length; i++) { |
||||
const post = data[i] |
||||
let isMatch = false |
||||
let resultStr = '' |
||||
const postTitle = post.title && post.title.trim() |
||||
const postContent = post.body && post.body.trim() |
||||
const postUrl = post.slug || '' |
||||
|
||||
if (postTitle !== '' && postContent !== '') { |
||||
keywords.forEach((keyword, i) => { |
||||
const regEx = new RegExp(keyword, 'gi') |
||||
let indexTitle = -1 |
||||
let indexContent = -1 |
||||
|
||||
indexTitle = postTitle.search(regEx) |
||||
indexContent = postContent.search(regEx) |
||||
|
||||
if (indexTitle < 0 && indexContent < 0) { |
||||
isMatch = false |
||||
} else { |
||||
isMatch = true |
||||
if (indexContent < 0) indexContent = 0 |
||||
|
||||
let start = 0 |
||||
let end = 0 |
||||
|
||||
start = indexContent < 11 ? 0 : indexContent - 10 |
||||
end = start === 0 ? 70 : indexContent + keyword.length + 60 |
||||
|
||||
if (end > postContent.length) end = postContent.length |
||||
|
||||
const matchContent = '...' + |
||||
postContent |
||||
.substring(start, end) |
||||
.replace(regEx, `<em class="search-keyword">${keyword}</em>`) + |
||||
'...' |
||||
|
||||
resultStr += matchContent |
||||
} |
||||
}) |
||||
|
||||
if (isMatch) { |
||||
const matchingPost = { |
||||
title: escapeHtml(postTitle), |
||||
content: resultStr, |
||||
url: postUrl |
||||
} |
||||
|
||||
matchingResults.push(matchingPost) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return matchingResults |
||||
} |
||||
|
||||
export function init (config, vm) { |
||||
helper = Docsify |
||||
|
||||
const isAuto = config.paths === 'auto' |
||||
const isExpired = localStorage.getItem('docsify.search.expires') < Date.now() |
||||
|
||||
INDEXS = JSON.parse(localStorage.getItem('docsify.search.index')) |
||||
|
||||
if (isExpired) { |
||||
INDEXS = {} |
||||
} else if (!isAuto) { |
||||
return |
||||
} |
||||
|
||||
const paths = isAuto ? getAllPaths() : config.paths |
||||
const len = paths.length |
||||
let count = 0 |
||||
|
||||
paths.forEach(path => { |
||||
if (INDEXS[path]) return count++ |
||||
|
||||
path = vm.$getFile(path) |
||||
helper |
||||
.get(path) |
||||
.then(result => { |
||||
genIndex(path, result) |
||||
len === ++count && saveData(config.maxAge) |
||||
}) |
||||
}) |
||||
} |
||||
|
Loading…
Reference in new issue