Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions claude-notes/plans/2026-05-22-callout-class-vocabulary-fix.md

Large diffs are not rendered by default.

94 changes: 86 additions & 8 deletions crates/quarto-core/resources/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -162,20 +162,38 @@ figcaption {
margin-top: 0.5rem;
}

/* ===== Callouts ===== */
/* ===== Callouts =====
*
* Selectors match the canonical TS Quarto / Bootstrap class vocabulary
* emitted by `CalloutResolveTransform` (see
* `crates/quarto-core/src/transforms/callout_resolve.rs` and TS Quarto's
* `src/resources/filters/modules/callouts.lua::render_to_bootstrap_div`).
*
* This stylesheet ships only under `theme: none` — themed documents
* compile Bootstrap SCSS in `resources/scss/bootstrap/_bootstrap-rules.scss`
* which has its own (more elaborate) rules over the same class vocabulary.
*
* Class summary on a fully-decorated callout:
* .callout.callout-style-{default|simple}.callout-{type}
* [.callout-titled][.no-icon][.callout-empty-content]
*/

.callout {
border-left: 4px solid var(--color-border);
padding: 0.75rem 1rem;
margin: 1rem 0;
background-color: var(--color-bg-subtle);
border-radius: 0 4px 4px 0;
border-left: 4px solid var(--color-border);
}

/* Default appearance: filled background, prominent header bar.
* Simple/minimal appearances override this further down. */
.callout.callout-style-default {
background-color: var(--color-bg-subtle);
}

/* Header bar (titled path only). */
.callout-header {
display: flex;
align-items: center;
padding: 0.4rem 0.75rem 0.3rem 0.75rem;
font-weight: 600;
margin-bottom: 0.5rem;
}

.callout-icon-container {
Expand All @@ -186,35 +204,95 @@ figcaption {
flex: 1;
}

/* Body — titled path: `.callout-body-container.callout-body` carries
* padding. Untitled path: outer `.callout-body.d-flex` contains the
* icon and a bare `.callout-body-container` whose padding lives here. */
.callout-body-container {
padding: 0.5rem 0.75rem;
}
.callout-body-container p:last-child {
margin-bottom: 0;
}

/* Callout types */
/* Untitled path: outer body wrapper carries `d-flex` so icon and body
* sit side-by-side. Match the header's vertical rhythm by using the
* same top padding. */
.callout:not(.callout-titled) > .callout-body {
padding-top: 0.5rem;
padding-left: 0.75rem;
}

/* Bootstrap utility-class shims so `theme: none` docs still get the
* expected flex layout without a full Bootstrap import. */
.d-flex { display: flex; }
.align-content-center { align-content: center; }
.flex-fill { flex: 1 1 auto; }

/* Collapse wrapper: hide body when `.collapse` is set without `.show`. */
.callout-collapse.collapse:not(.show) {
display: none;
}

/* Empty-content callouts: drop trailing padding so the header sits
* flush against the bottom border. */
.callout.callout-empty-content .callout-header {
padding-bottom: 0.4rem;
}

/* `.no-icon` collapses any icon container that did sneak through. */
.callout.no-icon .callout-icon-container {
display: none;
}

/* Per-type accent colors. Border-left color is also the header tint. */
.callout-note {
border-left-color: #0d6efd;
background-color: rgba(13, 110, 253, 0.05);
}
.callout-note.callout-titled > .callout-header {
background-color: rgba(13, 110, 253, 0.08);
}

.callout-warning {
border-left-color: #ffc107;
background-color: rgba(255, 193, 7, 0.05);
}
.callout-warning.callout-titled > .callout-header {
background-color: rgba(255, 193, 7, 0.10);
}

.callout-tip {
border-left-color: #198754;
background-color: rgba(25, 135, 84, 0.05);
}
.callout-tip.callout-titled > .callout-header {
background-color: rgba(25, 135, 84, 0.08);
}

.callout-important {
border-left-color: #dc3545;
background-color: rgba(220, 53, 69, 0.05);
}
.callout-important.callout-titled > .callout-header {
background-color: rgba(220, 53, 69, 0.08);
}

.callout-caution {
border-left-color: #fd7e14;
background-color: rgba(253, 126, 20, 0.05);
}
.callout-caution.callout-titled > .callout-header {
background-color: rgba(253, 126, 20, 0.10);
}

/* `callout-style-simple` is the minimal/borderless variant. Drop the
* background tint everywhere; keep just the left border accent. */
.callout.callout-style-simple {
background-color: transparent;
}
.callout.callout-style-simple.callout-titled > .callout-header {
background-color: transparent;
}

/* ===== Definition Lists ===== */
dl {
Expand Down
40 changes: 40 additions & 0 deletions crates/quarto-core/src/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,46 @@ mod tests {
assert!(DEFAULT_CSS.contains(".callout"));
}

/// `theme: none` documents ship `DEFAULT_CSS` as the only stylesheet.
/// It must therefore carry rules for the canonical Bootstrap-aligned
/// callout class vocabulary (matching what `CalloutResolveTransform`
/// emits and what `resources/scss/bootstrap/_bootstrap-rules.scss`
/// keys off of), not the pre-2026-05-22 q2-only scheme.
///
/// See `claude-notes/plans/2026-05-22-callout-class-vocabulary-fix.md`
/// Phase 4 for the rewrite. Until that phase lands this test will
/// fail — that's the intent.
#[test]
fn test_default_css_uses_canonical_callout_selectors() {
for selector in &[
".callout-style-default",
".callout-style-simple",
".callout-titled",
".no-icon",
] {
assert!(
DEFAULT_CSS.contains(selector),
"DEFAULT_CSS must contain canonical callout selector `{}` (see Phase 4 of \
2026-05-22-callout-class-vocabulary-fix.md). Current contents do not match TS \
Quarto's Bootstrap class scheme.",
selector
);
}
// Old q2-only selectors must not appear — they are not emitted
// by the new resolver and would be dead rules.
for legacy in &[
".callout-appearance-simple",
".callout-appearance-minimal",
".callout-appearance-default",
] {
assert!(
!DEFAULT_CSS.contains(legacy),
"DEFAULT_CSS must not contain legacy selector `{}`",
legacy
);
}
}

#[test]
fn test_write_html_resources_creates_directory() {
let runtime = NativeRuntime::new();
Expand Down
20 changes: 17 additions & 3 deletions crates/quarto-core/src/transforms/callout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,29 @@ fn convert_div_to_callout(
}

// Extract additional attributes from the div
let appearance = extract_attr_value(&div.attr, "appearance").unwrap_or("default".to_string());
let collapse = extract_attr_value(&div.attr, "collapse").is_some_and(|v| v == "true");
let icon = extract_attr_value(&div.attr, "icon").is_none_or(|v| v != "false");
let appearance_raw =
extract_attr_value(&div.attr, "appearance").unwrap_or("default".to_string());
let collapse_attr = extract_attr_value(&div.attr, "collapse");
let collapse = collapse_attr.is_some();
let collapse_starts_collapsed = collapse_attr.as_deref() == Some("true");
let icon_raw = extract_attr_value(&div.attr, "icon").is_none_or(|v| v != "false");

// Normalize `appearance="minimal"` → `appearance="simple"` AND `icon=false`,
// matching TS Quarto's `nameForCalloutStyle` (src/resources/filters/customnodes/callout.lua).
// Doing it here means the resolver only ever sees the canonical
// `default` or `simple` appearance string.
let (appearance, icon) = if appearance_raw == "minimal" {
("simple".to_string(), false)
} else {
(appearance_raw, icon_raw)
};

// Build the plain_data JSON
let mut plain_data = json!({
"type": callout_type,
"appearance": appearance,
"collapse": collapse,
"collapse_starts_collapsed": collapse_starts_collapsed,
"icon": icon
});

Expand Down
Loading
Loading