From fe88c154b0d48e1d5eb79f2c7dfcd1d60beb83cf Mon Sep 17 00:00:00 2001 From: "qingwei.li" Date: Sat, 18 Feb 2017 14:09:17 +0800 Subject: [PATCH] refactor(core): and markdown compiler --- .eslintrc | 3 - src/core/fetch/index.js | 11 +-- src/core/index.js | 2 +- src/core/render/compiler.js | 68 ++++++++++++--- src/core/render/emojify.js | 6 ++ src/core/render/gen-tree.js | 27 ++++++ src/core/render/index.js | 41 ++++++--- src/core/render/slugify.js | 27 ++++++ src/core/route/hash.js | 20 +++-- src/core/route/index.js | 10 ++- src/core/route/util.js | 45 +++++----- src/util.js | 166 ------------------------------------ 12 files changed, 194 insertions(+), 232 deletions(-) create mode 100644 src/core/render/emojify.js create mode 100644 src/core/render/gen-tree.js create mode 100644 src/core/render/slugify.js delete mode 100644 src/util.js diff --git a/.eslintrc b/.eslintrc index 527ed9ba..86d102d9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,8 +2,5 @@ "extends": ["vue"], "env": { "browser": true - }, - "globals": { - "$docsify": true } } diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js index 2fb7aa01..3602baf4 100644 --- a/src/core/fetch/index.js +++ b/src/core/fetch/index.js @@ -1,14 +1,15 @@ import { get } from './ajax' import { callHook } from '../init/lifecycle' -import { getCurrentRoot } from '../route/util' +import { getRoot } from '../route/util' +import { noop } from '../util/core' export function fetchMixin (Docsify) { let last - Docsify.prototype._fetch = function (cb) { + Docsify.prototype._fetch = function (cb = noop) { const { path } = this.route const { loadNavbar, loadSidebar } = this.config - const currentRoot = getCurrentRoot(path) + const root = getRoot(path) // Abort last request last && last.abort && last.abort() @@ -21,14 +22,14 @@ export function fetchMixin (Docsify) { const fn = result => { this._renderSidebar(result); cb() } // Load sidebar - get(this.$getFile(currentRoot + loadSidebar)) + get(this.$getFile(root + loadSidebar)) .then(fn, _ => get(loadSidebar).then(fn)) }, _ => this._renderMain(null)) // Load nav loadNavbar && - get(this.$getFile(currentRoot + loadNavbar)) + get(this.$getFile(root + loadNavbar)) .then( this._renderNav, _ => get(loadNavbar).then(this._renderNav) diff --git a/src/core/index.js b/src/core/index.js index e2c94a48..3ac8b56d 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -23,4 +23,4 @@ initGlobalAPI() /** * Run Docsify */ -setTimeout(() => new Docsify(), 0) +new Docsify() diff --git a/src/core/render/compiler.js b/src/core/render/compiler.js index daed38c1..2d202407 100644 --- a/src/core/render/compiler.js +++ b/src/core/render/compiler.js @@ -1,26 +1,56 @@ import marked from 'marked' 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() - -export function markdown () { - -} +let markdownCompiler = marked +let contentBase = '' +let renderer = new marked.Renderer() 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 * @link https://github.com/chjj/marked#overriding-renderer-methods */ renderer.heading = function (text, level) { const slug = slugify(text) - let route = '' + const url = toURL(contentBase, { id: slug }) - route = `#/${getRoute()}` - toc.push({ level, slug: `${route}#${encodeURIComponent(slug)}`, title: text }) + toc.push({ level, slug: url, title: text }) - return `${text}` + return `${text}` } // highlight code renderer.code = function (code, lang = '') { @@ -30,21 +60,31 @@ renderer.code = function (code, lang = '') { } renderer.link = function (href, title, text) { if (!/:|(\/{2})/.test(href)) { + // TODO href = `#/${href}`.replace(/\/+/g, '/') } return `${text}` } renderer.paragraph = function (text) { if (/^!>/.test(text)) { - return tpl.helper('tip', text) + return helperTpl('tip', text) } else if (/^\?>/.test(text)) { - return tpl.helper('warn', text) + return helperTpl('warn', text) } return `

${text}

` } renderer.image = function (href, title, text) { - const url = /:|(\/{2})/.test(href) ? href : ($docsify.basePath + href).replace(/\/+/g, '/') - const titleHTML = title ? ` title="${title}"` : '' + // TODO + // get base path + // const url = /:|(\/{2})/.test(href) ? href : ($docsify.basePath + href).replace(/\/+/g, '/') + // const titleHTML = title ? ` title="${title}"` : '' + + // return `${text}` +} + +/** + * Compile sidebar + */ +export function sidebar (text) { - return `${text}` } diff --git a/src/core/render/emojify.js b/src/core/render/emojify.js new file mode 100644 index 00000000..bc414a3b --- /dev/null +++ b/src/core/render/emojify.js @@ -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, '$1') + .replace(/__colon__/g, ':') +} diff --git a/src/core/render/gen-tree.js b/src/core/render/gen-tree.js new file mode 100644 index 00000000..84ff05e7 --- /dev/null +++ b/src/core/render/gen-tree.js @@ -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 +} diff --git a/src/core/render/index.js b/src/core/render/index.js index 2f9610f2..ff25518e 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -1,30 +1,47 @@ import * as dom from '../util/dom' import cssVars from '../util/polyfill/css-vars' import * as tpl from './tpl' +import { markdown, sidebar } from './compiler' +import { callHook } from '../init/lifecycle' -function renderMain () { - -} - -function renderNav () { -} - -function renderSidebar () { +function renderMain (html) { + if (!html) { + // TODO: Custom 404 page + } + this._renderTo('.markdown-section', html) } 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) if (node) node[replace ? 'outerHTML' : 'innerHTML'] = content } - Docsify.prototype._renderSidebar = renderSidebar - Docsify.prototype._renderNav = renderNav - Docsify.prototype._renderMain = renderMain + proto._renderSidebar = function (text) { + this._renderTo('.sidebar-nav', sidebar(text)) + // 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) { const config = vm.config + + // Init markdown compiler + markdown.init(vm.config.markdown) + const id = config.el || '#app' const navEl = dom.find('nav') || dom.create('nav') diff --git a/src/core/render/slugify.js b/src/core/render/slugify.js new file mode 100644 index 00000000..a6e9d39b --- /dev/null +++ b/src/core/render/slugify.js @@ -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 = {} +} diff --git a/src/core/route/hash.js b/src/core/route/hash.js index fd0607e6..b114f3db 100644 --- a/src/core/route/hash.js +++ b/src/core/route/hash.js @@ -1,4 +1,5 @@ -import { parseQuery } from './util' +import { merge } from '../util/core' +import { parseQuery, stringifyQuery, cleanPath } from './util' function replaceHash (path) { 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 } */ -export function parse () { - let path = window.location.href +export function parse (path = window.location.href) { let query = '' const queryIndex = path.indexOf('?') @@ -57,9 +58,14 @@ export function parse () { /** * to URL - * @param {String} path - * @param {String} qs query string + * @param {string} path + * @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 } diff --git a/src/core/route/index.js b/src/core/route/index.js index 0d4a3c61..3fa1020f 100644 --- a/src/core/route/index.js +++ b/src/core/route/index.js @@ -30,13 +30,19 @@ export function routeMixin (Docsify) { } } +let lastRoute = {} + export function initRoute (vm) { normalize() - vm.route = parse() + lastRoute = vm.route = parse() on('hashchange', _ => { normalize() - vm.route = parse() + lastRoute = vm.route = parse() + if (lastRoute.path === vm.route.path) { + // TODO: goto xxx + return + } vm._fetch() }) } diff --git a/src/core/route/util.js b/src/core/route/util.js index 300253a4..635273b3 100644 --- a/src/core/route/util.js +++ b/src/core/route/util.js @@ -1,6 +1,7 @@ import { cached } from '../util/core' const decode = decodeURIComponent +const encode = encodeURIComponent export const parseQuery = cached(query => { const res = {} @@ -11,35 +12,35 @@ export const parseQuery = cached(query => { return res } + // Simple parse 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] - } + res[parts[0]] = decode(parts[1]) }) - 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) { 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] -} diff --git a/src/util.js b/src/util.js deleted file mode 100644 index 8f182dff..00000000 --- a/src/util.js +++ /dev/null @@ -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, '$1') - .replace(/__colon__/g, ':') -}