diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js
index 3dcc0caf9d..a276391bed 100644
--- a/src/core/fetch/index.js
+++ b/src/core/fetch/index.js
@@ -186,7 +186,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);
@@ -206,17 +206,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);
}
}
@@ -226,16 +235,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();
+ });
+ }
+ });
}
/**
diff --git a/src/core/render/compiler.js b/src/core/render/compiler.js
index ef15709108..fbb542aed7 100644
--- a/src/core/render/compiler.js
+++ b/src/core/render/compiler.js
@@ -1,6 +1,7 @@
import { marked } from 'marked';
+/** @import {TokensList, Marked} from 'marked' */
import { isAbsolutePath, getPath, getParentPath } from '../router/util.js';
-import { isFn, cached, isPrimitive } from '../util/core.js';
+import { isFn, cached } from '../util/core.js';
import { tree as treeTpl } from './tpl.js';
import { genTree } from './gen-tree.js';
import { slugify } from './slugify.js';
@@ -32,6 +33,7 @@ export class Compiler {
this.contentBase = router.getBasePath();
this.renderer = this._initRenderer();
+ /** @type {typeof marked & Marked} */
let compile;
const mdConf = config.markdown || {};
@@ -43,10 +45,14 @@ export class Compiler {
renderer: Object.assign(this.renderer, mdConf.renderer),
}),
);
+ // @ts-expect-error FIXME temporary ugly Marked types
compile = marked;
}
+ /** @type {typeof marked & Marked} */
this._marked = compile;
+
+ /** @param {string | TokensList} text */
this.compile = text => {
let isCached = true;
@@ -59,8 +65,8 @@ export class Compiler {
return text;
}
- if (isPrimitive(text)) {
- html = compile(text);
+ if (typeof text === 'string') {
+ html = /** @type {string} */ (compile(text));
} else {
html = compile.parser(text);
}
@@ -113,7 +119,8 @@ export class Compiler {
}
let media;
- if (config.type && (media = compileMedia[config.type])) {
+ const configType = /** @type {string | undefined} */ (config.type);
+ if (configType && (media = compileMedia[configType])) {
embed = media.call(this, href, title);
embed.type = config.type;
} else {
@@ -273,8 +280,8 @@ export class Compiler {
/**
* Compile cover page
- * @param {Text} text Text content
- * @returns {String} Cover page
+ * @param {TokensList} text Text content
+ * @returns {string} Cover page
*/
cover(text) {
const cacheToc = this.toc.slice();
diff --git a/src/core/render/embed.js b/src/core/render/embed.js
index 8a55005341..5e38e35c74 100644
--- a/src/core/render/embed.js
+++ b/src/core/render/embed.js
@@ -1,5 +1,7 @@
import { stripIndent } from 'common-tags';
import { get } from '../util/ajax.js';
+/** @import { Compiler } from '../Docsify.js' */
+/** @import {TokensList} from 'marked' */
const cached = {};
@@ -32,7 +34,7 @@ function extractFragmentContent(text, fragment, fullLine) {
return stripIndent((match || [])[1] || '').trim();
}
-function walkFetchEmbed({ embedTokens, compile, fetch }, cb) {
+function walkFetchEmbed({ embedTokens, compile }, cb) {
let token;
let step = 0;
let count = 0;
@@ -132,7 +134,13 @@ function walkFetchEmbed({ embedTokens, compile, fetch }, cb) {
}
}
-export function prerenderEmbed({ compiler, raw = '', fetch }, done) {
+/**
+ * @param {Object} options
+ * @param {Compiler} options.compiler
+ * @param {string} [options.raw]
+ * @param {Function} done
+ */
+export function prerenderEmbed({ compiler, raw = '' }, done) {
const hit = cached[raw];
if (hit) {
const copy = hit.slice();
@@ -193,7 +201,7 @@ export function prerenderEmbed({ compiler, raw = '', fetch }, done) {
// are returned
const moves = [];
walkFetchEmbed(
- { compile, embedTokens, fetch },
+ { compile, embedTokens },
({ embedToken, token, rowIndex, cellIndex, tokenRef }) => {
if (token) {
if (typeof rowIndex === 'number' && typeof cellIndex === 'number') {
@@ -212,9 +220,14 @@ export function prerenderEmbed({ compiler, raw = '', fetch }, done) {
Object.assign(links, embedToken.links);
+ // FIXME This is an actual code error caught by TypeScript, but
+ // apparently we've not been effected by deleting the `.links` property
+ // yet.
+ // @ts-expect-error
tokens = tokens
- .slice(0, index)
+ .slice(0, index) // This deletes the original .links by returning a new array, so now we have Tokens[] instead of TokensList
.concat(embedToken, tokens.slice(index + 1));
+
moves.push({ start: index, length: embedToken.length - 1 });
}
} else {
diff --git a/src/core/render/index.js b/src/core/render/index.js
index 9e546a3d01..cc2f92ceb6 100644
--- a/src/core/render/index.js
+++ b/src/core/render/index.js
@@ -433,7 +433,6 @@ export function Render(Base) {
{
compiler: /** @type {Compiler} */ (this.compiler),
raw: result,
- fetch: undefined,
},
tokens => {
html = /** @type {Compiler} */ (this.compiler).compile(tokens);
@@ -444,108 +443,71 @@ export function Render(Base) {
});
}
- _renderCover(text, coverOnly) {
- const el = dom.getNode('.cover');
+ _renderCover(text, coverOnly, next) {
+ const el = /** @type {HTMLElement} */ (dom.getNode('.cover'));
const rootElm = document.documentElement;
+ // TODO this is now unused. What did we break?
+ // eslint-disable-next-line no-unused-vars
const coverBg = getComputedStyle(rootElm).getPropertyValue('--cover-bg');
dom.getNode('main').classList[coverOnly ? 'add' : 'remove']('hidden');
if (!text) {
el.classList.remove('show');
+ next();
return;
}
el.classList.add('show');
- let html = this.coverIsHTML
- ? text
- : /** @type {Compiler} */ (this.compiler).cover(text);
-
- if (!coverBg) {
- const mdBgMatch = html
+ const callback = html => {
+ const m = html
.trim()
- .match(
- '
]*?>([^<]*?)
$',
- );
-
- let mdCoverBg;
+ .match('([^<]*?)
$');
- if (mdBgMatch) {
- const [bgMatch, bgValue, bgType] = mdBgMatch;
+ if (m) {
+ if (m[2] === 'color') {
+ el.style.background = m[1] + (m[3] || '');
+ } else {
+ let path = m[1];
- // Color
- if (bgType === 'color') {
- mdCoverBg = bgValue;
- }
- // Image
- else {
- const path = !isAbsolutePath(bgValue)
- ? getPath(this.router.getBasePath(), bgValue)
- : bgValue;
+ el.classList.add('has-mask');
+ if (!isAbsolutePath(m[1])) {
+ path = getPath(this.router.getBasePath(), m[1]);
+ }
- mdCoverBg = `center center / cover url(${path})`;
+ el.style.backgroundImage = `url(${path})`;
+ el.style.backgroundSize = 'cover';
+ el.style.backgroundPosition = 'center center';
}
- html = html.replace(bgMatch, '');
+ html = html.replace(m[0], '');
}
- // Gradient background
- else {
- const degrees = Math.round((Math.random() * 120) / 2);
- let hue1 = Math.round(Math.random() * 360);
- let hue2 = Math.round(Math.random() * 360);
-
- // Ensure hue1 and hue2 are at least 50 degrees apart
- if (Math.abs(hue1 - hue2) < 50) {
- const hueShift = Math.round(Math.random() * 25) + 25;
-
- hue1 = Math.max(hue1, hue2) + hueShift;
- hue2 = Math.min(hue1, hue2) - hueShift;
- }
+ dom.setHTML('.cover-main', html);
+ next();
+ };
- // OKLCH color
- if (window?.CSS?.supports('color', 'oklch(0 0 0 / 1%)')) {
- const l = 90; // Lightness
- const c = 20; // Chroma
-
- // prettier-ignore
- mdCoverBg = `linear-gradient(
- ${degrees}deg,
- oklch(${l}% ${c}% ${hue1}) 0%,
- oklch(${l}% ${c}% ${hue2}) 100%
- )`.replace(/\s+/g, ' ');
- }
- // HSL color (Legacy)
- else {
- const s = 100; // Saturation
- const l = 85; // Lightness
- const o = 100; // Opacity
-
- // prettier-ignore
- mdCoverBg = `linear-gradient(
- ${degrees}deg,
- hsl(${hue1} ${s}% ${l}% / ${o}%) 0%,
- hsl(${hue2} ${s}% ${l}% / ${o}%) 100%
- )`.replace(/\s+/g, ' ');
- }
+ // 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 {
+ const compiler = this.compiler;
+ if (!compiler) {
+ throw new Error('Compiler is not initialized');
}
-
- rootElm.style.setProperty('--cover-bg', mdCoverBg);
+ prerenderEmbed(
+ {
+ compiler,
+ raw: text,
+ },
+ tokens => callback(compiler.cover(tokens)),
+ );
}
-
- dom.setHTML('.cover-main', html);
-
- // Button styles
- dom
- .findAll('.cover-main > p:last-of-type > a:not([class])')
- .forEach(elm => {
- const buttonType = elm.matches(':first-child')
- ? 'primary'
- : 'secondary';
-
- elm.classList.add('button', buttonType);
- });
}
_updateRender() {
diff --git a/src/core/util/core.js b/src/core/util/core.js
index 04f3fbdd12..3291c67dd1 100644
--- a/src/core/util/core.js
+++ b/src/core/util/core.js
@@ -33,9 +33,10 @@ export function isPrimitive(value) {
/**
* Performs no operation.
- * @void
+ * @param {...any} args Any arguments ignored.
+ * @returns {void}
*/
-export function noop() {}
+export function noop(...args) {}
/**
* Check if value is function
diff --git a/test/e2e/embed-files.test.js b/test/e2e/embed-files.test.js
new file mode 100644
index 0000000000..46ae1d1c3a
--- /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 b1e53d729a..7be946aa0b 100644
--- a/test/e2e/plugins.test.js
+++ b/test/e2e/plugins.test.js
@@ -164,6 +164,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 2c7f23933d..59730a2d44 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