Skip to content

Commit 9a0da8c

Browse files
cscheidclaude
andcommitted
fix(preview): sync sibling _brand.yml in single-file q2 preview (bd-ggvq1j68)
`q2 preview deck.qmd` on a bare file (no `_quarto.yml` ancestor) runs single-file mode, which deliberately does not walk the directory — so a deck referencing `brand: _brand.yml` failed brand resolution with "Path not found: /project/_brand.yml" (the sibling never reached the preview VFS). Pick up conventionally-named brand siblings (`_brand.yml`/`_brand.yaml`) that physically sit next to the target file, without walking the directory — so the bd-tnm3k "don't pull in arbitrary siblings" safety property is preserved. A deck whose `brand:` points elsewhere (e.g. `../shared/foo.yml`) is still unsupported in single-file mode; author it inside a project instead. E2E: `q2 preview deck.qmd` with a sibling `_brand.yml` now applies the brand theme (bg #fdf6ff, fg #2a1a3a, heading #6f42c1, Georgia) — matching `q2 render`. Tests: `test_single_file_picks_up_brand_sibling`, `test_single_file_no_brand_sibling_stays_narrow`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 650cbdd commit 9a0da8c

2 files changed

Lines changed: 75 additions & 1 deletion

File tree

crates/quarto-hub/src/context.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,13 @@ impl HubContext {
209209
// would index sibling files the user didn't ask for.
210210
let project_files = if let Some(ref project_root) = project_root {
211211
let files = match config.single_file.as_ref() {
212-
Some(rel) => ProjectFiles::single_file(rel.clone()),
212+
// bd-ggvq1j68: single-file mode skips the WalkDir, but a deck
213+
// that references `brand: _brand.yml` needs that sibling in the
214+
// VFS. Pick up the conventionally-named brand file next to the
215+
// target without walking the directory (stays narrow).
216+
Some(rel) => {
217+
ProjectFiles::single_file(rel.clone()).with_config_siblings(project_root, rel)
218+
}
213219
// bd-kjrpya2d: the bare walk can't see resources-scoped
214220
// `.html` (it falls through every category), so the
215221
// caller-resolved set is injected here to ride the text

crates/quarto-hub/src/discovery.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,30 @@ impl ProjectFiles {
164164
}
165165
}
166166

167+
/// In single-file mode, additionally sync well-known project-config
168+
/// siblings that physically sit next to the target file — currently
169+
/// `_brand.yml` / `_brand.yaml`, which a deck references via `brand:` for
170+
/// theme/brand resolution (bd-ggvq1j68).
171+
///
172+
/// This stays deliberately narrow: only these specific, conventionally-named
173+
/// config files, and only when they exist on disk. It does NOT walk the
174+
/// directory, so it preserves the bd-tnm3k safety property (don't pull in
175+
/// arbitrary siblings like `~/Downloads/*.qmd`). A deck whose `brand:` points
176+
/// somewhere non-conventional (e.g. `brand: ../shared/foo.yml`) is still
177+
/// unsupported in single-file mode — author it inside a project instead.
178+
pub fn with_config_siblings(mut self, project_root: &Path, relative: &Path) -> Self {
179+
let dir = relative.parent().unwrap_or_else(|| Path::new(""));
180+
for name in ["_brand.yml", "_brand.yaml"] {
181+
let rel = dir.join(name);
182+
if project_root.join(&rel).is_file() {
183+
self.config_files.push(rel);
184+
}
185+
}
186+
self.config_files.sort();
187+
self.config_files.dedup();
188+
self
189+
}
190+
167191
/// Attach resources-scoped `.html` files (resolved by the caller
168192
/// from `project.resources:`) to be synced into the VFS as text.
169193
/// Deduplicates and sorts for deterministic ordering. See
@@ -344,6 +368,50 @@ mod tests {
344368
);
345369
}
346370

371+
#[test]
372+
fn test_single_file_picks_up_brand_sibling() {
373+
// bd-ggvq1j68: `q2 preview deck.qmd` (single-file mode, no `_quarto.yml`)
374+
// with `brand: _brand.yml` should still resolve the brand. Single-file
375+
// mode never walks the directory, but a conventionally-named `_brand.yml`
376+
// sitting next to the deck is exactly the file the deck references — sync
377+
// it so the preview VFS can read it.
378+
let temp = TempDir::new().unwrap();
379+
fs::write(temp.path().join("deck.qmd"), "# Hi").unwrap();
380+
fs::write(
381+
temp.path().join("_brand.yml"),
382+
"color:\n palette:\n p: \"#6f42c1\"\n primary: p\n",
383+
)
384+
.unwrap();
385+
386+
let files = ProjectFiles::single_file(PathBuf::from("deck.qmd"))
387+
.with_config_siblings(temp.path(), Path::new("deck.qmd"));
388+
389+
assert_eq!(files.qmd_files, vec![PathBuf::from("deck.qmd")]);
390+
assert!(
391+
files.config_files.contains(&PathBuf::from("_brand.yml")),
392+
"single-file mode must sync a sibling _brand.yml the deck references"
393+
);
394+
}
395+
396+
#[test]
397+
fn test_single_file_no_brand_sibling_stays_narrow() {
398+
// Without a `_brand.yml` next to the deck, single-file mode must NOT
399+
// pull in any siblings — the safety property of bd-tnm3k.
400+
let temp = TempDir::new().unwrap();
401+
fs::write(temp.path().join("deck.qmd"), "# Hi").unwrap();
402+
fs::write(temp.path().join("unrelated.qmd"), "# Other").unwrap();
403+
fs::write(temp.path().join("notes.txt"), "stuff").unwrap();
404+
405+
let files = ProjectFiles::single_file(PathBuf::from("deck.qmd"))
406+
.with_config_siblings(temp.path(), Path::new("deck.qmd"));
407+
408+
assert_eq!(files.qmd_files, vec![PathBuf::from("deck.qmd")]);
409+
assert!(
410+
files.config_files.is_empty(),
411+
"no _brand.yml → no siblings synced (must not walk the dir)"
412+
);
413+
}
414+
347415
#[test]
348416
fn test_discover_metadata_files() {
349417
let temp = TempDir::new().unwrap();

0 commit comments

Comments
 (0)