diff --git a/docs/configuration.md b/docs/configuration.md
index 9762b21438..6f0c4e475b 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 5495a87712..ad9b4bf70e 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 ba6c54b88f..ad339311c4 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 d766291593..5830656a93 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 c04fc1a72c..94a3981fe7 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 0000000000..8aa3c5f083
--- /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 0000000000..92f00ca568
--- /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');
+ });
+});