diff --git a/docs/configuration.md b/docs/configuration.md index 9762b2143..6f0c4e475 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -365,7 +365,7 @@ window.$docsify = { ## autoHeader -- type: `Boolean` +- Type: `Boolean` If `loadSidebar` and `autoHeader` are both enabled, for each link in `_sidebar.md`, prepend a header to the page before converting it to HTML. See [#78](https://github.com/docsifyjs/docsify/issues/78). @@ -378,7 +378,7 @@ window.$docsify = { ## executeScript -- type: `Boolean` +- Type: `Boolean` Execute the script on the page. Only parse the first script tag ([demo](themes)). If Vue is present, it is turned on by default. @@ -400,8 +400,8 @@ Note that if you are running an external script, e.g. an embedded jsfiddle demo, ## nativeEmoji -- type: `Boolean` -- default: `false` +- Type: `Boolean` +- Default: `false` Render emoji shorthand codes using GitHub-style emoji images or platform-native emoji characters. @@ -453,8 +453,8 @@ To render shorthand codes as text, replace `:` characters with the `:` HTM ## noEmoji -- type: `Boolean` -- default: `false` +- Type: `Boolean` +- Default: `false` Disabled emoji parsing and render all emoji shorthand as text. @@ -492,7 +492,7 @@ To disable emoji parsing of individual shorthand codes, replace `:` characters w ## mergeNavbar -- type: `Boolean` +- Type: `Boolean` Navbar will be merged with the sidebar on smaller screens. @@ -504,7 +504,7 @@ window.$docsify = { ## formatUpdated -- type: `String|Function` +- Type: `String|Function` We can display the file update date through **{docsify-updated}** variable. And format it by `formatUpdated`. See https://github.com/lukeed/tinydate#patterns @@ -523,8 +523,8 @@ window.$docsify = { ## externalLinkTarget -- type: `String` -- default: `_blank` +- Type: `String` +- Default: `_blank` Target to open external links inside the markdown. Default `'_blank'` (new window/tab) @@ -536,8 +536,8 @@ window.$docsify = { ## cornerExternalLinkTarget -- type:`String` -- default:`_blank` +- Type:`String` +- Default:`_blank` Target to open external link at the top right corner. Default `'_blank'` (new window/tab) @@ -549,8 +549,8 @@ window.$docsify = { ## externalLinkRel -- type: `String` -- default: `noopener` +- Type: `String` +- Default: `noopener` Default `'noopener'` (no opener) prevents the newly opened external page (when [externalLinkTarget](#externallinktarget) is `'_blank'`) from having the ability to control our page. No `rel` is set when it's not `'_blank'`. See [this post](https://mathiasbynens.github.io/rel-noopener/) for more information about why you may want to use this option. @@ -562,8 +562,8 @@ window.$docsify = { ## routerMode -- type: `String` -- default: `hash` +- Type: `String` +- Default: `hash` ```js window.$docsify = { @@ -573,7 +573,7 @@ window.$docsify = { ## crossOriginLinks -- type: `Array` +- Type: `Array` When `routerMode: 'history'`, you may face cross-origin issues. See [#1379](https://github.com/docsifyjs/docsify/issues/1379). In Markdown content, there is a simple way to solve it: see extends Markdown syntax `Cross-Origin link` in [helpers](helpers.md). @@ -586,7 +586,7 @@ window.$docsify = { ## noCompileLinks -- type: `Array` +- Type: `Array` Sometimes we do not want docsify to handle our links. See [#203](https://github.com/docsifyjs/docsify/issues/203). We can skip compiling of certain links by specifying an array of strings. Each string is converted into to a regular expression (`RegExp`) and the _whole_ href of a link is matched against it. @@ -598,7 +598,7 @@ window.$docsify = { ## onlyCover -- type: `Boolean` +- Type: `Boolean` Only coverpage is loaded when visiting the home page. @@ -610,7 +610,7 @@ window.$docsify = { ## requestHeaders -- type: `Object` +- Type: `Object` Set the request resource headers. @@ -634,7 +634,7 @@ window.$docsify = { ## ext -- type: `String` +- Type: `String` Request file extension. @@ -646,7 +646,7 @@ window.$docsify = { ## fallbackLanguages -- type: `Array` +- Type: `Array` List of languages that will fallback to the default language when a page is requested and it doesn't exist for the given locale. @@ -664,7 +664,7 @@ window.$docsify = { ## notFoundPage -- type: `Boolean` | `String` | `Object` +- Type: `Boolean` | `String` | `Object` Load the `_404.md` file: @@ -697,8 +697,8 @@ window.$docsify = { ## topMargin -- type: `Number` -- default: `0` +- Type: `Number` +- Default: `0` Adds a space on top when scrolling the content page to reach the selected section. This is useful in case you have a _sticky-header_ layout and you want to align anchors to the end of your header. @@ -710,7 +710,7 @@ window.$docsify = { ## vueComponents -- type: `Object` +- Type: `Object` Creates and registers global [Vue components](https://vuejs.org/v2/guide/components.html). Components are specified using the component name as the key with an object containing Vue options as the value. Component `data` is unique for each instance and will not persist as users navigate the site. @@ -743,7 +743,7 @@ window.$docsify = { ## vueGlobalOptions -- type: `Object` +- Type: `Object` Specifies [Vue options](https://vuejs.org/v2/api/#Options-Data) for use with Vue content not explicitly mounted with [vueMounts](#mounting-dom-elements), [vueComponents](#components), or a [markdown script](#markdown-script). Changes to global `data` will persist and be reflected anywhere global references are used. @@ -777,7 +777,7 @@ window.$docsify = { ## vueMounts -- type: `Object` +- Type: `Object` Specifies DOM elements to mount as [Vue instances](https://vuejs.org/v2/guide/instance.html) and their associated options. Mount elements are specified using a [CSS selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) as the key with an object containing Vue options as their value. Docsify will mount the first matching element in the main content area each time a new page is loaded. Mount element `data` is unique for each instance and will not persist as users navigate the site. @@ -808,3 +808,10 @@ window.$docsify = { {{ count }} + +## catchPluginErrors + +- Type: `Boolean` +- Default: `true` + +Determines if Docsify should handle uncaught _synchronous_ plugin errors automatically. This can prevent plugin errors from affecting docsify's ability to properly render live site content. diff --git a/docs/write-a-plugin.md b/docs/write-a-plugin.md index 5495a8771..ad9b4bf70 100644 --- a/docs/write-a-plugin.md +++ b/docs/write-a-plugin.md @@ -1,85 +1,232 @@ # Write a plugin -A plugin is simply a function that takes `hook` as an argument. The hook supports handling of asynchronous tasks. +A docsify plugin is a function with the ability to execute custom JavaScript code at various stages of Docsify's lifecycle. -## Full configuration +## Setup + +Docsify plugins can be added directly to the `plugins` array. ```js window.$docsify = { plugins: [ - function(hook, vm) { - hook.init(function() { - // Called when the script starts running, only trigger once, no arguments, - }); + function myPlugin1(hook, vm) { + // ... + }, + function myPlugin2(hook, vm) { + // ... + }, + ], +}; +``` - hook.beforeEach(function(content) { - // Invoked each time before parsing the Markdown file. - // ... - return content; - }); +Alternatively, a plugin can be stored in a separate file and "installed" using a standard ` +``` - hook.mounted(function() { - // Called after initial completion. Only trigger once, no arguments. - }); +## Template - hook.ready(function() { - // Called after initial completion, no arguments. - }); - } - ] -}; +Below is a plugin template with placeholders for all available lifecycle hooks. + +1. Copy the template +1. Modify the `myPlugin` name as appropriate +1. Add your plugin logic +1. Remove unused lifecycle hooks +1. Save the file as `docsify-plugin-[name].js` +1. Load your plugin using a standard ` diff --git a/src/core/Docsify.js b/src/core/Docsify.js index ba6c54b88..ad339311c 100644 --- a/src/core/Docsify.js +++ b/src/core/Docsify.js @@ -28,9 +28,18 @@ export class Docsify extends Fetch(Events(Render(Router(Lifecycle(Object))))) { } initPlugin() { - [] - .concat(this.config.plugins) - .forEach(fn => isFn(fn) && fn(this._lifecycle, this)); + [].concat(this.config.plugins).forEach(fn => { + try { + isFn(fn) && fn(this._lifecycle, this); + } catch (err) { + if (this.config.catchPluginErrors) { + const errTitle = 'Docsify plugin error'; + console.error(errTitle, err); + } else { + throw err; + } + } + }); } } diff --git a/src/core/config.js b/src/core/config.js index d76629159..5830656a9 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -37,6 +37,7 @@ export default function (vm) { crossOriginLinks: [], relativePath: false, topMargin: 0, + catchPluginErrors: true, }, typeof window.$docsify === 'function' ? window.$docsify(vm) diff --git a/src/core/init/lifecycle.js b/src/core/init/lifecycle.js index c04fc1a72..94a3981fe 100644 --- a/src/core/init/lifecycle.js +++ b/src/core/init/lifecycle.js @@ -29,6 +29,7 @@ export function Lifecycle(Base) { callHook(hookName, data, next = noop) { const queue = this._hooks[hookName]; + const catchPluginErrors = this.config.catchPluginErrors; const step = function (index) { const hookFn = queue[index]; @@ -36,15 +37,38 @@ export function Lifecycle(Base) { if (index >= queue.length) { next(data); } else if (typeof hookFn === 'function') { + const errTitle = 'Docsify plugin error'; + if (hookFn.length === 2) { - hookFn(data, result => { - data = result; + try { + hookFn(data, result => { + data = result; + step(index + 1); + }); + } catch (err) { + if (catchPluginErrors) { + console.error(errTitle, err); + } else { + throw err; + } + step(index + 1); - }); + } } else { - const result = hookFn(data); - data = result === undefined ? data : result; - step(index + 1); + try { + const result = hookFn(data); + + data = result === undefined ? data : result; + step(index + 1); + } catch (err) { + if (catchPluginErrors) { + console.error(errTitle, err); + } else { + throw err; + } + + step(index + 1); + } } } else { step(index + 1); diff --git a/test/e2e/configuration.test.js b/test/e2e/configuration.test.js new file mode 100644 index 000000000..8aa3c5f08 --- /dev/null +++ b/test/e2e/configuration.test.js @@ -0,0 +1,67 @@ +const docsifyInit = require('../helpers/docsify-init'); +const { test, expect } = require('./fixtures/docsify-init-fixture'); + +test.describe('Configuration options', () => { + test('catchPluginErrors:true (handles uncaught errors)', async ({ page }) => { + let consoleMsg, errorMsg; + + page.on('console', msg => (consoleMsg = msg.text())); + page.on('pageerror', err => (errorMsg = err.message)); + + await docsifyInit({ + config: { + catchPluginErrors: true, + plugins: [ + function (hook, vm) { + hook.init(function () { + // eslint-disable-next-line no-undef + fail(); + }); + hook.beforeEach(function (markdown) { + return `${markdown}\n\nbeforeEach`; + }); + }, + ], + }, + markdown: { + homepage: '# Hello World', + }, + // _logHTML: true, + }); + + const mainElm = page.locator('#main'); + + expect(errorMsg).toBeUndefined(); + expect(consoleMsg).toContain('Docsify plugin error'); + await expect(mainElm).toContainText('Hello World'); + await expect(mainElm).toContainText('beforeEach'); + }); + + test('catchPluginErrors:false (throws uncaught errors)', async ({ page }) => { + let consoleMsg, errorMsg; + + page.on('console', msg => (consoleMsg = msg.text())); + page.on('pageerror', err => (errorMsg = err.message)); + + await docsifyInit({ + config: { + catchPluginErrors: false, + plugins: [ + function (hook, vm) { + hook.ready(function () { + // eslint-disable-next-line no-undef + fail(); + }); + }, + ], + }, + markdown: { + homepage: '# Hello World', + }, + // _logHTML: true, + }); + + expect(consoleMsg).toBeUndefined(); + expect(errorMsg).toContain('fail'); + }); +}); diff --git a/test/e2e/plugins.test.js b/test/e2e/plugins.test.js new file mode 100644 index 000000000..92f00ca56 --- /dev/null +++ b/test/e2e/plugins.test.js @@ -0,0 +1,156 @@ +const docsifyInit = require('../helpers/docsify-init'); +const { test, expect } = require('./fixtures/docsify-init-fixture'); + +test.describe('Plugins', () => { + test('Hook order', async ({ page }) => { + const consoleMsgs = []; + const expectedMsgs = [ + 'init', + 'mounted', + 'beforeEach-async', + 'beforeEach', + // 'afterEach-async', + 'afterEach', + 'doneEach', + 'ready', + ]; + + page.on('console', msg => consoleMsgs.push(msg.text())); + + await docsifyInit({ + config: { + plugins: [ + function (hook, vm) { + hook.init(function () { + console.log('init'); + }); + + hook.mounted(function () { + console.log('mounted'); + }); + + hook.beforeEach(function (markdown, next) { + setTimeout(function () { + console.log('beforeEach-async'); + next(markdown); + }, 100); + }); + + hook.beforeEach(function (markdown) { + console.log('beforeEach'); + return markdown; + }); + + // FIXME: https://github.com/docsifyjs/docsify/issues/449 + // hook.afterEach(function (html, next) { + // setTimeout(function () { + // console.log('afterEach-async'); + // next(html); + // }, 100); + // }); + + hook.afterEach(function (html) { + console.log('afterEach'); + return html; + }); + + hook.doneEach(function () { + console.log('doneEach'); + }); + + hook.ready(function () { + console.log('ready'); + }); + }, + ], + }, + markdown: { + homepage: '# Hello World', + }, + // _logHTML: true, + }); + + expect(consoleMsgs).toEqual(expectedMsgs); + }); + + test('beforeEach() return value', async ({ page }) => { + await docsifyInit({ + config: { + plugins: [ + function (hook, vm) { + hook.beforeEach(function (markdown) { + return 'beforeEach'; + }); + }, + ], + }, + // _logHTML: true, + }); + + await expect(page.locator('#main')).toContainText('beforeEach'); + }); + + test('beforeEach() async return value', async ({ page }) => { + await docsifyInit({ + config: { + plugins: [ + function (hook, vm) { + hook.beforeEach(function (markdown, next) { + setTimeout(function () { + next('beforeEach'); + }, 100); + }); + }, + ], + }, + markdown: { + homepage: '# Hello World', + }, + // _logHTML: true, + }); + + await expect(page.locator('#main')).toContainText('beforeEach'); + }); + + test('afterEach() return value', async ({ page }) => { + await docsifyInit({ + config: { + plugins: [ + function (hook, vm) { + hook.afterEach(function (html) { + return '

afterEach

'; + }); + }, + ], + }, + markdown: { + homepage: '# Hello World', + }, + // _logHTML: true, + }); + + await expect(page.locator('#main')).toContainText('afterEach'); + }); + + test('afterEach() async return value', async ({ page }) => { + await docsifyInit({ + config: { + plugins: [ + function (hook, vm) { + hook.afterEach(function (html, next) { + setTimeout(function () { + next('

afterEach

'); + }, 100); + }); + }, + ], + }, + markdown: { + homepage: '# Hello World', + }, + // _logHTML: true, + }); + + await expect(page.locator('#main')).toContainText('afterEach'); + }); +});