Merge pull request #1685 from docsifyjs/classes

convert Docsify and mixins to ES classes
This commit is contained in:
Joe Pea 2021-12-09 15:25:30 -08:00 committed by GitHub
commit 40a5fa8f44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 558 additions and 509 deletions

View File

@ -1,22 +1,39 @@
import { initMixin } from './init';
import { routerMixin } from './router';
import { renderMixin } from './render';
import { fetchMixin } from './fetch';
import { eventMixin } from './event';
import initGlobalAPI from './global-api';
import { Router } from './router/index.js';
import { Render } from './render/index.js';
import { Fetch } from './fetch/index.js';
import { Events } from './event/index.js';
import initGlobalAPI from './global-api.js';
export function Docsify() {
this._init();
import config from './config.js';
import { isFn } from './util/core';
import { Lifecycle } from './init/lifecycle';
/** @typedef {new (...args: any[]) => any} Constructor */
// eslint-disable-next-line new-cap
export class Docsify extends Fetch(Events(Render(Router(Lifecycle(Object))))) {
constructor() {
super();
this.config = config(this);
this.initLifecycle(); // Init hooks
this.initPlugin(); // Install plugins
this.callHook('init');
this.initRouter(); // Add router
this.initRender(); // Render base DOM
this.initEvent(); // Bind events
this.initFetch(); // Fetch data
this.callHook('mounted');
}
initPlugin() {
[]
.concat(this.config.plugins)
.forEach(fn => isFn(fn) && fn(this._lifecycle, this));
}
}
const proto = Docsify.prototype;
initMixin(proto);
routerMixin(proto);
renderMixin(proto);
fetchMixin(proto);
eventMixin(proto);
/**
* Global API
*/

View File

@ -2,6 +2,7 @@ import { merge, hyphenate, isPrimitive, hasOwn } from './util/core';
const currentScript = document.currentScript;
/** @param {import('./Docsify').Docsify} vm */
export default function(vm) {
const config = merge(
{

View File

@ -3,39 +3,47 @@ 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;
/** @typedef {import('../Docsify').Constructor} Constructor */
(() => {
// Rely on the browser's scroll auto-restoration when going back or forward
if (source === 'history') {
return;
}
// Scroll to ID if specified
if (this.route.query.id) {
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);
}
})();
/**
* @template {!Constructor} T
* @param {T} Base - The class to extend
*/
export function Events(Base) {
return class Events extends Base {
$resetEvents(source) {
const { auto2top } = this.config;
if (this.config.loadNavbar) {
sidebar.getAndActive(this.router, 'nav');
(() => {
// Rely on the browser's scroll auto-restoration when going back or forward
if (source === 'history') {
return;
}
// Scroll to ID if specified
if (this.route.query.id) {
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);
}
})();
if (this.config.loadNavbar) {
sidebar.getAndActive(this.router, 'nav');
}
}
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');
}
}
};
}
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');
}
}

View File

@ -1,5 +1,4 @@
/* eslint-disable no-unused-vars */
import { callHook } from '../init/lifecycle';
import { getParentPath, stringifyQuery } from '../router/util';
import { noop, isExternal } from '../util/core';
import { getAndActive } from '../event/sidebar';
@ -20,7 +19,13 @@ function loadNested(path, qs, file, next, vm, first) {
).then(next, _ => loadNested(path, qs, file, next, vm));
}
export function fetchMixin(proto) {
/** @typedef {import('../Docsify').Constructor} Constructor */
/**
* @template {!Constructor} T
* @param {T} Base - The class to extend
*/
export function Fetch(Base) {
let last;
const abort = () => last && last.abort && last.abort();
@ -59,44 +64,143 @@ export function fetchMixin(proto) {
return path404;
};
proto._loadSideAndNav = function(path, qs, loadSidebar, cb) {
return () => {
if (!loadSidebar) {
return cb();
}
return class Fetch extends Base {
_loadSideAndNav(path, qs, loadSidebar, cb) {
return () => {
if (!loadSidebar) {
return cb();
}
const fn = result => {
this._renderSidebar(result);
const fn = result => {
this._renderSidebar(result);
cb();
};
// Load sidebar
loadNested(path, qs, loadSidebar, fn, this, true);
};
}
_fetch(cb = noop) {
const { query } = this.route;
let { path } = this.route;
// Prevent loading remote content via URL hash
// Ex: https://foo.com/#//bar.com/file.md
if (isExternal(path)) {
history.replaceState(null, '', '#');
this.router.normalize();
} else {
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);
this.isRemoteUrl = isExternal(file);
// 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(path, qs, cb) ||
this._fetch404(file, qs, cb);
}
);
// 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;
}
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;
}
}
$fetch(cb = noop, $resetEvents = this.$resetEvents.bind(this)) {
const done = () => {
this.callHook('doneEach');
cb();
};
// Load sidebar
loadNested(path, qs, loadSidebar, fn, this, true);
};
};
const onlyCover = this._fetchCover();
proto._fetch = function(cb = noop) {
const { query } = this.route;
let { path } = this.route;
if (onlyCover) {
done();
} else {
this._fetch(() => {
$resetEvents();
done();
});
}
}
// Prevent loading remote content via URL hash
// Ex: https://foo.com/#//bar.com/file.md
if (isExternal(path)) {
history.replaceState(null, '', '#');
this.router.normalize();
} else {
const qs = stringifyQuery(query, ['id']);
const { loadNavbar, requestHeaders, loadSidebar } = this.config;
// Abort last request
_fetchFallbackPage(path, qs, cb = noop) {
const { requestHeaders, fallbackLanguages, loadSidebar } = this.config;
const file = this.router.getFile(path);
const req = request(file + qs, true, requestHeaders);
if (!fallbackLanguages) {
return false;
}
this.isRemoteUrl = isExternal(file);
// Current page is html
this.isHTML = /\.html$/g.test(file);
const local = path.split('/')[1];
if (fallbackLanguages.indexOf(local) === -1) {
return false;
}
const newPath = this.router.getFile(
path.replace(new RegExp(`^/${local}`), '')
);
const req = request(newPath + qs, true, requestHeaders);
// Load main content
req.then(
(text, opt) =>
this._renderMain(
@ -104,154 +208,55 @@ export function fetchMixin(proto) {
opt,
this._loadSideAndNav(path, qs, loadSidebar, cb)
),
_ => {
this._fetchFallbackPage(path, qs, cb) || this._fetch404(file, qs, cb);
}
() => this._fetch404(path, qs, cb)
);
// Load nav
loadNavbar &&
loadNested(
path,
qs,
loadNavbar,
text => this._renderNav(text),
this,
true
);
}
};
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;
}
} 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);
}
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();
});
}
};
proto._fetchFallbackPage = function(path, qs, cb = noop) {
const { requestHeaders, fallbackLanguages, loadSidebar } = this.config;
if (!fallbackLanguages) {
return false;
}
const local = path.split('/')[1];
if (fallbackLanguages.indexOf(local) === -1) {
return false;
}
const newPath = this.router.getFile(
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;
};
/**
* 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)
);
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;
export function initFetch(vm) {
const { loadSidebar } = vm.config;
const fnLoadSideAndNav = this._loadSideAndNav(path, qs, loadSidebar, cb);
if (notFoundPage) {
const path404 = get404Path(path, 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__;
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;
}
vm._bindEventOnRendered(activeEl);
vm.$resetEvents();
callHook(vm, 'doneEach');
callHook(vm, 'ready');
} else {
vm.$fetch(_ => callHook(vm, 'ready'));
}
initFetch() {
const { loadSidebar } = this.config;
// Server-Side Rendering
if (this.rendered) {
const activeEl = getAndActive(this.router, '.sidebar-nav', true, true);
if (loadSidebar && activeEl) {
activeEl.parentNode.innerHTML += window.__SUB_SIDEBAR__;
}
this._bindEventOnRendered(activeEl);
this.$resetEvents();
this.callHook('doneEach');
this.callHook('ready');
} else {
this.$fetch(_ => this.callHook('ready'));
}
}
};
}

View File

@ -1,27 +0,0 @@
import config from '../config';
import { initRender } from '../render';
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(proto) {
proto._init = function() {
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');
};
}
function initPlugin(vm) {
[].concat(vm.config.plugins).forEach(fn => isFn(fn) && fn(vm._lifecycle, vm));
}

View File

@ -1,46 +1,57 @@
import { noop } from '../util/core';
export function initLifecycle(vm) {
const hooks = [
'init',
'mounted',
'beforeEach',
'afterEach',
'doneEach',
'ready',
];
/** @typedef {import('../Docsify').Constructor} Constructor */
vm._hooks = {};
vm._lifecycle = {};
hooks.forEach(hook => {
const arr = (vm._hooks[hook] = []);
vm._lifecycle[hook] = fn => arr.push(fn);
});
}
/**
* @template {!Constructor} T
* @param {T} Base - The class to extend
*/
export function Lifecycle(Base) {
return class Lifecycle extends Base {
initLifecycle() {
const hooks = [
'init',
'mounted',
'beforeEach',
'afterEach',
'doneEach',
'ready',
];
export function callHook(vm, hookName, data, next = noop) {
const queue = vm._hooks[hookName];
this._hooks = {};
this._lifecycle = {};
const step = function(index) {
const hookFn = queue[index];
hooks.forEach(hook => {
const arr = (this._hooks[hook] = []);
this._lifecycle[hook] = fn => arr.push(fn);
});
}
if (index >= queue.length) {
next(data);
} else if (typeof hookFn === 'function') {
if (hookFn.length === 2) {
hookFn(data, result => {
data = result;
callHook(hookName, data, next = noop) {
const queue = this._hooks[hookName];
const step = function(index) {
const hookFn = queue[index];
if (index >= queue.length) {
next(data);
} else if (typeof hookFn === 'function') {
if (hookFn.length === 2) {
hookFn(data, result => {
data = result;
step(index + 1);
});
} else {
const result = hookFn(data);
data = result === undefined ? data : result;
step(index + 1);
}
} else {
step(index + 1);
});
} else {
const result = hookFn(data);
data = result === undefined ? data : result;
step(index + 1);
}
} else {
step(index + 1);
}
};
step(0);
}
};
step(0);
}

View File

@ -3,7 +3,6 @@ import tinydate from 'tinydate';
import DOMPurify from 'dompurify';
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';
@ -239,223 +238,235 @@ 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;
}
};
/** @typedef {import('../Docsify').Constructor} Constructor */
proto._renderSidebar = function(text) {
const { maxLevel, subMaxLevel, loadSidebar, hideSidebar } = this.config;
if (hideSidebar) {
// FIXME : better styling solution
[
document.querySelector('aside.sidebar'),
document.querySelector('button.sidebar-toggle'),
].forEach(node => node.parentNode.removeChild(node));
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();
}
// Bind event
this._bindEventOnRendered(activeEl);
};
proto._bindEventOnRendered = function(activeEl) {
const { autoHeader } = this.config;
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]);
/**
* @template {!Constructor} T
* @param {T} Base - The class to extend
*/
export function Render(Base) {
return class Render extends Base {
_renderTo(el, content, replace) {
const node = dom.getNode(el);
if (node) {
node[replace ? 'outerHTML' : 'innerHTML'] = content;
}
}
};
proto._renderNav = function(text) {
text && this._renderTo('nav', this.compiler.compile(text));
if (this.config.loadNavbar) {
getAndActive(this.router, 'nav');
}
};
_renderSidebar(text) {
const { maxLevel, subMaxLevel, loadSidebar, hideSidebar } = this.config;
proto._renderMain = function(text, opt = {}, next) {
if (!text) {
return renderMain.call(this, text);
}
if (hideSidebar) {
// FIXME : better styling solution
[
document.querySelector('aside.sidebar'),
document.querySelector('button.sidebar-toggle'),
].forEach(node => node.parentNode.removeChild(node));
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;
}
callHook(this, 'beforeEach', text, result => {
let html;
const callback = () => {
if (opt.updatedAt) {
html = formatUpdated(html, opt.updatedAt, this.config.formatUpdated);
}
callHook(this, 'afterEach', html, hookData =>
renderMain.call(this, hookData)
);
};
if (this.isHTML) {
html = this.result = text;
callback();
next();
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 {
prerenderEmbed(
{
compiler: this.compiler,
raw: result,
},
tokens => {
html = this.compiler.compile(tokens);
html = this.isRemoteUrl
? DOMPurify.sanitize(html, { ADD_TAGS: ['script'] })
: html;
callback();
next();
// Reset toc
this.compiler.subSidebar();
}
// Bind event
this._bindEventOnRendered(activeEl);
}
_bindEventOnRendered(activeEl) {
const { autoHeader } = this.config;
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]);
}
}
}
_renderNav(text) {
text && this._renderTo('nav', this.compiler.compile(text));
if (this.config.loadNavbar) {
getAndActive(this.router, 'nav');
}
}
_renderMain(text, opt = {}, next) {
if (!text) {
return renderMain.call(this, text);
}
this.callHook('beforeEach', text, result => {
let html;
const callback = () => {
if (opt.updatedAt) {
html = formatUpdated(
html,
opt.updatedAt,
this.config.formatUpdated
);
}
);
}
});
};
proto._renderCover = function(text, coverOnly) {
const el = dom.getNode('.cover');
this.callHook('afterEach', html, hookData =>
renderMain.call(this, hookData)
);
};
dom.toggleClass(
dom.getNode('main'),
coverOnly ? 'add' : 'remove',
'hidden'
);
if (!text) {
dom.toggleClass(el, 'remove', 'show');
return;
if (this.isHTML) {
html = this.result = text;
callback();
next();
} else {
prerenderEmbed(
{
compiler: this.compiler,
raw: result,
},
tokens => {
html = this.compiler.compile(tokens);
html = this.isRemoteUrl
? DOMPurify.sanitize(html, { ADD_TAGS: ['script'] })
: html;
callback();
next();
}
);
}
});
}
dom.toggleClass(el, 'add', 'show');
_renderCover(text, coverOnly) {
const el = dom.getNode('.cover');
let html = this.coverIsHTML ? text : this.compiler.cover(text);
dom.toggleClass(
dom.getNode('main'),
coverOnly ? 'add' : 'remove',
'hidden'
);
if (!text) {
dom.toggleClass(el, 'remove', 'show');
return;
}
const m = html
.trim()
.match('<p><img.*?data-origin="(.*?)"[^a]+alt="(.*?)">([^<]*?)</p>$');
dom.toggleClass(el, 'add', 'show');
if (m) {
if (m[2] === 'color') {
el.style.background = m[1] + (m[3] || '');
} else {
let path = m[1];
let html = this.coverIsHTML ? text : this.compiler.cover(text);
dom.toggleClass(el, 'add', 'has-mask');
if (!isAbsolutePath(m[1])) {
path = getPath(this.router.getBasePath(), m[1]);
const m = html
.trim()
.match('<p><img.*?data-origin="(.*?)"[^a]+alt="(.*?)">([^<]*?)</p>$');
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]);
}
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);
};
}
export function initRender(vm) {
const config = vm.config;
// Init markdown compiler
vm.compiler = new Compiler(config, vm.router);
if (inBrowser) {
/* eslint-disable-next-line camelcase */
window.__current_docsify_compiler__ = vm.compiler;
}
const id = config.el || '#app';
const navEl = dom.find('nav') || dom.create('nav');
const el = dom.find(id);
let html = '';
let navAppendToTarget = dom.body;
if (el) {
if (config.repo) {
html += tpl.corner(config.repo, config.cornerExternalLinkTarge);
_updateRender() {
// Render name link
renderNameLink(this);
}
if (config.coverpage) {
html += tpl.cover();
}
initRender() {
const config = this.config;
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);
// Init markdown compiler
this.compiler = new Compiler(config, this.router);
if (inBrowser) {
/* eslint-disable-next-line camelcase */
window.__current_docsify_compiler__ = this.compiler;
}
const id = config.el || '#app';
const navEl = dom.find('nav') || dom.create('nav');
const el = dom.find(id);
let html = '';
let navAppendToTarget = dom.body;
if (el) {
if (config.repo) {
html += tpl.corner(config.repo, config.cornerExternalLinkTarge);
}
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 (!isBase64 && !isExternal && !isRelative) {
config.logo = getPath(this.router.getBasePath(), config.logo);
}
}
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.repo) {
navEl.classList.add('no-badge');
}
}
// 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);
}
this._updateRender();
dom.toggleClass(dom.body, 'ready');
}
html += tpl.main(config);
// Render main app
vm._renderTo(el, html, true);
} else {
vm.rendered = true;
}
if (config.mergeNavbar && isMobile) {
navAppendToTarget = dom.find('.sidebar');
} else {
navEl.classList.add('app-nav');
if (!config.repo) {
navEl.classList.add('no-badge');
}
}
// 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);
}
vm._updateRender();
dom.toggleClass(dom.body, 'ready');
};
}

View File

@ -36,6 +36,7 @@ export class HashHistory extends History {
return index === -1 ? '' : href.slice(index + 1);
}
/** @param {((params: {source: TODO}) => void)} [cb] */
onchange(cb = noop) {
// The hashchange event does not tell us if it originated from
// a clicked link or by moving back/forward in the history;
@ -100,3 +101,5 @@ export class HashHistory extends History {
return '#' + super.toURL(path, params, currentRoute);
}
}
/** @typedef {any} TODO */

View File

@ -4,44 +4,64 @@ import { noop } from '../util/core';
import { HashHistory } from './history/hash';
import { HTML5History } from './history/html5';
export function routerMixin(proto) {
proto.route = {};
}
/**
* @typedef {{
* path?: string
* }} Route
*/
/** @type {Route} */
let lastRoute = {};
function updateRender(vm) {
vm.router.normalize();
vm.route = vm.router.parse();
dom.body.setAttribute('data-page', vm.route.file);
}
/** @typedef {import('../Docsify').Constructor} Constructor */
export function initRouter(vm) {
const config = vm.config;
const mode = config.routerMode || 'hash';
let router;
/**
* @template {!Constructor} T
* @param {T} Base - The class to extend
*/
export function Router(Base) {
return class Router extends Base {
/** @param {any[]} args */
constructor(...args) {
super(...args);
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;
this.route = {};
}
vm.$fetch(noop, vm.$resetEvents.bind(vm, params.source));
lastRoute = vm.route;
});
updateRender() {
this.router.normalize();
this.route = this.router.parse();
dom.body.setAttribute('data-page', this.route.file);
}
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.updateRender();
lastRoute = this.route;
// eslint-disable-next-line no-unused-vars
router.onchange(params => {
this.updateRender();
this._updateRender();
if (lastRoute.path === this.route.path) {
this.$resetEvents(params.source);
return;
}
this.$fetch(noop, this.$resetEvents.bind(this, params.source));
lastRoute = this.route;
});
}
};
}

View File

@ -56,8 +56,8 @@ describe('Sidebar Tests', function() {
expect(page.url()).toMatch(/\/test-foo$/);
await page.click('a[href="#/test.foo"]');
await expect(page).toEqualText('.sidebar-nav li[class=active]', 'Test .');
expect(page.url()).toMatch(/\/test.foo$/);
await expect(page).toEqualText('.sidebar-nav li[class=active]', 'Test .');
await page.click('a[href="#/test>foo"]');
await expect(page).toEqualText('.sidebar-nav li[class=active]', 'Test >');