refactor(core): and markdown compiler

This commit is contained in:
qingwei.li 2017-02-18 14:09:17 +08:00 committed by cinwell.li
parent 30da0d5d46
commit fe88c154b0
12 changed files with 194 additions and 232 deletions

View File

@ -2,8 +2,5 @@
"extends": ["vue"], "extends": ["vue"],
"env": { "env": {
"browser": true "browser": true
},
"globals": {
"$docsify": true
} }
} }

View File

@ -1,14 +1,15 @@
import { get } from './ajax' import { get } from './ajax'
import { callHook } from '../init/lifecycle' import { callHook } from '../init/lifecycle'
import { getCurrentRoot } from '../route/util' import { getRoot } from '../route/util'
import { noop } from '../util/core'
export function fetchMixin (Docsify) { export function fetchMixin (Docsify) {
let last let last
Docsify.prototype._fetch = function (cb) { Docsify.prototype._fetch = function (cb = noop) {
const { path } = this.route const { path } = this.route
const { loadNavbar, loadSidebar } = this.config const { loadNavbar, loadSidebar } = this.config
const currentRoot = getCurrentRoot(path) const root = getRoot(path)
// Abort last request // Abort last request
last && last.abort && last.abort() last && last.abort && last.abort()
@ -21,14 +22,14 @@ export function fetchMixin (Docsify) {
const fn = result => { this._renderSidebar(result); cb() } const fn = result => { this._renderSidebar(result); cb() }
// Load sidebar // Load sidebar
get(this.$getFile(currentRoot + loadSidebar)) get(this.$getFile(root + loadSidebar))
.then(fn, _ => get(loadSidebar).then(fn)) .then(fn, _ => get(loadSidebar).then(fn))
}, },
_ => this._renderMain(null)) _ => this._renderMain(null))
// Load nav // Load nav
loadNavbar && loadNavbar &&
get(this.$getFile(currentRoot + loadNavbar)) get(this.$getFile(root + loadNavbar))
.then( .then(
this._renderNav, this._renderNav,
_ => get(loadNavbar).then(this._renderNav) _ => get(loadNavbar).then(this._renderNav)

View File

@ -23,4 +23,4 @@ initGlobalAPI()
/** /**
* Run Docsify * Run Docsify
*/ */
setTimeout(() => new Docsify(), 0) new Docsify()

View File

@ -1,26 +1,56 @@
import marked from 'marked' import marked from 'marked'
import Prism from 'prismjs' import Prism from 'prismjs'
import { helper as helperTpl } from './tpl'
import { slugify, clearSlugCache } from './slugify'
import { emojify } from './emojify'
import { toURL } from '../route/hash'
import { isFn, merge, cached } from '../util/core'
export const renderer = new marked.Renderer() let markdownCompiler = marked
let contentBase = ''
export function markdown () { let renderer = new marked.Renderer()
}
const toc = [] const toc = []
/**
* Compile markdown content
*/
export const markdown = cached(text => {
let html = ''
if (!text) return text
html = markdownCompiler(text)
html = emojify(html)
clearSlugCache()
return html
})
markdown.renderer = renderer
markdown.init = function (config = {}, context = window.location.pathname) {
contentBase = context
if (isFn(config)) {
markdownCompiler = config(marked, renderer)
} else {
renderer = merge(renderer, config.renderer)
marked.setOptions(merge(config, { renderer }))
}
}
/** /**
* render anchor tag * render anchor tag
* @link https://github.com/chjj/marked#overriding-renderer-methods * @link https://github.com/chjj/marked#overriding-renderer-methods
*/ */
renderer.heading = function (text, level) { renderer.heading = function (text, level) {
const slug = slugify(text) const slug = slugify(text)
let route = '' const url = toURL(contentBase, { id: slug })
route = `#/${getRoute()}` toc.push({ level, slug: url, title: text })
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}>` return `<h${level} id="${slug}"><a href="${url}" data-id="${slug}" class="anchor"><span>${text}</span></a></h${level}>`
} }
// highlight code // highlight code
renderer.code = function (code, lang = '') { renderer.code = function (code, lang = '') {
@ -30,21 +60,31 @@ renderer.code = function (code, lang = '') {
} }
renderer.link = function (href, title, text) { renderer.link = function (href, title, text) {
if (!/:|(\/{2})/.test(href)) { if (!/:|(\/{2})/.test(href)) {
// TODO
href = `#/${href}`.replace(/\/+/g, '/') href = `#/${href}`.replace(/\/+/g, '/')
} }
return `<a href="${href}" title="${title || ''}">${text}</a>` return `<a href="${href}" title="${title || ''}">${text}</a>`
} }
renderer.paragraph = function (text) { renderer.paragraph = function (text) {
if (/^!&gt;/.test(text)) { if (/^!&gt;/.test(text)) {
return tpl.helper('tip', text) return helperTpl('tip', text)
} else if (/^\?&gt;/.test(text)) { } else if (/^\?&gt;/.test(text)) {
return tpl.helper('warn', text) return helperTpl('warn', text)
} }
return `<p>${text}</p>` return `<p>${text}</p>`
} }
renderer.image = function (href, title, text) { renderer.image = function (href, title, text) {
const url = /:|(\/{2})/.test(href) ? href : ($docsify.basePath + href).replace(/\/+/g, '/') // TODO
const titleHTML = title ? ` title="${title}"` : '' // get base path
// const url = /:|(\/{2})/.test(href) ? href : ($docsify.basePath + href).replace(/\/+/g, '/')
// const titleHTML = title ? ` title="${title}"` : ''
// return `<img src="${url}" alt="${text}"${titleHTML} />`
}
/**
* Compile sidebar
*/
export function sidebar (text) {
return `<img src="${url}" alt="${text}"${titleHTML} />`
} }

View File

@ -0,0 +1,6 @@
export function emojify (text) {
return text
.replace(/<(pre|template)[^>]*?>([\s\S]+)<\/(pre|template)>/g, m => m.replace(/:/g, '__colon__'))
.replace(/:(\w+?):/ig, '<img class="emoji" src="https://assets-cdn.github.com/images/icons/emoji/$1.png" alt="$1" />')
.replace(/__colon__/g, ':')
}

View File

@ -0,0 +1,27 @@
/**
* gen toc tree
* @link https://github.com/killercup/grock/blob/5280ae63e16c5739e9233d9009bc235ed7d79a50/styles/solarized/assets/js/behavior.coffee#L54-L81
* @param {Array} toc
* @param {Number} maxLevel
* @return {Array}
*/
export function genTree (toc, maxLevel) {
const headlines = []
const last = {}
toc.forEach(headline => {
const level = headline.level || 1
const len = level - 1
if (level > maxLevel) return
if (last[len]) {
last[len].children = last[len].children || []
last[len].children.push(headline)
} else {
headlines.push(headline)
}
last[level] = headline
})
return headlines
}

View File

@ -1,30 +1,47 @@
import * as dom from '../util/dom' import * as dom from '../util/dom'
import cssVars from '../util/polyfill/css-vars' import cssVars from '../util/polyfill/css-vars'
import * as tpl from './tpl' import * as tpl from './tpl'
import { markdown, sidebar } from './compiler'
import { callHook } from '../init/lifecycle'
function renderMain () { function renderMain (html) {
if (!html) {
} // TODO: Custom 404 page
}
function renderNav () { this._renderTo('.markdown-section', html)
}
function renderSidebar () {
} }
export function renderMixin (Docsify) { export function renderMixin (Docsify) {
Docsify.prototype._renderTo = function (el, content, replace) { const proto = Docsify.prototype
proto._renderTo = function (el, content, replace) {
const node = dom.getNode(el) const node = dom.getNode(el)
if (node) node[replace ? 'outerHTML' : 'innerHTML'] = content if (node) node[replace ? 'outerHTML' : 'innerHTML'] = content
} }
Docsify.prototype._renderSidebar = renderSidebar proto._renderSidebar = function (text) {
Docsify.prototype._renderNav = renderNav this._renderTo('.sidebar-nav', sidebar(text))
Docsify.prototype._renderMain = renderMain // bind event
}
proto._renderNav = function (text) {
this._renderTo('nav', markdown(text))
}
proto._renderMain = function (text) {
callHook(this, 'beforeEach', text, result => {
const html = markdown(result)
callHook(this, 'afterEach', html, text => renderMain.call(this, text))
})
}
} }
export function initRender (vm) { export function initRender (vm) {
const config = vm.config const config = vm.config
// Init markdown compiler
markdown.init(vm.config.markdown)
const id = config.el || '#app' const id = config.el || '#app'
const navEl = dom.find('nav') || dom.create('nav') const navEl = dom.find('nav') || dom.create('nav')

View File

@ -0,0 +1,27 @@
let cache = {}
const re = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,.\/:;<=>?@\[\]^`{|}~]/g
export function slugify (str) {
if (typeof str !== 'string') return ''
let slug = str.toLowerCase().trim()
.replace(/<[^>\d]+>/g, '')
.replace(re, '')
.replace(/\s/g, '-')
.replace(/-+/g, '-')
.replace(/^(\d)/, '_$1')
let count = cache[slug]
count = cache.hasOwnProperty(slug) ? (count + 1) : 0
cache[slug] = count
if (count) {
slug = slug + '-' + count
}
return slug
}
export function clearSlugCache () {
cache = {}
}

View File

@ -1,4 +1,5 @@
import { parseQuery } from './util' import { merge } from '../util/core'
import { parseQuery, stringifyQuery, cleanPath } from './util'
function replaceHash (path) { function replaceHash (path) {
const i = window.location.href.indexOf('#') const i = window.location.href.indexOf('#')
@ -34,11 +35,11 @@ export function getHash () {
} }
/** /**
* Parse the current url * Parse the url
* @param {string} [path=window.location.herf]
* @return {object} { path, query } * @return {object} { path, query }
*/ */
export function parse () { export function parse (path = window.location.href) {
let path = window.location.href
let query = '' let query = ''
const queryIndex = path.indexOf('?') const queryIndex = path.indexOf('?')
@ -57,9 +58,14 @@ export function parse () {
/** /**
* to URL * to URL
* @param {String} path * @param {string} path
* @param {String} qs query string * @param {object} qs query params
*/ */
export function toURL (path, qs) { export function toURL (path, params) {
const route = parse(path)
route.query = merge({}, route.query, params)
path = route.path + stringifyQuery(route.query)
return '#' + path
} }

View File

@ -30,13 +30,19 @@ export function routeMixin (Docsify) {
} }
} }
let lastRoute = {}
export function initRoute (vm) { export function initRoute (vm) {
normalize() normalize()
vm.route = parse() lastRoute = vm.route = parse()
on('hashchange', _ => { on('hashchange', _ => {
normalize() normalize()
vm.route = parse() lastRoute = vm.route = parse()
if (lastRoute.path === vm.route.path) {
// TODO: goto xxx
return
}
vm._fetch() vm._fetch()
}) })
} }

View File

@ -1,6 +1,7 @@
import { cached } from '../util/core' import { cached } from '../util/core'
const decode = decodeURIComponent const decode = decodeURIComponent
const encode = encodeURIComponent
export const parseQuery = cached(query => { export const parseQuery = cached(query => {
const res = {} const res = {}
@ -11,35 +12,35 @@ export const parseQuery = cached(query => {
return res return res
} }
// Simple parse
query.split('&').forEach(function (param) { query.split('&').forEach(function (param) {
const parts = param.replace(/\+/g, ' ').split('=') 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[parts[0]] = decode(parts[1])
res[key] = val
} else if (Array.isArray(res[key])) {
res[key].push(val)
} else {
res[key] = [res[key], val]
}
}) })
return res return res
}) })
export function stringifyQuery (obj) {
const qs = []
for (const key in obj) {
qs.push(`${encode(key)}=${encode(obj[key])}`)
}
return qs.length ? `?${qs.join('&')}` : ''
}
export const getBasePath = cached(base => {
return /^(\/|https?:)/g.test(base)
? base
: cleanPath(window.location.pathname + '/' + base)
})
export const getRoot = cached(path => {
return /\/$/g.test(path) ? path : path.match(/(\S*\/)[^\/]+$/)[1]
})
export function cleanPath (path) { export function cleanPath (path) {
return path.replace(/\/+/g, '/') return path.replace(/\/+/g, '/')
} }
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]
}

View File

@ -1,166 +0,0 @@
/**
* Simple ajax
* @param {String} url
* @param {String} [method=GET]
* @param {Function} [loading] handle loading
* @return {Promise}
*/
export function load (url, method = 'GET', loading) {
const xhr = new XMLHttpRequest()
xhr.open(method, url)
xhr.send()
return {
then: function (success, error = function () {}) {
if (loading) {
const id = setInterval(_ =>
loading({ step: Math.floor(Math.random() * 5 + 1) }),
500)
xhr.addEventListener('progress', loading)
xhr.addEventListener('loadend', evt => {
loading(evt)
clearInterval(id)
})
}
xhr.addEventListener('error', error)
xhr.addEventListener('load', ({ target }) => {
target.status >= 400 ? error(target) : success(target.response)
})
},
abort: () => xhr.readyState !== 4 && xhr.abort()
}
}
/**
* gen toc tree
* @link https://github.com/killercup/grock/blob/5280ae63e16c5739e9233d9009bc235ed7d79a50/styles/solarized/assets/js/behavior.coffee#L54-L81
* @param {Array} toc
* @param {Number} maxLevel
* @return {Array}
*/
export function genTree (toc, maxLevel) {
const headlines = []
const last = {}
toc.forEach(headline => {
const level = headline.level || 1
const len = level - 1
if (level > maxLevel) return
if (last[len]) {
last[len].children = last[len].children || []
last[len].children.push(headline)
} else {
headlines.push(headline)
}
last[level] = headline
})
return headlines
}
/**
* camel to kebab
* @link https://github.com/bokuweb/kebab2camel/blob/master/index.js
* @param {String} str
* @return {String}
*/
export function camel2kebab (str) {
return str.replace(/([A-Z])/g, m => '-' + m.toLowerCase())
}
/**
* is nil
* @param {Object} object
* @return {Boolean}
*/
export function isNil (o) {
return o === null || o === undefined
}
let cacheRoute = null
let cacheHash = null
/**
* hash route
*/
export function getRoute () {
const loc = window.location
if (cacheHash === loc.hash && !isNil(cacheRoute)) return cacheRoute
let route = loc.hash.replace(/%23/g, '#').match(/^#\/([^#]+)/)
if (route && route.length === 2) {
route = route[1]
} else {
route = /^#\//.test(loc.hash) ? '' : loc.pathname
}
cacheRoute = route
cacheHash = loc.hash
return route
}
export function isMobile () {
return document.body.clientWidth <= 600
}
export function slugify (string) {
const re = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,.\/:;<=>?@\[\]^`{|}~]/g
const maintainCase = false
const replacement = '-'
slugify.occurrences = slugify.occurrences || {}
if (typeof string !== 'string') return ''
if (!maintainCase) string = string.toLowerCase()
let slug = string.trim()
.replace(/<[^>\d]+>/g, '')
.replace(re, '')
.replace(/\s/g, replacement)
.replace(/-+/g, replacement)
.replace(/^(\d)/, '_$1')
let occurrences = slugify.occurrences[slug]
if (slugify.occurrences.hasOwnProperty(slug)) {
occurrences++
} else {
occurrences = 0
}
slugify.occurrences[slug] = occurrences
if (occurrences) {
slug = slug + '-' + occurrences
}
return slug
}
slugify.clear = function () {
slugify.occurrences = {}
}
const hasOwnProperty = Object.prototype.hasOwnProperty
export const merge = Object.assign || function (to) {
for (let i = 1; i < arguments.length; i++) {
const from = Object(arguments[i])
for (const key in from) {
if (hasOwnProperty.call(from, key)) {
to[key] = from[key]
}
}
}
return to
}
export function emojify (text) {
return text
.replace(/<(pre|template)[^>]*?>([\s\S]+)<\/(pre|template)>/g, match => match.replace(/:/g, '__colon__'))
.replace(/:(\w+?):/ig, '<img class="emoji" src="https://assets-cdn.github.com/images/icons/emoji/$1.png" alt="$1" />')
.replace(/__colon__/g, ':')
}