From b9fe1ce6250562a212c4472a67f4153490543394 Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 16 Jul 2023 10:54:02 -0700 Subject: [PATCH 1/6] chore: add missing Vue support for Vercel builds --- index.html | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/index.html b/index.html index aeef81c1e..9bc8ad0bb 100644 --- a/index.html +++ b/index.html @@ -84,6 +84,58 @@ }, pathNamespaces: ['/es', '/de-de', '/ru-ru', '/zh-cn'], }, + vueComponents: { + 'button-counter': { + template: /* html */ ``, + data() { + return { + count: 0, + }; + }, + }, + }, + vueGlobalOptions: { + data() { + return { + count: 0, + message: 'Hello, World!', + // Fake API response + images: [ + { title: 'Image 1', url: 'https://picsum.photos/150?random=1' }, + { title: 'Image 2', url: 'https://picsum.photos/150?random=2' }, + { title: 'Image 3', url: 'https://picsum.photos/150?random=3' }, + ], + }; + }, + computed: { + timeOfDay() { + const date = new Date(); + const hours = date.getHours(); + + if (hours < 12) { + return 'morning'; + } else if (hours < 18) { + return 'afternoon'; + } else { + return 'evening'; + } + }, + }, + methods: { + hello() { + alert(this.message); + }, + }, + }, + vueMounts: { + '#counter': { + data() { + return { + count: 0, + }; + }, + }, + }, plugins: [ DocsifyCarbon.create('CEBI6KQE', 'docsifyjsorg'), function (hook, vm) { @@ -121,5 +173,7 @@ + + From 9f8c3c98d497f0b098589020f1b7cd3af511494f Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Sun, 16 Jul 2023 16:37:17 -0700 Subject: [PATCH 2/6] 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 initGlobalAPI out of Docsify.js to start to encapsulate Docsify - 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 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 f0d74fff9..ccf414c26 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 5e076bfdb..687dfd230 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 d5619c0b0..516f1fb27 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 13a4ef717..000000000 --- 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 0e8442914..000000000 --- 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 cbea24ca5..aa7e195d9 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 1673f0f92..24be8ae2c 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 7074b3b73..897ac66b3 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 531ef354d..36eb4c7d5 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 9386f4266..3514136a4 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 6ad117667..be09b05d1 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 fcd8bd7f5..ed3db4f36 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 4502efbb5..2ee9755be 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 @@ -18,7 +18,10 @@ Alternatively, use [compressed files](#compressed-file). ```html - + @@ -28,7 +31,10 @@ Alternatively, use [compressed files](#compressed-file). ```html - + @@ -36,7 +42,10 @@ Alternatively, use [compressed files](#compressed-file). ```html - + diff --git a/docs/cover.md b/docs/cover.md index bf8c3c54e..1be3b39ce 100644 --- a/docs/cover.md +++ b/docs/cover.md @@ -11,8 +11,8 @@ Set `coverpage` to **true**, and create a `_coverpage.md`: ``` @@ -81,7 +81,7 @@ Now, you can set ```js window.$docsify = { - coverpage: ['/', '/zh-cn/'] + coverpage: ['/', '/zh-cn/'], }; ``` @@ -91,7 +91,7 @@ Or a special file name window.$docsify = { coverpage: { '/': 'cover.md', - '/zh-cn/': 'cover.md' - } + '/zh-cn/': 'cover.md', + }, }; ``` diff --git a/docs/custom-navbar.md b/docs/custom-navbar.md index 0d05a243f..d1722fbd5 100644 --- a/docs/custom-navbar.md +++ b/docs/custom-navbar.md @@ -27,8 +27,8 @@ Alternatively, you can create a custom markdown-based navigation file by setting ``` @@ -36,8 +36,8 @@ Alternatively, you can create a custom markdown-based navigation file by setting ```markdown -* [En](/) -* [chinese](/zh-cn/) +- [En](/) +- [chinese](/zh-cn/) ``` !> You need to create a `.nojekyll` in `./docs` to prevent GitHub Pages from ignoring files that begin with an underscore. @@ -51,19 +51,19 @@ You can create sub-lists by indenting items that are under a certain parent. ```markdown -* Getting started +- Getting started - * [Quick start](quickstart.md) - * [Writing more pages](more-pages.md) - * [Custom navbar](custom-navbar.md) - * [Cover page](cover.md) + - [Quick start](quickstart.md) + - [Writing more pages](more-pages.md) + - [Custom navbar](custom-navbar.md) + - [Cover page](cover.md) -* Configuration - * [Configuration](configuration.md) - * [Themes](themes.md) - * [Using plugins](plugins.md) - * [Markdown configuration](markdown.md) - * [Language highlight](language-highlight.md) +- Configuration + - [Configuration](configuration.md) + - [Themes](themes.md) + - [Using plugins](plugins.md) + - [Markdown configuration](markdown.md) + - [Language highlight](language-highlight.md) ``` renders as @@ -80,7 +80,7 @@ If you use the [emoji plugin](plugins#emoji): @@ -91,6 +91,6 @@ you could, for example, use flag emojis in your custom navbar Markdown file: ```markdown -* [:us:, :uk:](/) -* [:cn:](/zh-cn/) +- [:us:, :uk:](/) +- [:cn:](/zh-cn/) ``` diff --git a/docs/deploy.md b/docs/deploy.md index 9ac3ca50f..ce6a82a07 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -96,14 +96,14 @@ When using the HTML5 router, you need to set up redirect rules that redirect all ## AWS Amplify -1. Set the routerMode in the Docsify project `index.html` to *history* mode. +1. Set the routerMode in the Docsify project `index.html` to _history_ mode. ```html ``` @@ -125,17 +125,15 @@ frontend: - '**/*' cache: paths: [] - ``` 6. Add the following Redirect rules in their displayed order. Note that the second record is a PNG image where you can change it with any image format you are using. | Source address | Target address | Type | -|----------------|----------------|---------------| -| /<*>.md | /<*>.md | 200 (Rewrite) | -| /<*>.png | /<*>.png | 200 (Rewrite) | -| /<*> | /index.html | 200 (Rewrite) | - +| -------------- | -------------- | ------------- | +| /<\*>.md | /<\*>.md | 200 (Rewrite) | +| /<\*>.png | /<\*>.png | 200 (Rewrite) | +| /<\*> | /index.html | 200 (Rewrite) | ## Docker @@ -144,10 +142,10 @@ frontend: You need prepare the initial files instead of making them inside the container. See the [Quickstart](https://docsify.js.org/#/quickstart) section for instructions on how to create these files manually or using [docsify-cli](https://github.com/docsifyjs/docsify-cli). - ```sh - index.html - README.md - ``` + ```sh + index.html + README.md + ``` - Create Dockerfile @@ -180,4 +178,3 @@ frontend: ```sh docker run -itp 3000:3000 --name=docsify -v $(pwd):/docs docsify/demo ``` - diff --git a/docs/embed-files.md b/docs/embed-files.md index 936494bcb..f06d121f8 100644 --- a/docs/embed-files.md +++ b/docs/embed-files.md @@ -26,11 +26,11 @@ Currently, file extensions are automatically recognized and embedded in differen These types are supported: -* **iframe** `.html`, `.htm` -* **markdown** `.markdown`, `.md` -* **audio** `.mp3` -* **video** `.mp4`, `.ogg` -* **code** other file extension +- **iframe** `.html`, `.htm` +- **markdown** `.markdown`, `.md` +- **audio** `.mp3` +- **video** `.mp4`, `.ogg` +- **code** other file extension Of course, you can force the specified type. For example, a Markdown file can be embedded as a code block by setting `:type=code`. @@ -74,6 +74,7 @@ Example: If you embed the file as `iframe`, `audio` and `video`, then you may need to set the attributes of these tags. ?> Note, for the `audio` and `video` types, docsify adds the `controls` attribute by default. When you want add more attributes, the `controls` attribute need to be added manually if need be. + ```md [filename](_media/example.mp4 ':include :type=video controls width=100%') ``` @@ -114,11 +115,11 @@ Start by viewing a gist on `gist.github.com`. For the purposes of this guide, we Identify the following items from the gist: -Field | Example | Description ---- | --- | --- -**Username** | `anikethsaha` | The gist's owner. -**Gist ID** | `c2bece08f27c4277001f123898d16a7c` | Identifier for the gist. This is fixed for the gist's lifetime. -**Filename** | `content.md` | Select a name of a file in the gist. This needed even on a single-file gist for embedding to work. +| Field | Example | Description | +| ------------ | ---------------------------------- | -------------------------------------------------------------------------------------------------- | +| **Username** | `anikethsaha` | The gist's owner. | +| **Gist ID** | `c2bece08f27c4277001f123898d16a7c` | Identifier for the gist. This is fixed for the gist's lifetime. | +| **Filename** | `content.md` | Select a name of a file in the gist. This needed even on a single-file gist for embedding to work. | You will need those to build the _raw gist URL_ for the target file. This has the following format: diff --git a/docs/language-highlight.md b/docs/language-highlight.md index e95823d03..0ea9b6618 100644 --- a/docs/language-highlight.md +++ b/docs/language-highlight.md @@ -2,10 +2,10 @@ Docsify uses [Prism](https://prismjs.com) to highlight code blocks in your pages. Prism supports the following languages by default: -* Markup - `markup`, `html`, `xml`, `svg`, `mathml`, `ssml`, `atom`, `rss` -* CSS - `css` -* C-like - `clike` -* JavaScript - `javascript`, `js` +- Markup - `markup`, `html`, `xml`, `svg`, `mathml`, `ssml`, `atom`, `rss` +- CSS - `css` +- C-like - `clike` +- JavaScript - `javascript`, `js` Support for [additional languages](https://prismjs.com/#supported-languages) is available by loading the language-specific [grammar files](https://cdn.jsdelivr.net/npm/prismjs@1/components/) via CDN: @@ -29,7 +29,7 @@ echo "hello" ``` ```php -function getAdder(int $x): int +function getAdder(int $x): int { return 123; } @@ -48,18 +48,19 @@ echo "hello" ``` ```php -function getAdder(int $x): int +function getAdder(int $x): int { return 123; } ``` ## Highlighting Dynamic Content + Code blocks [dynamically created from javascript](https://docsify.js.org/#/configuration?id=executescript) can be highlighted using the method `Prism.highlightElement` like so: ```javascript -const code = document.createElement("code"); +const code = document.createElement('code'); code.innerHTML = "console.log('Hello World!')"; -code.setAttribute("class", "lang-javascript"); +code.setAttribute('class', 'lang-javascript'); Prism.highlightElement(code); ``` diff --git a/docs/markdown.md b/docs/markdown.md index 6adc7f035..5cca17b92 100644 --- a/docs/markdown.md +++ b/docs/markdown.md @@ -9,10 +9,10 @@ window.$docsify = { renderer: { link() { // ... - } - } - } -} + }, + }, + }, +}; ``` ?> Configuration Options Reference: [marked documentation](https://marked.js.org/#/USING_ADVANCED.md) @@ -24,9 +24,9 @@ window.$docsify = { markdown(marked, renderer) { // ... - return marked - } -} + return marked; + }, +}; ``` ## Supports mermaid @@ -43,14 +43,17 @@ window.$docsify = { markdown: { renderer: { code(code, lang) { - if (lang === "mermaid") { + if (lang === 'mermaid') { return /* html */ ` -
${mermaid.render('mermaid-svg-' + num++, code)}
+
${mermaid.render( + 'mermaid-svg-' + num++, + code + )}
`; } return this.origin.code.apply(this, arguments); - } - } - } -} + }, + }, + }, +}; ``` diff --git a/docs/more-pages.md b/docs/more-pages.md index ed4082fb0..ab7d8fd40 100644 --- a/docs/more-pages.md +++ b/docs/more-pages.md @@ -34,8 +34,8 @@ First, you need to set `loadSidebar` to **true**. Details are available in the [ ``` @@ -45,8 +45,8 @@ Create the `_sidebar.md`: ```markdown -* [Home](/) -* [Guide](guide.md) +- [Home](/) +- [Guide](guide.md) ``` You need to create a `.nojekyll` in `./docs` to prevent GitHub Pages from ignoring files that begin with an underscore. @@ -76,9 +76,9 @@ You can specify `alias` to avoid unnecessary fallback. window.$docsify = { loadSidebar: true, alias: { - '/.*/_sidebar.md': '/_sidebar.md' - } - } + '/.*/_sidebar.md': '/_sidebar.md', + }, + }; ``` @@ -90,8 +90,9 @@ A page's `title` tag is generated from the _selected_ sidebar item name. For bet ```markdown -* [Home](/) -* [Guide](guide.md "The greatest guide in the world") + +- [Home](/) +- [Guide](guide.md 'The greatest guide in the world') ``` ## Table of Contents @@ -106,8 +107,8 @@ A custom sidebar can also automatically generate a table of contents by setting ``` diff --git a/docs/pwa.md b/docs/pwa.md index 464f0c5be..3e9e0c755 100644 --- a/docs/pwa.md +++ b/docs/pwa.md @@ -8,7 +8,7 @@ It is also very easy to use. Create a `sw.js` file in your project's root directory and copy the following code: -*sw.js* +_sw.js_ ```js /* =========================================================== @@ -19,24 +19,24 @@ Create a `sw.js` file in your project's root directory and copy the following co * Register service worker. * ========================================================== */ -const RUNTIME = 'docsify' +const RUNTIME = 'docsify'; const HOSTNAME_WHITELIST = [ self.location.hostname, 'fonts.gstatic.com', 'fonts.googleapis.com', - 'cdn.jsdelivr.net' -] + 'cdn.jsdelivr.net', +]; // The Util Function to hack URLs of intercepted requests -const getFixedUrl = (req) => { - const now = Date.now() - const url = new URL(req.url) +const getFixedUrl = req => { + const now = Date.now(); + const url = new URL(req.url); // 1. fixed http URL // Just keep syncing with location.protocol // fetch(httpURL) belongs to active mixed content. // And fetch(httpRequest) is not supported yet. - url.protocol = self.location.protocol + url.protocol = self.location.protocol; // 2. add query for caching-busting. // Github Pages served with Cache-Control: max-age=600 @@ -44,10 +44,10 @@ const getFixedUrl = (req) => { // Until cache mode of Fetch API landed, we have to workaround cache-busting with query string. // Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190 if (url.hostname === self.location.hostname) { - url.search += (url.search ? '&' : '?') + 'cache-bust=' + now + url.search += (url.search ? '&' : '?') + 'cache-bust=' + now; } - return url.href -} + return url.href; +}; /** * @Lifecycle Activate @@ -56,8 +56,8 @@ const getFixedUrl = (req) => { * waitUntil(): activating ====> activated */ self.addEventListener('activate', event => { - event.waitUntil(self.clients.claim()) -}) + event.waitUntil(self.clients.claim()); +}); /** * @Functional Fetch @@ -71,10 +71,10 @@ self.addEventListener('fetch', event => { // Stale-while-revalidate // similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale // Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1 - const cached = caches.match(event.request) - const fixedUrl = getFixedUrl(event.request) - const fetched = fetch(fixedUrl, { cache: 'no-store' }) - const fetchedCopy = fetched.then(resp => resp.clone()) + const cached = caches.match(event.request); + const fixedUrl = getFixedUrl(event.request); + const fetched = fetch(fixedUrl, { cache: 'no-store' }); + const fetchedCopy = fetched.then(resp => resp.clone()); // Call respondWith() with whatever we get first. // If the fetch fails (e.g disconnected), wait for the cache. @@ -83,29 +83,36 @@ self.addEventListener('fetch', event => { event.respondWith( Promise.race([fetched.catch(_ => cached), cached]) .then(resp => resp || fetched) - .catch(_ => { /* eat any errors */ }) - ) + .catch(_ => { + /* eat any errors */ + }) + ); // Update the cache with the version we fetched (only for ok status) event.waitUntil( Promise.all([fetchedCopy, caches.open(RUNTIME)]) - .then(([response, cache]) => response.ok && cache.put(event.request, response)) - .catch(_ => { /* eat any errors */ }) - ) + .then( + ([response, cache]) => + response.ok && cache.put(event.request, response) + ) + .catch(_ => { + /* eat any errors */ + }) + ); } -}) +}); ``` ## Register Now, register it in your `index.html`. It only works on some modern browsers, so we need to check: -*index.html* +_index.html_ ```html ``` diff --git a/index.html b/index.html index 9bc8ad0bb..17d28d098 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,12 @@ - +