refactor(plugins): update search plugin
This commit is contained in:
parent
86594a3118
commit
079bd00395
22 changed files with 348 additions and 393 deletions
|
|
@ -2,5 +2,9 @@
|
||||||
"extends": ["vue"],
|
"extends": ["vue"],
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true
|
"browser": true
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"Docsify": true,
|
||||||
|
"$docsify": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,26 +32,26 @@ build({
|
||||||
plugins: [commonjs(), nodeResolve()]
|
plugins: [commonjs(), nodeResolve()]
|
||||||
})
|
})
|
||||||
|
|
||||||
// build({
|
build({
|
||||||
// entry: 'plugins/search.js',
|
entry: 'plugins/search/index.js',
|
||||||
// output: 'plugins/search.js',
|
output: 'plugins/search.js',
|
||||||
// moduleName: 'D.Search'
|
moduleName: 'D.Search'
|
||||||
// })
|
})
|
||||||
|
|
||||||
// build({
|
build({
|
||||||
// entry: 'plugins/ga.js',
|
entry: 'plugins/ga.js',
|
||||||
// output: 'plugins/ga.js',
|
output: 'plugins/ga.js',
|
||||||
// moduleName: 'D.GA'
|
moduleName: 'D.GA'
|
||||||
// })
|
})
|
||||||
|
|
||||||
if (isProd) {
|
if (isProd) {
|
||||||
build({
|
build({
|
||||||
entry: 'index.js',
|
entry: 'core/index.js',
|
||||||
output: 'docsify.min.js',
|
output: 'docsify.min.js',
|
||||||
plugins: [commonjs(), nodeResolve(), uglify()]
|
plugins: [commonjs(), nodeResolve(), uglify()]
|
||||||
})
|
})
|
||||||
build({
|
build({
|
||||||
entry: 'plugins/search.js',
|
entry: 'plugins/search/index.js',
|
||||||
output: 'plugins/search.min.js',
|
output: 'plugins/search.min.js',
|
||||||
moduleName: 'D.Search',
|
moduleName: 'D.Search',
|
||||||
plugins: [uglify()]
|
plugins: [uglify()]
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
|
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav data-cloak>
|
||||||
<a href="#/">En</a>
|
<a href="#/">En</a>
|
||||||
<a href="#/zh-cn/">中文</a>
|
<a href="#/zh-cn/">中文</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ window.$docsify = {
|
||||||
})
|
})
|
||||||
|
|
||||||
hook.ready(function() {
|
hook.ready(function() {
|
||||||
// Called after initialization is complete. Only trigger once, no arguments.
|
// Called after initial completion, no arguments.
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ window.$docsify = {
|
||||||
})
|
})
|
||||||
|
|
||||||
hook.ready(function() {
|
hook.ready(function() {
|
||||||
// 初始化完成后调用,只调用一次,没有参数。
|
// 初始化并第一次加完成数据后调用,没有参数。
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"build": "rm -rf lib themes && node build/build.js && mkdir lib/themes && mkdir themes && node build/build-css.js",
|
"build": "rm -rf lib themes && node build/build.js && mkdir lib/themes && mkdir themes && node build/build-css.js",
|
||||||
"dev:build": "rm -rf lib themes && mkdir themes && node build/build.js --dev && node build/build-css.js --dev",
|
"dev:build": "rm -rf lib themes && mkdir themes && node build/build.js --dev && node build/build-css.js --dev",
|
||||||
"dev": "node app.js & nodemon -w src -e js,css --exec 'npm run dev:build'",
|
"dev": "node app.js & nodemon -w src -e js,css --exec 'npm run dev:build'",
|
||||||
"test": "eslint src test"
|
"test": "eslint src"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
|
|
@ -39,4 +39,6 @@ if (script) {
|
||||||
if (config.name === true) config.name = ''
|
if (config.name === true) config.name = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.$docsify = config
|
||||||
|
|
||||||
export default config
|
export default config
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,8 @@ export function scrollActiveSidebar () {
|
||||||
const li = nav[last.getAttribute('data-id')]
|
const li = nav[last.getAttribute('data-id')]
|
||||||
|
|
||||||
if (!li || li === active) return
|
if (!li || li === active) return
|
||||||
if (active) active.classList.remove('active')
|
|
||||||
|
|
||||||
|
active && active.classList.remove('active')
|
||||||
li.classList.add('active')
|
li.classList.add('active')
|
||||||
active = li
|
active = li
|
||||||
|
|
||||||
|
|
@ -79,7 +79,7 @@ export function scrollActiveSidebar () {
|
||||||
|
|
||||||
export function scrollIntoView (id) {
|
export function scrollIntoView (id) {
|
||||||
const section = dom.find('#' + id)
|
const section = dom.find('#' + id)
|
||||||
section && setTimeout(() => section.scrollIntoView(), 0)
|
section && section.scrollIntoView()
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollEl = dom.$.scrollingElement || dom.$.documentElement
|
const scrollEl = dom.$.scrollingElement || dom.$.documentElement
|
||||||
|
|
|
||||||
|
|
@ -57,15 +57,16 @@ export function fetchMixin (proto) {
|
||||||
.then(text => this._renderCover(text))
|
.then(text => this._renderCover(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
proto.$fetch = function () {
|
proto.$fetch = function (cb = noop) {
|
||||||
this._fetchCover()
|
this._fetchCover()
|
||||||
this._fetch(result => {
|
this._fetch(result => {
|
||||||
this.$resetEvents()
|
this.$resetEvents()
|
||||||
callHook(this, 'doneEach')
|
callHook(this, 'doneEach')
|
||||||
|
cb()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initFetch (vm) {
|
export function initFetch (vm) {
|
||||||
vm.$fetch()
|
vm.$fetch(_ => callHook(vm, 'ready'))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import * as util from './util'
|
import * as util from './util'
|
||||||
import * as dom from './util/dom'
|
import * as dom from './util/dom'
|
||||||
import * as render from './render/compiler'
|
import * as render from './render/compiler'
|
||||||
import * as route from './route/util'
|
import * as route from './route/hash'
|
||||||
import { get } from './fetch/ajax'
|
import { get } from './fetch/ajax'
|
||||||
import marked from 'marked'
|
import marked from 'marked'
|
||||||
import prism from 'prismjs'
|
import prism from 'prismjs'
|
||||||
|
|
|
||||||
|
|
@ -25,4 +25,5 @@ initGlobalAPI()
|
||||||
/**
|
/**
|
||||||
* Run Docsify
|
* Run Docsify
|
||||||
*/
|
*/
|
||||||
new Docsify()
|
|
||||||
|
setTimeout(_ => new Docsify(), 0)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ export function initMixin (proto) {
|
||||||
initEvent(vm) // Bind events
|
initEvent(vm) // Bind events
|
||||||
initRoute(vm) // Add hashchange eventListener
|
initRoute(vm) // Add hashchange eventListener
|
||||||
initFetch(vm) // Fetch data
|
initFetch(vm) // Fetch data
|
||||||
callHook(vm, 'ready')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { genTree } from './gen-tree'
|
||||||
import { slugify, clearSlugCache } from './slugify'
|
import { slugify, clearSlugCache } from './slugify'
|
||||||
import { emojify } from './emojify'
|
import { emojify } from './emojify'
|
||||||
import { toURL, parse } from '../route/hash'
|
import { toURL, parse } from '../route/hash'
|
||||||
import { getBasePath, isResolvePath, getPath } from '../route/util'
|
import { getBasePath, isAbsolutePath, getPath } from '../route/util'
|
||||||
import { isFn, merge, cached } from '../util/core'
|
import { isFn, merge, cached } from '../util/core'
|
||||||
|
|
||||||
let markdownCompiler = marked
|
let markdownCompiler = marked
|
||||||
|
|
@ -87,7 +87,7 @@ renderer.image = function (href, title, text) {
|
||||||
let url = href
|
let url = href
|
||||||
const titleHTML = title ? ` title="${title}"` : ''
|
const titleHTML = title ? ` title="${title}"` : ''
|
||||||
|
|
||||||
if (!isResolvePath(href)) {
|
if (!isAbsolutePath(href)) {
|
||||||
url = getPath(contentBase, href)
|
url = getPath(contentBase, href)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import cssVars from '../util/polyfill/css-vars'
|
||||||
import * as tpl from './tpl'
|
import * as tpl from './tpl'
|
||||||
import { markdown, sidebar, subSidebar, cover } from './compiler'
|
import { markdown, sidebar, subSidebar, cover } from './compiler'
|
||||||
import { callHook } from '../init/lifecycle'
|
import { callHook } from '../init/lifecycle'
|
||||||
import { getBasePath, getPath, isResolvePath } from '../route/util'
|
import { getBasePath, getPath, isAbsolutePath } from '../route/util'
|
||||||
|
|
||||||
function executeScript () {
|
function executeScript () {
|
||||||
const script = dom.findAll('.markdown-section>script')
|
const script = dom.findAll('.markdown-section>script')
|
||||||
|
|
@ -101,7 +101,7 @@ export function renderMixin (proto) {
|
||||||
let path = m[1]
|
let path = m[1]
|
||||||
|
|
||||||
dom.toggleClass(el, 'add', 'has-mask')
|
dom.toggleClass(el, 'add', 'has-mask')
|
||||||
if (isResolvePath(m[1])) {
|
if (isAbsolutePath(m[1])) {
|
||||||
path = getPath(getBasePath(this.config.basePath), m[1])
|
path = getPath(getBasePath(this.config.basePath), m[1])
|
||||||
}
|
}
|
||||||
el.style.backgroundImage = `url(${path})`
|
el.style.backgroundImage = `url(${path})`
|
||||||
|
|
@ -157,4 +157,5 @@ export function initRender (vm) {
|
||||||
// Polyfll
|
// Polyfll
|
||||||
cssVars(config.themeColor)
|
cssVars(config.themeColor)
|
||||||
}
|
}
|
||||||
|
dom.toggleClass(dom.body, 'ready')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { merge, cached } from '../util/core'
|
import { merge, cached } from '../util/core'
|
||||||
import { parseQuery, stringifyQuery, cleanPath } from './util'
|
import { parseQuery, stringifyQuery, cleanPath } from './util'
|
||||||
|
export * from './util'
|
||||||
|
|
||||||
function replaceHash (path) {
|
function replaceHash (path) {
|
||||||
const i = window.location.href.indexOf('#')
|
const i = window.location.href.indexOf('#')
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { normalize, parse } from './hash'
|
import { normalize, parse } from './hash'
|
||||||
import { getBasePath, getPath } from './util'
|
import { getBasePath, getPath, isAbsolutePath } from './util'
|
||||||
import { on } from '../util/dom'
|
import { on } from '../util/dom'
|
||||||
|
|
||||||
function getAlias (path, alias) {
|
function getAlias (path, alias) {
|
||||||
if (alias[path]) return getAlias(alias[path], alias)
|
return alias[path] ? getAlias(alias[path], alias) : path
|
||||||
return path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileName (path) {
|
function getFileName (path) {
|
||||||
|
|
@ -24,7 +23,7 @@ export function routeMixin (proto) {
|
||||||
path = getAlias(path, config.alias)
|
path = getAlias(path, config.alias)
|
||||||
path = getFileName(path)
|
path = getFileName(path)
|
||||||
path = path === '/README.md' ? (config.homepage || path) : path
|
path = path === '/README.md' ? (config.homepage || path) : path
|
||||||
path = getPath(base, path)
|
path = isAbsolutePath(path) ? path : getPath(base, path)
|
||||||
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export function stringifyQuery (obj) {
|
||||||
const qs = []
|
const qs = []
|
||||||
|
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
qs.push(`${encode(key)}=${encode(obj[key])}`)
|
qs.push(`${encode(key)}=${encode(obj[key])}`.toLowerCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
return qs.length ? `?${qs.join('&')}` : ''
|
return qs.length ? `?${qs.join('&')}` : ''
|
||||||
|
|
@ -41,8 +41,8 @@ export function getPath (...args) {
|
||||||
return cleanPath(args.join('/'))
|
return cleanPath(args.join('/'))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isResolvePath = cached(path => {
|
export const isAbsolutePath = cached(path => {
|
||||||
return /:|(\/{2})/.test(path)
|
return /(:|(\/{2}))/.test(path)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getRoot = cached(path => {
|
export const getRoot = cached(path => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// From https://github.com/egoist/vue-ga/blob/master/src/index.js
|
// From https://github.com/egoist/vue-ga/blob/master/src/index.js
|
||||||
|
|
||||||
function appendScript () {
|
function appendScript () {
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
script.async = true
|
script.async = true
|
||||||
|
|
@ -24,19 +23,13 @@ function collect () {
|
||||||
window.ga('send', 'pageview')
|
window.ga('send', 'pageview')
|
||||||
}
|
}
|
||||||
|
|
||||||
const install = function () {
|
const install = function (hook) {
|
||||||
if (install.installed) return
|
|
||||||
install.installed = true
|
|
||||||
|
|
||||||
if (!window.$docsify.ga) {
|
if (!window.$docsify.ga) {
|
||||||
console.error('[Docsify] ga is required.')
|
console.error('[Docsify] ga is required.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
window.$docsify.plugins = [].concat(function (hook) {
|
hook.beforeEach(collect)
|
||||||
hook.init(collect)
|
|
||||||
hook.beforeEach(collect)
|
|
||||||
}, window.$docsify.plugins)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default install()
|
window.$docsify.plugins = [].concat(install, window.$docsify.plugins)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
116
src/plugins/search/component.js
Normal file
116
src/plugins/search/component.js
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
31
src/plugins/search/index.js
Normal file
31
src/plugins/search/index.js
Normal file
|
|
@ -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)
|
||||||
156
src/plugins/search/search.js
Normal file
156
src/plugins/search/search.js
Normal file
|
|
@ -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…
Add table
Add a link
Reference in a new issue