diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js index 8c6cc8356..b5ab0dafd 100644 --- a/src/core/fetch/index.js +++ b/src/core/fetch/index.js @@ -150,7 +150,7 @@ export function Fetch(Base) { } } - _fetchCover() { + _fetchCover(cb = noop) { const { coverpage, requestHeaders } = this.config; const query = this.route.query; const root = getParentPath(this.route.path); @@ -170,17 +170,26 @@ export function Fetch(Base) { } const coverOnly = Boolean(path) && this.config.onlyCover; + const next = () => cb(coverOnly); 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), + text => this._renderCover(text, coverOnly, next), + (event, response) => { + this.coverIsHTML = false; + this._renderCover( + `# ${response.status} - ${response.statusText}`, + coverOnly, + next, + ); + }, ); } else { - this._renderCover(null, coverOnly); + this._renderCover(null, coverOnly, next); } - - return coverOnly; + } else { + cb(false); } } @@ -190,16 +199,16 @@ export function Fetch(Base) { cb(); }; - const onlyCover = this._fetchCover(); - - if (onlyCover) { - done(); - } else { - this._fetch(() => { - onNavigate(); + this._fetchCover(onlyCover => { + if (onlyCover) { done(); - }); - } + } else { + this._fetch(() => { + onNavigate(); + done(); + }); + } + }); } _fetchFallbackPage(path, qs, cb = noop) { diff --git a/src/core/render/index.js b/src/core/render/index.js index bf2f859d6..d3faeff85 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -382,7 +382,7 @@ export function Render(Base) { }); } - _renderCover(text, coverOnly) { + _renderCover(text, coverOnly, next) { const el = dom.getNode('.cover'); dom.toggleClass( @@ -392,37 +392,56 @@ export function Render(Base) { ); if (!text) { dom.toggleClass(el, 'remove', 'show'); + next(); return; } dom.toggleClass(el, 'add', 'show'); - let html = this.coverIsHTML ? text : this.compiler.cover(text); + const callback = html => { + const m = html + .trim() + .match('

([^<]*?)

$'); - const m = html - .trim() - .match('

([^<]*?)

$'); + if (m) { + if (m[2] === 'color') { + el.style.background = m[1] + (m[3] || ''); + } else { + let path = m[1]; - 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]); + } - 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); + next(); + }; - this._renderTo('.cover-main', html); + // TODO: Call the 'beforeEach' and 'afterEach' hooks. + // However, when the cover and the home page are on the same page, + // the 'beforeEach' and 'afterEach' hooks are called multiple times. + // It is difficult to determine the target of the hook within the + // hook functions. We might need to make some changes. + if (this.coverIsHTML) { + callback(text); + } else { + prerenderEmbed( + { + compiler: this.compiler, + raw: text, + }, + tokens => callback(this.compiler.cover(tokens)), + ); + } } _updateRender() { diff --git a/test/e2e/embed-files.test.js b/test/e2e/embed-files.test.js new file mode 100644 index 000000000..46ae1d1c3 --- /dev/null +++ b/test/e2e/embed-files.test.js @@ -0,0 +1,33 @@ +import docsifyInit from '../helpers/docsify-init.js'; +import { test, expect } from './fixtures/docsify-init-fixture.js'; + +test.describe('Embed files', () => { + const routes = { + 'fragment.md': '## Fragment', + }; + + test('embed into homepage', async ({ page }) => { + await docsifyInit({ + routes, + markdown: { + homepage: "# Hello World\n\n[fragment](fragment.md ':include')", + }, + // _logHTML: {}, + }); + + await expect(page.locator('#main')).toContainText('Fragment'); + }); + + test('embed into cover', async ({ page }) => { + await docsifyInit({ + routes, + markdown: { + homepage: '# Hello World', + coverpage: "# Cover\n\n[fragment](fragment.md ':include')", + }, + // _logHTML: {}, + }); + + await expect(page.locator('.cover-main')).toContainText('Fragment'); + }); +}); diff --git a/test/e2e/plugins.test.js b/test/e2e/plugins.test.js index 68534830d..00f31b0ad 100644 --- a/test/e2e/plugins.test.js +++ b/test/e2e/plugins.test.js @@ -158,6 +158,74 @@ test.describe('Plugins', () => { }); }); + test.describe('doneEach()', () => { + test('callback after cover loads', async ({ page }) => { + const consoleMessages = []; + + page.on('console', msg => consoleMessages.push(msg.text())); + + await docsifyInit({ + config: { + plugins: [ + function (hook) { + hook.doneEach(() => { + const homepageTitle = document.querySelector('#homepage-title'); + const coverTitle = document.querySelector('#cover-title'); + console.log(homepageTitle?.textContent); + console.log(coverTitle?.textContent); + }); + }, + ], + }, + markdown: { + homepage: '# Hello World :id=homepage-title', + coverpage: () => { + return new Promise(resolve => { + setTimeout(() => resolve('# Cover Page :id=cover-title'), 500); + }); + }, + }, + // _logHTML: {}, + }); + + await expect(consoleMessages).toEqual(['Hello World', 'Cover Page']); + }); + + test('only cover', async ({ page }) => { + const consoleMessages = []; + + page.on('console', msg => consoleMessages.push(msg.text())); + + await docsifyInit({ + config: { + onlyCover: true, + plugins: [ + function (hook) { + hook.doneEach(() => { + const homepageTitle = document.querySelector('#homepage-title'); + const coverTitle = document.querySelector('#cover-title'); + console.log(homepageTitle?.textContent); + console.log(coverTitle?.textContent); + }); + }, + ], + }, + markdown: { + homepage: '# Hello World :id=homepage-title', + coverpage: () => { + return new Promise(resolve => { + setTimeout(() => resolve('# Cover Page :id=cover-title'), 500); + }); + }, + }, + waitForSelector: '.cover-main > *:first-child', + // _logHTML: {}, + }); + + await expect(consoleMessages).toEqual(['undefined', 'Cover Page']); + }); + }); + test.describe('route data accessible to plugins', () => { let routeData = null; diff --git a/test/helpers/docsify-init.js b/test/helpers/docsify-init.js index bbc357022..6fd4f5d0c 100644 --- a/test/helpers/docsify-init.js +++ b/test/helpers/docsify-init.js @@ -17,11 +17,11 @@ const docsifyURL = '/dist/docsify.js'; // Playwright * @param {Function|Object} [options.config] docsify configuration (merged with default) * @param {String} [options.html] HTML content to use for docsify `index.html` page * @param {Object} [options.markdown] Docsify markdown content - * @param {String} [options.markdown.coverpage] coverpage markdown - * @param {String} [options.markdown.homepage] homepage markdown - * @param {String} [options.markdown.navbar] navbar markdown - * @param {String} [options.markdown.sidebar] sidebar markdown - * @param {Object} [options.routes] custom routes defined as `{ pathOrGlob: responseText }` + * @param {String|(()=>Promise|String)} [options.markdown.coverpage] coverpage markdown + * @param {String|(()=>Promise|String)} [options.markdown.homepage] homepage markdown + * @param {String|(()=>Promise|String)} [options.markdown.navbar] navbar markdown + * @param {String|(()=>Promise|String)} [options.markdown.sidebar] sidebar markdown + * @param {RecordPromise|String)>} [options.routes] custom routes defined as `{ pathOrGlob: response }` * @param {String} [options.script] JS to inject via