From 2d25c735c25cb8da7c46669e30e4bf718dd3b880 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 10 Jun 2026 18:12:49 +0200 Subject: [PATCH 1/3] test: add failing tests for extension format detection (#14582) Extension format strings like 'acm-pdf' are not recognized by format detection functions (isPdfOutput, isHtmlOutput, etc.) because isFormatTo uses startsWith which only matches when the base format is at the start. Includes manual test fixture from reporter's reproduction project. --- .../dummy-extension/_extension.yml | 5 + .../preview/extension-pdf-book/_quarto.yml | 12 ++ .../preview/extension-pdf-book/chapter1.qmd | 3 + .../preview/extension-pdf-book/index.qmd | 5 + tests/unit/format-detection.test.ts | 103 ++++++++++++++++++ 5 files changed, 128 insertions(+) create mode 100644 tests/docs/manual/preview/extension-pdf-book/_extensions/dummy-extension/_extension.yml create mode 100644 tests/docs/manual/preview/extension-pdf-book/_quarto.yml create mode 100644 tests/docs/manual/preview/extension-pdf-book/chapter1.qmd create mode 100644 tests/docs/manual/preview/extension-pdf-book/index.qmd create mode 100644 tests/unit/format-detection.test.ts diff --git a/tests/docs/manual/preview/extension-pdf-book/_extensions/dummy-extension/_extension.yml b/tests/docs/manual/preview/extension-pdf-book/_extensions/dummy-extension/_extension.yml new file mode 100644 index 0000000000..ef12969388 --- /dev/null +++ b/tests/docs/manual/preview/extension-pdf-book/_extensions/dummy-extension/_extension.yml @@ -0,0 +1,5 @@ +name: dummy-extension +contributes: + formats: + pdf: default + html: default diff --git a/tests/docs/manual/preview/extension-pdf-book/_quarto.yml b/tests/docs/manual/preview/extension-pdf-book/_quarto.yml new file mode 100644 index 0000000000..2db614adf4 --- /dev/null +++ b/tests/docs/manual/preview/extension-pdf-book/_quarto.yml @@ -0,0 +1,12 @@ +project: + type: book + +book: + title: "Bug reproduction" + chapters: + - index.qmd + - chapter1.qmd + +format: + dummy-extension-pdf: default + dummy-extension-html: default diff --git a/tests/docs/manual/preview/extension-pdf-book/chapter1.qmd b/tests/docs/manual/preview/extension-pdf-book/chapter1.qmd new file mode 100644 index 0000000000..a1709fee99 --- /dev/null +++ b/tests/docs/manual/preview/extension-pdf-book/chapter1.qmd @@ -0,0 +1,3 @@ +# Introduction + +This is chapter 1. diff --git a/tests/docs/manual/preview/extension-pdf-book/index.qmd b/tests/docs/manual/preview/extension-pdf-book/index.qmd new file mode 100644 index 0000000000..df3a49db1f --- /dev/null +++ b/tests/docs/manual/preview/extension-pdf-book/index.qmd @@ -0,0 +1,5 @@ +--- +title: "Preface" +--- + +This is the preface. diff --git a/tests/unit/format-detection.test.ts b/tests/unit/format-detection.test.ts new file mode 100644 index 0000000000..dd47cfaf87 --- /dev/null +++ b/tests/unit/format-detection.test.ts @@ -0,0 +1,103 @@ +import { unitTest } from "../test.ts"; +import { assert } from "testing/asserts"; +import { + isHtmlOutput, + isJatsOutput, + isPdfOutput, + isRevealjsOutput, + isTypstOutput, +} from "../../src/config/format.ts"; + +unitTest( + "isPdfOutput - direct formats", + // deno-lint-ignore require-await + async () => { + assert(isPdfOutput("pdf")); + assert(isPdfOutput("beamer")); + assert(!isPdfOutput("html")); + assert(!isPdfOutput("typst")); + assert(!isPdfOutput("docx")); + }, +); + +unitTest( + "isPdfOutput - extension formats", + // deno-lint-ignore require-await + async () => { + assert(isPdfOutput("acm-pdf")); + assert(isPdfOutput("dummy-extension-pdf")); + assert(isPdfOutput("acm-2023-pdf")); + assert(isPdfOutput("journal-beamer")); + assert(!isPdfOutput("acm-html")); + assert(!isPdfOutput("my-ext-typst")); + }, +); + +unitTest( + "isPdfOutput - modifiers and edge cases", + // deno-lint-ignore require-await + async () => { + assert(!isPdfOutput("default")); + assert(!isPdfOutput("")); + assert(isPdfOutput("pdf+draft")); + assert(isPdfOutput("acm-pdf+draft")); + }, +); + +unitTest( + "isHtmlOutput - extension formats", + // deno-lint-ignore require-await + async () => { + assert(isHtmlOutput("html")); + assert(isHtmlOutput("acm-html")); + assert(isHtmlOutput("nature-html")); + assert(!isHtmlOutput("acm-pdf")); + }, +); + +unitTest( + "isTypstOutput - extension formats", + // deno-lint-ignore require-await + async () => { + assert(isTypstOutput("typst")); + assert(isTypstOutput("orange-book-typst")); + assert(!isTypstOutput("acm-pdf")); + }, +); + +unitTest( + "isRevealjsOutput - extension formats", + // deno-lint-ignore require-await + async () => { + assert(isRevealjsOutput("revealjs")); + assert(isRevealjsOutput("clean-revealjs")); + assert(isRevealjsOutput("my-theme-revealjs")); + assert(!isRevealjsOutput("html")); + assert(!isRevealjsOutput("acm-pdf")); + }, +); + +unitTest( + "isJatsOutput - extension formats", + // deno-lint-ignore require-await + async () => { + assert(isJatsOutput("jats")); + assert(isJatsOutput("jats_archiving")); + assert(isJatsOutput("plos-jats")); + assert(isJatsOutput("my-journal-jats_archiving")); + assert(!isJatsOutput("html")); + assert(!isJatsOutput("acm-pdf")); + }, +); + +unitTest( + "format detection - malformed inputs", + // deno-lint-ignore require-await + async () => { + assert(!isPdfOutput("---")); + assert(!isPdfOutput("123")); + assert(isHtmlOutput("")); // empty defaults to "html" internally + assert(!isRevealjsOutput("")); + assert(!isJatsOutput("")); + }, +); From 6b914518a8b2f91b23b92b2c0574075b7c00ac32 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 10 Jun 2026 18:15:30 +0200 Subject: [PATCH 2/3] fix: format detection now handles extension format strings (#14582) isFormatTo(), isRevealjsOutput(), and isJatsOutput() used startsWith() to detect format types, which predates the extension format naming convention (-, e.g. acm-pdf). Extension formats put the base format at the end, not the start, so detection always returned false. Add parseFormatString() fallback when startsWith fails. This fixes format detection for all extension formats across preview (PDF.js viewer), watch (revealjs), manuscript (notebook filtering, MECA bundles), and website (HTML-first ordering) flows. --- src/config/format.ts | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/config/format.ts b/src/config/format.ts index b8adee18a2..48c486d4cb 100644 --- a/src/config/format.ts +++ b/src/config/format.ts @@ -6,6 +6,7 @@ import { kBaseFormat, kPreferHtml } from "../config/constants.ts"; import { Format, FormatPandoc } from "./types.ts"; +import { parseFormatString } from "../core/pandoc/pandoc-formats.ts"; export function isPdfOutput(format: string): boolean; export function isPdfOutput(format: FormatPandoc): boolean; @@ -97,14 +98,20 @@ export function isJatsOutput(format?: string | FormatPandoc) { format = format?.to || "html"; } - return [ + const jatsFormats = [ "jats", "jats_archiving", "jats_articleauthoring", "jats_publishing", - ].find((formatStr) => { - return (format as string).startsWith(formatStr); - }) !== undefined; + ]; + const formatStr = format as string; + if (jatsFormats.some((f) => formatStr.startsWith(f))) return true; + try { + const base = parseFormatString(formatStr).baseFormat; + return jatsFormats.some((f) => base.startsWith(f)); + } catch { + return false; + } } export function isPresentationOutput(format: FormatPandoc) { @@ -123,7 +130,12 @@ export function isRevealjsOutput(format?: string | FormatPandoc) { format = format?.to; } format = format || "html"; - return format.startsWith("revealjs"); + if (format.startsWith("revealjs")) return true; + try { + return parseFormatString(format).baseFormat.startsWith("revealjs"); + } catch { + return false; + } } export function isNativeOutput(format: FormatPandoc) { @@ -146,7 +158,12 @@ function isFormatTo(format: string | FormatPandoc, to: string) { const formatStr = typeof format === "string" ? format : (format?.to || "html"); - return formatStr.startsWith(to); + if (formatStr.startsWith(to)) return true; + try { + return parseFormatString(formatStr).baseFormat.startsWith(to); + } catch { + return false; + } } export function isMarkdownOutput( From c63378003d524b35f194a0ee8ab677ea9507aefa Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 10 Jun 2026 18:16:53 +0200 Subject: [PATCH 3/3] docs: add manual preview test matrix and changelog for #14582 --- news/changelog-1.10.md | 3 ++- tests/docs/manual/preview/README.md | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index 529b08cc4e..de520e2fb9 100644 --- a/news/changelog-1.10.md +++ b/news/changelog-1.10.md @@ -92,4 +92,5 @@ All changes included in 1.10: - ([#14359](https://github.com/quarto-dev/quarto-cli/issues/14359)): Fix intermediate `.quarto_ipynb` file not being deleted after rendering a `.qmd` with Jupyter engine, causing numbered variants (`_1`, `_2`, ...) to accumulate on disk across renders. - ([#14461](https://github.com/quarto-dev/quarto-cli/issues/14461)): Fix `quarto render --to pdf` aborting with `ERROR: Problem running 'fmtutil-sys --all' to rebuild format tree.` when an automatically-installed LaTeX package's post-update format rebuild fails. Format-tree rebuild is now treated as best-effort housekeeping (matching upstream `tinytex` R behavior) — the failure is logged as a warning and the package install completes. - ([#14472](https://github.com/quarto-dev/quarto-cli/issues/14472)): Add support for Kotlin in code annotations and YAML cell options. (author: @barendgehrels) -- ([#14529](https://github.com/quarto-dev/quarto-cli/issues/14529)): Fix bundled Julia engine path leaking into rendered YAML metadata and pandoc log output when running an installed Quarto. The internal subtree-engine filter only matched the source-tree share-path layout (`resources/extension-subtrees/`) and missed installed layouts where the path is `share/extension-subtrees/`. \ No newline at end of file +- ([#14529](https://github.com/quarto-dev/quarto-cli/issues/14529)): Fix bundled Julia engine path leaking into rendered YAML metadata and pandoc log output when running an installed Quarto. The internal subtree-engine filter only matched the source-tree share-path layout (`resources/extension-subtrees/`) and missed installed layouts where the path is `share/extension-subtrees/`. +- ([#14582](https://github.com/quarto-dev/quarto-cli/issues/14582)): Fix format detection for extension formats (e.g. `acm-pdf`) in project preview, manuscript notebooks, MECA bundles, and website format ordering. \ No newline at end of file diff --git a/tests/docs/manual/preview/README.md b/tests/docs/manual/preview/README.md index a18df4debb..95f4a9dfc3 100644 --- a/tests/docs/manual/preview/README.md +++ b/tests/docs/manual/preview/README.md @@ -216,6 +216,22 @@ format edit is detected on the **first** render request. - **Expected:** No preview process restart. Render runs in the same process. - **Catches:** Over-eager invalidation triggering 404 when the format is in fact unchanged +## Extension Format PDF Preview (#14582) + +### P1: Core functionality + +#### T26: Book preview with extension PDF format +- **Setup**: `extension-pdf-book/` fixture +- **Steps**: `quarto preview . --to dummy-extension-pdf --no-browser --port 4444` +- **Expected**: Browse URL contains `web/viewer.html`. PDF.js viewer loads in browser with live-reload. +- **Catches**: `isFormatTo` startsWith check failing for extension format strings. + +#### T27: Book preview with extension HTML format (no regression) +- **Setup**: Same fixture +- **Steps**: `quarto preview . --to dummy-extension-html --no-browser --port 4444` +- **Expected**: Normal HTML preview with live-reload. No PDF.js viewer. +- **Catches**: Fix doesn't accidentally enable PDF mode for non-PDF extension formats. + ## Test File Templates **Minimal Python .qmd:**