From 2e5787fc8ec6b9b3c03d0737522286174cba97e9 Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 16 Jul 2023 16:37:17 -0700 Subject: [PATCH] refactor: move some functions and module-level state into classes as private methods and properties to start to encapsulate Docsify Also some small tweaks: - move ajax to utils folder - fix some type definitions and improve content in some JSDoc comments - use concise class field syntax - consolidate duplicate docsify-ignore comment removal code - move initGlobalAPI out of Docsify.js to start to encapsulate Docsify This handles a task in [Simplify and modernize Docsify](https://github.com/docsifyjs/docsify/issues/2104), as well as works towards [Encapsulating Docsify](https://github.com/docsifyjs/docsify/issues/2135). --- src/core/Docsify.js | 10 +- src/core/config.js | 2 +- src/core/event/index.js | 291 +++++++++++++++++- src/core/event/scroll.js | 163 ---------- src/core/event/sidebar.js | 106 ------- src/core/fetch/index.js | 125 ++++---- src/core/global-api.js | 6 +- src/core/index.js | 13 +- src/core/init/lifecycle.js | 8 +- src/core/render/compiler.js | 4 +- src/core/render/compiler/headline.js | 4 +- src/core/render/embed.js | 2 +- src/core/render/index.js | 432 ++++++++++++++------------- src/core/render/utils.js | 10 +- src/core/router/history/base.js | 51 ++-- src/core/router/history/hash.js | 6 +- src/core/router/history/html5.js | 5 +- src/core/router/index.js | 9 +- src/core/{fetch => util}/ajax.js | 2 +- src/core/util/str.js | 8 - src/core/virtual-routes/index.js | 20 +- src/core/virtual-routes/next.js | 4 +- src/plugins/search/search.js | 10 +- test/unit/render-util.test.js | 10 +- 24 files changed, 663 insertions(+), 638 deletions(-) delete mode 100644 src/core/event/scroll.js delete mode 100644 src/core/event/sidebar.js rename src/core/{fetch => util}/ajax.js (98%) delete mode 100644 src/core/util/str.js diff --git a/src/core/Docsify.js b/src/core/Docsify.js index f0d74fff9d..ccf414c269 100644 --- a/src/core/Docsify.js +++ b/src/core/Docsify.js @@ -3,7 +3,6 @@ import { Render } from './render/index.js'; import { Fetch } from './fetch/index.js'; import { Events } from './event/index.js'; import { VirtualRoutes } from './virtual-routes/index.js'; -import initGlobalAPI from './global-api.js'; import config from './config.js'; import { isFn } from './util/core.js'; @@ -16,11 +15,11 @@ export class Docsify extends Fetch( // eslint-disable-next-line new-cap Events(Render(VirtualRoutes(Router(Lifecycle(Object))))) ) { + config = config(this); + constructor() { super(); - this.config = config(this); - this.initLifecycle(); // Init hooks this.initPlugin(); // Install plugins this.callHook('init'); @@ -46,8 +45,3 @@ export class Docsify extends Fetch( }); } } - -/** - * Global API - */ -initGlobalAPI(); diff --git a/src/core/config.js b/src/core/config.js index 5e076bfdb5..687dfd230a 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -3,7 +3,7 @@ import { hyphenate, isPrimitive } from './util/core.js'; const currentScript = document.currentScript; -/** @param {import('./Docsify').Docsify} vm */ +/** @param {import('./Docsify.js').Docsify} vm */ export default function (vm) { const config = Object.assign( { diff --git a/src/core/event/index.js b/src/core/event/index.js index d5619c0b05..516f1fb270 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -1,9 +1,11 @@ +import Tweezer from 'tweezer.js'; import { isMobile } from '../util/env.js'; import { body, on } from '../util/dom.js'; -import * as sidebar from './sidebar.js'; -import { scrollIntoView, scroll2Top } from './scroll.js'; +import * as dom from '../util/dom.js'; +import { removeParams } from '../router/util.js'; +import config from '../config.js'; -/** @typedef {import('../Docsify').Constructor} Constructor */ +/** @typedef {import('../Docsify.js').Constructor} Constructor */ /** * @template {!Constructor} T @@ -18,29 +20,300 @@ export function Events(Base) { if (source !== 'history') { // Scroll to ID if specified if (this.route.query.id) { - scrollIntoView(this.route.path, this.route.query.id); + this.#scrollIntoView(this.route.path, this.route.query.id); } // Scroll to top if a link was clicked and auto2top is enabled if (source === 'navigate') { - auto2top && scroll2Top(auto2top); + auto2top && this.#scroll2Top(auto2top); } } if (this.config.loadNavbar) { - sidebar.getAndActive(this.router, 'nav'); + this.__getAndActive(this.router, 'nav'); } } initEvent() { // Bind toggle button - sidebar.btn('button.sidebar-toggle', this.router); - sidebar.collapse('.sidebar', this.router); + this.#btn('button.sidebar-toggle', this.router); + this.#collapse('.sidebar', this.router); // Bind sticky effect if (this.config.coverpage) { - !isMobile && on('scroll', sidebar.sticky); + !isMobile && on('scroll', this.__sticky); } else { body.classList.add('sticky'); } } + + /** @readonly */ + #nav = {}; + + #hoverOver = false; + #scroller = null; + #enableScrollEvent = true; + #coverHeight = 0; + + #scrollTo(el, offset = 0) { + if (this.#scroller) { + this.#scroller.stop(); + } + + this.#enableScrollEvent = false; + this.#scroller = new Tweezer({ + start: window.pageYOffset, + end: + Math.round(el.getBoundingClientRect().top) + + window.pageYOffset - + offset, + duration: 500, + }) + .on('tick', v => window.scrollTo(0, v)) + .on('done', () => { + this.#enableScrollEvent = true; + this.#scroller = null; + }) + .begin(); + } + + #highlight(path) { + if (!this.#enableScrollEvent) { + return; + } + + const sidebar = dom.getNode('.sidebar'); + const anchors = dom.findAll('.anchor'); + const wrap = dom.find(sidebar, '.sidebar-nav'); + let active = dom.find(sidebar, 'li.active'); + const doc = document.documentElement; + const top = + ((doc && doc.scrollTop) || document.body.scrollTop) - this.#coverHeight; + let last; + + for (const node of anchors) { + if (node.offsetTop > top) { + if (!last) { + last = node; + } + + break; + } else { + last = node; + } + } + + if (!last) { + return; + } + + const li = this.#nav[this.#getNavKey(path, last.getAttribute('data-id'))]; + + if (!li || li === active) { + return; + } + + active && active.classList.remove('active'); + li.classList.add('active'); + active = li; + + // Scroll into view + // https://github.com/vuejs/vuejs.org/blob/master/themes/vue/source/js/common.js#L282-L297 + if (!this.#hoverOver && dom.body.classList.contains('sticky')) { + const height = sidebar.clientHeight; + const curOffset = 0; + const cur = active.offsetTop + active.clientHeight + 40; + const isInView = + active.offsetTop >= wrap.scrollTop && cur <= wrap.scrollTop + height; + const notThan = cur - curOffset < height; + + sidebar.scrollTop = isInView + ? wrap.scrollTop + : notThan + ? curOffset + : cur - height; + } + } + + #getNavKey(path, id) { + return `${decodeURIComponent(path)}?id=${decodeURIComponent(id)}`; + } + + __scrollActiveSidebar(router) { + const cover = dom.find('.cover.show'); + this.#coverHeight = cover ? cover.offsetHeight : 0; + + const sidebar = dom.getNode('.sidebar'); + let lis = []; + if (sidebar !== null && sidebar !== undefined) { + lis = dom.findAll(sidebar, 'li'); + } + + for (const li of lis) { + const a = li.querySelector('a'); + if (!a) { + continue; + } + + let href = a.getAttribute('href'); + + if (href !== '/') { + const { + query: { id }, + path, + } = router.parse(href); + if (id) { + href = this.#getNavKey(path, id); + } + } + + if (href) { + this.#nav[decodeURIComponent(href)] = li; + } + } + + if (isMobile) { + return; + } + + const path = removeParams(router.getCurrentPath()); + dom.off('scroll', () => this.#highlight(path)); + dom.on('scroll', () => this.#highlight(path)); + dom.on(sidebar, 'mouseover', () => { + this.#hoverOver = true; + }); + dom.on(sidebar, 'mouseleave', () => { + this.#hoverOver = false; + }); + } + + #scrollIntoView(path, id) { + if (!id) { + return; + } + const topMargin = config().topMargin; + // Use [id='1234'] instead of #id to handle special cases such as reserved characters and pure number id + // https://stackoverflow.com/questions/37270787/uncaught-syntaxerror-failed-to-execute-queryselector-on-document + const section = dom.find("[id='" + id + "']"); + section && this.#scrollTo(section, topMargin); + + const li = this.#nav[this.#getNavKey(path, id)]; + const sidebar = dom.getNode('.sidebar'); + const active = dom.find(sidebar, 'li.active'); + active && active.classList.remove('active'); + li && li.classList.add('active'); + } + + #scrollEl = dom.$.scrollingElement || dom.$.documentElement; + + #scroll2Top(offset = 0) { + this.#scrollEl.scrollTop = offset === true ? 0 : Number(offset); + } + + /** @readonly */ + #title = dom.$.title; + + /** + * Toggle button + * @param {Element} el Button to be toggled + * @void + */ + #btn(el) { + const toggle = _ => dom.body.classList.toggle('close'); + + el = dom.getNode(el); + if (el === null || el === undefined) { + return; + } + + dom.on(el, 'click', e => { + e.stopPropagation(); + toggle(); + }); + + isMobile && + dom.on( + dom.body, + 'click', + _ => dom.body.classList.contains('close') && toggle() + ); + } + + #collapse(el) { + el = dom.getNode(el); + if (el === null || el === undefined) { + return; + } + + dom.on(el, 'click', ({ target }) => { + if ( + target.nodeName === 'A' && + target.nextSibling && + target.nextSibling.classList && + target.nextSibling.classList.contains('app-sub-sidebar') + ) { + dom.toggleClass(target.parentNode, 'collapse'); + } + }); + } + + __sticky = () => { + const cover = dom.getNode('section.cover'); + if (!cover) { + return; + } + + const coverHeight = cover.getBoundingClientRect().height; + + if ( + window.pageYOffset >= coverHeight || + cover.classList.contains('hidden') + ) { + dom.toggleClass(dom.body, 'add', 'sticky'); + } else { + dom.toggleClass(dom.body, 'remove', 'sticky'); + } + }; + + /** + * Get and active link + * @param {Object} router Router + * @param {String|Element} el Target element + * @param {Boolean} isParent Active parent + * @param {Boolean} autoTitle Automatically set title + * @return {Element} Active element + */ + __getAndActive(router, el, isParent, autoTitle) { + el = dom.getNode(el); + let links = []; + if (el !== null && el !== undefined) { + links = dom.findAll(el, 'a'); + } + + const hash = decodeURI(router.toURL(router.getCurrentPath())); + let target; + + links + .sort((a, b) => b.href.length - a.href.length) + .forEach(a => { + const href = decodeURI(a.getAttribute('href')); + const node = isParent ? a.parentNode : a; + + a.title = a.title || a.innerText; + + if (hash.indexOf(href) === 0 && !target) { + target = a; + dom.toggleClass(node, 'add', 'active'); + } else { + dom.toggleClass(node, 'remove', 'active'); + } + }); + + if (autoTitle) { + dom.$.title = target + ? target.title || `${target.innerText} - ${this.#title}` + : this.#title; + } + + return target; + } }; } diff --git a/src/core/event/scroll.js b/src/core/event/scroll.js deleted file mode 100644 index 13a4ef7172..0000000000 --- a/src/core/event/scroll.js +++ /dev/null @@ -1,163 +0,0 @@ -import Tweezer from 'tweezer.js'; -import { isMobile } from '../util/env.js'; -import * as dom from '../util/dom.js'; -import { removeParams } from '../router/util.js'; -import config from '../config.js'; - -const nav = {}; -let hoverOver = false; -let scroller = null; -let enableScrollEvent = true; -let coverHeight = 0; - -function scrollTo(el, offset = 0) { - if (scroller) { - scroller.stop(); - } - - enableScrollEvent = false; - scroller = new Tweezer({ - start: window.pageYOffset, - end: - Math.round(el.getBoundingClientRect().top) + window.pageYOffset - offset, - duration: 500, - }) - .on('tick', v => window.scrollTo(0, v)) - .on('done', () => { - enableScrollEvent = true; - scroller = null; - }) - .begin(); -} - -function highlight(path) { - if (!enableScrollEvent) { - return; - } - - const sidebar = dom.getNode('.sidebar'); - const anchors = dom.findAll('.anchor'); - const wrap = dom.find(sidebar, '.sidebar-nav'); - let active = dom.find(sidebar, 'li.active'); - const doc = document.documentElement; - const top = ((doc && doc.scrollTop) || document.body.scrollTop) - coverHeight; - let last; - - for (const node of anchors) { - if (node.offsetTop > top) { - if (!last) { - last = node; - } - - break; - } else { - last = node; - } - } - - if (!last) { - return; - } - - const li = nav[getNavKey(path, last.getAttribute('data-id'))]; - - if (!li || li === active) { - return; - } - - active && active.classList.remove('active'); - li.classList.add('active'); - active = li; - - // Scroll into view - // https://github.com/vuejs/vuejs.org/blob/master/themes/vue/source/js/common.js#L282-L297 - if (!hoverOver && dom.body.classList.contains('sticky')) { - const height = sidebar.clientHeight; - const curOffset = 0; - const cur = active.offsetTop + active.clientHeight + 40; - const isInView = - active.offsetTop >= wrap.scrollTop && cur <= wrap.scrollTop + height; - const notThan = cur - curOffset < height; - - sidebar.scrollTop = isInView - ? wrap.scrollTop - : notThan - ? curOffset - : cur - height; - } -} - -function getNavKey(path, id) { - return `${decodeURIComponent(path)}?id=${decodeURIComponent(id)}`; -} - -export function scrollActiveSidebar(router) { - const cover = dom.find('.cover.show'); - coverHeight = cover ? cover.offsetHeight : 0; - - const sidebar = dom.getNode('.sidebar'); - let lis = []; - if (sidebar !== null && sidebar !== undefined) { - lis = dom.findAll(sidebar, 'li'); - } - - for (const li of lis) { - const a = li.querySelector('a'); - if (!a) { - continue; - } - - let href = a.getAttribute('href'); - - if (href !== '/') { - const { - query: { id }, - path, - } = router.parse(href); - if (id) { - href = getNavKey(path, id); - } - } - - if (href) { - nav[decodeURIComponent(href)] = li; - } - } - - if (isMobile) { - return; - } - - const path = removeParams(router.getCurrentPath()); - dom.off('scroll', () => highlight(path)); - dom.on('scroll', () => highlight(path)); - dom.on(sidebar, 'mouseover', () => { - hoverOver = true; - }); - dom.on(sidebar, 'mouseleave', () => { - hoverOver = false; - }); -} - -export function scrollIntoView(path, id) { - if (!id) { - return; - } - const topMargin = config().topMargin; - // Use [id='1234'] instead of #id to handle special cases such as reserved characters and pure number id - // https://stackoverflow.com/questions/37270787/uncaught-syntaxerror-failed-to-execute-queryselector-on-document - const section = dom.find("[id='" + id + "']"); - section && scrollTo(section, topMargin); - - const li = nav[getNavKey(path, id)]; - const sidebar = dom.getNode('.sidebar'); - const active = dom.find(sidebar, 'li.active'); - active && active.classList.remove('active'); - li && li.classList.add('active'); -} - -const scrollEl = dom.$.scrollingElement || dom.$.documentElement; - -export function scroll2Top(offset = 0) { - scrollEl.scrollTop = offset === true ? 0 : Number(offset); -} diff --git a/src/core/event/sidebar.js b/src/core/event/sidebar.js deleted file mode 100644 index 0e8442914c..0000000000 --- a/src/core/event/sidebar.js +++ /dev/null @@ -1,106 +0,0 @@ -/* eslint-disable no-unused-vars */ -import { isMobile } from '../util/env.js'; -import * as dom from '../util/dom.js'; - -const title = dom.$.title; -/** - * Toggle button - * @param {Element} el Button to be toggled - * @void - */ -export function btn(el) { - const toggle = _ => dom.body.classList.toggle('close'); - - el = dom.getNode(el); - if (el === null || el === undefined) { - return; - } - - dom.on(el, 'click', e => { - e.stopPropagation(); - toggle(); - }); - - isMobile && - dom.on( - dom.body, - 'click', - _ => dom.body.classList.contains('close') && toggle() - ); -} - -export function collapse(el) { - el = dom.getNode(el); - if (el === null || el === undefined) { - return; - } - - dom.on(el, 'click', ({ target }) => { - if ( - target.nodeName === 'A' && - target.nextSibling && - target.nextSibling.classList && - target.nextSibling.classList.contains('app-sub-sidebar') - ) { - dom.toggleClass(target.parentNode, 'collapse'); - } - }); -} - -export function sticky() { - const cover = dom.getNode('section.cover'); - if (!cover) { - return; - } - - const coverHeight = cover.getBoundingClientRect().height; - - if (window.pageYOffset >= coverHeight || cover.classList.contains('hidden')) { - dom.toggleClass(dom.body, 'add', 'sticky'); - } else { - dom.toggleClass(dom.body, 'remove', 'sticky'); - } -} - -/** - * Get and active link - * @param {Object} router Router - * @param {String|Element} el Target element - * @param {Boolean} isParent Active parent - * @param {Boolean} autoTitle Automatically set title - * @return {Element} Active element - */ -export function getAndActive(router, el, isParent, autoTitle) { - el = dom.getNode(el); - let links = []; - if (el !== null && el !== undefined) { - links = dom.findAll(el, 'a'); - } - - const hash = decodeURI(router.toURL(router.getCurrentPath())); - let target; - - links - .sort((a, b) => b.href.length - a.href.length) - .forEach(a => { - const href = decodeURI(a.getAttribute('href')); - const node = isParent ? a.parentNode : a; - - a.title = a.title || a.innerText; - - if (hash.indexOf(href) === 0 && !target) { - target = a; - dom.toggleClass(node, 'add', 'active'); - } else { - dom.toggleClass(node, 'remove', 'active'); - } - }); - - if (autoTitle) { - dom.$.title = target - ? target.title || `${target.innerText} - ${title}` - : title; - } - - return target; -} diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js index cbea24ca54..aa7e195d90 100644 --- a/src/core/fetch/index.js +++ b/src/core/fetch/index.js @@ -1,70 +1,70 @@ /* eslint-disable no-unused-vars */ import { getParentPath, stringifyQuery } from '../router/util.js'; import { noop, isExternal } from '../util/core.js'; -import { getAndActive } from '../event/sidebar.js'; -import { get } from './ajax.js'; - -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, _error => loadNested(path, qs, file, next, vm)); -} +import { get } from '../util/ajax.js'; -/** @typedef {import('../Docsify').Constructor} Constructor */ +/** @typedef {import('../Docsify.js').Constructor} Constructor */ /** * @template {!Constructor} T * @param {T} Base - The class to extend */ export function Fetch(Base) { - let last; + return class Fetch extends Base { + #loadNested(path, qs, file, next, vm, first) { + path = first ? path : path.replace(/\/$/, ''); + path = getParentPath(path); - const abort = () => last && last.abort && last.abort(); - const request = (url, requestHeaders) => { - abort(); - last = get(url, true, requestHeaders); - return last; - }; + if (!path) { + return; + } - 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) - .filter(k => path.match(new RegExp('^' + k)))[0]; - - path404 = (key && notFoundPage[key]) || defaultPath; - break; - - default: - break; + get( + vm.router.getFile(path + file) + qs, + false, + vm.config.requestHeaders + ).then(next, _error => this.#loadNested(path, qs, file, next, vm)); } - return path404; - }; + #last; + + #abort = () => this.#last && this.#last.abort && this.#last.abort(); + + #request = (url, requestHeaders) => { + this.#abort(); + this.#last = get(url, true, requestHeaders); + return this.#last; + }; + + #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) + .filter(k => path.match(new RegExp('^' + k)))[0]; + + path404 = (key && notFoundPage[key]) || defaultPath; + break; + + default: + break; + } + + return path404; + }; - return class Fetch extends Base { _loadSideAndNav(path, qs, loadSidebar, cb) { return () => { if (!loadSidebar) { @@ -77,7 +77,7 @@ export function Fetch(Base) { }; // Load sidebar - loadNested(path, qs, loadSidebar, fn, this, true); + this.#loadNested(path, qs, loadSidebar, fn, this, true); }; } @@ -121,7 +121,7 @@ export function Fetch(Base) { if (typeof contents === 'string') { contentFetched(contents); } else { - request(file + qs, requestHeaders).then( + this.#request(file + qs, requestHeaders).then( contentFetched, contentFailedToFetch ); @@ -129,7 +129,7 @@ export function Fetch(Base) { }); } else { // if the requested url is not local, just fetch the file - request(file + qs, requestHeaders).then( + this.#request(file + qs, requestHeaders).then( contentFetched, contentFailedToFetch ); @@ -137,7 +137,7 @@ export function Fetch(Base) { // Load nav loadNavbar && - loadNested( + this.#loadNested( path, qs, loadNavbar, @@ -216,7 +216,7 @@ export function Fetch(Base) { const newPath = this.router.getFile( path.replace(new RegExp(`^/${local}`), '') ); - const req = request(newPath + qs, requestHeaders); + const req = this.#request(newPath + qs, requestHeaders); req.then( (text, opt) => @@ -244,9 +244,9 @@ export function Fetch(Base) { 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), requestHeaders).then( + this.#request(this.router.getFile(path404), requestHeaders).then( (text, opt) => this._renderMain(text, opt, fnLoadSideAndNav), _error => this._renderMain(null, {}, fnLoadSideAndNav) ); @@ -262,7 +262,12 @@ export function Fetch(Base) { // Server-Side Rendering if (this.rendered) { - const activeEl = getAndActive(this.router, '.sidebar-nav', true, true); + const activeEl = this.__getAndActive( + this.router, + '.sidebar-nav', + true, + true + ); if (loadSidebar && activeEl) { activeEl.parentNode.innerHTML += window.__SUB_SIDEBAR__; } diff --git a/src/core/global-api.js b/src/core/global-api.js index 1673f0f92c..24be8ae2c4 100644 --- a/src/core/global-api.js +++ b/src/core/global-api.js @@ -4,13 +4,13 @@ import * as util from './util/index.js'; import * as dom from './util/dom.js'; import { Compiler } from './render/compiler.js'; import { slugify } from './render/slugify.js'; -import { get } from './fetch/ajax.js'; +import { get } from './util/ajax.js'; -// TODO This is deprecated, kept for backwards compatibility. Remove in next +// TODO This is deprecated, kept for backwards compatibility. Remove in a // major release. We'll tell people to get everything from the DOCSIFY global // when using the global build, but we'll highly recommend for them to import // from the ESM build (f.e. lib/docsify.esm.js and lib/docsify.min.esm.js). -export default function () { +export default function initGlobalAPI() { window.Docsify = { util, dom, diff --git a/src/core/index.js b/src/core/index.js index 7074b3b738..897ac66b3a 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -1,8 +1,17 @@ import { documentReady } from './util/dom.js'; import { Docsify } from './Docsify.js'; +import initGlobalAPI from './global-api.js'; + +// TODO This global API and auto-running Docsify will be deprecated, and removed +// in a major release. Instead we'll tell users to use `new Docsify()` to create +// and manage their instance(s). + +/** + * Global API + */ +initGlobalAPI(); /** * Run Docsify */ -// eslint-disable-next-line no-unused-vars -documentReady(_ => new Docsify()); +documentReady(() => new Docsify()); diff --git a/src/core/init/lifecycle.js b/src/core/init/lifecycle.js index 531ef354d3..36eb4c7d5f 100644 --- a/src/core/init/lifecycle.js +++ b/src/core/init/lifecycle.js @@ -1,6 +1,6 @@ import { noop } from '../util/core.js'; -/** @typedef {import('../Docsify').Constructor} Constructor */ +/** @typedef {import('../Docsify.js').Constructor} Constructor */ /** * @template {!Constructor} T @@ -8,6 +8,9 @@ import { noop } from '../util/core.js'; */ export function Lifecycle(Base) { return class Lifecycle extends Base { + _hooks = {}; + _lifecycle = {}; + initLifecycle() { const hooks = [ 'init', @@ -18,9 +21,6 @@ export function Lifecycle(Base) { 'ready', ]; - this._hooks = {}; - this._lifecycle = {}; - hooks.forEach(hook => { const arr = (this._hooks[hook] = []); this._lifecycle[hook] = fn => arr.push(fn); diff --git a/src/core/render/compiler.js b/src/core/render/compiler.js index 9386f42668..3514136a46 100644 --- a/src/core/render/compiler.js +++ b/src/core/render/compiler.js @@ -8,7 +8,7 @@ import { emojify } from './emojify.js'; import { getAndRemoveConfig, removeAtag, - getAndRemoveDocisfyIgnorConfig, + getAndRemoveDocisfyIgnoreConfig, } from './utils.js'; import { imageCompiler } from './compiler/image.js'; import { highlightCodeCompiler } from './compiler/code.js'; @@ -214,7 +214,7 @@ export class Compiler { const nextToc = { level, title: str }; const { content, ignoreAllSubs, ignoreSubHeading } = - getAndRemoveDocisfyIgnorConfig(str); + getAndRemoveDocisfyIgnoreConfig(str); str = content.trim(); nextToc.title = removeAtag(str); diff --git a/src/core/render/compiler/headline.js b/src/core/render/compiler/headline.js index 6ad117667c..be09b05d10 100644 --- a/src/core/render/compiler/headline.js +++ b/src/core/render/compiler/headline.js @@ -1,7 +1,7 @@ import { getAndRemoveConfig, removeAtag, - getAndRemoveDocisfyIgnorConfig, + getAndRemoveDocisfyIgnoreConfig, } from '../utils.js'; import { slugify } from './slugify.js'; @@ -11,7 +11,7 @@ export const headingCompiler = ({ renderer, router, _self }) => const nextToc = { level, title: str }; const { content, ignoreAllSubs, ignoreSubHeading } = - getAndRemoveDocisfyIgnorConfig(str); + getAndRemoveDocisfyIgnoreConfig(str); str = content.trim(); nextToc.title = removeAtag(str); diff --git a/src/core/render/embed.js b/src/core/render/embed.js index fcd8bd7f55..ed3db4f36c 100644 --- a/src/core/render/embed.js +++ b/src/core/render/embed.js @@ -1,5 +1,5 @@ import stripIndent from 'strip-indent'; -import { get } from '../fetch/ajax.js'; +import { get } from '../util/ajax.js'; const cached = {}; diff --git a/src/core/render/index.js b/src/core/render/index.js index 4502efbb50..ba8d3efe13 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -1,248 +1,247 @@ /* eslint-disable no-unused-vars */ import tinydate from 'tinydate'; import * as dom from '../util/dom.js'; -import { getAndActive, sticky } from '../event/sidebar.js'; import { getPath, isAbsolutePath } from '../router/util.js'; import { isMobile, inBrowser } from '../util/env.js'; import { isPrimitive } from '../util/core.js'; -import { scrollActiveSidebar } from '../event/scroll.js'; import { Compiler } from './compiler.js'; import * as tpl from './tpl.js'; import { prerenderEmbed } from './embed.js'; -let vueGlobalData; +/** @typedef {import('../Docsify.js').Constructor} Constructor */ -function executeScript() { - const script = dom - .findAll('.markdown-section>script') - .filter(s => !/template/.test(s.type))[0]; - if (!script) { - return false; - } +/** + * @template {!Constructor} T + * @param {T} Base - The class to extend + */ +export function Render(Base) { + return class Render extends Base { + #vueGlobalData; + + #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; - } + const code = script.innerText.trim(); + if (!code) { + return false; + } - new Function(code)(); -} + new Function(code)(); + } -function formatUpdated(html, updated, fn) { - updated = - typeof fn === 'function' - ? fn(updated) - : typeof fn === 'string' - ? tinydate(fn)(new Date(updated)) - : updated; + #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); -} + return html.replace(/{docsify-updated}/g, updated); + } -function renderMain(html) { - const docsifyConfig = this.config; - const markdownElm = dom.find('.markdown-section'); - const vueVersion = - 'Vue' in window && - window.Vue.version && - Number(window.Vue.version.charAt(0)); + #renderMain(html) { + const docsifyConfig = this.config; + const markdownElm = dom.find('.markdown-section'); + const vueVersion = + 'Vue' in window && + window.Vue.version && + Number(window.Vue.version.charAt(0)); - const isMountedVue = elm => { - const isVue2 = Boolean(elm.__vue__ && elm.__vue__._isVue); - const isVue3 = Boolean(elm._vnode && elm._vnode.__v_skip); + const isMountedVue = elm => { + const isVue2 = Boolean(elm.__vue__ && elm.__vue__._isVue); + const isVue3 = Boolean(elm._vnode && elm._vnode.__v_skip); - return isVue2 || isVue3; - }; + return isVue2 || isVue3; + }; - if (!html) { - html = /* html */ `

404 - Not found

`; - } - - if ('Vue' in window) { - const mountedElms = dom - .findAll('.markdown-section > *') - .filter(elm => isMountedVue(elm)); - - // Destroy/unmount existing Vue instances - for (const mountedElm of mountedElms) { - if (vueVersion === 2) { - mountedElm.__vue__.$destroy(); - } else if (vueVersion === 3) { - mountedElm.__vue_app__.unmount(); + if (!html) { + html = /* html */ `

404 - Not found

`; } - } - } - this._renderTo(markdownElm, html); - - // Render sidebar with the TOC - !docsifyConfig.loadSidebar && this._renderSidebar(); + if ('Vue' in window) { + const mountedElms = dom + .findAll('.markdown-section > *') + .filter(elm => isMountedVue(elm)); + + // Destroy/unmount existing Vue instances + for (const mountedElm of mountedElms) { + if (vueVersion === 2) { + mountedElm.__vue__.$destroy(); + } else if (vueVersion === 3) { + mountedElm.__vue_app__.unmount(); + } + } + } - // Execute markdown