diff --git a/.claude/rules/testing/smoke-all-tests.md b/.claude/rules/testing/smoke-all-tests.md index ea80419bcdf..7900aaeb19b 100644 --- a/.claude/rules/testing/smoke-all-tests.md +++ b/.claude/rules/testing/smoke-all-tests.md @@ -119,6 +119,23 @@ _quarto: Valid OS values: `linux`, `darwin`, `windows` +## Project Rendering (cross-file resolution) + +Each `_quarto.tests` block renders only its own input file (`quarto render --to `). In a multi-file project (book/website) that leaves cross-file references resolved at the project post-render stage (e.g. an appendix cross-reference pointing into another chapter) unresolved. + +Set `render-project: true` as a sibling of `tests:` (under `_quarto`) to render the whole project first: + +```yaml +_quarto: + render-project: true + tests: + html: + ensureFileRegexMatches: + - ['>Whatever A<'] +``` + +Gotcha: the project pre-render runs `quarto render ` with **no `--to`**, so it builds only the formats **declared in `_quarto.yml`**. To test a format's cross-file output, that format must be declared in the project config — otherwise the pre-render skips it and the per-file render alone cannot resolve cross-file refs. Example: `tests/docs/smoke-all/2026/06/23/issue-11772` (book must declare `format: html` for the HTML appendix cross-reference test). + ## Pattern Specificity Avoid patterns that match template boilerplate instead of document content: diff --git a/llm-docs/testing-patterns.md b/llm-docs/testing-patterns.md index 2ddcfa0c364..86968475b5f 100644 --- a/llm-docs/testing-patterns.md +++ b/llm-docs/testing-patterns.md @@ -394,6 +394,23 @@ Run test **without fix** first to verify it fails, then verify it passes with fi Smoke-all tests embed test specifications directly in `.qmd` files using `_quarto.tests` metadata. See `.claude/rules/testing/smoke-all-tests.md` for full documentation. +### Project pre-render for cross-file resolution + +A `_quarto.tests` block renders only its own input file (`quarto render --to `). In a multi-file project (book/website), cross-file references that are resolved at the project post-render stage — e.g. an appendix cross-reference in one chapter pointing at another chapter — stay unresolved under a single-file render. + +Set `render-project: true` as a sibling of `tests:` to render the whole project first: + +```yaml +_quarto: + render-project: true + tests: + html: + ensureFileRegexMatches: + - ['>Whatever A<'] +``` + +The harness runs `quarto render ` (in `smoke-all.test.ts`) before the per-file render. **Gotcha:** that pre-render has no `--to`, so it builds only the formats declared in `_quarto.yml`. To test a format's cross-file output, declare that format in the project config — otherwise the pre-render skips it and the per-file render alone cannot resolve cross-file references. Reference fixture: `tests/docs/smoke-all/2026/06/23/issue-11772` (the book declares `format: html` so the HTML appendix cross-reference resolves; the per-file HTML render then reuses the project crossref index). + ### YAML String Escaping for Regex **Critical rule:** In YAML single-quoted strings, `'\('` and `"\\("` are equivalent - both produce a literal `\(` in the regex. diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index 7cf310f1b2b..50156fb6f3c 100644 --- a/news/changelog-1.10.md +++ b/news/changelog-1.10.md @@ -50,6 +50,10 @@ All changes included in 1.10: - ([#14562](https://github.com/quarto-dev/quarto-cli/issues/14562)): Fix a heading inside `content-hidden when-format="llms-txt"` (visible in HTML) losing its `
` wrapper and `id` in a website with `llms-txt` enabled, which broke its table-of-contents link, anchors, and cross-references. - ([#14563](https://github.com/quarto-dev/quarto-cli/issues/14563)): Fix a fatal error when a shortcode is used inside conditional content (e.g. `content-visible when-format="llms-txt"`) in a website with `llms-txt` enabled. +### Books + +- ([#11772](https://github.com/quarto-dev/quarto-cli/issues/11772)): Fix `crossref.appendix-title` not being applied to appendix cross-references in PDF output. Appendix cross-references now use the configured title (e.g. `See Whatever A`) instead of always showing the default `Appendix` prefix. + ## Commands ### `quarto preview` diff --git a/src/project/types/book/book-crossrefs.ts b/src/project/types/book/book-crossrefs.ts index 6b2d3fc6284..cc120e0f555 100644 --- a/src/project/types/book/book-crossrefs.ts +++ b/src/project/types/book/book-crossrefs.ts @@ -18,6 +18,8 @@ import { import { pathWithForwardSlashes } from "../../../core/path.ts"; import { + kCrossref, + kCrossrefAppendixTitle, kCrossrefApxPrefix, kCrossrefChapterId, kCrossrefChapters, @@ -37,7 +39,7 @@ import { WebsiteProjectOutputFile } from "../website/website.ts"; import { inputTargetIndex } from "../../project-index.ts"; import { bookConfigRenderItems } from "./book-config.ts"; import { isMultiFileBookFormat } from "./book-shared.ts"; -import { Format } from "../../../config/types.ts"; +import { Format, Metadata } from "../../../config/types.ts"; export async function bookCrossrefsPostRender( context: ProjectContext, @@ -305,11 +307,21 @@ function formatCrossref( // if this is a section we need a prefix const refNumber = numberOption(entry.order, options, type); if (type === "sec" && !noPrefix) { - const prefix = (options[kCrossrefChapters] && isChapterRef(entry)) - ? options[kCrossrefChaptersAppendix] - ? language[kCrossrefApxPrefix] - : language[kCrossrefChPrefix] - : language[kCrossrefSecPrefix]; + let prefix; + if (options[kCrossrefChapters] && isChapterRef(entry)) { + if (options[kCrossrefChaptersAppendix]) { + // appendix cross-references prefer crossref.appendix-title (the same + // option that titles the appendix chapter heading) so the reference + // text matches the heading + const crossref = format.metadata?.[kCrossref] as Metadata | undefined; + prefix = (crossref?.[kCrossrefAppendixTitle] as string) ?? + language[kCrossrefApxPrefix]; + } else { + prefix = language[kCrossrefChPrefix]; + } + } else { + prefix = language[kCrossrefSecPrefix]; + } const crossref = prefix + " " + refNumber; return crossref; } else { diff --git a/src/resources/filters/crossref/format.lua b/src/resources/filters/crossref/format.lua index 6c6908b285b..7f5d7dfc0cf 100644 --- a/src/resources/filters/crossref/format.lua +++ b/src/resources/filters/crossref/format.lua @@ -77,6 +77,14 @@ function refPrefix(type, upper) end default = stringToInlines(default) local prefix = crossrefOption(opt, default) + -- appendix cross-references prefer crossref.appendix-title (the same option that + -- titles the appendix chapter heading) so the reference text matches the heading + if type == "apx" then + local appendixTitle = crossrefOption("appendix-title") + if appendixTitle ~= nil then + prefix = appendixTitle + end + end if upper then local el = pandoc.Plain(prefix) local firstStr = true diff --git a/tests/docs/smoke-all/2026/06/23/issue-11772/.gitignore b/tests/docs/smoke-all/2026/06/23/issue-11772/.gitignore new file mode 100644 index 00000000000..ad293093b07 --- /dev/null +++ b/tests/docs/smoke-all/2026/06/23/issue-11772/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/2026/06/23/issue-11772/_quarto.yml b/tests/docs/smoke-all/2026/06/23/issue-11772/_quarto.yml new file mode 100644 index 00000000000..a5929d6f7d7 --- /dev/null +++ b/tests/docs/smoke-all/2026/06/23/issue-11772/_quarto.yml @@ -0,0 +1,21 @@ +project: + type: book + +book: + title: "issue-11772" + author: "Norah Jones" + chapters: + - index.qmd + - intro.qmd + appendices: + - summary.qmd + +format: + html: default + pdf: + documentclass: scrreprt + keep-tex: true + +crossref: + appendix-title: "Whatever" + appendix-delim: ":" diff --git a/tests/docs/smoke-all/2026/06/23/issue-11772/index.qmd b/tests/docs/smoke-all/2026/06/23/issue-11772/index.qmd new file mode 100644 index 00000000000..726a6cad132 --- /dev/null +++ b/tests/docs/smoke-all/2026/06/23/issue-11772/index.qmd @@ -0,0 +1,19 @@ +--- +format: + pdf: + keep-tex: true +_quarto: + tests: + pdf: + # crossref.appendix-title ("Whatever") must drive the appendix + # cross-reference prefix in PDF, like it does in HTML output. + # Currently the prefix falls back to the default "Appendix" + # (only language.crossref-apx-prefix is honored) — issue #11772. + ensureLatexFileRegexMatches: + - ['See Whatever~\\ref\{sec-summary\}'] + - ['See Appendix~\\ref\{sec-summary\}'] +--- + +# Preface {.unnumbered} + +This is a book testing crossref appendix options in PDF (issue #11772). diff --git a/tests/docs/smoke-all/2026/06/23/issue-11772/intro.qmd b/tests/docs/smoke-all/2026/06/23/issue-11772/intro.qmd new file mode 100644 index 00000000000..f55602f21e4 --- /dev/null +++ b/tests/docs/smoke-all/2026/06/23/issue-11772/intro.qmd @@ -0,0 +1,17 @@ +--- +_quarto: + render-project: true + tests: + html: + # crossref.appendix-title ("Whatever") must drive the appendix + # cross-reference prefix in HTML too. This cross-file reference from a + # numbered chapter is resolved by the book post-render step, which + # previously only honored language.crossref-apx-prefix — issue #11772. + ensureFileRegexMatches: + - ['>Whatever A<'] + - ['>Appendix A<'] +--- + +# Introduction + +See @sec-summary. diff --git a/tests/docs/smoke-all/2026/06/23/issue-11772/summary.qmd b/tests/docs/smoke-all/2026/06/23/issue-11772/summary.qmd new file mode 100644 index 00000000000..8e2c15d0532 --- /dev/null +++ b/tests/docs/smoke-all/2026/06/23/issue-11772/summary.qmd @@ -0,0 +1,3 @@ +# Summary {#sec-summary} + +In summary, this book has no content whatsoever.