refactor(core): add router
This commit is contained in:
parent
8bee8f0213
commit
30da0d5d46
17 changed files with 316 additions and 88 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { merge, camelize, isPrimitive } from './util/core'
|
||||
import { merge, hyphenate, isPrimitive } from './util/core'
|
||||
|
||||
const config = merge({
|
||||
el: '#app',
|
||||
|
|
@ -23,7 +23,7 @@ const script = document.currentScript ||
|
|||
|
||||
if (script) {
|
||||
for (const prop in config) {
|
||||
const val = script.getAttribute('data-' + camelize(prop))
|
||||
const val = script.getAttribute('data-' + hyphenate(prop))
|
||||
|
||||
if (isPrimitive(val)) {
|
||||
config[prop] = val === '' ? true : val
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { isMobile } from '../util/env'
|
||||
import { dom, on } from '../util/dom'
|
||||
import { body, on } from '../util/dom'
|
||||
import * as sidebar from './sidebar'
|
||||
|
||||
export function eventMixin (Docsify) {
|
||||
|
|
@ -14,6 +14,6 @@ export function initEvent (vm) {
|
|||
if (vm.config.coverpage) {
|
||||
!isMobile && on('scroll', sidebar.sticky)
|
||||
} else {
|
||||
dom.body.classList.add('sticky')
|
||||
body.classList.add('sticky')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { isMobile } from '../util/env'
|
||||
import { getNode, on, dom } from '../util/dom'
|
||||
import { getNode, on, body } from '../util/dom'
|
||||
/**
|
||||
* Toggle button
|
||||
*/
|
||||
export function btn (el) {
|
||||
const toggle = () => dom.body.classList.toggle('close')
|
||||
const toggle = () => body.classList.toggle('close')
|
||||
|
||||
el = getNode(el)
|
||||
on(el, 'click', toggle)
|
||||
|
|
|
|||
|
|
@ -1,23 +1,33 @@
|
|||
import progressbar from '../render/progressbar'
|
||||
import { noop } from '../util/core'
|
||||
|
||||
const cache = {}
|
||||
const RUN_VERSION = Date.now()
|
||||
|
||||
/**
|
||||
* Simple ajax get
|
||||
* @param {String} url
|
||||
* @param {Boolean} [loading=false] has loading bar
|
||||
* @param {string} url
|
||||
* @param {boolean} [hasBar=false] has progress bar
|
||||
* @return { then(resolve, reject), abort }
|
||||
*/
|
||||
export function get (url, hasLoading = false) {
|
||||
export function get (url, hasBar = false) {
|
||||
const xhr = new XMLHttpRequest()
|
||||
const on = function () {
|
||||
xhr.addEventListener.apply(xhr, arguments)
|
||||
}
|
||||
|
||||
url += (/\?(\w+)=/g.test(url) ? '&' : '?') + `v=${RUN_VERSION}`
|
||||
|
||||
if (cache[url]) {
|
||||
return { then: cb => cb(cache[url]), abort: noop }
|
||||
}
|
||||
|
||||
xhr.open('GET', url)
|
||||
xhr.send()
|
||||
|
||||
return {
|
||||
then: function (success, error = noop) {
|
||||
const on = xhr.addEventListener
|
||||
|
||||
if (hasLoading) {
|
||||
if (hasBar) {
|
||||
const id = setInterval(_ => progressbar({}), 500)
|
||||
|
||||
on('progress', progressbar)
|
||||
|
|
@ -29,9 +39,14 @@ export function get (url, hasLoading = false) {
|
|||
|
||||
on('error', error)
|
||||
on('load', ({ target }) => {
|
||||
target.status >= 400 ? error(target) : success(target.response)
|
||||
if (target.status >= 400) {
|
||||
error(target)
|
||||
} else {
|
||||
cache[url] = target.response
|
||||
success(target.response)
|
||||
}
|
||||
})
|
||||
},
|
||||
abort: () => xhr.readyState !== 4 && xhr.abort()
|
||||
abort: _ => xhr.readyState !== 4 && xhr.abort()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,43 @@
|
|||
import { get } from './ajax'
|
||||
import { callHook } from '../init/lifecycle'
|
||||
import { getCurrentRoot } from '../route/util'
|
||||
|
||||
export function fetchMixin (Docsify) {
|
||||
Docsify.prototype.$fetch = function (path) {
|
||||
// 加载侧边栏、导航、内容
|
||||
let last
|
||||
|
||||
Docsify.prototype._fetch = function (cb) {
|
||||
const { path } = this.route
|
||||
const { loadNavbar, loadSidebar } = this.config
|
||||
const currentRoot = getCurrentRoot(path)
|
||||
|
||||
// Abort last request
|
||||
last && last.abort && last.abort()
|
||||
|
||||
last = get(this.$getFile(path), true)
|
||||
last.then(text => {
|
||||
this._renderMain(text)
|
||||
if (!loadSidebar) return cb()
|
||||
|
||||
const fn = result => { this._renderSidebar(result); cb() }
|
||||
|
||||
// Load sidebar
|
||||
get(this.$getFile(currentRoot + loadSidebar))
|
||||
.then(fn, _ => get(loadSidebar).then(fn))
|
||||
},
|
||||
_ => this._renderMain(null))
|
||||
|
||||
// Load nav
|
||||
loadNavbar &&
|
||||
get(this.$getFile(currentRoot + loadNavbar))
|
||||
.then(
|
||||
this._renderNav,
|
||||
_ => get(loadNavbar).then(this._renderNav)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function initFetch (vm) {
|
||||
vm.$fetch(result => {
|
||||
vm._fetch(result => {
|
||||
vm.$resetEvents()
|
||||
callHook(vm, 'doneEach')
|
||||
})
|
||||
|
|
|
|||
13
src/core/global-api.js
Normal file
13
src/core/global-api.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import * as util from './util'
|
||||
import * as dom from './util/dom'
|
||||
import * as render from './render/compiler'
|
||||
import * as route from './route/util'
|
||||
import { get } from './fetch/ajax'
|
||||
import marked from 'marked'
|
||||
import prism from 'prismjs'
|
||||
|
||||
export default function () {
|
||||
window.Docsify = { util, dom, render, route, get }
|
||||
window.marked = marked
|
||||
window.Prism = prism
|
||||
}
|
||||
|
|
@ -3,9 +3,7 @@ import { routeMixin } from './route'
|
|||
import { renderMixin } from './render'
|
||||
import { fetchMixin } from './fetch'
|
||||
import { eventMixin } from './event'
|
||||
import * as util from './util'
|
||||
import { get as load } from './fetch/ajax'
|
||||
import * as routeUtil from './route/util'
|
||||
import initGlobalAPI from './global-api'
|
||||
|
||||
function Docsify () {
|
||||
this._init()
|
||||
|
|
@ -20,9 +18,7 @@ eventMixin(Docsify)
|
|||
/**
|
||||
* Global API
|
||||
*/
|
||||
window.Docsify = {
|
||||
util: util.merge({ load }, util, routeUtil)
|
||||
}
|
||||
initGlobalAPI()
|
||||
|
||||
/**
|
||||
* Run Docsify
|
||||
|
|
|
|||
50
src/core/render/compiler.js
Normal file
50
src/core/render/compiler.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import marked from 'marked'
|
||||
import Prism from 'prismjs'
|
||||
|
||||
export const renderer = new marked.Renderer()
|
||||
|
||||
export function markdown () {
|
||||
|
||||
}
|
||||
|
||||
const toc = []
|
||||
|
||||
/**
|
||||
* render anchor tag
|
||||
* @link https://github.com/chjj/marked#overriding-renderer-methods
|
||||
*/
|
||||
renderer.heading = function (text, level) {
|
||||
const slug = slugify(text)
|
||||
let route = ''
|
||||
|
||||
route = `#/${getRoute()}`
|
||||
toc.push({ level, slug: `${route}#${encodeURIComponent(slug)}`, title: text })
|
||||
|
||||
return `<h${level} id="${slug}"><a href="${route}#${slug}" data-id="${slug}" class="anchor"><span>${text}</span></a></h${level}>`
|
||||
}
|
||||
// highlight code
|
||||
renderer.code = function (code, lang = '') {
|
||||
const hl = Prism.highlight(code, Prism.languages[lang] || Prism.languages.markup)
|
||||
|
||||
return `<pre v-pre data-lang="${lang}"><code class="lang-${lang}">${hl}</code></pre>`
|
||||
}
|
||||
renderer.link = function (href, title, text) {
|
||||
if (!/:|(\/{2})/.test(href)) {
|
||||
href = `#/${href}`.replace(/\/+/g, '/')
|
||||
}
|
||||
return `<a href="${href}" title="${title || ''}">${text}</a>`
|
||||
}
|
||||
renderer.paragraph = function (text) {
|
||||
if (/^!>/.test(text)) {
|
||||
return tpl.helper('tip', text)
|
||||
} else if (/^\?>/.test(text)) {
|
||||
return tpl.helper('warn', text)
|
||||
}
|
||||
return `<p>${text}</p>`
|
||||
}
|
||||
renderer.image = function (href, title, text) {
|
||||
const url = /:|(\/{2})/.test(href) ? href : ($docsify.basePath + href).replace(/\/+/g, '/')
|
||||
const titleHTML = title ? ` title="${title}"` : ''
|
||||
|
||||
return `<img src="${url}" alt="${text}"${titleHTML} />`
|
||||
}
|
||||
|
|
@ -1,12 +1,26 @@
|
|||
import { getNode, dom } from '../util/dom'
|
||||
import * as dom from '../util/dom'
|
||||
import cssVars from '../util/polyfill/css-vars'
|
||||
import * as tpl from './tpl'
|
||||
|
||||
function renderMain () {
|
||||
|
||||
}
|
||||
|
||||
function renderNav () {
|
||||
}
|
||||
|
||||
function renderSidebar () {
|
||||
}
|
||||
|
||||
export function renderMixin (Docsify) {
|
||||
Docsify.prototype._renderTo = function (el, content, replace) {
|
||||
const node = getNode(el)
|
||||
const node = dom.getNode(el)
|
||||
if (node) node[replace ? 'outerHTML' : 'innerHTML'] = content
|
||||
}
|
||||
|
||||
Docsify.prototype._renderSidebar = renderSidebar
|
||||
Docsify.prototype._renderNav = renderNav
|
||||
Docsify.prototype._renderMain = renderMain
|
||||
}
|
||||
|
||||
export function initRender (vm) {
|
||||
|
|
@ -40,7 +54,7 @@ export function initRender (vm) {
|
|||
dom.body.insertBefore(navEl, dom.body.children[0])
|
||||
|
||||
if (config.themeColor) {
|
||||
dom.head += tpl.theme(config.themeColor)
|
||||
dom.$.head += tpl.theme(config.themeColor)
|
||||
// Polyfll
|
||||
cssVars(config.themeColor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
import { dom } from '../util/dom'
|
||||
import * as dom from '../util/dom'
|
||||
import { isPrimitive } from '../util/core'
|
||||
|
||||
let loadingEl
|
||||
let barEl
|
||||
let timeId
|
||||
|
||||
/**
|
||||
* Init progress component
|
||||
*/
|
||||
function init () {
|
||||
if (loadingEl) return
|
||||
const div = dom.create('div')
|
||||
|
||||
div.classList.add('progress')
|
||||
dom.appendTo(div, dom.body)
|
||||
loadingEl = div
|
||||
dom.appendTo(dom.body, div)
|
||||
barEl = div
|
||||
}
|
||||
/**
|
||||
* Render progress bar
|
||||
|
|
@ -21,26 +20,26 @@ function init () {
|
|||
export default function ({ loaded, total, step }) {
|
||||
let num
|
||||
|
||||
loadingEl = init()
|
||||
!barEl && init()
|
||||
|
||||
if (!isPrimitive(step)) {
|
||||
step = Math.floor(Math.random() * 5 + 1)
|
||||
}
|
||||
if (step) {
|
||||
num = parseInt(loadingEl.style.width, 10) + step
|
||||
num = parseInt(barEl.style.width, 10) + step
|
||||
num = num > 80 ? 80 : num
|
||||
} else {
|
||||
num = Math.floor(loaded / total * 100)
|
||||
}
|
||||
|
||||
loadingEl.style.opacity = 1
|
||||
loadingEl.style.width = num >= 95 ? '100%' : num + '%'
|
||||
barEl.style.opacity = 1
|
||||
barEl.style.width = num >= 95 ? '100%' : num + '%'
|
||||
|
||||
if (num >= 95) {
|
||||
clearTimeout(timeId)
|
||||
timeId = setTimeout(_ => {
|
||||
loadingEl.style.opacity = 0
|
||||
loadingEl.style.width = '0%'
|
||||
barEl.style.opacity = 0
|
||||
barEl.style.width = '0%'
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,27 @@
|
|||
// import { cleanPath, getLocation } from './util'
|
||||
export function ensureSlash () {
|
||||
const path = getHash()
|
||||
if (path.charAt(0) === '/') return
|
||||
import { parseQuery } from './util'
|
||||
|
||||
function replaceHash (path) {
|
||||
const i = window.location.href.indexOf('#')
|
||||
window.location.replace(
|
||||
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the current url
|
||||
*
|
||||
* @example
|
||||
* domain.com/docs/ => domain.com/docs/#/
|
||||
* domain.com/docs/#/#slug => domain.com/docs/#/?id=slug
|
||||
*/
|
||||
export function normalize () {
|
||||
let path = getHash()
|
||||
|
||||
path = path
|
||||
.replace('#', '?id=')
|
||||
.replace(/\?(\w+)=/g, (_, slug) => slug === 'id' ? '?id=' : `&${slug}=`)
|
||||
|
||||
if (path.charAt(0) === '/') return replaceHash(path)
|
||||
replaceHash('/' + path)
|
||||
}
|
||||
|
||||
|
|
@ -13,11 +33,33 @@ export function getHash () {
|
|||
return index === -1 ? '' : href.slice(index + 1)
|
||||
}
|
||||
|
||||
function replaceHash (path) {
|
||||
const i = window.location.href.indexOf('#')
|
||||
window.location.replace(
|
||||
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
|
||||
)
|
||||
/**
|
||||
* Parse the current url
|
||||
* @return {object} { path, query }
|
||||
*/
|
||||
export function parse () {
|
||||
let path = window.location.href
|
||||
let query = ''
|
||||
|
||||
const queryIndex = path.indexOf('?')
|
||||
if (queryIndex >= 0) {
|
||||
query = path.slice(queryIndex + 1)
|
||||
path = path.slice(0, queryIndex)
|
||||
}
|
||||
|
||||
const hashIndex = path.indexOf('#')
|
||||
if (hashIndex) {
|
||||
path = path.slice(hashIndex + 1)
|
||||
}
|
||||
|
||||
return { path, query: parseQuery(query) }
|
||||
}
|
||||
|
||||
// TODO 把第二个 hash 转成 ?id=
|
||||
/**
|
||||
* to URL
|
||||
* @param {String} path
|
||||
* @param {String} qs query string
|
||||
*/
|
||||
export function toURL (path, qs) {
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,42 @@
|
|||
import { ensureSlash } from './hash'
|
||||
import { normalize, parse } from './hash'
|
||||
import { getBasePath, cleanPath } from './util'
|
||||
import { on } from '../util/dom'
|
||||
|
||||
function getAlias (path, alias) {
|
||||
if (alias[path]) return getAlias(alias[path], alias)
|
||||
return path
|
||||
}
|
||||
|
||||
function getFileName (path) {
|
||||
return /\.(md|html)$/g.test(path)
|
||||
? path
|
||||
: /\/$/g.test(path)
|
||||
? `${path}README.md`
|
||||
: `${path}.md`
|
||||
}
|
||||
|
||||
export function routeMixin (Docsify) {
|
||||
Docsify.prototype.$route = {
|
||||
query: location.query || {},
|
||||
path: location.path || '/',
|
||||
base: ''
|
||||
Docsify.prototype.route = {}
|
||||
Docsify.prototype.$getFile = function (path) {
|
||||
const { config } = this
|
||||
const base = getBasePath(config.basePath)
|
||||
|
||||
path = getAlias(path, config.alias)
|
||||
path = getFileName(path)
|
||||
path = path === '/README.md' ? ('/' + config.homepage || path) : path
|
||||
path = cleanPath(base + path)
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
export function initRoute (vm) {
|
||||
ensureSlash()
|
||||
window.addEventListener('hashchange', () => {
|
||||
ensureSlash()
|
||||
vm.$fetch()
|
||||
normalize()
|
||||
vm.route = parse()
|
||||
|
||||
on('hashchange', _ => {
|
||||
normalize()
|
||||
vm.route = parse()
|
||||
vm._fetch()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,45 @@
|
|||
import { cached } from '../util/core'
|
||||
|
||||
const decode = decodeURIComponent
|
||||
|
||||
export const parseQuery = cached(query => {
|
||||
const res = {}
|
||||
|
||||
query = query.trim().replace(/^(\?|#|&)/, '')
|
||||
|
||||
if (!query) {
|
||||
return res
|
||||
}
|
||||
|
||||
query.split('&').forEach(function (param) {
|
||||
const parts = param.replace(/\+/g, ' ').split('=')
|
||||
const key = decode(parts.shift())
|
||||
const val = parts.length > 0
|
||||
? decode(parts.join('='))
|
||||
: null
|
||||
|
||||
if (res[key] === undefined) {
|
||||
res[key] = val
|
||||
} else if (Array.isArray(res[key])) {
|
||||
res[key].push(val)
|
||||
} else {
|
||||
res[key] = [res[key], val]
|
||||
}
|
||||
})
|
||||
|
||||
return res
|
||||
})
|
||||
|
||||
export function cleanPath (path) {
|
||||
return path.replace(/\/+/g, '/')
|
||||
}
|
||||
|
||||
export function getLocation (base) {
|
||||
let path = window.location.pathname
|
||||
if (base && path.indexOf(base) === 0) {
|
||||
path = path.slice(base.length)
|
||||
}
|
||||
return (path || '/') + window.location.search + window.location.hash
|
||||
export function getBasePath (base) {
|
||||
return /^(\/|https?:)/g.test(base)
|
||||
? base
|
||||
: cleanPath(window.location.pathname + '/' + base)
|
||||
}
|
||||
|
||||
export function getCurrentRoot (path) {
|
||||
return /\/$/g.test(path) ? path : path.match(/(\S*\/)[^\/]+$/)[1]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Create a cached version of a pure function.
|
||||
*/
|
||||
function cached (fn) {
|
||||
export function cached (fn) {
|
||||
const cache = Object.create(null)
|
||||
return function cachedFn (str) {
|
||||
const hit = cache[str]
|
||||
|
|
@ -10,11 +10,10 @@ function cached (fn) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Camelize a hyphen-delimited string.
|
||||
* Hyphenate a camelCase string.
|
||||
*/
|
||||
const camelizeRE = /-(\w)/g
|
||||
export const camelize = cached((str) => {
|
||||
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
|
||||
export const hyphenate = cached(str => {
|
||||
return str.replace(/([A-Z])/g, m => '-' + m.toLowerCase())
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,22 +10,34 @@ const cacheNode = {}
|
|||
*/
|
||||
export function getNode (el, noCache = false) {
|
||||
if (typeof el === 'string') {
|
||||
el = noCache ? dom.find(el) : (cacheNode[el] || dom.find(el))
|
||||
el = noCache ? find(el) : (cacheNode[el] || find(el))
|
||||
}
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
export const dom = {
|
||||
body: document.body,
|
||||
head: document.head,
|
||||
find: node => document.querySelector(node),
|
||||
findAll: node => document.querySelectorAll(node),
|
||||
create: (node, tpl) => {
|
||||
node = document.createElement(node)
|
||||
if (tpl) node.innerHTML = tpl
|
||||
},
|
||||
appendTo: (target, el) => target.appendChild(el)
|
||||
export const $ = document
|
||||
|
||||
export const body = $.body
|
||||
|
||||
export const head = $.head
|
||||
|
||||
export function find (node) {
|
||||
return $.querySelector(node)
|
||||
}
|
||||
|
||||
export function findAll (node) {
|
||||
return [].clice.call($.querySelectorAll(node))
|
||||
}
|
||||
|
||||
export function create (node, tpl) {
|
||||
node = $.createElement(node)
|
||||
if (tpl) node.innerHTML = tpl
|
||||
return node
|
||||
}
|
||||
|
||||
export function appendTo (target, el) {
|
||||
return target.appendChild(el)
|
||||
}
|
||||
|
||||
export function on (el, type, handler) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
export * from './core'
|
||||
export * from './env'
|
||||
export * from './dom'
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import { dom } from '../dom'
|
||||
import * as dom from '../dom'
|
||||
import { get } from '../../fetch/ajax'
|
||||
|
||||
function replaceVar (block, themeColor) {
|
||||
function replaceVar (block, color) {
|
||||
block.innerHTML = block.innerHTML
|
||||
.replace(/var\(\s*--theme-color.*?\)/g, themeColor)
|
||||
.replace(/var\(\s*--theme-color.*?\)/g, color)
|
||||
}
|
||||
|
||||
export default function (themeColor) {
|
||||
export default function (color) {
|
||||
// Variable support
|
||||
if (window.CSS
|
||||
&& window.CSS.supports
|
||||
&& window.CSS.supports('(--foo: red)')) return
|
||||
if (window.CSS &&
|
||||
window.CSS.supports &&
|
||||
window.CSS.supports('(--v:red)')) return
|
||||
|
||||
const styleBlocks = dom.findAll('style:not(.inserted),link')
|
||||
|
||||
;[].forEach.call(styleBlocks, block => {
|
||||
if (block.nodeName === 'STYLE') {
|
||||
replaceVar(block, themeColor)
|
||||
replaceVar(block, color)
|
||||
} else if (block.nodeName === 'LINK') {
|
||||
const href = block.getAttribute('href')
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ export default function (themeColor) {
|
|||
const style = dom.create('style', res)
|
||||
|
||||
dom.head.appendChild(style)
|
||||
replaceVar(style, themeColor)
|
||||
replaceVar(style, color)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue