From 4ecc34ed2c240648d55f95f044cf22e1e66e539f Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 28 Sep 2026 14:58:06 +0800 Subject: [PATCH 1/2] fix(embed): scope cached includes to their page context --- src/core/render/embed.js | 11 ++++++++--- test/e2e/embed.test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 test/e2e/embed.test.js diff --git a/src/core/render/embed.js b/src/core/render/embed.js index d6044846d..fb5134200 100644 --- a/src/core/render/embed.js +++ b/src/core/render/embed.js @@ -132,7 +132,12 @@ function walkFetchEmbed({ embedTokens, compile, fetch, frontMatter }, cb) { } export function prerenderEmbed({ compiler, raw = '', fetch }, done) { - const hit = cached[raw]; + const cacheKey = JSON.stringify([ + compiler.contentBase, + compiler.router.getCurrentPath(), + raw, + ]); + const hit = cached[cacheKey]; if (hit) { const copy = hit.slice(); copy.links = hit.links; @@ -271,8 +276,8 @@ export function prerenderEmbed({ compiler, raw = '', fetch }, done) { } } } else if (!token) { - cached[raw] = tokens.concat(); - tokens.links = cached[raw].links = links; + cached[cacheKey] = tokens.concat(); + tokens.links = cached[cacheKey].links = links; done(tokens); } }, diff --git a/test/e2e/embed.test.js b/test/e2e/embed.test.js new file mode 100644 index 000000000..4c211786e --- /dev/null +++ b/test/e2e/embed.test.js @@ -0,0 +1,31 @@ +import docsifyInit from '../helpers/docsify-init.js'; +import { test, expect } from './fixtures/docsify-init-fixture.js'; + +test('resolves identical includes relative to each page directory', async ({ + page, +}) => { + const markdown = '# Example\n\n[snippet](snippet.js ":include :type=code")'; + + await docsifyInit({ + markdown: { + homepage: '# Home', + sidebar: '- [First](first/)\n- [Second](second/)', + }, + routes: { + '/first/README.md': markdown, + '/second/README.md': markdown, + '/first/snippet.js': 'const example = "first";', + '/second/snippet.js': 'const example = "second";', + }, + }); + + const sidebar = page.locator('.sidebar-nav'); + const code = page.locator('#main pre code'); + + await sidebar.getByRole('link', { name: 'First', exact: true }).click(); + await expect(code).toHaveText('const example = "first";'); + await sidebar.getByRole('link', { name: 'Second', exact: true }).click(); + await expect(code).toHaveText('const example = "second";'); + await sidebar.getByRole('link', { name: 'First', exact: true }).click(); + await expect(code).toHaveText('const example = "first";'); +}); From 923280a1900619c72875d1719d86e6165cb6ca35 Mon Sep 17 00:00:00 2001 From: Luffy Date: Fri, 13 Nov 2026 12:46:38 +0800 Subject: [PATCH 2/2] perf(embed): cache prerendered content by directory and raw Markdown --- src/core/render/embed.js | 25 ++++++++++++++--------- test/unit/embed-cache.test.js | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 test/unit/embed-cache.test.js diff --git a/src/core/render/embed.js b/src/core/render/embed.js index fb5134200..13151ec2e 100644 --- a/src/core/render/embed.js +++ b/src/core/render/embed.js @@ -1,7 +1,8 @@ import { stripIndent } from 'common-tags'; +import { getParentPath, getPath } from '../router/util.js'; import { get } from '../util/ajax.js'; -const cached = {}; +const cached = new Map(); /** * Extracts the content between matching fragment markers in the text. @@ -132,14 +133,20 @@ function walkFetchEmbed({ embedTokens, compile, fetch, frontMatter }, cb) { } export function prerenderEmbed({ compiler, raw = '', fetch }, done) { - const cacheKey = JSON.stringify([ + // Match the base used by compileEmbed to resolve relative includes. + const contentDirectory = getPath( compiler.contentBase, - compiler.router.getCurrentPath(), - raw, - ]); - const hit = cached[cacheKey]; + getParentPath(compiler.router.getCurrentPath()), + ); + let directoryCache = cached.get(contentDirectory); + if (!directoryCache) { + directoryCache = new Map(); + cached.set(contentDirectory, directoryCache); + } + + const hit = directoryCache.get(raw); if (hit) { - const copy = hit.slice(); + const copy = hit.tokens.slice(); copy.links = hit.links; return done(copy); } @@ -276,8 +283,8 @@ export function prerenderEmbed({ compiler, raw = '', fetch }, done) { } } } else if (!token) { - cached[cacheKey] = tokens.concat(); - tokens.links = cached[cacheKey].links = links; + directoryCache.set(raw, { tokens: tokens.concat(), links }); + tokens.links = links; done(tokens); } }, diff --git a/test/unit/embed-cache.test.js b/test/unit/embed-cache.test.js new file mode 100644 index 000000000..1b9ee40ee --- /dev/null +++ b/test/unit/embed-cache.test.js @@ -0,0 +1,37 @@ +import { prerenderEmbed } from '../../src/core/render/embed.js'; + +test('reuses prerendered tokens across anchors without mixing content directories', () => { + let currentPath = '/first/?id=one'; + let lexerCalls = 0; + const compiler = { + contentBase: '/docs/', + router: { getCurrentPath: () => currentPath }, + config: {}, + _marked: { + lexer: () => { + lexerCalls += 1; + return Object.assign([], { links: {} }); + }, + }, + }; + const render = target => + prerenderEmbed({ compiler: target, raw: '# Example' }, () => {}); + + render(compiler); + currentPath = '/first/?id=two'; + render(compiler); + currentPath = '/first/page'; + render(compiler); + expect(lexerCalls).toBe(1); + + currentPath = '/second/'; + render(compiler); + expect(lexerCalls).toBe(2); + + currentPath = '/first/'; + render(compiler); + expect(lexerCalls).toBe(2); + + render({ ...compiler, contentBase: '/other/' }); + expect(lexerCalls).toBe(3); +});