Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ To disable emoji parsing of individual shorthand codes, replace `:` characters w
- Type: `Boolean` | `String` | `Object`
- Default: `false`

Display default "404 - Not found" message:
Display default "404 - Not Found" message:

```js
window.$docsify = {
Expand Down
6 changes: 4 additions & 2 deletions src/core/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ export function Fetch(Base) {
this.isHTML = /\.html$/g.test(file);

// create a handler that should be called if content was fetched successfully
const contentFetched = (text, opt) => {
const contentFetched = (text, opt, response) => {
this.route.response = response;
this._renderMain(
text,
opt,
Expand All @@ -111,7 +112,8 @@ export function Fetch(Base) {
};

// and a handler that is called if content failed to fetch
const contentFailedToFetch = _error => {
const contentFailedToFetch = (_error, response) => {
this.route.response = response;
this._fetchFallbackPage(path, qs, cb) || this._fetch404(file, qs, cb);
};

Expand Down
12 changes: 6 additions & 6 deletions src/core/render/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@ export function Render(Base) {
return isVue2 || isVue3;
};

if (!html) {
html = /* html */ `<h1>404 - Not found</h1>`;
}

if ('Vue' in window) {
const mountedElms = dom
.findAll('.markdown-section > *')
Expand Down Expand Up @@ -310,8 +306,12 @@ export function Render(Base) {
}

_renderMain(text, opt = {}, next) {
if (!text) {
return this.#renderMain(text);
const { response } = this.route;

// Note: It is possible for the response to be undefined in envrionments
// where XMLHttpRequest has been modified or mocked
if (response && !response.ok) {
text = `# ${response.status} - ${response.statusText}`;
}

this.callHook('beforeEach', text, result => {
Expand Down
1 change: 1 addition & 0 deletions src/core/router/history/hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export class HashHistory extends History {
path,
file: this.getFile(path, true),
query: parseQuery(query),
response: {},
};
}

Expand Down
1 change: 1 addition & 0 deletions src/core/router/history/html5.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class HTML5History extends History {
path,
file: this.getFile(path),
query: parseQuery(query),
response: {},
};
}
}
24 changes: 17 additions & 7 deletions src/core/util/ajax.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import progressbar from '../render/progressbar.js';
import { noop } from './core.js';

/** @typedef {{updatedAt: string}} CacheOpt */

/** @typedef {{content: string, opt: CacheOpt}} CacheItem */

/** @typedef {{ok: boolean, status: number, statusText: string}} ResponseStatus */
/** @type {Record<string, CacheItem>} */

const cache = {};

/**
Expand Down Expand Up @@ -37,10 +37,16 @@ export function get(url, hasBar = false, headers = {}) {

return {
/**
* @param {(text: string, opt: CacheOpt) => void} success
* @param {(event: ProgressEvent<XMLHttpRequestEventTarget>) => void} error
* @param {(text: string, opt: CacheOpt, response: ResponseStatus) => void} success
* @param {(event: ProgressEvent<XMLHttpRequestEventTarget>, response: ResponseStatus) => void} error
*/
then(success, error = noop) {
const getResponseStatus = event => ({
ok: event.target.status >= 200 && event.target.status < 300,
status: event.target.status,
statusText: event.target.statusText,
});

if (hasBar) {
const id = setInterval(
_ =>
Expand All @@ -57,11 +63,15 @@ export function get(url, hasBar = false, headers = {}) {
});
}

xhr.addEventListener('error', error);
xhr.addEventListener('error', event => {
error(event, getResponseStatus(event));
});

xhr.addEventListener('load', event => {
const target = /** @type {XMLHttpRequest} */ (event.target);

if (target.status >= 400) {
error(event);
error(event, getResponseStatus(event));
} else {
if (typeof target.response !== 'string') {
throw new TypeError('Unsupported content type.');
Expand All @@ -74,7 +84,7 @@ export function get(url, hasBar = false, headers = {}) {
},
});

success(result.content, result.opt);
success(result.content, result.opt, getResponseStatus(event));
}
});
},
Expand Down
50 changes: 48 additions & 2 deletions test/e2e/virtual-routes.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import docsifyInit from '../helpers/docsify-init.js';
import { test, expect } from './fixtures/docsify-init-fixture.js';
import { waitForFunction } from '../helpers/wait-for.js';

/**
* Navigate to a specific route in the site
Expand Down Expand Up @@ -221,7 +222,7 @@ test.describe('Virtual Routes - Generate Dynamic Content via Config', () => {
await navigateToRoute(page, '/d');

const mainElm = page.locator('#main');
await expect(mainElm).toContainText('404 - Not found');
await expect(mainElm).toContainText('404 - Not Found');
});

test('skip routes that returned a falsy value that is not a boolean', async ({
Expand Down Expand Up @@ -263,7 +264,7 @@ test.describe('Virtual Routes - Generate Dynamic Content via Config', () => {
await navigateToRoute(page, '/multiple/matches');

const mainElm = page.locator('#main');
await expect(mainElm).toContainText('404 - Not found');
await expect(mainElm).toContainText('404 - Not Found');
});

test('skip routes that are not a valid string or function', async ({
Expand All @@ -290,4 +291,49 @@ test.describe('Virtual Routes - Generate Dynamic Content via Config', () => {
await expect(titleElm).toContainText('Last Match');
});
});

test.describe('Route Data', () => {
test('route data accessible to plugins', async ({ page }) => {
const failLink = page.locator('a[href*="fail"]');

let routeData = null;

// Store route data set via plugin hook (below)
page.on('console', async msg => {
for (const arg of msg.args()) {
const val = await arg.jsonValue();
const obj = JSON.parse(val);
obj.response && (routeData = obj);
}
});

await docsifyInit({
markdown: {
homepage: '[Fail](fail.md)',
},
config: {
plugins: [
function (hook, vm) {
hook.doneEach(html => {
console.log(JSON.stringify(vm.route));
});
},
],
},
});

expect(routeData).toHaveProperty('response');
expect(routeData.response).toHaveProperty('ok', true);
expect(routeData.response).toHaveProperty('status', 200);
expect(routeData.response).toHaveProperty('statusText', 'OK');

await failLink.click();
await waitForFunction(() => routeData?.response?.status !== 200);

expect(routeData).toHaveProperty('response');
expect(routeData.response).toHaveProperty('ok', false);
expect(routeData.response).toHaveProperty('status', 404);
expect(routeData.response).toHaveProperty('statusText', 'Not Found');
});
});
});