From b5e53db60dc7c4ab20496d4e738a1fcf5da209aa Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 7 Oct 2026 15:07:15 +0200 Subject: [PATCH 1/4] Add playwright test for copy button covering annotation marker Since the code-copy scaffold moved the copy button out of the pre, the rule that shifted it clear of the annotation gutter no longer matches, so the button sits on top of the marker and swallows its hover. The test checks that the marker is the element under its own center and that the annotation tooltip opens. It fails in chromium, firefox and webkit until the CSS is fixed. Refs #13444 --- .../html/code-annotation-copy-button.qmd | 11 ++++++ .../html-code-annotation-copy-button.spec.ts | 35 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/docs/playwright/html/code-annotation-copy-button.qmd create mode 100644 tests/integration/playwright/tests/html-code-annotation-copy-button.spec.ts diff --git a/tests/docs/playwright/html/code-annotation-copy-button.qmd b/tests/docs/playwright/html/code-annotation-copy-button.qmd new file mode 100644 index 0000000000..832f7cda52 --- /dev/null +++ b/tests/docs/playwright/html/code-annotation-copy-button.qmd @@ -0,0 +1,11 @@ +--- +title: "Code copy button and code annotation marker" +format: html +code-annotations: hover +--- + +```{.python} +import polars as pl # <1> +``` + +1. This is a code block with a copy button and an annotation. diff --git a/tests/integration/playwright/tests/html-code-annotation-copy-button.spec.ts b/tests/integration/playwright/tests/html-code-annotation-copy-button.spec.ts new file mode 100644 index 0000000000..e7c975e345 --- /dev/null +++ b/tests/integration/playwright/tests/html-code-annotation-copy-button.spec.ts @@ -0,0 +1,35 @@ +import { expect, test } from "@playwright/test"; +import { getUrl } from "../src/utils"; + +// Regression test for #13444. The code copy button sits in the top-right +// corner of the code block, where the annotation gutter shows the marker of +// an annotated first line. The button must stay clear of the gutter so the +// marker can still be hovered to show its annotation. +test("code copy button does not cover the code annotation marker", async ({ + page, +}) => { + await page.goto(getUrl("html/code-annotation-copy-button.html"), { + waitUntil: "load", + }); + + const anchor = page.locator("button.code-annotation-anchor"); + await expect(anchor).toHaveCount(1); + const box = await anchor.boundingBox(); + expect(box).not.toBeNull(); + const x = box!.x + box!.width / 2; + const y = box!.y + box!.height / 2; + + // Hovering the code block reveals the copy button, so check the marker is + // still the top-most element at its own center while the button is shown. + await page.mouse.move(x, y); + const hit = await page.evaluate( + ([px, py]) => + document.elementFromPoint(px, py)?.closest( + ".code-annotation-anchor, .code-copy-button", + )?.className ?? null, + [x, y], + ); + expect(hit).toContain("code-annotation-anchor"); + + await expect(page.locator(".code-annotation-tip-content")).toBeVisible(); +}); From 6f4faf6bd21bbf37e41fe7cf29c184de637cdb8b Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 21 Oct 2026 17:08:58 +0200 Subject: [PATCH 2/4] Keep the code copy button clear of the annotation gutter Since the copy button moved out of the pre into the outer scaffold, the rule that shifted it left of the annotation gutter matched nothing, so the button sat on top of the marker of an annotated first line and swallowed its hover or click. The scaffold stays: it keeps the button from scrolling with the code while line numbers need visible overflow. The rule now targets the button through the scaffold that holds an annotated pre. Its margin can no longer be 1.25em: that relied on the button inheriting the code font size, and as a sibling of div.sourceCode its em is now the body size. The margin is the gutter width, calc(20px + 0.5em) in code font size, rewritten as 0.5 times $code-block-font-size so it holds when a theme changes that size, plus the default 5px. The button and the pre resolve em from the same parent, so the two stay in step. The test now also covers select mode, a theme with a 0.75em code block font size, and checks that the button of a block without annotations keeps its default position. Fixes #13444 --- news/changelog-1.11.md | 1 + .../html/bootstrap/_bootstrap-rules.scss | 7 +- .../code-annotation-copy-button-font-size.qmd | 13 +++ ...code-annotation-copy-button-font-size.scss | 2 + .../code-annotation-copy-button-select.qmd | 11 ++ .../html/code-annotation-copy-button.qmd | 6 + .../html-code-annotation-copy-button.spec.ts | 104 +++++++++++++----- 7 files changed, 115 insertions(+), 29 deletions(-) create mode 100644 tests/docs/playwright/html/code-annotation-copy-button-font-size.qmd create mode 100644 tests/docs/playwright/html/code-annotation-copy-button-font-size.scss create mode 100644 tests/docs/playwright/html/code-annotation-copy-button-select.qmd diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index c843b730a2..5411e38afa 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -2,6 +2,7 @@ All changes included in 1.11: ## Regression fixes +- ([#13444](https://github.com/quarto-dev/quarto-cli/issues/13444)): Fix the code copy button in `html` covering the code annotation marker on the first line of an annotated code block, which stopped the annotation from showing on hover or click (regression introduced in v1.8.15). - ([#14920](https://github.com/quarto-dev/quarto-cli/issues/14920)): Fix Windows ARM diagnostics for x64 R crashes to preserve the specialized error in `quarto check`, JSON output, and render failures, and update the native ARM64 R installation guidance. - ([#14731](https://github.com/quarto-dev/quarto-cli/pull/14731)): Fix `QUARTO_VENDOR_BINARIES=false` builds silently succeeding with missing binaries. The `configure` step's "is it on PATH?" check for `typst-gather`, `typst`, `pandoc` and `esbuild` compared an unawaited `Promise` against `undefined`, so the guard never fired. A build with `typst-gather` neither vendored nor on PATH reported success and produced a Quarto tree without it, surfacing only at render time as `typst-gather analyze failed; staging all packages as fallback`. The missing-binary error message now also names the path that was checked and the `QUARTO_TYPST_GATHER` environment variable. - ([#14741](https://github.com/quarto-dev/quarto-cli/issues/14741)): Fix PDF rendering of a cross-referenced table failing with `! Extra }, or forgotten \endgroup.` when the `endfloat` package defers `longtable` environments (`\DeclareDelayedFloatFlavor*{longtable}{table}`). diff --git a/src/resources/formats/html/bootstrap/_bootstrap-rules.scss b/src/resources/formats/html/bootstrap/_bootstrap-rules.scss index 06db8a53d8..3e1e9667eb 100644 --- a/src/resources/formats/html/bootstrap/_bootstrap-rules.scss +++ b/src/resources/formats/html/bootstrap/_bootstrap-rules.scss @@ -1997,8 +1997,11 @@ textarea { // Code Annotation -.code-annotated .code-copy-button { - margin-right: 1.25em; +// The copy button is a sibling of div.sourceCode, so its em is not the code +// font size. Clear the gutter (calc(20px + 0.5em) in code font size) plus the +// default 5px margin. +.code-copy-outer-scaffold:has(pre.code-annotated) > .code-copy-button { + margin-right: calc(20px + 0.5 * #{$code-block-font-size} + 5px); margin-top: 0; padding-bottom: 0; padding-top: 3px; diff --git a/tests/docs/playwright/html/code-annotation-copy-button-font-size.qmd b/tests/docs/playwright/html/code-annotation-copy-button-font-size.qmd new file mode 100644 index 0000000000..091cf1c5bd --- /dev/null +++ b/tests/docs/playwright/html/code-annotation-copy-button-font-size.qmd @@ -0,0 +1,13 @@ +--- +title: "Code copy button and code annotation marker (code block font size)" +format: + html: + theme: [default, code-annotation-copy-button-font-size.scss] +code-annotations: hover +--- + +```{.python} +import polars as pl # <1> +``` + +1. This is a code block with a copy button and an annotation. diff --git a/tests/docs/playwright/html/code-annotation-copy-button-font-size.scss b/tests/docs/playwright/html/code-annotation-copy-button-font-size.scss new file mode 100644 index 0000000000..e881e9c2a3 --- /dev/null +++ b/tests/docs/playwright/html/code-annotation-copy-button-font-size.scss @@ -0,0 +1,2 @@ +/*-- scss:defaults --*/ +$code-block-font-size: 0.75em; diff --git a/tests/docs/playwright/html/code-annotation-copy-button-select.qmd b/tests/docs/playwright/html/code-annotation-copy-button-select.qmd new file mode 100644 index 0000000000..ab0331c2d3 --- /dev/null +++ b/tests/docs/playwright/html/code-annotation-copy-button-select.qmd @@ -0,0 +1,11 @@ +--- +title: "Code copy button and code annotation marker (select)" +format: html +code-annotations: select +--- + +```{.python} +import polars as pl # <1> +``` + +1. This is a code block with a copy button and an annotation. diff --git a/tests/docs/playwright/html/code-annotation-copy-button.qmd b/tests/docs/playwright/html/code-annotation-copy-button.qmd index 832f7cda52..9bb02b760e 100644 --- a/tests/docs/playwright/html/code-annotation-copy-button.qmd +++ b/tests/docs/playwright/html/code-annotation-copy-button.qmd @@ -9,3 +9,9 @@ import polars as pl # <1> ``` 1. This is a code block with a copy button and an annotation. + +A code block without annotations keeps the copy button in its default place. + +```{.python} +import polars as pl +``` diff --git a/tests/integration/playwright/tests/html-code-annotation-copy-button.spec.ts b/tests/integration/playwright/tests/html-code-annotation-copy-button.spec.ts index e7c975e345..7965dded9e 100644 --- a/tests/integration/playwright/tests/html-code-annotation-copy-button.spec.ts +++ b/tests/integration/playwright/tests/html-code-annotation-copy-button.spec.ts @@ -1,35 +1,85 @@ -import { expect, test } from "@playwright/test"; +import { expect, Locator, test } from "@playwright/test"; import { getUrl } from "../src/utils"; // Regression test for #13444. The code copy button sits in the top-right // corner of the code block, where the annotation gutter shows the marker of // an annotated first line. The button must stay clear of the gutter so the -// marker can still be hovered to show its annotation. -test("code copy button does not cover the code annotation marker", async ({ - page, -}) => { - await page.goto(getUrl("html/code-annotation-copy-button.html"), { - waitUntil: "load", - }); +// marker can still be hovered (or clicked) to show its annotation. - const anchor = page.locator("button.code-annotation-anchor"); - await expect(anchor).toHaveCount(1); - const box = await anchor.boundingBox(); +const rightEdge = async (locator: Locator) => { + const box = await locator.boundingBox(); expect(box).not.toBeNull(); - const x = box!.x + box!.width / 2; - const y = box!.y + box!.height / 2; - - // Hovering the code block reveals the copy button, so check the marker is - // still the top-most element at its own center while the button is shown. - await page.mouse.move(x, y); - const hit = await page.evaluate( - ([px, py]) => - document.elementFromPoint(px, py)?.closest( - ".code-annotation-anchor, .code-copy-button", - )?.className ?? null, - [x, y], - ); - expect(hit).toContain("code-annotation-anchor"); - - await expect(page.locator(".code-annotation-tip-content")).toBeVisible(); + return box!.x + box!.width; +}; + +const testCases = [ + { name: "hover", url: "html/code-annotation-copy-button.html" }, + { name: "select", url: "html/code-annotation-copy-button-select.html" }, + { + name: "hover with a custom code block font size", + url: "html/code-annotation-copy-button-font-size.html", + }, +]; + +test.describe("code copy button and code annotation marker", () => { + for (const { name, url } of testCases) { + test(`${name} — copy button does not cover the marker`, async ({ page }) => { + await page.goto(getUrl(url), { waitUntil: "load" }); + + const scaffold = page.locator(".code-copy-outer-scaffold").filter({ + has: page.locator("pre.code-annotated"), + }); + const anchor = scaffold.locator("button.code-annotation-anchor"); + await expect(anchor).toHaveCount(1); + const box = await anchor.boundingBox(); + expect(box).not.toBeNull(); + const x = box!.x + box!.width / 2; + const y = box!.y + box!.height / 2; + + // Hovering the code block reveals the copy button, so check the marker + // is still the top-most element at its own center while the button is + // shown. + await page.mouse.move(x, y); + const hit = await page.evaluate( + ([px, py]) => + document.elementFromPoint(px, py)?.closest( + ".code-annotation-anchor, .code-copy-button", + )?.className ?? null, + [x, y], + ); + expect(hit).toContain("code-annotation-anchor"); + + // The button sits the default 5px left of the gutter, whatever the code + // block font size the gutter width is derived from. + const gutterLeft = (await scaffold.locator(".code-annotation-gutter") + .boundingBox())!.x; + const buttonRight = await rightEdge( + scaffold.locator(".code-copy-button"), + ); + expect(Math.abs(gutterLeft - buttonRight - 5)).toBeLessThanOrEqual(1); + + if (name === "select") { + await page.mouse.click(x, y); + } + await expect(page.locator(".code-annotation-tip-content")).toBeVisible(); + }); + } + + test("copy button of a block without annotations is unchanged", async ({ + page, + }) => { + await page.goto(getUrl("html/code-annotation-copy-button.html"), { + waitUntil: "load", + }); + + const scaffold = page.locator(".code-copy-outer-scaffold").filter({ + hasNot: page.locator("pre.code-annotated"), + }); + await expect(scaffold).toHaveCount(1); + const button = scaffold.locator(".code-copy-button"); + await expect(button).toHaveCSS("right", "0px"); + await expect(button).toHaveCSS("margin-right", "5px"); + expect((await rightEdge(scaffold)) - (await rightEdge(button))) + .toBeCloseTo(5, 0); + }); }); From 69fce0385caf4665074c0da0ff4846bb6c729e76 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 21 Oct 2026 17:30:37 +0200 Subject: [PATCH 3/4] Move #13444 changelog entry to the html section The fix is not backported to v1.10, and on main the Regression fixes section marks entries that are. --- news/changelog-1.11.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index 5411e38afa..b16e5035d9 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -2,7 +2,6 @@ All changes included in 1.11: ## Regression fixes -- ([#13444](https://github.com/quarto-dev/quarto-cli/issues/13444)): Fix the code copy button in `html` covering the code annotation marker on the first line of an annotated code block, which stopped the annotation from showing on hover or click (regression introduced in v1.8.15). - ([#14920](https://github.com/quarto-dev/quarto-cli/issues/14920)): Fix Windows ARM diagnostics for x64 R crashes to preserve the specialized error in `quarto check`, JSON output, and render failures, and update the native ARM64 R installation guidance. - ([#14731](https://github.com/quarto-dev/quarto-cli/pull/14731)): Fix `QUARTO_VENDOR_BINARIES=false` builds silently succeeding with missing binaries. The `configure` step's "is it on PATH?" check for `typst-gather`, `typst`, `pandoc` and `esbuild` compared an unawaited `Promise` against `undefined`, so the guard never fired. A build with `typst-gather` neither vendored nor on PATH reported success and produced a Quarto tree without it, surfacing only at render time as `typst-gather analyze failed; staging all packages as fallback`. The missing-binary error message now also names the path that was checked and the `QUARTO_TYPST_GATHER` environment variable. - ([#14741](https://github.com/quarto-dev/quarto-cli/issues/14741)): Fix PDF rendering of a cross-referenced table failing with `! Extra }, or forgotten \endgroup.` when the `endfloat` package defers `longtable` environments (`\DeclareDelayedFloatFlavor*{longtable}{table}`). @@ -61,6 +60,7 @@ All changes included in 1.11: - ([#12452](https://github.com/quarto-dev/quarto-cli/issues/12452)): Fix repeated HTML escaping in Google Scholar metadata, including apostrophes in titles, special characters in authors and URLs, and nested reference metadata. - ([#12650](https://github.com/quarto-dev/quarto-cli/issues/12650)): Fix markdown formatting and HTML entities in a document title appearing literally in the appendix citation, the BibTeX citation, and the Google Scholar `citation_title`. (author: @jolars) +- ([#13444](https://github.com/quarto-dev/quarto-cli/issues/13444)): Fix the code copy button covering the code annotation marker on the first line of an annotated code block, which stopped the annotation from showing on hover or click (regression introduced in v1.8.15). - ([#14684](https://github.com/quarto-dev/quarto-cli/issues/14684)): Add a "Skip to main content" link to Bootstrap-themed HTML output (documents, websites, books, dashboards) so keyboard users can bypass the navbar and sidebars. - ([#14973](https://github.com/quarto-dev/quarto-cli/issues/14973)): Fix Safari sometimes opening pages with a light and dark theme at the top instead of at the URL fragment target, and toggling the color scheme scrolling back to the top of the page. - ([#14976](https://github.com/quarto-dev/quarto-cli/pull/14976)): Fix KaTeX math not rendering on pages with Jupyter widgets when `html-math-method` is set with the object form (`method: katex` and `url`). From 4a18e9d4a948a2d0eea739657a01ea2f82acdc95 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 21 Oct 2026 17:49:51 +0200 Subject: [PATCH 4/4] Size the annotated copy button margin in the code font size The margin multiplied $code-block-font-size inside calc(), which only matches the gutter for length units. A percentage, which a theme or a brand typography size can set, resolved against the scaffold width in a margin, and pushed the button about 290px into the code. The button now takes the code block font size itself, so it shares the pre's em, and the margin repeats the gutter's own calc(20px + 0.5em) plus 5px. That holds for any value the pre accepts. The copy icon is sized in rem, so it keeps its size. --- .../formats/html/bootstrap/_bootstrap-rules.scss | 9 +++++---- ...ode-annotation-copy-button-font-size-percent.qmd | 13 +++++++++++++ ...de-annotation-copy-button-font-size-percent.scss | 2 ++ .../tests/html-code-annotation-copy-button.spec.ts | 4 ++++ 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 tests/docs/playwright/html/code-annotation-copy-button-font-size-percent.qmd create mode 100644 tests/docs/playwright/html/code-annotation-copy-button-font-size-percent.scss diff --git a/src/resources/formats/html/bootstrap/_bootstrap-rules.scss b/src/resources/formats/html/bootstrap/_bootstrap-rules.scss index 3e1e9667eb..97b0e374e1 100644 --- a/src/resources/formats/html/bootstrap/_bootstrap-rules.scss +++ b/src/resources/formats/html/bootstrap/_bootstrap-rules.scss @@ -1997,11 +1997,12 @@ textarea { // Code Annotation -// The copy button is a sibling of div.sourceCode, so its em is not the code -// font size. Clear the gutter (calc(20px + 0.5em) in code font size) plus the -// default 5px margin. +// The copy button is a sibling of div.sourceCode, so it takes the code font +// size here to share the gutter's em, then clears the gutter +// (calc(20px + 0.5em)) plus the default 5px margin. Its icon is sized in rem. .code-copy-outer-scaffold:has(pre.code-annotated) > .code-copy-button { - margin-right: calc(20px + 0.5 * #{$code-block-font-size} + 5px); + font-size: $code-block-font-size; + margin-right: calc(20px + 0.5em + 5px); margin-top: 0; padding-bottom: 0; padding-top: 3px; diff --git a/tests/docs/playwright/html/code-annotation-copy-button-font-size-percent.qmd b/tests/docs/playwright/html/code-annotation-copy-button-font-size-percent.qmd new file mode 100644 index 0000000000..ab1db376fa --- /dev/null +++ b/tests/docs/playwright/html/code-annotation-copy-button-font-size-percent.qmd @@ -0,0 +1,13 @@ +--- +title: "Code copy button and code annotation marker (code block font size in percent)" +format: + html: + theme: [default, code-annotation-copy-button-font-size-percent.scss] +code-annotations: hover +--- + +```{.python} +import polars as pl # <1> +``` + +1. This is a code block with a copy button and an annotation. diff --git a/tests/docs/playwright/html/code-annotation-copy-button-font-size-percent.scss b/tests/docs/playwright/html/code-annotation-copy-button-font-size-percent.scss new file mode 100644 index 0000000000..5962b76d06 --- /dev/null +++ b/tests/docs/playwright/html/code-annotation-copy-button-font-size-percent.scss @@ -0,0 +1,2 @@ +/*-- scss:defaults --*/ +$code-block-font-size: 75%; diff --git a/tests/integration/playwright/tests/html-code-annotation-copy-button.spec.ts b/tests/integration/playwright/tests/html-code-annotation-copy-button.spec.ts index 7965dded9e..bfdd3dd60e 100644 --- a/tests/integration/playwright/tests/html-code-annotation-copy-button.spec.ts +++ b/tests/integration/playwright/tests/html-code-annotation-copy-button.spec.ts @@ -19,6 +19,10 @@ const testCases = [ name: "hover with a custom code block font size", url: "html/code-annotation-copy-button-font-size.html", }, + { + name: "hover with a code block font size in percent", + url: "html/code-annotation-copy-button-font-size-percent.html", + }, ]; test.describe("code copy button and code annotation marker", () => {