Skip to content

Fix revealjs and epub callout id rendered on two divs - #15041

Merged
cderv merged 2 commits into
mainfrom
fix/revealjs-callout-duplicate-id
Oct 14, 2026
Merged

cderv merged 2 commits into
mainfrom
fix/revealjs-callout-duplicate-id

Conversation

@cderv

@cderv cderv commented Oct 13, 2026

Copy link
Copy Markdown
Member

In revealjs and EPUB output, a callout with an id renders that id on two nested divs:

<div id="my-id" title="Titled">
<div id="my-id" class="callout callout-note callout-titled callout-style-default">

Duplicate ids are invalid HTML. getElementById() returns only the outer div, and a CSS #my-id rule applies to both (doubled borders, margins, padding). HTML output is not affected: the bootstrap renderer emits a single .callout div.

Root cause

The revealjs/epub renderer wraps the .callout div in an outer div so leftover classes such as .fragment survive (#5208, #6853). The wrapper was created whenever the callout had an id, and it received the full attribute set, identifier included. When cross-referenceable callouts were added (a0a7b4a, first released in v1.4.458), the inner .callout div started carrying the id as well, so both divs have it. Reproduced on released 1.4.549 and v1.9.38, and on current main.

Fix

The id stays on the .callout div, the same element as in HTML output, so .callout#x selectors behave the same across formats. The wrapper is now created only when classes or attributes are left over, and it gets a copy of the attributes without the identifier. An id-only callout renders as a single div.

Alternative considered: keeping the id on the wrapper and blanking the inner one. This was rejected because the id would no longer be on the .callout element, and .callout#x (used in the existing revealjs-callouts.spec.ts locators) would stop matching.

This changes what #x selects in revealjs and EPUB, so it targets 1.11 without a backport. Selectors such as #x > .callout or getElementById('x').querySelector('.callout') no longer match, and neither works in HTML. Reveal fragment-event code keyed on event.fragment.id for {#x .callout-note .fragment} is affected too, since .fragment stays on the wrapper.

The title attribute still leaks onto the wrapper as an HTML attribute, which keeps titled callouts wrapped. That is separate from this fix.

Test plan

  • div.callout#… selectors and absence of [id=x] [id=x] in the new smoke-all document, reverse-applying the fix gives an assertion failure
  • 6853.qmd (fragment wrapper) still passes
  • EPUB: unzip the rendered .epub and grep -rhn 'id="my-id\|id="other' EPUB/text/ shows one line per id, each on class="callout …"

Fixes #15036

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes
  • updated the appropriate changelog in the PR
  • ensured the present test suite passes
  • added new tests
  • created a separate documentation PR in Quarto's website repo and linked it to this PR
AI-assisted PR
  • AI tool used: Claude Code
  • Codebase grounding: local clone
  • Human review: I have reviewed, tested, and verified the AI-generated content before submitting.

Note: autonomous AI agents submitting PRs without human oversight are not permitted — see the Code of Conduct.

@posit-snyk-bot

posit-snyk-bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

cderv added 2 commits October 14, 2026 11:22
The revealjs/epub callout renderer wraps the `.callout` div in an outer
div when the callout has leftover classes or attributes, so that user
classes such as `.fragment` survive (#5208, #6853). The wrapper was also
created whenever the callout had an id, and it was given the whole
attribute set, identifier included.

Since xref callouts (v1.4.458) the inner `.callout` div carries the id
too, so every callout with an id emitted it twice. Duplicate ids are
invalid HTML: `getElementById()` returns the wrapper, and a CSS `#id`
rule applies to both divs.

Keep the id on the `.callout` div only, as in HTML output, so `.callout#x`
selectors match in every format. The wrapper is now created only when
classes or attributes are left over, and it gets a copy of the attributes
without the identifier. An id-only callout renders as a single div.

Fixes #15036
@cderv
cderv force-pushed the fix/revealjs-callout-duplicate-id branch from 8423544 to a614070 Compare October 14, 2026 09:22
@cderv
cderv merged commit 3b70cc9 into main Oct 14, 2026
51 checks passed
@cderv
cderv deleted the fix/revealjs-callout-duplicate-id branch October 14, 2026 10:38
cderv added a commit that referenced this pull request Oct 14, 2026
A callout's title option was also written out as an HTML title
attribute: on the .callout div in html and on the wrapper div in
revealjs and epub. Browsers showed it as a tooltip over the whole
callout, repeating the heading. The callout parser read title without
removing it from the div attributes, unlike appearance, collapse and
icon, so it was copied with the other leftover attributes.

- Remove title from the div attributes once the parser has read it
- Titled revealjs and epub callouts with only an id no longer get a
  wrapper div; other attributes such as data-* still pass through

Related to #15041
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Revealjs callout with an id renders the id twice

2 participants