diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index 754f3da5a13..d87c3aa27de 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -51,6 +51,7 @@ All changes included in 1.11: - ([#14996](https://github.com/quarto-dev/quarto-cli/pull/14996)): Fix `controls: auto` and `previewLinks: auto` set at the top level of a revealjs document's front matter producing a presentation that fails to load, because `auto` was written unquoted into the generated JavaScript. - ([#15007](https://github.com/quarto-dev/quarto-cli/pull/15007)): Fix Home/End on a focused tabset tab also jumping to the first or last slide in addition to switching tabs. +- ([#15014](https://github.com/quarto-dev/quarto-cli/issues/15014)): Fix code blocks at the end of a callout losing their bottom spacing, which made the code block border touch the callout border in untitled `appearance="simple"` callouts (regression introduced in v1.8.15). ### `typst` diff --git a/src/resources/formats/revealjs/quarto.scss b/src/resources/formats/revealjs/quarto.scss index a09f78152a5..2fbda00a504 100644 --- a/src/resources/formats/revealjs/quarto.scss +++ b/src/resources/formats/revealjs/quarto.scss @@ -920,7 +920,7 @@ kbd { > :last-child { margin-bottom: var(--r-block-margin); } - > :last-child:not(div.sourceCode) { + > :last-child:not(div.sourceCode, div.code-copy-outer-scaffold) { padding-bottom: 0.5rem; margin-bottom: 0; } @@ -970,9 +970,11 @@ kbd { } .callout-body { > .callout-content { - // Margin needs to be added when last child is div.sourceCode - // Other code cell border is mixed with callout border - > div.sourceCode:last-child { + // Margin needs to be added when the last child is a code block + // (div.sourceCode, or the code-copy scaffold wrapping it). + // Otherwise the code block border merges with the callout border. + > div.sourceCode:last-child, + > div.code-copy-outer-scaffold:last-child { margin-bottom: 1rem; } > :first-child { diff --git a/tests/docs/playwright/revealjs/callouts/callout-code-last.qmd b/tests/docs/playwright/revealjs/callouts/callout-code-last.qmd new file mode 100644 index 00000000000..347d82a7887 --- /dev/null +++ b/tests/docs/playwright/revealjs/callouts/callout-code-last.qmd @@ -0,0 +1,54 @@ +--- +title: callout ending with a code block +format: revealjs +--- + +## Default titled, ends with code + +::: {#default-titled-code .callout-note title="Titled"} +Some text before the code. + +```python +1 + 1 +``` +::: + +## Simple untitled, ends with code + +::: {#simple-untitled-code .callout-note appearance="simple"} +Some text before the code. + +```python +1 + 1 +``` +::: + +## Simple titled, ends with code + +::: {#simple-titled-code .callout-note appearance="simple" title="Titled"} +Some text before the code. + +```python +1 + 1 +``` +::: + +## Default titled, ends with paragraph + +::: {#default-titled-paragraph .callout-note title="Titled"} +```python +1 + 1 +``` + +Some text after the code. +::: + +## Simple untitled, ends with paragraph + +::: {#simple-untitled-paragraph .callout-note appearance="simple"} +```python +1 + 1 +``` + +Some text after the code. +::: diff --git a/tests/integration/playwright/tests/revealjs-callouts.spec.ts b/tests/integration/playwright/tests/revealjs-callouts.spec.ts new file mode 100644 index 00000000000..29b21044e95 --- /dev/null +++ b/tests/integration/playwright/tests/revealjs-callouts.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from '@playwright/test'; +import { expectCloseTo } from '../src/utils'; + +// Space below the last block of a callout, in CSS px, for callouts whose last +// block is a code block (wrapped in the code-copy scaffold) or a paragraph. +// Reveal scales the slide, so the measured gap is divided by Reveal.getScale() +// and the callout's own bottom border is subtracted. +// https://github.com/quarto-dev/quarto-cli/issues/15014 +type ExpectedGap = '--r-block-margin' | '1rem' | 0; + +interface CalloutCase { + name: string; + slide: string; + callout: string; + lastBlock: string; + expectedGap: ExpectedGap; +} + +const testCases: CalloutCase[] = [ + { + name: 'default titled callout ending with code', + slide: 'default-titled-ends-with-code', + callout: 'default-titled-code', + lastBlock: 'div.sourceCode', + expectedGap: '--r-block-margin', + }, + { + name: 'simple untitled callout ending with code', + slide: 'simple-untitled-ends-with-code', + callout: 'simple-untitled-code', + lastBlock: 'div.sourceCode', + expectedGap: '1rem', + }, + { + name: 'simple titled callout ending with code', + slide: 'simple-titled-ends-with-code', + callout: 'simple-titled-code', + lastBlock: 'div.sourceCode', + expectedGap: '--r-block-margin', + }, + { + name: 'default titled callout ending with paragraph', + slide: 'default-titled-ends-with-paragraph', + callout: 'default-titled-paragraph', + lastBlock: '.callout-content > p:last-child', + expectedGap: 0, + }, + { + name: 'simple untitled callout ending with paragraph', + slide: 'simple-untitled-ends-with-paragraph', + callout: 'simple-untitled-paragraph', + lastBlock: '.callout-content > p:last-child', + expectedGap: '--r-block-margin', + }, +]; + +test.describe('Revealjs callout bottom spacing', () => { + for (const { name, slide, callout, lastBlock, expectedGap } of testCases) { + test(`${name} has the expected space below the last block`, async ({ page }) => { + await page.goto(`./revealjs/callouts/callout-code-last.html#/${slide}`); + // Quarto puts the id on a wrapper div and on div.callout; target the one with the border. + const calloutBox = page.locator(`div.callout#${callout}`); + await expect(calloutBox).toBeVisible(); + // Reveal adds .ready once layout and scaling are done. + await expect(page.locator('.reveal.ready')).toBeAttached(); + + const { gap, expected } = await calloutBox.evaluate((el, { lastBlock, expectedGap }) => { + // Resolve a CSS length to px in the page. + const resolve = (value: string) => { + const probe = document.createElement('div'); + probe.style.cssText = `position:absolute;visibility:hidden;margin-bottom:${value}`; + document.body.appendChild(probe); + const px = parseFloat(getComputedStyle(probe).marginBottom); + probe.remove(); + return px; + }; + const block = el.querySelector(lastBlock) as HTMLElement; + const scale = (window as any).Reveal.getScale(); + const borderBottom = parseFloat(getComputedStyle(el).borderBottomWidth); + const rawGap = el.getBoundingClientRect().bottom - block.getBoundingClientRect().bottom; + const expected = expectedGap === 0 + ? 0 + : resolve(expectedGap === '1rem' ? '1rem' : `var(${expectedGap})`); + return { gap: rawGap / scale - borderBottom, expected }; + }, { lastBlock, expectedGap }); + + expectCloseTo(gap, expected, 1); + }); + } +});