diff --git a/src/core/render/compiler/blockquote.js b/src/core/render/compiler/blockquote.js index 2aab79beb..0b73c6991 100644 --- a/src/core/render/compiler/blockquote.js +++ b/src/core/render/compiler/blockquote.js @@ -15,12 +15,18 @@ export const blockquoteCompiler = ({ renderer, compiler }) => const calloutMark = calloutData[1]; // "[!TIP]" const calloutType = calloutData[2].toLowerCase(); // "tip" + // Avoid mutating tokens that may be reused from the Prerender cache. + tokens = tokens.slice(); + const paragraph = { ...firstParagraph }; + if (firstParagraph.tokens) { + paragraph.tokens = firstParagraph.tokens.map(t => ({ ...t })); + } + tokens[firstParagraphIndex] = paragraph; + // Remove the callout mark from the paragraph raw text - firstParagraph.raw = firstParagraph.raw - .replace(calloutMark, '') - .trimStart(); - if (firstParagraph.tokens && firstParagraph.tokens.length > 0) { - firstParagraph.tokens.forEach(t => { + paragraph.raw = paragraph.raw.replace(calloutMark, '').trimStart(); + if (paragraph.tokens && paragraph.tokens.length > 0) { + paragraph.tokens.forEach(t => { if (t.raw) { t.raw = t.raw.replace(calloutMark, ''); } @@ -31,7 +37,7 @@ export const blockquoteCompiler = ({ renderer, compiler }) => } // If the first paragraph is now empty after removing [!TIP], remove it - if (!firstParagraph.raw.trim()) { + if (!paragraph.raw.trim()) { tokens.splice(firstParagraphIndex, 1); } diff --git a/test/integration/callouts.test.js b/test/integration/callouts.test.js new file mode 100644 index 000000000..da6fee199 --- /dev/null +++ b/test/integration/callouts.test.js @@ -0,0 +1,45 @@ +import { waitForFunction, waitForText } from '../helpers/wait-for.js'; +import docsifyInit from '../helpers/docsify-init.js'; + +describe('callouts', () => { + test('render fully after returning to a previously visited page', async () => { + const calloutText = 'This callout should remain fully rendered.'; + + await docsifyInit({ + testURL: '/docsify-init.html#/custom-navbar', + markdown: { + sidebar: ` + - [Custom Navbar](custom-navbar) + - [Configuration](configuration) + `, + }, + routes: { + 'custom-navbar.md': ` + > [!IMPORTANT] + > ${calloutText} + `, + 'configuration.md': '# Configuration', + }, + }); + + expect(await waitForText('#main', calloutText)).toBeTruthy(); + expect(document.querySelector('#main .callout.important')).toBeTruthy(); + + document.querySelector('a[href="#/configuration"]').click(); + expect( + await waitForFunction(() => + /#\/configuration$/.test(window.location.href), + ), + ).toBeTruthy(); + expect(await waitForText('#main', 'Configuration')).toBeTruthy(); + + document.querySelector('a[href="#/custom-navbar"]').click(); + expect( + await waitForFunction(() => + /#\/custom-navbar$/.test(window.location.href), + ), + ).toBeTruthy(); + expect(await waitForText('#main', calloutText)).toBeTruthy(); + expect(document.querySelector('#main .callout.important')).toBeTruthy(); + }); +});