From ec2c289ef3b976f1c49b910e2bb585dbbb17262e Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 4 Feb 2026 15:22:42 +0100 Subject: [PATCH 1/3] Fix crash with crossref table div and conditional visibility When a div has both a cross-reference ID (e.g., #tbl-) and conditional visibility class (.content-visible), the slot content gets transformed by parsefiguredivs from Div to FloatRefTarget to Table. The render function in content-hidden.lua assumed the slot was always a Div and crashed when calling el.content on a Table. The fix checks if the rendered slot is still a Div before accessing .content. For transformed elements (Table, etc.), return the element wrapped in pandoc.Blocks. Fixes #13992 Co-Authored-By: Claude Opus 4.5 --- news/changelog-1.9.md | 1 + .../filters/customnodes/content-hidden.lua | 12 +++++- .../docs/smoke-all/2026/02/04/issue-13992.qmd | 38 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 tests/docs/smoke-all/2026/02/04/issue-13992.qmd diff --git a/news/changelog-1.9.md b/news/changelog-1.9.md index 1d9fe8db3ba..ea3c8a4902e 100644 --- a/news/changelog-1.9.md +++ b/news/changelog-1.9.md @@ -163,3 +163,4 @@ All changes included in 1.9: - ([#13890](https://github.com/quarto-dev/quarto-cli/issues/13890)): Fix render failure when using `embed-resources: true` with input path through a symlinked directory. The cleanup now resolves symlinks before comparing paths. - ([#13907](https://github.com/quarto-dev/quarto-cli/issues/13907)): Ignore AI assistant configuration files (`CLAUDE.md`, `AGENTS.md`) when scanning for project input files and in extension templates, similar to how `README.md` is handled. - ([#13935](https://github.com/quarto-dev/quarto-cli/issues/13935)): Fix `quarto install`, `quarto update`, and `quarto uninstall` interactive tool selection. +- ([#13992](https://github.com/quarto-dev/quarto-cli/issues/13992)): Fix crash when rendering div with both cross-reference ID and conditional visibility to PDF. diff --git a/src/resources/filters/customnodes/content-hidden.lua b/src/resources/filters/customnodes/content-hidden.lua index 341e938a66c..2846aaf5a5a 100644 --- a/src/resources/filters/customnodes/content-hidden.lua +++ b/src/resources/filters/customnodes/content-hidden.lua @@ -59,8 +59,16 @@ _quarto.ast.add_handler({ local visible = is_visible(node) if visible then local el = node.node - clearHiddenVisibleAttributes(el) - return el.content + -- Handle case where slot content was transformed (e.g., Div → FloatRefTarget → Table) + if is_regular_node(el, "Div") then + -- Original behavior for Div: clear attributes and return inner content + clearHiddenVisibleAttributes(el) + return el.content + else + -- Slot was transformed to another type (Table, etc.) + -- Return the rendered element wrapped in Blocks + return pandoc.Blocks({el}) + end else return {} end diff --git a/tests/docs/smoke-all/2026/02/04/issue-13992.qmd b/tests/docs/smoke-all/2026/02/04/issue-13992.qmd new file mode 100644 index 00000000000..5465040958f --- /dev/null +++ b/tests/docs/smoke-all/2026/02/04/issue-13992.qmd @@ -0,0 +1,38 @@ +--- +title: "Crossref table with conditional visibility (#13992)" +keep-tex: true +keep-typ: true +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + - ['Type 1', 'Type 2', 'Item 1', 'Item 2', 'tbl-mytypes'] + - [] + html: + ensureHtmlElements: + - [] + - ['#tbl-mytypes', 'table'] + ensureFileRegexMatches: + - [] + - ['Type 1', 'Type 2', 'Item 1', 'Item 2'] + typst: + ensureTypstFileRegexMatches: + - ['#ref\(', 'Type 1', 'Type 2', 'Item 1', 'Item 2'] + - [] +--- + +This tests that a table div with both a cross-reference ID and conditional visibility renders correctly. + +See @tbl-mytypes for the table. + +::: {#tbl-mytypes .content-visible unless-format="html"} + +| Type 1 | Type 2 | +| ------ | ------ | +| Item 1 | Item 2 | + +: Test table caption + +::: + +The table above should appear in PDF and Typst but not HTML. From fc83e911c83b494b9b8a2ab816c62a7da78dd40c Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 4 Feb 2026 16:36:39 +0100 Subject: [PATCH 2/3] Add test coverage for content-hidden custom node scenarios (#13992) Tests for conditional visibility with various custom nodes that were affected by the fix in content-hidden.lua: - Figure (FloatRefTarget) - Listing (FloatRefTarget) - Theorem (Theorem custom node) - Proof (Proof custom node) - Plain div (baseline) - Nested callout (Callout custom node) - Nested tabset (HTML-specific) - Rename table test to reflect its focus on Table (Table custom node) Each test verifies PDF, HTML, and Typst output with structure checks. Co-Authored-By: Claude Opus 4.5 --- .../2026/02/04/issue-13992-figure.qmd | 31 ++++++++++++++++++ .../2026/02/04/issue-13992-listing.qmd | 32 +++++++++++++++++++ .../2026/02/04/issue-13992-nested-callout.qmd | 30 +++++++++++++++++ .../2026/02/04/issue-13992-nested-tabset.qmd | 31 ++++++++++++++++++ .../2026/02/04/issue-13992-plain.qmd | 25 +++++++++++++++ .../2026/02/04/issue-13992-proof.qmd | 25 +++++++++++++++ ...{issue-13992.qmd => issue-13992-table.qmd} | 1 + .../2026/02/04/issue-13992-theorem.qmd | 30 +++++++++++++++++ 8 files changed, 205 insertions(+) create mode 100644 tests/docs/smoke-all/2026/02/04/issue-13992-figure.qmd create mode 100644 tests/docs/smoke-all/2026/02/04/issue-13992-listing.qmd create mode 100644 tests/docs/smoke-all/2026/02/04/issue-13992-nested-callout.qmd create mode 100644 tests/docs/smoke-all/2026/02/04/issue-13992-nested-tabset.qmd create mode 100644 tests/docs/smoke-all/2026/02/04/issue-13992-plain.qmd create mode 100644 tests/docs/smoke-all/2026/02/04/issue-13992-proof.qmd rename tests/docs/smoke-all/2026/02/04/{issue-13992.qmd => issue-13992-table.qmd} (97%) create mode 100644 tests/docs/smoke-all/2026/02/04/issue-13992-theorem.qmd diff --git a/tests/docs/smoke-all/2026/02/04/issue-13992-figure.qmd b/tests/docs/smoke-all/2026/02/04/issue-13992-figure.qmd new file mode 100644 index 00000000000..cb3cc27cf36 --- /dev/null +++ b/tests/docs/smoke-all/2026/02/04/issue-13992-figure.qmd @@ -0,0 +1,31 @@ +--- +title: "Conditional visibility with figure (#13992)" +keep-tex: true +keep-typ: true +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + - ['Figure content visible', 'fig-cond', '\\begin{figure}', 'Figure~\\ref\{fig-cond\}'] + - [] + html: + ensureHtmlElements: + - [] + - ['#fig-cond'] + ensureFileRegexMatches: + - [] + - ['Figure content visible'] + typst: + ensureTypstFileRegexMatches: + - ['#figure', 'Figure content visible', '#ref\('] + - [] + native: default +--- + +See @fig-cond for conditional figure. + +::: {#fig-cond .content-visible unless-format="html"} +![]({{< placeholder 300 >}}) + +Figure content visible +::: diff --git a/tests/docs/smoke-all/2026/02/04/issue-13992-listing.qmd b/tests/docs/smoke-all/2026/02/04/issue-13992-listing.qmd new file mode 100644 index 00000000000..0457a3b5e3c --- /dev/null +++ b/tests/docs/smoke-all/2026/02/04/issue-13992-listing.qmd @@ -0,0 +1,32 @@ +--- +title: "Conditional visibility with listing (#13992)" +keep-tex: true +keep-typ: true +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + - ['Listing content visible', 'lst-cond', '\\begin{codelisting}', 'Listing~\\ref\{lst-cond\}'] + - [] + html: + ensureHtmlElements: + - [] + - ['#lst-cond'] + ensureFileRegexMatches: + - [] + - ['Listing content visible'] + typst: + ensureTypstFileRegexMatches: + - ['Listing content visible', '#ref\('] + - [] + native: default +--- + +See @lst-cond for conditional listing. + +::: {#lst-cond .content-visible unless-format="html"} +```python +# Listing content visible +print("hello") +``` +::: diff --git a/tests/docs/smoke-all/2026/02/04/issue-13992-nested-callout.qmd b/tests/docs/smoke-all/2026/02/04/issue-13992-nested-callout.qmd new file mode 100644 index 00000000000..3fb4f722916 --- /dev/null +++ b/tests/docs/smoke-all/2026/02/04/issue-13992-nested-callout.qmd @@ -0,0 +1,30 @@ +--- +title: "Conditional visibility with nested callout (#13992)" +keep-tex: true +keep-typ: true +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + - ['Nested callout visible', 'tcolorbox', 'quarto-callout-note'] + - [] + html: + ensureHtmlElements: + - [] + - ['div.callout'] + ensureFileRegexMatches: + - [] + - ['Nested callout visible'] + typst: + ensureTypstFileRegexMatches: + - ['Nested callout visible', '#callout'] + - [] +--- + +This tests a callout nested inside a conditional visibility div. + +::: {.content-visible unless-format="html"} +::: {.callout-note} +Nested callout visible. +::: +::: diff --git a/tests/docs/smoke-all/2026/02/04/issue-13992-nested-tabset.qmd b/tests/docs/smoke-all/2026/02/04/issue-13992-nested-tabset.qmd new file mode 100644 index 00000000000..15084cb484b --- /dev/null +++ b/tests/docs/smoke-all/2026/02/04/issue-13992-nested-tabset.qmd @@ -0,0 +1,31 @@ +--- +title: "Conditional visibility with nested tabset (#13992)" +keep-tex: true +keep-typ: true +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + - [] + - ['Nested tabset visible', 'Tab A'] + html: + ensureHtmlElements: + - ['div.panel-tabset'] + - [] + ensureFileRegexMatches: + - ['Nested tabset visible', 'Tab A'] + - [] + typst: + ensureTypstFileRegexMatches: + - [] + - ['Nested tabset visible', 'Tab A'] +--- + +This tests a tabset nested inside a conditional visibility div. + +::: {.content-visible when-format="html"} +::: {.panel-tabset} +### Tab A +Nested tabset visible. +::: +::: diff --git a/tests/docs/smoke-all/2026/02/04/issue-13992-plain.qmd b/tests/docs/smoke-all/2026/02/04/issue-13992-plain.qmd new file mode 100644 index 00000000000..55f721644a5 --- /dev/null +++ b/tests/docs/smoke-all/2026/02/04/issue-13992-plain.qmd @@ -0,0 +1,25 @@ +--- +title: "Conditional visibility with plain div (#13992)" +keep-tex: true +keep-typ: true +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + - ['Plain div content visible'] + - [] + html: + ensureFileRegexMatches: + - [] + - ['Plain div content visible'] + typst: + ensureTypstFileRegexMatches: + - ['Plain div content visible'] + - [] +--- + +This tests the baseline case of a plain div with `.content-visible`. + +::: {.content-visible unless-format="html"} +Plain div content visible. +::: diff --git a/tests/docs/smoke-all/2026/02/04/issue-13992-proof.qmd b/tests/docs/smoke-all/2026/02/04/issue-13992-proof.qmd new file mode 100644 index 00000000000..6c1b98a8a77 --- /dev/null +++ b/tests/docs/smoke-all/2026/02/04/issue-13992-proof.qmd @@ -0,0 +1,25 @@ +--- +title: "Conditional visibility with proof (#13992)" +keep-tex: true +keep-typ: true +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + - ['Proof content visible', '\\begin{proof}', '\\end{proof}'] + - [] + html: + ensureFileRegexMatches: + - [] + - ['Proof content visible'] + typst: + ensureTypstFileRegexMatches: + - ['#emph\[Proof\]\. Proof content visible'] + - [] +--- + +This tests a proof div with `.proof` class and `.content-visible`. + +::: {.proof .content-visible unless-format="html"} +Proof content visible. +::: diff --git a/tests/docs/smoke-all/2026/02/04/issue-13992.qmd b/tests/docs/smoke-all/2026/02/04/issue-13992-table.qmd similarity index 97% rename from tests/docs/smoke-all/2026/02/04/issue-13992.qmd rename to tests/docs/smoke-all/2026/02/04/issue-13992-table.qmd index 5465040958f..29bec204e4b 100644 --- a/tests/docs/smoke-all/2026/02/04/issue-13992.qmd +++ b/tests/docs/smoke-all/2026/02/04/issue-13992-table.qmd @@ -19,6 +19,7 @@ _quarto: ensureTypstFileRegexMatches: - ['#ref\(', 'Type 1', 'Type 2', 'Item 1', 'Item 2'] - [] + native: default --- This tests that a table div with both a cross-reference ID and conditional visibility renders correctly. diff --git a/tests/docs/smoke-all/2026/02/04/issue-13992-theorem.qmd b/tests/docs/smoke-all/2026/02/04/issue-13992-theorem.qmd new file mode 100644 index 00000000000..59989bd7b24 --- /dev/null +++ b/tests/docs/smoke-all/2026/02/04/issue-13992-theorem.qmd @@ -0,0 +1,30 @@ +--- +title: "Conditional visibility with theorem (#13992)" +keep-tex: true +keep-typ: true +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + - ['Theorem content visible', 'thm-cond', '\\begin\{theorem\}'] + - [] + html: + ensureHtmlElements: + - [] + - ['#thm-cond'] + ensureFileRegexMatches: + - [] + - ['Theorem content visible'] + typst: + ensureTypstFileRegexMatches: + - ['Theorem content visible', '#ref\(', '#theorem'] + - [] +--- + +See @thm-cond for conditional theorem. + +::: {#thm-cond .content-visible unless-format="html"} +## Test Theorem + +Theorem content visible. +::: From 5922beb2f98384d365037a8655f4ce949d0cd101 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Thu, 5 Feb 2026 12:24:16 +0100 Subject: [PATCH 3/3] Add explanatory comment for defensive clearHiddenVisibleAttributes call Document that the clearHiddenVisibleAttributes() call in ConditionalBlock render is defensive (typically a no-op) since parse() already strips visibility attrs. Kept as safety net for potential future code changes. --- src/resources/filters/customnodes/content-hidden.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/resources/filters/customnodes/content-hidden.lua b/src/resources/filters/customnodes/content-hidden.lua index 2846aaf5a5a..fe38b12b539 100644 --- a/src/resources/filters/customnodes/content-hidden.lua +++ b/src/resources/filters/customnodes/content-hidden.lua @@ -61,7 +61,9 @@ _quarto.ast.add_handler({ local el = node.node -- Handle case where slot content was transformed (e.g., Div → FloatRefTarget → Table) if is_regular_node(el, "Div") then - -- Original behavior for Div: clear attributes and return inner content + -- Defensive: parse() already stripped visibility attrs (lines 46-47), so this is + -- typically a no-op. Kept as safety net in case future code adds attrs between + -- parse and render. See issue #13992 investigation for AST trace evidence. clearHiddenVisibleAttributes(el) return el.content else