diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index 529b08cc4e8..de520e2fb93 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/src/config/format.ts b/src/config/format.ts index b8adee18a2b..48c486d4cb3 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( diff --git a/tests/docs/manual/preview/README.md b/tests/docs/manual/preview/README.md index a18df4debb7..95f4a9dfc3d 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:** 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 00000000000..ef129693881 --- /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 00000000000..2db614adf43 --- /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 00000000000..a1709fee991 --- /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 00000000000..df3a49db1fb --- /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 00000000000..dd47cfaf87e --- /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("")); + }, +);