refactor(router): dupports multiple mode

This commit is contained in:
qingwei.li 2017-05-29 19:21:08 +08:00 committed by cinwell.li
parent 2a21c40f17
commit 2c7041c8fb
20 changed files with 377 additions and 220 deletions

View File

@ -23,7 +23,7 @@
"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": "node app.js & nodemon -w src -e js,css --exec 'npm run dev:build'",
"test": "eslint src"
"test": "eslint src --fix"
},
"dependencies": {
"marked": "^0.3.6",

View File

@ -1,6 +1,6 @@
import { isMobile } from '../util/env'
import * as dom from '../util/dom'
import { parse } from '../route/hash'
import { parse } from '../router/hash'
const nav = {}
let hoverOver = false

View File

@ -1,6 +1,6 @@
import { isMobile } from '../util/env'
import * as dom from '../util/dom'
import { getHash } from '../route/hash'
import { getHash } from '../router/hash'
const title = dom.$.title
/**

View File

@ -1,6 +1,6 @@
import { get } from './ajax'
import { callHook } from '../init/lifecycle'
import { getParentPath } from '../route/util'
import { getParentPath } from '../router/util'
import { noop } from '../util/core'
function loadNested (path, file, next, vm, first) {
@ -9,7 +9,7 @@ function loadNested (path, file, next, vm, first) {
if (!path) return
get(vm.$getFile(path + file))
get(vm.router.getFile(path + file))
.then(next, _ => loadNested(path, file, next, vm))
}
@ -22,7 +22,7 @@ export function fetchMixin (proto) {
// Abort last request
last && last.abort && last.abort()
last = get(this.$getFile(path), true)
last = get(this.router.getFile(path), true)
// Current page is html
this.isHTML = /\.html$/g.test(path)
@ -47,7 +47,7 @@ export function fetchMixin (proto) {
proto._fetchCover = function () {
const { coverpage } = this.config
const root = getParentPath(this.route.path)
const path = this.$getFile(root + coverpage)
const path = this.router.getFile(root + coverpage)
if (this.route.path !== '/' || !coverpage) {
this._renderCover()

View File

@ -1,7 +1,7 @@
import * as util from './util'
import * as dom from './util/dom'
import * as render from './render/compiler'
import * as route from './route/hash'
import * as route from './router/hash'
import { slugify } from './render/slugify'
import { get } from './fetch/ajax'
import marked from 'marked'

View File

@ -1,5 +1,5 @@
import { initMixin } from './init'
import { routeMixin } from './route'
import { routerMixin } from './router'
import { renderMixin } from './render'
import { fetchMixin } from './fetch'
import { eventMixin } from './event'
@ -12,7 +12,7 @@ function Docsify () {
const proto = Docsify.prototype
initMixin(proto)
routeMixin(proto)
routerMixin(proto)
renderMixin(proto)
fetchMixin(proto)
eventMixin(proto)

View File

@ -1,7 +1,7 @@
import config from '../config'
import { initLifecycle, callHook } from './lifecycle'
import { initRender } from '../render'
import { initRoute } from '../route'
import { initRouter } from '../router'
import { initEvent } from '../event'
import { initFetch } from '../fetch'
import { isFn } from '../util/core'
@ -14,9 +14,9 @@ export function initMixin (proto) {
initLifecycle(vm) // Init hooks
initPlugin(vm) // Install plugins
callHook(vm, 'init')
initRouter(vm) // Add router
initRender(vm) // Render base DOM
initEvent(vm) // Bind events
initRoute(vm) // Add hashchange eventListener
initFetch(vm) // Fetch data
callHook(vm, 'mounted')
}

View File

@ -4,160 +4,165 @@ import { helper as helperTpl, tree as treeTpl } from './tpl'
import { genTree } from './gen-tree'
import { slugify } from './slugify'
import { emojify } from './emojify'
import { toURL, parse } from '../route/hash'
import { getBasePath, isAbsolutePath, getPath } from '../route/util'
import { getBasePath, isAbsolutePath, getPath } from '../router/util'
import { isFn, merge, cached } from '../util/core'
let markdownCompiler = marked
let contentBase = ''
let currentPath = ''
let linkTarget = '_blank'
let renderer = new marked.Renderer()
const cacheTree = {}
let toc = []
export class Compiler {
constructor (config, router) {
this.config = config
this.router = router
this.cacheTree = {}
this.toc = []
this.linkTarget = config.externalLinkTarget || '_blank'
this.contentBase = getBasePath(config.base)
/**
* Compile markdown content
*/
export const markdown = cached(text => {
let html = ''
const renderer = this._initRenderer()
let runner
const mdConf = config.markdown || {}
if (!text) return text
if (isFn(mdConf)) {
runner = mdConf(marked, renderer)
} else {
marked.setOptions(merge(mdConf, {
renderer: merge(renderer, mdConf.renderer)
}))
runner = marked
}
html = markdownCompiler(text)
html = emojify(html)
slugify.clear()
this.runner = cached(text => {
let html = ''
return html
})
if (!text) return text
markdown.renderer = renderer
html = runner(text)
html = emojify(html)
slugify.clear()
markdown.init = function (config = {}, {
base = window.location.pathname,
externalLinkTarget
}) {
contentBase = getBasePath(base)
linkTarget = externalLinkTarget || linkTarget
if (isFn(config)) {
markdownCompiler = config(marked, renderer)
} else {
renderer = merge(renderer, config.renderer)
marked.setOptions(merge(config, { renderer }))
}
}
markdown.update = function () {
currentPath = parse().path
}
/**
* render anchor tag
* @link https://github.com/chjj/marked#overriding-renderer-methods
*/
renderer.heading = function (text, level) {
const nextToc = { level, title: text }
if (/{docsify-ignore}/g.test(text)) {
text = text.replace('{docsify-ignore}', '')
nextToc.title = text
nextToc.ignoreSubHeading = true
return html
})
}
if (/{docsify-ignore-all}/g.test(text)) {
text = text.replace('{docsify-ignore-all}', '')
nextToc.title = text
nextToc.ignoreAllSubs = true
_initRenderer () {
const renderer = new marked.Renderer()
const { linkTarget, router, toc } = this
/**
* render anchor tag
* @link https://github.com/chjj/marked#overriding-renderer-methods
*/
renderer.heading = function (text, level) {
const nextToc = { level, title: text }
if (/{docsify-ignore}/g.test(text)) {
text = text.replace('{docsify-ignore}', '')
nextToc.title = text
nextToc.ignoreSubHeading = true
}
if (/{docsify-ignore-all}/g.test(text)) {
text = text.replace('{docsify-ignore-all}', '')
nextToc.title = text
nextToc.ignoreAllSubs = true
}
const slug = slugify(text)
const url = router.toURL(router.getCurrentPath(), { id: slug })
nextToc.slug = url
toc.push(nextToc)
return `<h${level} id="${slug}"><a href="${url}" 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) {
let blank = ''
if (!/:|(\/{2})/.test(href)) {
href = router.toURL(href, null, router.getCurrentPath())
} else {
blank = ` target="${linkTarget}"`
}
if (title) {
title = ` title="${title}"`
}
return `<a href="${href}"${title || ''}${blank}>${text}</a>`
}
renderer.paragraph = function (text) {
if (/^!&gt;/.test(text)) {
return helperTpl('tip', text)
} else if (/^\?&gt;/.test(text)) {
return helperTpl('warn', text)
}
return `<p>${text}</p>`
}
renderer.image = function (href, title, text) {
let url = href
const titleHTML = title ? ` title="${title}"` : ''
if (!isAbsolutePath(href)) {
url = getPath(this.contentBase, href)
}
return `<img src="${url}" data-origin="${href}" alt="${text}"${titleHTML}>`
}
return renderer
}
const slug = slugify(text)
const url = toURL(currentPath, { id: slug })
nextToc.slug = url
toc.push(nextToc)
/**
* Compile sidebar
*/
sidebar (text, level) {
const currentPath = this.router.getCurrentPath()
let html = ''
return `<h${level} id="${slug}"><a href="${url}" 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)
if (text) {
html = this.runner(text)
html = html.match(/<ul[^>]*>([\s\S]+)<\/ul>/g)[0]
} else {
const tree = this.cacheTree[currentPath] || genTree(this.toc, level)
html = treeTpl(tree, '<ul>')
this.cacheTree[currentPath] = tree
}
return `<pre v-pre data-lang="${lang}"><code class="lang-${lang}">${hl}</code></pre>`
}
renderer.link = function (href, title, text) {
let blank = ''
if (!/:|(\/{2})/.test(href)) {
href = toURL(href, null, currentPath)
} else {
blank = ` target="${linkTarget}"`
}
if (title) {
title = ` title="${title}"`
}
return `<a href="${href}"${title || ''}${blank}>${text}</a>`
}
renderer.paragraph = function (text) {
if (/^!&gt;/.test(text)) {
return helperTpl('tip', text)
} else if (/^\?&gt;/.test(text)) {
return helperTpl('warn', text)
}
return `<p>${text}</p>`
}
renderer.image = function (href, title, text) {
let url = href
const titleHTML = title ? ` title="${title}"` : ''
if (!isAbsolutePath(href)) {
url = getPath(contentBase, href)
return html
}
return `<img src="${url}" data-origin="${href}" alt="${text}"${titleHTML}>`
}
/**
* Compile sub sidebar
*/
subSidebar (level) {
const currentPath = this.router.getCurrentPath()
const { cacheTree, toc } = this
/**
* Compile sidebar
*/
export function sidebar (text, level) {
let html = ''
if (text) {
html = markdown(text)
html = html.match(/<ul[^>]*>([\s\S]+)<\/ul>/g)[0]
} else {
const tree = cacheTree[currentPath] || genTree(toc, level)
html = treeTpl(tree, '<ul>')
cacheTree[currentPath] = tree
}
return html
}
/**
* Compile sub sidebar
*/
export function subSidebar (el, level) {
if (el) {
toc[0] && toc[0].ignoreAllSubs && (toc = [])
toc[0] && toc[0].ignoreAllSubs && (this.toc = [])
toc[0] && toc[0].level === 1 && toc.shift()
toc.forEach((node, i) => {
node.ignoreSubHeading && toc.splice(i, 1)
})
const tree = cacheTree[currentPath] || genTree(toc, level)
el.parentNode.innerHTML += treeTpl(tree, '<ul class="app-sub-sidebar">')
cacheTree[currentPath] = tree
this.toc = []
return treeTpl(tree, '<ul class="app-sub-sidebar">')
}
article (text) {
return this.runner(text)
}
/**
* Compile cover page
*/
cover (text) {
const cacheToc = this.toc.slice()
const html = this.runner(text)
this.toc = cacheToc.slice()
return html
}
toc = []
}
/**
* Compile cover page
*/
export function cover (text) {
const cacheToc = toc.slice()
const html = markdown(text)
toc = cacheToc.slice()
return html
}

View File

@ -3,9 +3,9 @@ import { getAndActive, sticky } from '../event/sidebar'
import { scrollActiveSidebar, scroll2Top } from '../event/scroll'
import cssVars from '../util/polyfill/css-vars'
import * as tpl from './tpl'
import { markdown, sidebar, subSidebar, cover } from './compiler'
import { Compiler } from './compiler'
import { callHook } from '../init/lifecycle'
import { getBasePath, getPath, isAbsolutePath } from '../route/util'
import { getBasePath, getPath, isAbsolutePath } from '../router/util'
import { isPrimitive } from '../util/core'
import { isMobile } from '../util/env'
import tinydate from 'tinydate'
@ -85,32 +85,34 @@ export function renderMixin (proto) {
proto._renderSidebar = function (text) {
const { maxLevel, subMaxLevel, autoHeader, loadSidebar } = this.config
this._renderTo('.sidebar-nav', sidebar(text, maxLevel))
const active = getAndActive('.sidebar-nav', true, true)
subSidebar(loadSidebar ? active : '', subMaxLevel)
this._renderTo('.sidebar-nav', this.compiler.sidebar(text, maxLevel))
const activeEl = getAndActive('.sidebar-nav', true, true)
if (loadSidebar && activeEl) {
activeEl.parentNode.innerHTML += this.compiler.subSidebar(subMaxLevel)
}
// bind event
this.activeLink = active
this.activeLink = activeEl
scrollActiveSidebar()
if (autoHeader && active) {
if (autoHeader && activeEl) {
const main = dom.getNode('#main')
const firstNode = main.children[0]
if (firstNode && firstNode.tagName !== 'H1') {
const h1 = dom.create('h1')
h1.innerText = active.innerText
h1.innerText = activeEl.innerText
dom.before(main, h1)
}
}
}
proto._renderNav = function (text) {
text && this._renderTo('nav', markdown(text))
text && this._renderTo('nav', this.compiler.runner(text))
getAndActive('nav')
}
proto._renderMain = function (text, opt = {}) {
callHook(this, 'beforeEach', text, result => {
let html = this.isHTML ? result : markdown(result)
let html = this.isHTML ? result : this.compiler.runner(result)
if (opt.updatedAt) {
html = formatUpdated(html, opt.updatedAt, this.config.formatUpdated)
}
@ -127,7 +129,7 @@ export function renderMixin (proto) {
}
dom.toggleClass(el, 'add', 'show')
let html = this.coverIsHTML ? text : cover(text)
let html = this.coverIsHTML ? text : this.compiler.cover(text)
const m = html.trim().match('<p><img.*?data-origin="(.*?)"[^a]+alt="(.*?)">([^<]*?)</p>$')
if (m) {
@ -152,7 +154,6 @@ export function renderMixin (proto) {
}
proto._updateRender = function () {
markdown.update()
// render name link
renderNameLink(this)
}
@ -162,7 +163,7 @@ export function initRender (vm) {
const config = vm.config
// Init markdown compiler
markdown.init(config.markdown, config)
vm.compiler = new Compiler(config, vm.router)
const id = config.el || '#app'
const navEl = dom.find('nav') || dom.create('nav')

View File

@ -1,52 +0,0 @@
import { normalize, parse } from './hash'
import { getBasePath, getPath, isAbsolutePath } from './util'
import { on } from '../util/dom'
function getAlias (path, alias) {
return alias[path] ? getAlias(alias[path], alias) : path
}
function getFileName (path) {
return /\.(md|html)$/g.test(path)
? path
: /\/$/g.test(path)
? `${path}README.md`
: `${path}.md`
}
export function routeMixin (proto) {
proto.route = {}
proto.$getFile = function (path) {
const { config } = this
const base = getBasePath(config.basePath)
path = config.alias ? getAlias(path, config.alias) : path
path = getFileName(path)
path = path === '/README.md' ? (config.homepage || path) : path
path = isAbsolutePath(path) ? path : getPath(base, path)
return path
}
}
let lastRoute = {}
export function initRoute (vm) {
normalize()
lastRoute = vm.route = parse()
vm._updateRender()
on('hashchange', _ => {
normalize()
vm.route = parse()
vm._updateRender()
if (lastRoute.path === vm.route.path) {
vm.$resetEvents()
return
}
vm.$fetch()
lastRoute = vm.route
})
}

View File

@ -0,0 +1,8 @@
import { History } from './base'
export class AbstractHistory extends History {
constructor (config) {
super(config)
this.mode = 'abstract'
}
}

View File

@ -0,0 +1,44 @@
import { getBasePath, getPath, isAbsolutePath } from '../util'
import { noop } from '../../util/core'
function getAlias (path, alias) {
return alias[path] ? getAlias(alias[path], alias) : path
}
function getFileName (path) {
return /\.(md|html)$/g.test(path)
? path
: /\/$/g.test(path)
? `${path}README.md`
: `${path}.md`
}
export class History {
constructor (config) {
this.config = config
}
onchange (cb = noop) {
cb()
}
getFile (path) {
const { config } = this
const base = getBasePath(config.basePath)
path = config.alias ? getAlias(path, config.alias) : path
path = getFileName(path)
path = path === '/README.md' ? (config.homepage || path) : path
path = isAbsolutePath(path) ? path : getPath(base, path)
return path
}
getCurrentPath () {}
normalize () {}
parse () {}
toURL () {}
}

View File

@ -0,0 +1,78 @@
import { History } from './base'
import { merge, cached, noop } from '../../util/core'
import { parseQuery, stringifyQuery, cleanPath } from '../util'
import { on } from '../../util/dom'
function replaceHash (path) {
const i = location.href.indexOf('#')
location.replace(
location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}
const replaceSlug = cached(path => {
return path.replace('#', '?id=')
})
export class HashHistory extends History {
constructor (config) {
super(config)
this.mode = 'hash'
}
getCurrentPath () {
// We can't use location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
const href = location.href
const index = href.indexOf('#')
return index === -1 ? '' : href.slice(index + 1)
}
onchange (cb = noop) {
on('hashchange', cb)
}
normalize () {
let path = this.getCurrentPath()
path = replaceSlug(path)
if (path.charAt(0) === '/') return replaceHash(path)
replaceHash('/' + path)
}
/**
* Parse the url
* @param {string} [path=location.herf]
* @return {object} { path, query }
*/
parse (path = 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) }
}
toURL (path, params, currentRoute) {
const local = currentRoute && path[0] === '#'
const route = this.parse(replaceSlug(path))
route.query = merge({}, route.query, params)
path = route.path + stringifyQuery(route.query)
path = path.replace(/\.md(\?)|\.md$/, '$1')
if (local) path = currentRoute + path
return cleanPath('#/' + path)
}
}

View File

@ -0,0 +1,8 @@
import { History } from './base'
export class HTML5History extends History {
constructor (config) {
super(config)
this.mode = 'history'
}
}

44
src/core/router/index.js Normal file
View File

@ -0,0 +1,44 @@
import { AbstractHistory } from './history/abstract'
import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { supportsPushState, inBrowser } from '../util/env'
export function routerMixin (proto) {
proto.route = {}
}
let lastRoute = {}
export function initRouter (vm) {
const config = vm.config
const mode = config.routerMode || 'hash'
let router
if (mode === 'history' && supportsPushState) {
router = new HTML5History(config)
} else if (!inBrowser || mode === 'abstract') {
router = new AbstractHistory(config)
} else {
router = new HashHistory(config)
}
vm.router = router
router.normalize()
lastRoute = vm.route = router.parse()
vm._updateRender()
router.onchange(_ => {
router.normalize()
vm.route = router.parse()
vm._updateRender()
if (lastRoute.path === vm.route.path) {
vm.$resetEvents()
return
}
vm.$fetch()
lastRoute = vm.route
})
}

View File

@ -3,3 +3,20 @@ export const UA = window.navigator.userAgent.toLowerCase()
export const isIE = UA && /msie|trident/.test(UA)
export const isMobile = document.body.clientWidth <= 600
export const inBrowser = typeof window !== 'undefined'
export const supportsPushState = inBrowser && (function () {
const ua = window.navigator.userAgent
if (
(ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
ua.indexOf('Mobile Safari') !== -1 &&
ua.indexOf('Chrome') === -1 &&
ua.indexOf('Windows Phone') === -1
) {
return false
}
return window.history && 'pushState' in window.history
})()

View File

@ -7,24 +7,28 @@ function appendScript () {
}
function init (id) {
if (!window.ga) {
let ga = window.ga
if (!ga) {
appendScript()
window.ga = window.ga || function () {
(window.ga.q = window.ga.q || []).push(arguments)
ga = ga || function () {
(ga.q = ga.q || []).push(arguments)
}
window.ga.l = Number(new Date())
window.ga('create', id, 'auto')
ga.l = Number(new Date())
ga('create', id, 'auto')
}
return ga
}
function collect () {
init(window.$docsify.ga)
window.ga('set', 'page', location.hash)
window.ga('send', 'pageview')
const ga = init($docsify.ga)
ga('set', 'page', location.hash)
ga('send', 'pageview')
}
const install = function (hook) {
if (!window.$docsify.ga) {
if (!$docsify.ga) {
console.error('[Docsify] ga is required.')
return
}
@ -32,4 +36,4 @@ const install = function (hook) {
hook.beforeEach(collect)
}
window.$docsify.plugins = [].concat(install, window.$docsify.plugins)
$docsify.plugins = [].concat(install, $docsify.plugins)

View File

@ -153,7 +153,7 @@ export function init (config, vm) {
if (INDEXS[path]) return count++
helper
.get(vm.$getFile(path))
.get(vm.router.getFile(path))
.then(result => {
INDEXS[path] = genIndex(path, result)
len === ++count && saveData(config.maxAge)