From 98da099160f448154749d634350bf3eef0265c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:22:10 +0200 Subject: [PATCH 1/2] fix(html): make code-tools toggle work with code-copy enabled The code-copy scaffold added in 1.8.15 wraps each code block in a div.code-copy-outer-scaffold, which left the "Show All Code" and "Hide All Code" handler matching nothing: its selectors assumed .sourceCode was a direct child of
and of .cell, and it reached the
via .parentElement. Key the selectors off details.code-fold and div.sourceCode.cell-code instead, which the scaffold cannot displace. Apply the same selector to the server-side toggle detection, which until now only worked because that post-processor happens to run before the scaffold is inserted. --- news/changelog-1.10.md | 1 + src/command/render/codetools.ts | 2 +- .../html/templates/quarto-html-after-body.ejs | 8 ++--- .../docs/smoke-all/2026/07/18/issue-13583.qmd | 33 +++++++++++++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 tests/docs/smoke-all/2026/07/18/issue-13583.qmd diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index eae5df492c2..1872f1df15a 100644 --- a/news/changelog-1.10.md +++ b/news/changelog-1.10.md @@ -2,6 +2,7 @@ All changes included in 1.10: ## Regression fixes +- ([#13583](https://github.com/quarto-dev/quarto-cli/issues/13583)): Fix "Show All Code" and "Hide All Code" in the `code-tools` menu doing nothing when `code-copy` is enabled. (author: @mcanouil) - ([#14267](https://github.com/quarto-dev/quarto-cli/issues/14267)): Fix Windows paths with accented characters (e.g., `C:\Users\Sébastien\`) breaking dart-sass compilation. - ([#14281](https://github.com/quarto-dev/quarto-cli/issues/14281)): Fix transient `.quarto_ipynb` files accumulating during `quarto preview` with Jupyter engine. - ([#14298](https://github.com/quarto-dev/quarto-cli/issues/14298)): Fix `quarto preview` browse URL including output filename (e.g., `hello.html`) for single-file documents, breaking Posit Workbench proxied server access. diff --git a/src/command/render/codetools.ts b/src/command/render/codetools.ts index d7a7cee1d5f..a617fd38287 100644 --- a/src/command/render/codetools.ts +++ b/src/command/render/codetools.ts @@ -353,7 +353,7 @@ function resolveCodeTools(format: Format, doc: Document): CodeTools { // if we have requested toggle, make sure there are things to toggle if (codeToolsResolved.toggle) { - const codeDetails = doc.querySelector(".cell > details > .sourceCode"); + const codeDetails = doc.querySelector(".cell details.code-fold"); // we don't OJS hidden cells in this check, since when echo: false, we emit them hidden const codeHidden = doc.querySelector( diff --git a/src/resources/formats/html/templates/quarto-html-after-body.ejs b/src/resources/formats/html/templates/quarto-html-after-body.ejs index 55fade9537c..6e7adb4d0af 100644 --- a/src/resources/formats/html/templates/quarto-html-after-body.ejs +++ b/src/resources/formats/html/templates/quarto-html-after-body.ejs @@ -149,16 +149,16 @@ } function toggleCodeHandler(show) { return function(e) { - const detailsSrc = window.document.querySelectorAll(".cell > details > .sourceCode"); - for (let i=0; i .sourceCode"); + const cellCodeDivs = window.document.querySelectorAll(".cell div.sourceCode.cell-code"); const fromCls = show ? "hidden" : "unhidden"; const toCls = show ? "unhidden" : "hidden"; for (let i=0; i div.sourceCode" + - "div.sourceCode.cell-code.hidden" # echo: false cell + - [] +--- + +With `code-copy` enabled (the default), each code block is wrapped in a +`div.code-copy-outer-scaffold`, which broke the "Show All Code" / "Hide All +Code" `code-tools` selectors that assumed `.sourceCode` was a direct child of +`
` or `.cell` (issue #13583). This document checks the post-fix DOM: +the folded cell keeps its `details.code-fold`, the scaffold wraps the +`div.sourceCode`, and the `echo: false` cell (kept via `keep-hidden: true`) +still carries `div.sourceCode.cell-code.hidden`. + +```{python} +print("visible, folded code") +``` + +```{python} +#| echo: false +print("hidden code") +``` From c16d7a3eb2943c6eb3f78fd29646d6be42f0d8ab Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Tue, 21 Jul 2026 13:01:16 +0200 Subject: [PATCH 2/2] test(html): add Playwright regression test for code-tools toggle (#13583) The smoke fixture only asserts on rendered HTML, which is byte-identical before and after this fix (the render pipeline is untouched), so it passes on the buggy selectors too and cannot guard the behavior. This Playwright test exercises the actual client-side JS: it clicks Show All Code / Hide All Code and asserts every
opens/closes and the echo:false cell's source swaps hidden<->unhidden, with code-copy enabled. It fails on the pre-fix selectors (details never open) and passes on the fix. Uses an {ojs} cell so the fixture needs no Python/R kernel. --- .../playwright/html/code-tools-toggle.qmd | 22 +++++++++ .../tests/html-code-tools-toggle.spec.ts | 45 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/docs/playwright/html/code-tools-toggle.qmd create mode 100644 tests/integration/playwright/tests/html-code-tools-toggle.spec.ts diff --git a/tests/docs/playwright/html/code-tools-toggle.qmd b/tests/docs/playwright/html/code-tools-toggle.qmd new file mode 100644 index 00000000000..02d8a1280f0 --- /dev/null +++ b/tests/docs/playwright/html/code-tools-toggle.qmd @@ -0,0 +1,22 @@ +--- +title: Code tools toggle with code-copy +format: html +code-fold: true +code-tools: true +code-copy: true +keep-hidden: true +--- + +Regression test for #13583: with `code-copy` enabled (the default), each code +block is wrapped in `div.code-copy-outer-scaffold`. The "Show All Code" / +"Hide All Code" items in the `code-tools` menu must still open/close every +folded `
` and reveal/hide the `echo: false` cell's source. + +```{ojs} +x = 5 +``` + +```{ojs} +//| echo: false +y = 10 +``` diff --git a/tests/integration/playwright/tests/html-code-tools-toggle.spec.ts b/tests/integration/playwright/tests/html-code-tools-toggle.spec.ts new file mode 100644 index 00000000000..552ea49f966 --- /dev/null +++ b/tests/integration/playwright/tests/html-code-tools-toggle.spec.ts @@ -0,0 +1,45 @@ +import { expect, test } from "@playwright/test"; +import { getUrl } from "../src/utils"; + +// Regression test for #13583. With code-copy enabled (the default), each code +// block is wrapped in div.code-copy-outer-scaffold, which used to break the +// direct-child selectors the code-tools "Show All Code" / "Hide All Code" +// handler relied on. The toggle must still open/close every folded
+// and swap the hidden<->unhidden class on the echo:false cell's source. +test("code-tools Show/Hide All Code toggles folded and hidden code with code-copy", async ({ + page, +}) => { + await page.goto(getUrl("html/code-tools-toggle.html"), { + waitUntil: "load", + }); + + const details = page.locator(".cell details.code-fold"); + const hiddenCode = page.locator("div.sourceCode.cell-code.hidden"); + const unhiddenCode = page.locator("div.sourceCode.cell-code.unhidden"); + + // Baseline: two folded cells, both collapsed; the echo:false cell's source + // is emitted hidden (keep-hidden). + await expect(details).toHaveCount(2); + await expect(details.nth(0)).toHaveJSProperty("open", false); + await expect(details.nth(1)).toHaveJSProperty("open", false); + await expect(hiddenCode).toHaveCount(1); + await expect(unhiddenCode).toHaveCount(0); + + // Show All Code: every
opens, the hidden source becomes unhidden. + await page.locator("#quarto-code-tools-menu").click(); + await page.locator("#quarto-show-all-code").click(); + + await expect(details.nth(0)).toHaveJSProperty("open", true); + await expect(details.nth(1)).toHaveJSProperty("open", true); + await expect(hiddenCode).toHaveCount(0); + await expect(unhiddenCode).toHaveCount(1); + + // Hide All Code: every
closes, the source goes back to hidden. + await page.locator("#quarto-code-tools-menu").click(); + await page.locator("#quarto-hide-all-code").click(); + + await expect(details.nth(0)).toHaveJSProperty("open", false); + await expect(details.nth(1)).toHaveJSProperty("open", false); + await expect(hiddenCode).toHaveCount(1); + await expect(unhiddenCode).toHaveCount(0); +});