From f2d5b3d7c1a07a7e419707db149bac202d965703 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 12 Oct 2026 17:47:22 +0200 Subject: [PATCH] Restore bottom spacing of code blocks ending a revealjs callout Since 1.8.15 the HTML postprocessor wraps code blocks in div.code-copy-outer-scaffold for the copy button, and revealjs uses it. The last child of .callout-content is then the scaffold, not div.sourceCode, so the two last-child rules written for a code block in last position stopped matching: - titled callouts: the scaffold was not excluded from the paragraph rule, so it got 0.5rem padding and no margin instead of the block margin (8px instead of 12px below the code); - simple untitled callouts: the 1rem margin rule never matched, so the code block border touched the callout border. Both selectors now also match the scaffold. The margin goes on the scaffold itself: it has no padding or border, so this reproduces the 1.7.34 layout exactly. The :not() selector list keeps the rule's specificity unchanged. The plain div.sourceCode selectors stay for code-copy: false, where there is no scaffold. The Playwright test measures the gap below the last block, normalized by Reveal.getScale() and the callout border, for three callouts ending with code and two ending with a paragraph as controls. Fixes #15014 --- news/changelog-1.11.md | 1 + src/resources/formats/revealjs/quarto.scss | 10 ++- .../revealjs/callouts/callout-code-last.qmd | 54 +++++++++++ .../tests/revealjs-callouts.spec.ts | 90 +++++++++++++++++++ 4 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 tests/docs/playwright/revealjs/callouts/callout-code-last.qmd create mode 100644 tests/integration/playwright/tests/revealjs-callouts.spec.ts 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); + }); + } +});