Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
37 changes: 23 additions & 14 deletions src/core/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
}

Expand All @@ -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();
});
}
});
}

/**
Expand Down
19 changes: 13 additions & 6 deletions src/core/render/compiler.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 || {};

Expand All @@ -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;

Expand All @@ -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);
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down
21 changes: 17 additions & 4 deletions src/core/render/embed.js
Original file line number Diff line number Diff line change
@@ -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 = {};

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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') {
Expand All @@ -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 {
Expand Down
122 changes: 42 additions & 80 deletions src/core/render/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is now unused in the PR. Is the feature relating to that now deleted? If so, we need to fix that.

Also there are type errors I'm fixing (my recent changes added type checking to the code base).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the error, a method moved to the utils file.

Now the only thing failing is snapshots for the cover page. The diff shows this failure:

Screenshot 2025-12-05 at 7 01 30 PM

Looks like a couple classes are missing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the visual difference:

Before:

Screenshot 2025-12-05 at 7 05 51 PM

After (broken):

Screenshot 2025-12-05 at 7 05 49 PM

Maybe I missed something when merging in develop?


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(
'<p><img.*?data-origin="(.*?)".*?alt="(.*?)"[^>]*?>([^<]*?)</p>$',
);

let mdCoverBg;
.match('<p><img.*?data-origin="(.*?)"[^a]+alt="(.*?)">([^<]*?)</p>$');

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this gradient background code is removed. Need to restore it. @YiiGuxing would you mind restoring the random gradient?


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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YiiGuxing I think we should update the hooks in a backwards compatible way, such that the hook can somehow see which content it is called for. For example it can see string values like "cover", "sidebar", etc. The default (right now no sort of value to determine the target of the hook) would work the same as before for anyone who hasn't updated their plugin code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ultimate result should be, that all markdown files are handled the same, regardless where in the app they are rendered (cover, sidebar, nav, etc) but any plugin can easily decide which to handle.

Something like

hook.doneEach((section) => {
  if (section === 'cover') {
    // run logic after cover rendered
  } else if (section === 'sidebar') {
    // run logic after sidebar rendered
  } else if (section === 'nav') {
    // run logic after nav bar rendered
  } else if (section === 'main') {
    // run logic after main content rendered (new way)
  } else {
    // run logic after main content rendered (old way, fallback)
  }
})

or similar.

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() {
Expand Down
5 changes: 3 additions & 2 deletions src/core/util/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions test/e2e/embed-files.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading