From 4055395abf8345669e2f12aa1b2eb9727634cc29 Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 7 Jun 2020 15:59:56 -0700 Subject: [PATCH 01/12] convert Docsify to an ES class to start modernizing the code base --- src/core/Docsify.js | 17 +- src/core/event/index.js | 14 +- src/core/fetch/index.js | 353 +++++++++++++++++++------------------- src/core/init/index.js | 26 +-- src/core/render/index.js | 234 +++++++++++++------------ src/core/router/index.js | 4 +- test/_helper.js | 20 +-- test/unit/docsify.test.js | 6 +- 8 files changed, 332 insertions(+), 342 deletions(-) diff --git a/src/core/Docsify.js b/src/core/Docsify.js index fcb40336b..d586059c5 100644 --- a/src/core/Docsify.js +++ b/src/core/Docsify.js @@ -5,18 +5,15 @@ import { fetchMixin } from './fetch'; import { eventMixin } from './event'; import initGlobalAPI from './global-api'; -export function Docsify() { - this._init(); +export class Docsify extends initMixin( + routerMixin(renderMixin(fetchMixin(eventMixin()))) +) { + constructor() { + super(); + this._init(); + } } -const proto = Docsify.prototype; - -initMixin(proto); -routerMixin(proto); -renderMixin(proto); -fetchMixin(proto); -eventMixin(proto); - /** * Global API */ diff --git a/src/core/event/index.js b/src/core/event/index.js index 4a312421e..4d3efc2a9 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -3,11 +3,11 @@ import { body, on } from '../util/dom'; import * as sidebar from './sidebar'; import { scrollIntoView, scroll2Top } from './scroll'; -export function eventMixin(proto) { - proto.$resetEvents = function(source) { - const { auto2top } = this.config; +export function eventMixin(Base = class {}) { + return class extends Base { + $resetEvents(source) { + const { auto2top } = this.config; - (() => { // Rely on the browser's scroll auto-restoration when going back or forward if (source === 'history') { return; @@ -20,10 +20,10 @@ export function eventMixin(proto) { if (source === 'navigate') { auto2top && scroll2Top(auto2top); } - })(); - if (this.config.loadNavbar) { - sidebar.getAndActive(this.router, 'nav'); + if (this.config.loadNavbar) { + sidebar.getAndActive(this.router, 'nav'); + } } }; } diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js index 28fb84434..6fa4df595 100644 --- a/src/core/fetch/index.js +++ b/src/core/fetch/index.js @@ -20,208 +20,207 @@ function loadNested(path, qs, file, next, vm, first) { ).then(next, _ => loadNested(path, qs, file, next, vm)); } -export function fetchMixin(proto) { - let last; - - const abort = () => last && last.abort && last.abort(); - const request = (url, hasbar, requestHeaders) => { - abort(); - last = get(url, true, requestHeaders); - return last; - }; +let last; + +const abort = () => last && last.abort && last.abort(); +const request = (url, hasbar, requestHeaders) => { + abort(); + last = get(url, true, requestHeaders); + return last; +}; + +const get404Path = (path, config) => { + const { notFoundPage, ext } = config; + const defaultPath = '_404' + (ext || '.md'); + let key; + let path404; + + switch (typeof notFoundPage) { + case 'boolean': + path404 = defaultPath; + break; + case 'string': + path404 = notFoundPage; + break; + + case 'object': + key = Object.keys(notFoundPage) + .sort((a, b) => b.length - a.length) + .find(key => path.match(new RegExp('^' + key))); + + path404 = (key && notFoundPage[key]) || defaultPath; + break; + + default: + break; + } - const get404Path = (path, config) => { - const { notFoundPage, ext } = config; - const defaultPath = '_404' + (ext || '.md'); - let key; - let path404; - - switch (typeof notFoundPage) { - case 'boolean': - path404 = defaultPath; - break; - case 'string': - path404 = notFoundPage; - break; - - case 'object': - key = Object.keys(notFoundPage) - .sort((a, b) => b.length - a.length) - .find(key => path.match(new RegExp('^' + key))); - - path404 = (key && notFoundPage[key]) || defaultPath; - break; - - default: - break; - } + return path404; +}; - return path404; - }; +export function fetchMixin(Base = class {}) { + return class extends Base { + _loadSideAndNav(path, qs, loadSidebar, cb) { + return () => { + if (!loadSidebar) { + return cb(); + } - proto._loadSideAndNav = function(path, qs, loadSidebar, cb) { - return () => { - if (!loadSidebar) { - return cb(); - } + const fn = result => { + this._renderSidebar(result); + cb(); + }; - const fn = result => { - this._renderSidebar(result); - cb(); + // Load sidebar + loadNested(path, qs, loadSidebar, fn, this, true); }; + } - // Load sidebar - loadNested(path, qs, loadSidebar, fn, this, true); - }; - }; - - proto._fetch = function(cb = noop) { - const { path, query } = this.route; - const qs = stringifyQuery(query, ['id']); - const { loadNavbar, requestHeaders, loadSidebar } = this.config; - // Abort last request - - const file = this.router.getFile(path); - const req = request(file + qs, true, requestHeaders); - - // Current page is html - this.isHTML = /\.html$/g.test(file); - - // Load main content - req.then( - (text, opt) => - this._renderMain( - text, - opt, - this._loadSideAndNav(path, qs, loadSidebar, cb) - ), - _ => { - this._fetchFallbackPage(file, qs, cb) || this._fetch404(file, qs, cb); - } - ); - - // Load nav - loadNavbar && - loadNested( - path, - qs, - loadNavbar, - text => this._renderNav(text), - this, - true + _fetch(cb = noop) { + const { path, query } = this.route; + const qs = stringifyQuery(query, ['id']); + const { loadNavbar, requestHeaders, loadSidebar } = this.config; + // Abort last request + + const file = this.router.getFile(path); + const req = request(file + qs, true, requestHeaders); + + // Current page is html + this.isHTML = /\.html$/g.test(file); + + // Load main content + req.then( + (text, opt) => + this._renderMain( + text, + opt, + this._loadSideAndNav(path, qs, loadSidebar, cb) + ), + _ => { + this._fetchFallbackPage(file, qs, cb) || this._fetch404(file, qs, cb); + } ); - }; - proto._fetchCover = function() { - const { coverpage, requestHeaders } = this.config; - const query = this.route.query; - const root = getParentPath(this.route.path); - - if (coverpage) { - let path = null; - const routePath = this.route.path; - if (typeof coverpage === 'string') { - if (routePath === '/') { - path = coverpage; + // Load nav + loadNavbar && + loadNested( + path, + qs, + loadNavbar, + text => this._renderNav(text), + this, + true + ); + } + + _fetchCover() { + const { coverpage, requestHeaders } = this.config; + const query = this.route.query; + const root = getParentPath(this.route.path); + + if (coverpage) { + let path = null; + const routePath = this.route.path; + if (typeof coverpage === 'string') { + if (routePath === '/') { + path = coverpage; + } + } else if (Array.isArray(coverpage)) { + path = coverpage.indexOf(routePath) > -1 && '_coverpage'; + } else { + const cover = coverpage[routePath]; + path = cover === true ? '_coverpage' : cover; } - } else if (Array.isArray(coverpage)) { - path = coverpage.indexOf(routePath) > -1 && '_coverpage'; - } else { - const cover = coverpage[routePath]; - path = cover === true ? '_coverpage' : cover; - } - const coverOnly = Boolean(path) && this.config.onlyCover; - if (path) { - path = this.router.getFile(root + path); - this.coverIsHTML = /\.html$/g.test(path); - get( - path + stringifyQuery(query, ['id']), - false, - requestHeaders - ).then(text => this._renderCover(text, coverOnly)); - } else { - this._renderCover(null, coverOnly); - } + const coverOnly = Boolean(path) && this.config.onlyCover; + if (path) { + path = this.router.getFile(root + path); + this.coverIsHTML = /\.html$/g.test(path); + get( + path + stringifyQuery(query, ['id']), + false, + requestHeaders + ).then(text => this._renderCover(text, coverOnly)); + } else { + this._renderCover(null, coverOnly); + } - return coverOnly; + return coverOnly; + } } - }; - proto.$fetch = function( - cb = noop, - $resetEvents = this.$resetEvents.bind(this) - ) { - const done = () => { - callHook(this, 'doneEach'); - cb(); - }; - - const onlyCover = this._fetchCover(); - - if (onlyCover) { - done(); - } else { - this._fetch(() => { - $resetEvents(); - done(); - }); - } - }; + $fetch(cb = noop, $resetEvents = this.$resetEvents.bind(this)) { + const done = () => { + callHook(this, 'doneEach'); + cb(); + }; - proto._fetchFallbackPage = function(path, qs, cb = noop) { - const { requestHeaders, fallbackLanguages, loadSidebar } = this.config; + const onlyCover = this._fetchCover(); - if (!fallbackLanguages) { - return false; + if (onlyCover) { + done(); + } else { + this._fetch(() => { + $resetEvents(); + done(); + }); + } } - const local = path.split('/')[1]; + _fetchFallbackPage(path, qs, cb = noop) { + const { requestHeaders, fallbackLanguages, loadSidebar } = this.config; - if (fallbackLanguages.indexOf(local) === -1) { - return false; - } - - const newPath = path.replace(new RegExp(`^/${local}`), ''); - const req = request(newPath + qs, true, requestHeaders); + if (!fallbackLanguages) { + return false; + } - req.then( - (text, opt) => - this._renderMain( - text, - opt, - this._loadSideAndNav(path, qs, loadSidebar, cb) - ), - () => this._fetch404(path, qs, cb) - ); + const local = path.split('/')[1]; - return true; - }; + if (fallbackLanguages.indexOf(local) === -1) { + return false; + } - /** - * Load the 404 page - * @param {String} path URL to be loaded - * @param {*} qs TODO: define - * @param {Function} cb Callback - * @returns {Boolean} True if the requested page is not found - * @private - */ - proto._fetch404 = function(path, qs, cb = noop) { - const { loadSidebar, requestHeaders, notFoundPage } = this.config; - - const fnLoadSideAndNav = this._loadSideAndNav(path, qs, loadSidebar, cb); - if (notFoundPage) { - const path404 = get404Path(path, this.config); - - request(this.router.getFile(path404), true, requestHeaders).then( - (text, opt) => this._renderMain(text, opt, fnLoadSideAndNav), - () => this._renderMain(null, {}, fnLoadSideAndNav) + const newPath = path.replace(new RegExp(`^/${local}`), ''); + const req = request(newPath + qs, true, requestHeaders); + + req.then( + (text, opt) => + this._renderMain( + text, + opt, + this._loadSideAndNav(path, qs, loadSidebar, cb) + ), + () => this._fetch404(path, qs, cb) ); + return true; } - this._renderMain(null, {}, fnLoadSideAndNav); - return false; + /** + * Load the 404 page + * @param {String} path URL to be loaded + * @param {*} qs TODO: define + * @param {Function} cb Callback + * @returns {Boolean} True if the requested page is not found + * @private + */ + _fetch404(path, qs, cb = noop) { + const { loadSidebar, requestHeaders, notFoundPage } = this.config; + + const fnLoadSideAndNav = this._loadSideAndNav(path, qs, loadSidebar, cb); + if (notFoundPage) { + const path404 = get404Path(path, this.config); + + request(this.router.getFile(path404), true, requestHeaders).then( + (text, opt) => this._renderMain(text, opt, fnLoadSideAndNav), + () => this._renderMain(null, {}, fnLoadSideAndNav) + ); + return true; + } + + this._renderMain(null, {}, fnLoadSideAndNav); + return false; + } }; } diff --git a/src/core/init/index.js b/src/core/init/index.js index 255aad06c..1fa6ff140 100644 --- a/src/core/init/index.js +++ b/src/core/init/index.js @@ -6,19 +6,21 @@ import { initFetch } from '../fetch'; import { isFn } from '../util/core'; import { initLifecycle, callHook } from './lifecycle'; -export function initMixin(proto) { - proto._init = function() { - const vm = this; - vm.config = config(vm); +export function initMixin(Base = class {}) { + return class extends Base { + _init() { + const vm = this; + vm.config = config(vm); - initLifecycle(vm); // Init hooks - initPlugin(vm); // Install plugins - callHook(vm, 'init'); - initRouter(vm); // Add router - initRender(vm); // Render base DOM - initEvent(vm); // Bind events - initFetch(vm); // Fetch data - callHook(vm, 'mounted'); + initLifecycle(vm); // Init hooks + initPlugin(vm); // Install plugins + callHook(vm, 'init'); + initRouter(vm); // Add router + initRender(vm); // Render base DOM + initEvent(vm); // Bind events + initFetch(vm); // Fetch data + callHook(vm, 'mounted'); + } }; } diff --git a/src/core/render/index.js b/src/core/render/index.js index 37cd6af34..1aad379d5 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -86,147 +86,155 @@ function renderNameLink(vm) { } } -export function renderMixin(proto) { - proto._renderTo = function(el, content, replace) { - const node = dom.getNode(el); - if (node) { - node[replace ? 'outerHTML' : 'innerHTML'] = content; +export function renderMixin(Base = class {}) { + return class extends Base { + _renderTo(el, content, replace) { + const node = dom.getNode(el); + if (node) { + node[replace ? 'outerHTML' : 'innerHTML'] = content; + } } - }; - proto._renderSidebar = function(text) { - const { maxLevel, subMaxLevel, loadSidebar, hideSidebar } = this.config; - - if (hideSidebar) { - // FIXME : better styling solution - document.querySelector('aside.sidebar').remove(); - document.querySelector('button.sidebar-toggle').remove(); - document.querySelector('section.content').style.right = 'unset'; - document.querySelector('section.content').style.left = 'unset'; - document.querySelector('section.content').style.position = 'relative'; - document.querySelector('section.content').style.width = '100%'; - return null; - } + _renderSidebar(text) { + const { maxLevel, subMaxLevel, loadSidebar, hideSidebar } = this.config; + + if (hideSidebar) { + // FIXME : better styling solution + document.querySelector('aside.sidebar').remove(); + document.querySelector('button.sidebar-toggle').remove(); + document.querySelector('section.content').style.right = 'unset'; + document.querySelector('section.content').style.left = 'unset'; + document.querySelector('section.content').style.position = 'relative'; + document.querySelector('section.content').style.width = '100%'; + return null; + } - this._renderTo('.sidebar-nav', this.compiler.sidebar(text, maxLevel)); - const activeEl = getAndActive(this.router, '.sidebar-nav', true, true); - if (loadSidebar && activeEl) { - activeEl.parentNode.innerHTML += - this.compiler.subSidebar(subMaxLevel) || ''; - } else { - // Reset toc - this.compiler.subSidebar(); - } + this._renderTo('.sidebar-nav', this.compiler.sidebar(text, maxLevel)); + const activeEl = getAndActive(this.router, '.sidebar-nav', true, true); + if (loadSidebar && activeEl) { + activeEl.parentNode.innerHTML += + this.compiler.subSidebar(subMaxLevel) || ''; + } else { + // Reset toc + this.compiler.subSidebar(); + } - // Bind event - this._bindEventOnRendered(activeEl); - }; + // Bind event + this._bindEventOnRendered(activeEl); + } - proto._bindEventOnRendered = function(activeEl) { - const { autoHeader } = this.config; + _bindEventOnRendered(activeEl) { + const { autoHeader } = this.config; - scrollActiveSidebar(this.router); + scrollActiveSidebar(this.router); - if (autoHeader && activeEl) { - const main = dom.getNode('#main'); - const firstNode = main.children[0]; - if (firstNode && firstNode.tagName !== 'H1') { - const h1 = this.compiler.header(activeEl.innerText, 1); - const wrapper = dom.create('div', h1); - dom.before(main, wrapper.children[0]); + if (autoHeader && activeEl) { + const main = dom.getNode('#main'); + const firstNode = main.children[0]; + if (firstNode && firstNode.tagName !== 'H1') { + const h1 = this.compiler.header(activeEl.innerText, 1); + const wrapper = dom.create('div', h1); + dom.before(main, wrapper.children[0]); + } } } - }; - proto._renderNav = function(text) { - text && this._renderTo('nav', this.compiler.compile(text)); - if (this.config.loadNavbar) { - getAndActive(this.router, 'nav'); + _renderNav(text) { + text && this._renderTo('nav', this.compiler.compile(text)); + if (this.config.loadNavbar) { + getAndActive(this.router, 'nav'); + } } - }; - proto._renderMain = function(text, opt = {}, next) { - if (!text) { - return renderMain.call(this, text); - } + _renderMain(text, opt = {}, next) { + if (!text) { + return renderMain.call(this, text); + } - callHook(this, 'beforeEach', text, result => { - let html; - const callback = () => { - if (opt.updatedAt) { - html = formatUpdated(html, opt.updatedAt, this.config.formatUpdated); - } + callHook(this, 'beforeEach', text, result => { + let html; + const callback = () => { + if (opt.updatedAt) { + html = formatUpdated( + html, + opt.updatedAt, + this.config.formatUpdated + ); + } - callHook(this, 'afterEach', html, text => renderMain.call(this, text)); - }; + callHook(this, 'afterEach', html, text => + renderMain.call(this, text) + ); + }; + + if (this.isHTML) { + html = this.result = text; + callback(); + next(); + } else { + prerenderEmbed( + { + compiler: this.compiler, + raw: result, + }, + tokens => { + html = this.compiler.compile(tokens); + callback(); + next(); + } + ); + } + }); + } - if (this.isHTML) { - html = this.result = text; - callback(); - next(); - } else { - prerenderEmbed( - { - compiler: this.compiler, - raw: result, - }, - tokens => { - html = this.compiler.compile(tokens); - callback(); - next(); - } - ); + _renderCover(text, coverOnly) { + const el = dom.getNode('.cover'); + + dom.toggleClass( + dom.getNode('main'), + coverOnly ? 'add' : 'remove', + 'hidden' + ); + if (!text) { + dom.toggleClass(el, 'remove', 'show'); + return; } - }); - }; - proto._renderCover = function(text, coverOnly) { - const el = dom.getNode('.cover'); + dom.toggleClass(el, 'add', 'show'); - dom.toggleClass( - dom.getNode('main'), - coverOnly ? 'add' : 'remove', - 'hidden' - ); - if (!text) { - dom.toggleClass(el, 'remove', 'show'); - return; - } - - dom.toggleClass(el, 'add', 'show'); + let html = this.coverIsHTML ? text : this.compiler.cover(text); - let html = this.coverIsHTML ? text : this.compiler.cover(text); + const m = html + .trim() + .match('

([^<]*?)

$'); - const m = html - .trim() - .match('

([^<]*?)

$'); + if (m) { + if (m[2] === 'color') { + el.style.background = m[1] + (m[3] || ''); + } else { + let path = m[1]; - if (m) { - if (m[2] === 'color') { - el.style.background = m[1] + (m[3] || ''); - } else { - let path = m[1]; + dom.toggleClass(el, 'add', 'has-mask'); + if (!isAbsolutePath(m[1])) { + path = getPath(this.router.getBasePath(), m[1]); + } - dom.toggleClass(el, 'add', 'has-mask'); - if (!isAbsolutePath(m[1])) { - path = getPath(this.router.getBasePath(), m[1]); + el.style.backgroundImage = `url(${path})`; + el.style.backgroundSize = 'cover'; + el.style.backgroundPosition = 'center center'; } - el.style.backgroundImage = `url(${path})`; - el.style.backgroundSize = 'cover'; - el.style.backgroundPosition = 'center center'; + html = html.replace(m[0], ''); } - html = html.replace(m[0], ''); + this._renderTo('.cover-main', html); + sticky(); } - this._renderTo('.cover-main', html); - sticky(); - }; - - proto._updateRender = function() { - // Render name link - renderNameLink(this); + _updateRender() { + // Render name link + renderNameLink(this); + } }; } diff --git a/src/core/router/index.js b/src/core/router/index.js index 923168898..7a7ba863e 100644 --- a/src/core/router/index.js +++ b/src/core/router/index.js @@ -4,8 +4,8 @@ import { noop } from '../util/core'; import { HashHistory } from './history/hash'; import { HTML5History } from './history/html5'; -export function routerMixin(proto) { - proto.route = {}; +export function routerMixin(Base = class {}) { + return class extends Base {}; } let lastRoute = {}; diff --git a/test/_helper.js b/test/_helper.js index 39ee5bdb1..37d26d4fc 100644 --- a/test/_helper.js +++ b/test/_helper.js @@ -57,30 +57,14 @@ module.exports.init = function( const dom = initJSDOM(markup); dom.reconfigure({ url: 'file:///' + rootPath }); - // Mimic src/core/index.js but for Node.js - function Docsify() { - this._init(); - } - - const proto = Docsify.prototype; - - const { initMixin } = require('../src/core/init'); - const { routerMixin } = require('../src/core//router'); - const { renderMixin } = require('../src/core//render'); - const { fetchMixin } = require('../src/core/fetch'); - const { eventMixin } = require('../src/core//event'); - - initMixin(proto); - routerMixin(proto); - renderMixin(proto); - fetchMixin(proto); - eventMixin(proto); + const { Docsify } = require('../src/core/Docsify'); const NOT_INIT_PATTERN = ''; return new Promise(resolve => { ready(() => { const docsify = new Docsify(); + // NOTE: I was not able to get it working with a callback, but polling works usually at the first time const id = setInterval(() => { if ( diff --git a/test/unit/docsify.test.js b/test/unit/docsify.test.js index 5f38cab63..c3c46163b 100644 --- a/test/unit/docsify.test.js +++ b/test/unit/docsify.test.js @@ -7,7 +7,8 @@ const handler = require('serve-handler'); const { expect } = require('chai'); const { initJSDOM } = require('../_helper'); -const docsifySite = 'http://127.0.0.1:3000'; +const port = 5432; +const docsifySite = 'http://127.0.0.1:' + port; const markup = /* html */ ` @@ -30,7 +31,7 @@ describe('Docsify public API', () => { return handler(request, response); }); - await new Promise(r => server.listen(3000, r)); + await new Promise(r => server.listen(port, r)); }); after(async () => { @@ -80,7 +81,6 @@ describe('Docsify public API', () => { expect(vm.constructor.name).to.equal('Docsify'); expect(vm.$fetch).to.be.an.instanceof(Function); expect(vm.$resetEvents).to.be.an.instanceof(Function); - expect(vm.route).to.be.an.instanceof(Object); window.configFunctionCalled = true; From 115a99437433c4651e8e4b35a25886ee4102d10b Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 7 Jun 2020 19:29:43 -0700 Subject: [PATCH 02/12] move lifecycle to its own mixin --- src/core/Docsify.js | 3 ++- src/core/fetch/index.js | 9 +++---- src/core/init/index.js | 20 +++++++-------- src/core/init/lifecycle.js | 45 ---------------------------------- src/core/lifecycle/index.js | 49 +++++++++++++++++++++++++++++++++++++ src/core/render/index.js | 7 ++---- 6 files changed, 66 insertions(+), 67 deletions(-) delete mode 100644 src/core/init/lifecycle.js create mode 100644 src/core/lifecycle/index.js diff --git a/src/core/Docsify.js b/src/core/Docsify.js index d586059c5..ce9b302be 100644 --- a/src/core/Docsify.js +++ b/src/core/Docsify.js @@ -1,4 +1,5 @@ import { initMixin } from './init'; +import { lifecycleMixin } from './lifecycle'; import { routerMixin } from './router'; import { renderMixin } from './render'; import { fetchMixin } from './fetch'; @@ -6,7 +7,7 @@ import { eventMixin } from './event'; import initGlobalAPI from './global-api'; export class Docsify extends initMixin( - routerMixin(renderMixin(fetchMixin(eventMixin()))) + lifecycleMixin(routerMixin(renderMixin(fetchMixin(eventMixin())))) ) { constructor() { super(); diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js index 6fa4df595..0863617e8 100644 --- a/src/core/fetch/index.js +++ b/src/core/fetch/index.js @@ -1,5 +1,4 @@ /* eslint-disable no-unused-vars */ -import { callHook } from '../init/lifecycle'; import { getParentPath, stringifyQuery } from '../router/util'; import { noop } from '../util/core'; import { getAndActive } from '../event/sidebar'; @@ -151,7 +150,7 @@ export function fetchMixin(Base = class {}) { $fetch(cb = noop, $resetEvents = this.$resetEvents.bind(this)) { const done = () => { - callHook(this, 'doneEach'); + this.callHook('doneEach'); cb(); }; @@ -236,9 +235,9 @@ export function initFetch(vm) { vm._bindEventOnRendered(activeEl); vm.$resetEvents(); - callHook(vm, 'doneEach'); - callHook(vm, 'ready'); + vm.callHook('doneEach'); + vm.callHook('ready'); } else { - vm.$fetch(_ => callHook(vm, 'ready')); + vm.$fetch(_ => vm.callHook('ready')); } } diff --git a/src/core/init/index.js b/src/core/init/index.js index 1fa6ff140..39d410c4d 100644 --- a/src/core/init/index.js +++ b/src/core/init/index.js @@ -4,22 +4,20 @@ import { initRouter } from '../router'; import { initEvent } from '../event'; import { initFetch } from '../fetch'; import { isFn } from '../util/core'; -import { initLifecycle, callHook } from './lifecycle'; export function initMixin(Base = class {}) { return class extends Base { _init() { - const vm = this; - vm.config = config(vm); + this.config = config(this); - initLifecycle(vm); // Init hooks - initPlugin(vm); // Install plugins - callHook(vm, 'init'); - initRouter(vm); // Add router - initRender(vm); // Render base DOM - initEvent(vm); // Bind events - initFetch(vm); // Fetch data - callHook(vm, 'mounted'); + this.initLifecycle(); // Init hooks + initPlugin(this); // Install plugins + this.callHook('init'); + initRouter(this); // Add router + initRender(this); // Render base DOM + initEvent(this); // Bind events + initFetch(this); // Fetch data + this.callHook('mounted'); } }; } diff --git a/src/core/init/lifecycle.js b/src/core/init/lifecycle.js deleted file mode 100644 index d8106adc9..000000000 --- a/src/core/init/lifecycle.js +++ /dev/null @@ -1,45 +0,0 @@ -import { noop } from '../util/core'; - -export function initLifecycle(vm) { - const hooks = [ - 'init', - 'mounted', - 'beforeEach', - 'afterEach', - 'doneEach', - 'ready', - ]; - - vm._hooks = {}; - vm._lifecycle = {}; - hooks.forEach(hook => { - const arr = (vm._hooks[hook] = []); - vm._lifecycle[hook] = fn => arr.push(fn); - }); -} - -export function callHook(vm, hook, data, next = noop) { - const queue = vm._hooks[hook]; - - const step = function(index) { - const hook = queue[index]; - if (index >= queue.length) { - next(data); - } else if (typeof hook === 'function') { - if (hook.length === 2) { - hook(data, result => { - data = result; - step(index + 1); - }); - } else { - const result = hook(data); - data = result === undefined ? data : result; - step(index + 1); - } - } else { - step(index + 1); - } - }; - - step(0); -} diff --git a/src/core/lifecycle/index.js b/src/core/lifecycle/index.js new file mode 100644 index 000000000..98c4dd664 --- /dev/null +++ b/src/core/lifecycle/index.js @@ -0,0 +1,49 @@ +import { noop } from '../util/core'; + +export function lifecycleMixin(Base = class {}) { + return class Lifecycle extends Base { + initLifecycle() { + const hooks = [ + 'init', + 'mounted', + 'beforeEach', + 'afterEach', + 'doneEach', + 'ready', + ]; + + this._hooks = {}; + this._lifecycle = {}; + hooks.forEach(hook => { + const arr = (this._hooks[hook] = []); + this._lifecycle[hook] = fn => arr.push(fn); + }); + } + + callHook(hook, data, next = noop) { + const queue = this._hooks[hook]; + + const step = function(index) { + const hook = queue[index]; + if (index >= queue.length) { + next(data); + } else if (typeof hook === 'function') { + if (hook.length === 2) { + hook(data, result => { + data = result; + step(index + 1); + }); + } else { + const result = hook(data); + data = result === undefined ? data : result; + step(index + 1); + } + } else { + step(index + 1); + } + }; + + step(0); + } + }; +} diff --git a/src/core/render/index.js b/src/core/render/index.js index 1aad379d5..fc4c1f253 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -2,7 +2,6 @@ import tinydate from 'tinydate'; import * as dom from '../util/dom'; import cssVars from '../util/polyfill/css-vars'; -import { callHook } from '../init/lifecycle'; import { getAndActive, sticky } from '../event/sidebar'; import { getPath, isAbsolutePath } from '../router/util'; import { isMobile, inBrowser } from '../util/env'; @@ -151,7 +150,7 @@ export function renderMixin(Base = class {}) { return renderMain.call(this, text); } - callHook(this, 'beforeEach', text, result => { + this.callHook('beforeEach', text, result => { let html; const callback = () => { if (opt.updatedAt) { @@ -162,9 +161,7 @@ export function renderMixin(Base = class {}) { ); } - callHook(this, 'afterEach', html, text => - renderMain.call(this, text) - ); + this.callHook('afterEach', html, text => renderMain.call(this, text)); }; if (this.isHTML) { From 05b569a6af98dcb965e825dbfb5f142073004264 Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 7 Jun 2020 19:55:36 -0700 Subject: [PATCH 03/12] move initPlugin into its own pluginMixin, and clean up the mixin application site (update buble to fix an unhelpful error message) --- build/build.js | 2 +- build/ssr.js | 2 +- package-lock.json | 114 ++++++++++++++++++++++++++++++++++++++ package.json | 2 +- src/core/Docsify.js | 12 +++- src/core/init/index.js | 7 +-- src/core/plugin/index.js | 11 ++++ src/core/util/multiple.js | 13 +++++ 8 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 src/core/plugin/index.js create mode 100644 src/core/util/multiple.js diff --git a/build/build.js b/build/build.js index 7dfa6b536..9b1a80b3a 100644 --- a/build/build.js +++ b/build/build.js @@ -1,5 +1,5 @@ const rollup = require('rollup') -const buble = require('rollup-plugin-buble') +const buble = require('@rollup/plugin-buble') const commonjs = require('rollup-plugin-commonjs') const nodeResolve = require('rollup-plugin-node-resolve') const { uglify } = require('rollup-plugin-uglify') diff --git a/build/ssr.js b/build/ssr.js index 01fdd0518..3b046f796 100644 --- a/build/ssr.js +++ b/build/ssr.js @@ -1,5 +1,5 @@ var rollup = require('rollup') -var buble = require('rollup-plugin-buble') +var buble = require('@rollup/plugin-buble') var async = require('rollup-plugin-async') var replace = require('rollup-plugin-replace') diff --git a/package-lock.json b/package-lock.json index 404c907fb..24721e65c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1646,6 +1646,100 @@ "universal-user-agent": "^4.0.0" } }, + "@rollup/plugin-buble": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-buble/-/plugin-buble-0.21.3.tgz", + "integrity": "sha512-Iv8cCuFPnMdqV4pcyU+OrfjOfagPArRQ1PyQjx5KgHk3dARedI+8PNTLSMpJts0lQJr8yF2pAU4GxpxCBJ9HYw==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.0.8", + "@types/buble": "^0.19.2", + "buble": "^0.20.0" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, + "buble": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/buble/-/buble-0.20.0.tgz", + "integrity": "sha512-/1gnaMQE8xvd5qsNBl+iTuyjJ9XxeaVxAMF86dQ4EyxFJOZtsgOS8Ra+7WHgZTam5IFDtt4BguN0sH0tVTKrOw==", + "dev": true, + "requires": { + "acorn": "^6.4.1", + "acorn-dynamic-import": "^4.0.0", + "acorn-jsx": "^5.2.0", + "chalk": "^2.4.2", + "magic-string": "^0.25.7", + "minimist": "^1.2.5", + "regexpu-core": "4.5.4" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "regexpu-core": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.5.4.tgz", + "integrity": "sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.0.2", + "regjsgen": "^0.5.0", + "regjsparser": "^0.6.0", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.1.0" + } + } + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + } + } + }, "@samverschueren/stream-to-observable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", @@ -1655,6 +1749,26 @@ "any-observable": "^0.3.0" } }, + "@types/buble": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@types/buble/-/buble-0.19.2.tgz", + "integrity": "sha512-uUD8zIfXMKThmFkahTXDGI3CthFH1kMg2dOm3KLi4GlC5cbARA64bEcUMbbWdWdE73eoc/iBB9PiTMqH0dNS2Q==", + "dev": true, + "requires": { + "magic-string": "^0.25.0" + }, + "dependencies": { + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + } + } + }, "@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", diff --git a/package.json b/package.json index 02fd700bd..00565b9b4 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "tweezer.js": "^1.4.0" }, "devDependencies": { + "@rollup/plugin-buble": "^0.21.3", "autoprefixer-stylus": "^1.0.0", "babel-eslint": "^10.0.3", "chai": "^4.2.0", @@ -95,7 +96,6 @@ "rimraf": "^3.0.0", "rollup": "^1.23.1", "rollup-plugin-async": "^1.2.0", - "rollup-plugin-buble": "^0.19.8", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-replace": "^2.2.0", diff --git a/src/core/Docsify.js b/src/core/Docsify.js index ce9b302be..729925fcc 100644 --- a/src/core/Docsify.js +++ b/src/core/Docsify.js @@ -1,13 +1,21 @@ +import { multiple } from './util/multiple'; import { initMixin } from './init'; import { lifecycleMixin } from './lifecycle'; +import { pluginMixin } from './plugin'; import { routerMixin } from './router'; import { renderMixin } from './render'; import { fetchMixin } from './fetch'; import { eventMixin } from './event'; import initGlobalAPI from './global-api'; -export class Docsify extends initMixin( - lifecycleMixin(routerMixin(renderMixin(fetchMixin(eventMixin())))) +export class Docsify extends multiple( + initMixin, + lifecycleMixin, + pluginMixin, + routerMixin, + renderMixin, + fetchMixin, + eventMixin ) { constructor() { super(); diff --git a/src/core/init/index.js b/src/core/init/index.js index 39d410c4d..c4c4d8ce4 100644 --- a/src/core/init/index.js +++ b/src/core/init/index.js @@ -3,7 +3,6 @@ import { initRender } from '../render'; import { initRouter } from '../router'; import { initEvent } from '../event'; import { initFetch } from '../fetch'; -import { isFn } from '../util/core'; export function initMixin(Base = class {}) { return class extends Base { @@ -11,7 +10,7 @@ export function initMixin(Base = class {}) { this.config = config(this); this.initLifecycle(); // Init hooks - initPlugin(this); // Install plugins + this.initPlugin(); // Install plugins this.callHook('init'); initRouter(this); // Add router initRender(this); // Render base DOM @@ -21,7 +20,3 @@ export function initMixin(Base = class {}) { } }; } - -function initPlugin(vm) { - [].concat(vm.config.plugins).forEach(fn => isFn(fn) && fn(vm._lifecycle, vm)); -} diff --git a/src/core/plugin/index.js b/src/core/plugin/index.js new file mode 100644 index 000000000..232aa7422 --- /dev/null +++ b/src/core/plugin/index.js @@ -0,0 +1,11 @@ +import { isFn } from '../util/core'; + +export function pluginMixin(Base = class {}) { + return class Plugin extends Base { + initPlugin() { + [] + .concat(this.config.plugins) + .forEach(fn => isFn(fn) && fn(this._lifecycle, this)); + } + }; +} diff --git a/src/core/util/multiple.js b/src/core/util/multiple.js new file mode 100644 index 000000000..d8e190253 --- /dev/null +++ b/src/core/util/multiple.js @@ -0,0 +1,13 @@ +/** + * A function for applying multiple mixins more tersely (less verbose) + * @param {Function[]} mixins - All args to this function should be mixins that take a class and return a class. + */ +export function multiple(...mixins) { + let Base = class {}; + + mixins.forEach(mixin => { + Base = mixin(Base); + }); + + return Base; +} From 8e982abf04aa819dd8a04eeb0a561bca66b9941b Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 7 Jun 2020 20:07:30 -0700 Subject: [PATCH 04/12] move router code into the class --- src/core/init/index.js | 3 +- src/core/render/index.js | 4 +-- src/core/router/index.js | 75 +++++++++++++++++++++------------------- 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/core/init/index.js b/src/core/init/index.js index c4c4d8ce4..65ed0f93d 100644 --- a/src/core/init/index.js +++ b/src/core/init/index.js @@ -1,6 +1,5 @@ import config from '../config'; import { initRender } from '../render'; -import { initRouter } from '../router'; import { initEvent } from '../event'; import { initFetch } from '../fetch'; @@ -12,7 +11,7 @@ export function initMixin(Base = class {}) { this.initLifecycle(); // Init hooks this.initPlugin(); // Install plugins this.callHook('init'); - initRouter(this); // Add router + this.initRouter(); // Add router initRender(this); // Render base DOM initEvent(this); // Bind events initFetch(this); // Fetch data diff --git a/src/core/render/index.js b/src/core/render/index.js index fc4c1f253..e6e24b368 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -228,7 +228,7 @@ export function renderMixin(Base = class {}) { sticky(); } - _updateRender() { + _render_updateRender() { // Render name link renderNameLink(this); } @@ -300,6 +300,6 @@ export function initRender(vm) { cssVars(config.themeColor); } - vm._updateRender(); + vm._render_updateRender(); dom.toggleClass(dom.body, 'ready'); } diff --git a/src/core/router/index.js b/src/core/router/index.js index 7a7ba863e..4ff6a68b3 100644 --- a/src/core/router/index.js +++ b/src/core/router/index.js @@ -5,43 +5,46 @@ import { HashHistory } from './history/hash'; import { HTML5History } from './history/html5'; export function routerMixin(Base = class {}) { - return class extends Base {}; -} - -let lastRoute = {}; - -function updateRender(vm) { - vm.router.normalize(); - vm.route = vm.router.parse(); - dom.body.setAttribute('data-page', vm.route.file); -} + return class extends Base { + constructor() { + super(); + this._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 { - router = new HashHistory(config); - } - - vm.router = router; - updateRender(vm); - lastRoute = vm.route; - - // eslint-disable-next-line no-unused-vars - router.onchange(params => { - updateRender(vm); - vm._updateRender(); - - if (lastRoute.path === vm.route.path) { - vm.$resetEvents(params.source); - return; + initRouter() { + const config = this.config; + const mode = config.routerMode || 'hash'; + let router; + + if (mode === 'history' && supportsPushState) { + router = new HTML5History(config); + } else { + router = new HashHistory(config); + } + + this.router = router; + this._router_updateRender(); + this._lastRoute = this.route; + + // eslint-disable-next-line no-unused-vars + router.onchange(params => { + this._router_updateRender(); + this._render_updateRender(); + + if (this._lastRoute.path === this.route.path) { + this.$resetEvents(params.source); + return; + } + + this.$fetch(noop, this.$resetEvents.bind(this, params.source)); + this._lastRoute = this.route; + }); } - vm.$fetch(noop, vm.$resetEvents.bind(vm, params.source)); - lastRoute = vm.route; - }); + _router_updateRender() { + this.router.normalize(); + this.route = this.router.parse(); + dom.body.setAttribute('data-page', this.route.file); + } + }; } From aa4f97bcfe7324e81a770993fc4937b8241a5670 Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 7 Jun 2020 20:10:09 -0700 Subject: [PATCH 05/12] move initRender into the renderMixin class --- src/core/init/index.js | 3 +- src/core/render/index.js | 113 ++++++++++++++++++++------------------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/core/init/index.js b/src/core/init/index.js index 65ed0f93d..8d7ddab16 100644 --- a/src/core/init/index.js +++ b/src/core/init/index.js @@ -1,5 +1,4 @@ import config from '../config'; -import { initRender } from '../render'; import { initEvent } from '../event'; import { initFetch } from '../fetch'; @@ -12,7 +11,7 @@ export function initMixin(Base = class {}) { this.initPlugin(); // Install plugins this.callHook('init'); this.initRouter(); // Add router - initRender(this); // Render base DOM + this.initRender(); // Render base DOM initEvent(this); // Bind events initFetch(this); // Fetch data this.callHook('mounted'); diff --git a/src/core/render/index.js b/src/core/render/index.js index e6e24b368..398ddc314 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -232,74 +232,75 @@ export function renderMixin(Base = class {}) { // Render name link renderNameLink(this); } - }; -} -export function initRender(vm) { - const config = vm.config; + initRender() { + const config = this.config; - // Init markdown compiler - vm.compiler = new Compiler(config, vm.router); - if (inBrowser) { - window.__current_docsify_compiler__ = vm.compiler; - } + // Init markdown compiler + this.compiler = new Compiler(config, this.router); + if (inBrowser) { + // TODO @trusktr, get rid of globals! + window.__current_docsify_compiler__ = this.compiler; + } - const id = config.el || '#app'; - const navEl = dom.find('nav') || dom.create('nav'); + const id = config.el || '#app'; + const navEl = dom.find('nav') || dom.create('nav'); - const el = dom.find(id); - let html = ''; - let navAppendToTarget = dom.body; + const el = dom.find(id); + let html = ''; + let navAppendToTarget = dom.body; - if (el) { - if (config.repo) { - html += tpl.corner(config.repo, config.cornerExternalLinkTarge); - } + if (el) { + if (config.repo) { + html += tpl.corner(config.repo, config.cornerExternalLinkTarge); + } - if (config.coverpage) { - html += tpl.cover(); - } + if (config.coverpage) { + html += tpl.cover(); + } - if (config.logo) { - const isBase64 = /^data:image/.test(config.logo); - const isExternal = /(?:http[s]?:)?\/\//.test(config.logo); - const isRelative = /^\./.test(config.logo); + if (config.logo) { + const isBase64 = /^data:image/.test(config.logo); + const isExternal = /(?:http[s]?:)?\/\//.test(config.logo); + const isRelative = /^\./.test(config.logo); - if (!isBase64 && !isExternal && !isRelative) { - config.logo = getPath(vm.router.getBasePath(), config.logo); - } - } + if (!isBase64 && !isExternal && !isRelative) { + config.logo = getPath(this.router.getBasePath(), config.logo); + } + } - html += tpl.main(config); - // Render main app - vm._renderTo(el, html, true); - } else { - vm.rendered = true; - } + html += tpl.main(config); + // Render main app + this._renderTo(el, html, true); + } else { + this.rendered = true; + } - if (config.mergeNavbar && isMobile) { - navAppendToTarget = dom.find('.sidebar'); - } else { - navEl.classList.add('app-nav'); + if (config.mergeNavbar && isMobile) { + navAppendToTarget = dom.find('.sidebar'); + } else { + navEl.classList.add('app-nav'); - if (!config.repo) { - navEl.classList.add('no-badge'); - } - } + if (!config.repo) { + navEl.classList.add('no-badge'); + } + } - // Add nav - if (config.loadNavbar) { - dom.before(navAppendToTarget, navEl); - } + // Add nav + if (config.loadNavbar) { + dom.before(navAppendToTarget, navEl); + } - if (config.themeColor) { - dom.$.head.appendChild( - dom.create('div', tpl.theme(config.themeColor)).firstElementChild - ); - // Polyfll - cssVars(config.themeColor); - } + if (config.themeColor) { + dom.$.head.appendChild( + dom.create('div', tpl.theme(config.themeColor)).firstElementChild + ); + // Polyfll + cssVars(config.themeColor); + } - vm._render_updateRender(); - dom.toggleClass(dom.body, 'ready'); + this._render_updateRender(); + dom.toggleClass(dom.body, 'ready'); + } + }; } From 71b85be1d1929aaa4f7c3065005de6d80b641efa Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 7 Jun 2020 20:12:30 -0700 Subject: [PATCH 06/12] move initEvent function into the eventMixin class --- src/core/event/index.js | 24 ++++++++++++------------ src/core/init/index.js | 3 +-- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/core/event/index.js b/src/core/event/index.js index 4d3efc2a9..a0c6d9650 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -25,17 +25,17 @@ export function eventMixin(Base = class {}) { sidebar.getAndActive(this.router, 'nav'); } } - }; -} -export function initEvent(vm) { - // Bind toggle button - sidebar.btn('button.sidebar-toggle', vm.router); - sidebar.collapse('.sidebar', vm.router); - // Bind sticky effect - if (vm.config.coverpage) { - !isMobile && on('scroll', sidebar.sticky); - } else { - body.classList.add('sticky'); - } + initEvent() { + // Bind toggle button + sidebar.btn('button.sidebar-toggle', this.router); + sidebar.collapse('.sidebar', this.router); + // Bind sticky effect + if (this.config.coverpage) { + !isMobile && on('scroll', sidebar.sticky); + } else { + body.classList.add('sticky'); + } + } + }; } diff --git a/src/core/init/index.js b/src/core/init/index.js index 8d7ddab16..26ec6a58d 100644 --- a/src/core/init/index.js +++ b/src/core/init/index.js @@ -1,5 +1,4 @@ import config from '../config'; -import { initEvent } from '../event'; import { initFetch } from '../fetch'; export function initMixin(Base = class {}) { @@ -12,7 +11,7 @@ export function initMixin(Base = class {}) { this.callHook('init'); this.initRouter(); // Add router this.initRender(); // Render base DOM - initEvent(this); // Bind events + this.initEvent(); // Bind events initFetch(this); // Fetch data this.callHook('mounted'); } From db08edaff92eb4134084db798b1635f6c5674c9d Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 7 Jun 2020 20:19:24 -0700 Subject: [PATCH 07/12] move fetch code into the fetchMixin class --- src/core/fetch/index.js | 157 +++++++++++++++++++++------------------- src/core/init/index.js | 3 +- 2 files changed, 84 insertions(+), 76 deletions(-) diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js index 0863617e8..b98bc2a90 100644 --- a/src/core/fetch/index.js +++ b/src/core/fetch/index.js @@ -4,61 +4,56 @@ import { noop } from '../util/core'; import { getAndActive } from '../event/sidebar'; import { get } from './ajax'; -function loadNested(path, qs, file, next, vm, first) { - path = first ? path : path.replace(/\/$/, ''); - path = getParentPath(path); - - if (!path) { - return; - } - - get( - vm.router.getFile(path + file) + qs, - false, - vm.config.requestHeaders - ).then(next, _ => loadNested(path, qs, file, next, vm)); -} - -let last; - -const abort = () => last && last.abort && last.abort(); -const request = (url, hasbar, requestHeaders) => { - abort(); - last = get(url, true, requestHeaders); - return last; -}; - -const get404Path = (path, config) => { - const { notFoundPage, ext } = config; - const defaultPath = '_404' + (ext || '.md'); - let key; - let path404; - - switch (typeof notFoundPage) { - case 'boolean': - path404 = defaultPath; - break; - case 'string': - path404 = notFoundPage; - break; +export function fetchMixin(Base = class {}) { + return class extends Base { + constructor() { + super(); + this._lastRequest = null; + } - case 'object': - key = Object.keys(notFoundPage) - .sort((a, b) => b.length - a.length) - .find(key => path.match(new RegExp('^' + key))); + _abort() { + return ( + this._lastRequest && + this._lastRequest.abort && + this._lastRequest.abort() + ); + } - path404 = (key && notFoundPage[key]) || defaultPath; - break; + _request(url, hasbar, requestHeaders) { + this._abort(); + this._lastRequest = get(url, true, requestHeaders); + return this._lastRequest; + } - default: - break; - } + _get404Path(path, config) { + const { notFoundPage, ext } = config; + const defaultPath = '_404' + (ext || '.md'); + let key; + let path404; + + switch (typeof notFoundPage) { + case 'boolean': + path404 = defaultPath; + break; + case 'string': + path404 = notFoundPage; + break; + + case 'object': + key = Object.keys(notFoundPage) + .sort((a, b) => b.length - a.length) + .find(key => path.match(new RegExp('^' + key))); + + path404 = (key && notFoundPage[key]) || defaultPath; + break; + + default: + break; + } - return path404; -}; + return path404; + } -export function fetchMixin(Base = class {}) { - return class extends Base { _loadSideAndNav(path, qs, loadSidebar, cb) { return () => { if (!loadSidebar) { @@ -71,7 +66,7 @@ export function fetchMixin(Base = class {}) { }; // Load sidebar - loadNested(path, qs, loadSidebar, fn, this, true); + this._loadNested(path, qs, loadSidebar, fn, true); }; } @@ -82,7 +77,7 @@ export function fetchMixin(Base = class {}) { // Abort last request const file = this.router.getFile(path); - const req = request(file + qs, true, requestHeaders); + const req = this._request(file + qs, true, requestHeaders); // Current page is html this.isHTML = /\.html$/g.test(file); @@ -102,16 +97,30 @@ export function fetchMixin(Base = class {}) { // Load nav loadNavbar && - loadNested( + this._loadNested( path, qs, loadNavbar, text => this._renderNav(text), - this, true ); } + _loadNested(path, qs, file, next, first) { + path = first ? path : path.replace(/\/$/, ''); + path = getParentPath(path); + + if (!path) { + return; + } + + get( + this.router.getFile(path + file) + qs, + false, + this.config.requestHeaders + ).then(next, _ => this._loadNested(path, qs, file, next)); + } + _fetchCover() { const { coverpage, requestHeaders } = this.config; const query = this.route.query; @@ -180,7 +189,7 @@ export function fetchMixin(Base = class {}) { } const newPath = path.replace(new RegExp(`^/${local}`), ''); - const req = request(newPath + qs, true, requestHeaders); + const req = this._request(newPath + qs, true, requestHeaders); req.then( (text, opt) => @@ -208,9 +217,9 @@ export function fetchMixin(Base = class {}) { const fnLoadSideAndNav = this._loadSideAndNav(path, qs, loadSidebar, cb); if (notFoundPage) { - const path404 = get404Path(path, this.config); + const path404 = this._get404Path(path, this.config); - request(this.router.getFile(path404), true, requestHeaders).then( + this._request(this.router.getFile(path404), true, requestHeaders).then( (text, opt) => this._renderMain(text, opt, fnLoadSideAndNav), () => this._renderMain(null, {}, fnLoadSideAndNav) ); @@ -220,24 +229,24 @@ export function fetchMixin(Base = class {}) { this._renderMain(null, {}, fnLoadSideAndNav); return false; } - }; -} -export function initFetch(vm) { - const { loadSidebar } = vm.config; + initFetch() { + const { loadSidebar } = this.config; - // Server-Side Rendering - if (vm.rendered) { - const activeEl = getAndActive(vm.router, '.sidebar-nav', true, true); - if (loadSidebar && activeEl) { - activeEl.parentNode.innerHTML += window.__SUB_SIDEBAR__; - } + // Server-Side Rendering + if (this.rendered) { + const activeEl = getAndActive(this.router, '.sidebar-nav', true, true); + if (loadSidebar && activeEl) { + activeEl.parentNode.innerHTML += window.__SUB_SIDEBAR__; + } - vm._bindEventOnRendered(activeEl); - vm.$resetEvents(); - vm.callHook('doneEach'); - vm.callHook('ready'); - } else { - vm.$fetch(_ => vm.callHook('ready')); - } + this._bindEventOnRendered(activeEl); + this.$resetEvents(); + this.callHook('doneEach'); + this.callHook('ready'); + } else { + this.$fetch(_ => this.callHook('ready')); + } + } + }; } diff --git a/src/core/init/index.js b/src/core/init/index.js index 26ec6a58d..03868e0f9 100644 --- a/src/core/init/index.js +++ b/src/core/init/index.js @@ -1,5 +1,4 @@ import config from '../config'; -import { initFetch } from '../fetch'; export function initMixin(Base = class {}) { return class extends Base { @@ -12,7 +11,7 @@ export function initMixin(Base = class {}) { this.initRouter(); // Add router this.initRender(); // Render base DOM this.initEvent(); // Bind events - initFetch(this); // Fetch data + this.initFetch(); // Fetch data this.callHook('mounted'); } }; From 8aa4df2837b81d589d5085e5d0994652b3480471 Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 7 Jun 2020 20:20:59 -0700 Subject: [PATCH 08/12] remove unnecessary _init call --- src/core/Docsify.js | 7 +------ src/core/init/index.js | 4 +++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/core/Docsify.js b/src/core/Docsify.js index 729925fcc..997bab758 100644 --- a/src/core/Docsify.js +++ b/src/core/Docsify.js @@ -16,12 +16,7 @@ export class Docsify extends multiple( renderMixin, fetchMixin, eventMixin -) { - constructor() { - super(); - this._init(); - } -} +) {} /** * Global API diff --git a/src/core/init/index.js b/src/core/init/index.js index 03868e0f9..ece0d684b 100644 --- a/src/core/init/index.js +++ b/src/core/init/index.js @@ -2,7 +2,9 @@ import config from '../config'; export function initMixin(Base = class {}) { return class extends Base { - _init() { + constructor() { + super(); + this.config = config(this); this.initLifecycle(); // Init hooks From a91c2ce7b524915c6861a000038aec364ad9b5e9 Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 7 Jun 2020 20:42:11 -0700 Subject: [PATCH 09/12] move render methods into renderMixin class --- src/core/render/index.js | 143 +++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 72 deletions(-) diff --git a/src/core/render/index.js b/src/core/render/index.js index 398ddc314..db93f1d59 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -11,82 +11,81 @@ import { Compiler } from './compiler'; import * as tpl from './tpl'; import { prerenderEmbed } from './embed'; -function executeScript() { - const script = dom - .findAll('.markdown-section>script') - .filter(s => !/template/.test(s.type))[0]; - if (!script) { - return false; - } - - const code = script.innerText.trim(); - if (!code) { - return false; - } - - setTimeout(_ => { - window.__EXECUTE_RESULT__ = new Function(code)(); - }, 0); -} +export function renderMixin(Base = class {}) { + return class extends Base { + _executeScript() { + const script = dom + .findAll('.markdown-section>script') + .filter(s => !/template/.test(s.type))[0]; + if (!script) { + return false; + } -function formatUpdated(html, updated, fn) { - updated = - typeof fn === 'function' - ? fn(updated) - : typeof fn === 'string' - ? tinydate(fn)(new Date(updated)) - : updated; + const code = script.innerText.trim(); + if (!code) { + return false; + } - return html.replace(/{docsify-updated}/g, updated); -} + setTimeout(_ => { + window.__EXECUTE_RESULT__ = new Function(code)(); + }, 0); + } -function renderMain(html) { - if (!html) { - html = '

404 - Not found

'; - } - - this._renderTo('.markdown-section', html); - // Render sidebar with the TOC - !this.config.loadSidebar && this._renderSidebar(); - - // Execute script - if ( - this.config.executeScript !== false && - typeof window.Vue !== 'undefined' && - !executeScript() - ) { - setTimeout(_ => { - const vueVM = window.__EXECUTE_RESULT__; - vueVM && vueVM.$destroy && vueVM.$destroy(); - window.__EXECUTE_RESULT__ = new window.Vue().$mount('#main'); - }, 0); - } else { - this.config.executeScript && executeScript(); - } -} + _formatUpdated(html, updated, fn) { + updated = + typeof fn === 'function' + ? fn(updated) + : typeof fn === 'string' + ? tinydate(fn)(new Date(updated)) + : updated; + + return html.replace(/{docsify-updated}/g, updated); + } + + __renderMain(html) { + if (!html) { + html = '

404 - Not found

'; + } -function renderNameLink(vm) { - const el = dom.getNode('.app-name-link'); - const nameLink = vm.config.nameLink; - const path = vm.route.path; + this._renderTo('.markdown-section', html); + // Render sidebar with the TOC + !this.config.loadSidebar && this._renderSidebar(); + + // Execute script + if ( + this.config.executeScript !== false && + typeof window.Vue !== 'undefined' && + !this._executeScript() + ) { + setTimeout(_ => { + const vueVM = window.__EXECUTE_RESULT__; + vueVM && vueVM.$destroy && vueVM.$destroy(); + window.__EXECUTE_RESULT__ = new window.Vue().$mount('#main'); + }, 0); + } else { + this.config.executeScript && this._executeScript(); + } + } - if (!el) { - return; - } + _renderNameLink(vm) { + const el = dom.getNode('.app-name-link'); + const nameLink = vm.config.nameLink; + const path = vm.route.path; - if (isPrimitive(vm.config.nameLink)) { - el.setAttribute('href', nameLink); - } else if (typeof nameLink === 'object') { - const match = Object.keys(nameLink).filter( - key => path.indexOf(key) > -1 - )[0]; + if (!el) { + return; + } - el.setAttribute('href', nameLink[match]); - } -} + if (isPrimitive(vm.config.nameLink)) { + el.setAttribute('href', nameLink); + } else if (typeof nameLink === 'object') { + const match = Object.keys(nameLink).filter( + key => path.indexOf(key) > -1 + )[0]; -export function renderMixin(Base = class {}) { - return class extends Base { + el.setAttribute('href', nameLink[match]); + } + } _renderTo(el, content, replace) { const node = dom.getNode(el); if (node) { @@ -147,21 +146,21 @@ export function renderMixin(Base = class {}) { _renderMain(text, opt = {}, next) { if (!text) { - return renderMain.call(this, text); + return this.__renderMain(text); } this.callHook('beforeEach', text, result => { let html; const callback = () => { if (opt.updatedAt) { - html = formatUpdated( + html = this._formatUpdated( html, opt.updatedAt, this.config.formatUpdated ); } - this.callHook('afterEach', html, text => renderMain.call(this, text)); + this.callHook('afterEach', html, text => this.__renderMain(text)); }; if (this.isHTML) { @@ -230,7 +229,7 @@ export function renderMixin(Base = class {}) { _render_updateRender() { // Render name link - renderNameLink(this); + this._renderNameLink(this); } initRender() { From 4e6294a75ce99660a24a2055941f9c8c6da435ec Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 7 Jun 2020 20:55:22 -0700 Subject: [PATCH 10/12] initialize globals in the index file, so the Docsify.js file only exports the Docsify class --- src/core/Docsify.js | 6 ------ src/core/index.js | 3 +++ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/core/Docsify.js b/src/core/Docsify.js index 997bab758..5f433c896 100644 --- a/src/core/Docsify.js +++ b/src/core/Docsify.js @@ -6,7 +6,6 @@ import { routerMixin } from './router'; import { renderMixin } from './render'; import { fetchMixin } from './fetch'; import { eventMixin } from './event'; -import initGlobalAPI from './global-api'; export class Docsify extends multiple( initMixin, @@ -17,8 +16,3 @@ export class Docsify extends multiple( fetchMixin, eventMixin ) {} - -/** - * Global API - */ -initGlobalAPI(); diff --git a/src/core/index.js b/src/core/index.js index 6e6164186..f37614dea 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -1,5 +1,8 @@ import { documentReady } from './util/dom'; import { Docsify } from './Docsify'; +import initGlobalAPI from './global-api'; + +initGlobalAPI(); /** * Run Docsify From 511e1c56d5cba32950639636651b84a57fe0e8c9 Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 7 Jun 2020 20:55:51 -0700 Subject: [PATCH 11/12] add JSDoc comments to start describing each class --- src/core/Docsify.js | 1 + src/core/event/index.js | 7 +++++++ src/core/fetch/index.js | 4 ++++ src/core/init/index.js | 4 ++++ src/core/lifecycle/index.js | 1 + src/core/plugin/index.js | 1 + src/core/render/compiler.js | 4 ++++ src/core/render/index.js | 6 ++++++ src/core/router/index.js | 4 ++++ 9 files changed, 32 insertions(+) diff --git a/src/core/Docsify.js b/src/core/Docsify.js index 5f433c896..cabc9cb2c 100644 --- a/src/core/Docsify.js +++ b/src/core/Docsify.js @@ -7,6 +7,7 @@ import { renderMixin } from './render'; import { fetchMixin } from './fetch'; import { eventMixin } from './event'; +/** This class contains all the magic. */ export class Docsify extends multiple( initMixin, lifecycleMixin, diff --git a/src/core/event/index.js b/src/core/event/index.js index a0c6d9650..aa03cc2dc 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -3,6 +3,13 @@ import { body, on } from '../util/dom'; import * as sidebar from './sidebar'; import { scrollIntoView, scroll2Top } from './scroll'; +/** + * This class wires up some UI events using the `sidebar` utility. On each + * route change it re-initializes the events because the sidebar is re-rendered + * each time we go to a new page. + */ +// TODO @trusktr, this should need to re-initialize events, and the sidebar +// should not be re-rendered each time. export function eventMixin(Base = class {}) { return class extends Base { $resetEvents(source) { diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js index b98bc2a90..1ca16a29f 100644 --- a/src/core/fetch/index.js +++ b/src/core/fetch/index.js @@ -4,6 +4,10 @@ import { noop } from '../util/core'; import { getAndActive } from '../event/sidebar'; import { get } from './ajax'; +/** + * This class provides methods for fetching content (f.e. markdown pages). It + * coordinates renderMixin with help from the router. + */ export function fetchMixin(Base = class {}) { return class extends Base { constructor() { diff --git a/src/core/init/index.js b/src/core/init/index.js index ece0d684b..de664d81b 100644 --- a/src/core/init/index.js +++ b/src/core/init/index.js @@ -1,5 +1,9 @@ import config from '../config'; +/** + * This class is responsible to initializing all other mixins in a certain + * order, and calling lifecycle hooks at appropriate times. + */ export function initMixin(Base = class {}) { return class extends Base { constructor() { diff --git a/src/core/lifecycle/index.js b/src/core/lifecycle/index.js index 98c4dd664..adf4f549a 100644 --- a/src/core/lifecycle/index.js +++ b/src/core/lifecycle/index.js @@ -1,5 +1,6 @@ import { noop } from '../util/core'; +/** This class sets of lifecycle hooks that plugins can hook into. */ export function lifecycleMixin(Base = class {}) { return class Lifecycle extends Base { initLifecycle() { diff --git a/src/core/plugin/index.js b/src/core/plugin/index.js index 232aa7422..46ade0bd8 100644 --- a/src/core/plugin/index.js +++ b/src/core/plugin/index.js @@ -1,5 +1,6 @@ import { isFn } from '../util/core'; +/** This class gets all plugins that were specified in the Docsify config can calls them. */ export function pluginMixin(Base = class {}) { return class Plugin extends Base { initPlugin() { diff --git a/src/core/render/compiler.js b/src/core/render/compiler.js index 4eaf51215..a05618f03 100644 --- a/src/core/render/compiler.js +++ b/src/core/render/compiler.js @@ -57,6 +57,10 @@ const compileMedia = { }, }; +/** + * This class compiles the markdown of each page of a Docsify site into HTML + * (using some information from the provided router). + */ export class Compiler { constructor(config, router) { this.config = config; diff --git a/src/core/render/index.js b/src/core/render/index.js index db93f1d59..d48d6f86a 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -11,6 +11,11 @@ import { Compiler } from './compiler'; import * as tpl from './tpl'; import { prerenderEmbed } from './embed'; +/** + * This class is provides methods for rendering the Docsify site content using + * the Compiler to convert markdown into HTML. It wires up the Compiler with + * the router. + */ export function renderMixin(Base = class {}) { return class extends Base { _executeScript() { @@ -86,6 +91,7 @@ export function renderMixin(Base = class {}) { el.setAttribute('href', nameLink[match]); } } + _renderTo(el, content, replace) { const node = dom.getNode(el); if (node) { diff --git a/src/core/router/index.js b/src/core/router/index.js index 4ff6a68b3..99fc88882 100644 --- a/src/core/router/index.js +++ b/src/core/router/index.js @@ -4,6 +4,10 @@ import { noop } from '../util/core'; import { HashHistory } from './history/hash'; import { HTML5History } from './history/html5'; +/** + * This class wires up the mechanisms that react to URL changes of the browser + * address bar. + */ export function routerMixin(Base = class {}) { return class extends Base { constructor() { From 3ec9e975162e7833628eddcba7c2d479fb7b0956 Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 7 Jun 2020 20:59:03 -0700 Subject: [PATCH 12/12] update comment --- src/core/event/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/event/index.js b/src/core/event/index.js index aa03cc2dc..72fbab86c 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -8,8 +8,9 @@ import { scrollIntoView, scroll2Top } from './scroll'; * route change it re-initializes the events because the sidebar is re-rendered * each time we go to a new page. */ -// TODO @trusktr, this should need to re-initialize events, and the sidebar -// should not be re-rendered each time. +// TODO @trusktr, this should not need to re-initialize events, and the sidebar +// should not be re-rendered each time we navigate to a new page unless there is +// a new sidebar.md file for that page. export function eventMixin(Base = class {}) { return class extends Base { $resetEvents(source) {