diff --git a/src/core/render/embed.js b/src/core/render/embed.js index d6044846d..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,9 +133,20 @@ function walkFetchEmbed({ embedTokens, compile, fetch, frontMatter }, cb) { } export function prerenderEmbed({ compiler, raw = '', fetch }, done) { - const hit = cached[raw]; + // Match the base used by compileEmbed to resolve relative includes. + const contentDirectory = getPath( + compiler.contentBase, + 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); } @@ -271,8 +283,8 @@ export function prerenderEmbed({ compiler, raw = '', fetch }, done) { } } } else if (!token) { - cached[raw] = tokens.concat(); - tokens.links = cached[raw].links = links; + directoryCache.set(raw, { tokens: tokens.concat(), links }); + tokens.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";'); +}); 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); +});