diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index 5a46cc678c2..886550db8c3 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -38,6 +38,10 @@ All changes included in 1.11: - ([#14982](https://github.com/quarto-dev/quarto-cli/issues/14982)): Fix a footnote in document metadata, such as `title` or `author`, making PDF and Typst output shift heading levels, which turned the first level-1 heading into the document title. +### `revealjs` + +- ([#14996](https://github.com/quarto-dev/quarto-cli/pull/14996)): Fix `controls: auto` and `previewLinks: auto` set at the top level of a revealjs document's front matter producing a presentation that fails to load, because `auto` was written unquoted into the generated JavaScript. + ### `typst` - ([#14847](https://github.com/quarto-dev/quarto-cli/pull/14847)): Fix `toc_title` auto-fallback in typst outline template that was ignoring the computed fallback value when `toc_title` is `none`. @@ -57,6 +61,10 @@ All changes included in 1.11: - ([#14974](https://github.com/quarto-dev/quarto-cli/issues/14974)): Fix math in `.llms.md` files from `llms-txt` coming out garbled with the default `mathjax` method, and with `katex` and `webtex`. Math is now written as `$...$` and `$$...$$` with the `mathjax`, `katex`, `webtex` and `mathml` methods. +## Lua API + +- ([#14894](https://github.com/quarto-dev/quarto-cli/pull/14894)): Fix `quarto.version` and `quarto.config.version()` crashing filters with `table expected, got string` when the version string contains semver build metadata (such as a distro packager's revision suffix) or otherwise does not start with a digit. + ## Commands ### `call` @@ -90,12 +98,9 @@ All changes included in 1.11: - ([#13966](https://github.com/quarto-dev/quarto-cli/issues/13966)): Fix doubled newlines in HTML output of stdout and stderr containing ANSI escape codes. -## Lua API - -- ([#14894](https://github.com/quarto-dev/quarto-cli/pull/14894)): Fix `quarto.version` and `quarto.config.version()` crashing filters with `table expected, got string` when the version string contains semver build metadata (such as a distro packager's revision suffix) or otherwise does not start with a digit. - ## Other fixes and improvements +- ([#9864](https://github.com/quarto-dev/quarto-cli/issues/9864), [#11139](https://github.com/quarto-dev/quarto-cli/issues/11139)): Fix metadata defined both in a document's front matter and in `_quarto.yml`, a directory `_metadata.yml` or `metadata-files` reaching Pandoc templates and Lua filters with only the document's values. The values are now merged, and the document's values still take precedence for single values. Lists are combined instead of replaced, so a document can no longer drop entries that the project, a directory `_metadata.yml` or `metadata-files` add to a list such as `keywords`. (author: @jkrumbiegel) - ([#14775](https://github.com/quarto-dev/quarto-cli/issues/14775)): Fix a crash when the `QUARTO_R` environment variable is set to a malformed path. Quarto now warns and falls back to other R lookup methods. - ([#14865](https://github.com/quarto-dev/quarto-cli/issues/14865)): Fix internal links in a preview being treated as external when the preview is reached through a proxy, such as on Posit Workbench. Links are now classified against the origin the browser sees. - ([#14878](https://github.com/quarto-dev/quarto-cli/pull/14878)): Add `az` (Azerbaijani) language translation. (author: @abdanar) diff --git a/src/command/render/pandoc.ts b/src/command/render/pandoc.ts index 10293088d5e..067580e221b 100644 --- a/src/command/render/pandoc.ts +++ b/src/command/render/pandoc.ts @@ -1177,12 +1177,31 @@ export async function runPandoc( if (key === kTheme && isRevealjsOutput(options.format.pandoc)) { continue; } + // - controls and previewLinks are replaced for revealjs when not a boolean (e.g. 'auto'), which is not a valid reveal.js value + if ( + (key === "controls" || key === "previewLinks") && + typeof engineMetadata[key] !== "boolean" && + isRevealjsOutput(options.format.pandoc) + ) { + continue; + } // - categories are handled specifically already for website projects with a metadata override and should not be overridden by user input if (key === kFieldCategories && projectIsWebsite(options.project)) { continue; } - // perform the override - pandocMetadata[key] = engineMetadata[key]; + // a key that execution left unchanged keeps what other sources merged + // into it, with the document's own values on top; a key holding inline + // expressions takes the executed value as a whole + if (ld.isEqual(options.unexecutedMetadata?.[key], engineMetadata[key])) { + // wrapped so that arrays and scalars are concatenated or replaced, + // not merged by index + pandocMetadata[key] = mergeConfigs( + { value: pandocMetadata[key] }, + { value: options.unexecutedMetadata?.[key] }, + ).value; + } else { + pandocMetadata[key] = engineMetadata[key]; + } } } diff --git a/src/command/render/render.ts b/src/command/render/render.ts index 622a1f58775..15c5b5a4b60 100644 --- a/src/command/render/render.ts +++ b/src/command/render/render.ts @@ -194,6 +194,7 @@ export async function renderPandoc( args: recipe.args, services: context.options.services, metadata: executeResult.metadata, + unexecutedMetadata: context.target.metadata, quiet, flags: context.options.flags, }; diff --git a/src/command/render/types.ts b/src/command/render/types.ts index 2826cdf9f6e..7b70484aba0 100644 --- a/src/command/render/types.ts +++ b/src/command/render/types.ts @@ -214,6 +214,9 @@ export interface PandocOptions { // extra metadata to merge metadata?: Metadata; + // the input's metadata as written, before execution resolved inline expressions + unexecutedMetadata?: Metadata; + // optional execution engine executionEngine?: string; diff --git a/tests/docs/metadata-merge-cli/cli-metadata-file.qmd b/tests/docs/metadata-merge-cli/cli-metadata-file.qmd new file mode 100644 index 00000000000..925cf2331a2 --- /dev/null +++ b/tests/docs/metadata-merge-cli/cli-metadata-file.qmd @@ -0,0 +1,25 @@ +--- +title: Front matter and --metadata-file +shared: document-value +custom: + shared: document-value + from-document: document-value +keywords: + - document-keyword +--- + +::: {#shared} +{{< meta shared >}} +::: + +::: {#custom-shared} +{{< meta custom.shared >}} +::: + +::: {#custom-from-document} +{{< meta custom.from-document >}} +::: + +::: {#custom-from-metadata-file} +{{< meta custom.from-metadata-file >}} +::: diff --git a/tests/docs/metadata-merge-cli/cli-metadata-file.yml b/tests/docs/metadata-merge-cli/cli-metadata-file.yml new file mode 100644 index 00000000000..73820f4c8c9 --- /dev/null +++ b/tests/docs/metadata-merge-cli/cli-metadata-file.yml @@ -0,0 +1,6 @@ +shared: metadata-file-value +custom: + shared: metadata-file-value + from-metadata-file: metadata-file-value +keywords: + - metadata-file-keyword diff --git a/tests/docs/project/metadata-merge/.gitignore b/tests/docs/project/metadata-merge/.gitignore new file mode 100644 index 00000000000..ad293093b07 --- /dev/null +++ b/tests/docs/project/metadata-merge/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/project/metadata-merge/_quarto.yml b/tests/docs/project/metadata-merge/_quarto.yml new file mode 100644 index 00000000000..22c38d61c24 --- /dev/null +++ b/tests/docs/project/metadata-merge/_quarto.yml @@ -0,0 +1,22 @@ +project: + type: default + +format: + html: + template: template.html + +keywords: + - project-keyword + - shared-keyword + +tags: + - project-tag + +params: + from-project: project-value + +custom: + from-project: project-value + shared: project-value + list: + - project-item diff --git a/tests/docs/project/metadata-merge/knitr.qmd b/tests/docs/project/metadata-merge/knitr.qmd new file mode 100644 index 00000000000..e01da0db8c2 --- /dev/null +++ b/tests/docs/project/metadata-merge/knitr.qmd @@ -0,0 +1,26 @@ +--- +title: Knitr document +keywords: + - document-keyword + - shared-keyword +params: + from-document: document-value +custom: + from-document: document-value + shared: document-value + list: + - document-item +tags: + - '`{r} "project-tag"`' + - document-tag +evaluated: '`{r} paste0("evaluated", "-value")`' +--- + +```{r} +#| echo: false +invisible(1) +``` + +::: {#lua-params} +{{< meta params.from-project >}} +::: diff --git a/tests/docs/project/metadata-merge/markdown.qmd b/tests/docs/project/metadata-merge/markdown.qmd new file mode 100644 index 00000000000..a7b8680aeba --- /dev/null +++ b/tests/docs/project/metadata-merge/markdown.qmd @@ -0,0 +1,17 @@ +--- +title: Markdown document +keywords: + - document-keyword + - shared-keyword +params: + from-document: document-value +custom: + from-document: document-value + shared: document-value + list: + - document-item +--- + +::: {#lua-params} +{{< meta params.from-project >}} +::: diff --git a/tests/docs/project/metadata-merge/template.html b/tests/docs/project/metadata-merge/template.html new file mode 100644 index 00000000000..832a8fbba5f --- /dev/null +++ b/tests/docs/project/metadata-merge/template.html @@ -0,0 +1,9 @@ +KEYWORDS: [$for(keywords)$$it$$sep$, $endfor$] +TAGS: [$for(tags)$$it$$sep$, $endfor$] +PARAMS: [$params.from-project$] [$params.from-document$] +FROM-PROJECT: [$custom.from-project$] +SHARED: [$custom.shared$] +FROM-DOCUMENT: [$custom.from-document$] +CUSTOM-LIST: [$for(custom.list)$$it$$sep$, $endfor$] +EVALUATED: [$evaluated$] +$body$ diff --git a/tests/docs/smoke-all/metadata-merge/.gitignore b/tests/docs/smoke-all/metadata-merge/.gitignore new file mode 100644 index 00000000000..ad293093b07 --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/metadata-merge/book-chapter-title/.gitignore b/tests/docs/smoke-all/metadata-merge/book-chapter-title/.gitignore new file mode 100644 index 00000000000..7359d935c76 --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/book-chapter-title/.gitignore @@ -0,0 +1,4 @@ +/.quarto/ +_book/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/metadata-merge/book-chapter-title/_quarto.yml b/tests/docs/smoke-all/metadata-merge/book-chapter-title/_quarto.yml new file mode 100644 index 00000000000..59a8fd4797a --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/book-chapter-title/_quarto.yml @@ -0,0 +1,12 @@ +project: + type: book + +book: + title: "Metadata merge book" + chapters: + - index.qmd + - chapter.qmd + +format: + html: + number-sections: true diff --git a/tests/docs/smoke-all/metadata-merge/book-chapter-title/chapter.qmd b/tests/docs/smoke-all/metadata-merge/book-chapter-title/chapter.qmd new file mode 100644 index 00000000000..bcf6ae7a0af --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/book-chapter-title/chapter.qmd @@ -0,0 +1,14 @@ +--- +title: Chapter from YAML +_quarto: + tests: + html: + ensureHtmlElements: + - ['h1.title span.chapter-number'] + - [] + ensureHtmlElementContents: + selectors: ['h1.title'] + matches: ['^\s*1\s+Chapter from YAML\s*$'] +--- + +A book chapter whose title comes from the YAML `title` key keeps the chapter number Quarto computes for it. The document's own `title` value must not replace the numbered title. diff --git a/tests/docs/smoke-all/metadata-merge/book-chapter-title/index.qmd b/tests/docs/smoke-all/metadata-merge/book-chapter-title/index.qmd new file mode 100644 index 00000000000..ff4bd80cfb7 --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/book-chapter-title/index.qmd @@ -0,0 +1,3 @@ +# Preface {.unnumbered} + +Home page of the fixture. The assertions live in `chapter.qmd`. diff --git a/tests/docs/smoke-all/metadata-merge/format-key-precedence.qmd b/tests/docs/smoke-all/metadata-merge/format-key-precedence.qmd new file mode 100644 index 00000000000..5a9879960f6 --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/format-key-precedence.qmd @@ -0,0 +1,18 @@ +--- +title: Top-level title +subtitle: Top-level subtitle +format: + html: + title: Format title + subtitle: Format subtitle +_quarto: + tests: + html: + ensureFileRegexMatches: + - + - '

Format title

' + - '

Format subtitle

' + - [] +--- + +A key set both at the top level and under `format: html:` in the same document takes the format-level value in the output. The document is not executed (markdown engine), so this checks the merge order of document metadata on its own: the executed-front-matter substitution done before calling pandoc must not bring the top-level value back. diff --git a/tests/docs/smoke-all/metadata-merge/knitr-inline-yaml.qmd b/tests/docs/smoke-all/metadata-merge/knitr-inline-yaml.qmd new file mode 100644 index 00000000000..f4f005a6b02 --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/knitr-inline-yaml.qmd @@ -0,0 +1,26 @@ +--- +title: "Title `r 1 + 1`" +custom-key: "Custom `{r} 2 + 3`" +nested: + inner: "Nested `r 3 * 3`" +engine: knitr +_quarto: + tests: + html: + ensureFileRegexMatches: + - + - '

Title 2

' + - '
\s*

Custom 5

' + - '
\s*

Nested 9

' + - [] +--- + +Inline R expressions in YAML front matter are evaluated by knitr, and the evaluated values must reach pandoc: the standard `title`, a custom top-level key, and a nested key with no project-level counterpart. + +::: {#custom-key} +{{< meta custom-key >}} +::: + +::: {#nested-inner} +{{< meta nested.inner >}} +::: diff --git a/tests/docs/smoke-all/metadata-merge/metadata-files-extra.yml b/tests/docs/smoke-all/metadata-merge/metadata-files-extra.yml new file mode 100644 index 00000000000..73820f4c8c9 --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/metadata-files-extra.yml @@ -0,0 +1,6 @@ +shared: metadata-file-value +custom: + shared: metadata-file-value + from-metadata-file: metadata-file-value +keywords: + - metadata-file-keyword diff --git a/tests/docs/smoke-all/metadata-merge/metadata-files.qmd b/tests/docs/smoke-all/metadata-merge/metadata-files.qmd new file mode 100644 index 00000000000..57ae326f97c --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/metadata-files.qmd @@ -0,0 +1,40 @@ +--- +title: Front matter and metadata-files +metadata-files: + - metadata-files-extra.yml +shared: document-value +custom: + shared: document-value + from-document: document-value +keywords: + - document-keyword +_quarto: + tests: + html: + ensureFileRegexMatches: + - + - '' + - '
\s*

document-value

' + - '
\s*

document-value

' + - '
\s*

document-value

' + - '
\s*

metadata-file-value

' + - [] +--- + +The document's front matter wins over its `metadata-files` for a value both define, and what only the metadata file defines is kept, including entries of a list and keys of a mapping the front matter also defines. + +::: {#shared} +{{< meta shared >}} +::: + +::: {#custom-shared} +{{< meta custom.shared >}} +::: + +::: {#custom-from-document} +{{< meta custom.from-document >}} +::: + +::: {#custom-from-metadata-file} +{{< meta custom.from-metadata-file >}} +::: diff --git a/tests/docs/smoke-all/metadata-merge/revealjs-controls-auto.qmd b/tests/docs/smoke-all/metadata-merge/revealjs-controls-auto.qmd new file mode 100644 index 00000000000..6158e2cc9fe --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/revealjs-controls-auto.qmd @@ -0,0 +1,22 @@ +--- +title: Controls and preview links set to auto +format: revealjs +controls: auto +previewLinks: auto +_quarto: + tests: + revealjs: + ensureFileRegexMatches: + - + - 'controls: false,' + - "'controlsAuto': true," + - 'previewLinks: false,' + - "'previewLinksAuto': true," + - + - 'controls: auto,' + - 'previewLinks: auto,' +--- + +## Slide + +`auto` is not a valid reveal.js value: Quarto passes `false` to reveal.js and enables its own automatic behavior instead. diff --git a/tests/docs/smoke-all/metadata-merge/revealjs-controls-boolean-extra.yml b/tests/docs/smoke-all/metadata-merge/revealjs-controls-boolean-extra.yml new file mode 100644 index 00000000000..f6118275244 --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/revealjs-controls-boolean-extra.yml @@ -0,0 +1,2 @@ +controls: true +previewLinks: true diff --git a/tests/docs/smoke-all/metadata-merge/revealjs-controls-boolean.qmd b/tests/docs/smoke-all/metadata-merge/revealjs-controls-boolean.qmd new file mode 100644 index 00000000000..3e75b5e844b --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/revealjs-controls-boolean.qmd @@ -0,0 +1,22 @@ +--- +title: Controls and preview links set in front matter and metadata-files +format: revealjs +metadata-files: + - revealjs-controls-boolean-extra.yml +controls: false +previewLinks: false +_quarto: + tests: + revealjs: + ensureFileRegexMatches: + - + - 'controls: false,' + - 'previewLinks: false,' + - + - 'controls: true,' + - 'previewLinks: true,' +--- + +## Slide + +A boolean `controls` or `previewLinks` in the front matter wins over its `metadata-files`, like any other key. diff --git a/tests/docs/smoke-all/metadata-merge/website-categories/.gitignore b/tests/docs/smoke-all/metadata-merge/website-categories/.gitignore new file mode 100644 index 00000000000..812977e5f74 --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/website-categories/.gitignore @@ -0,0 +1,4 @@ +/.quarto/ +_site/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/metadata-merge/website-categories/_quarto.yml b/tests/docs/smoke-all/metadata-merge/website-categories/_quarto.yml new file mode 100644 index 00000000000..b0ae96b01d0 --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/website-categories/_quarto.yml @@ -0,0 +1,8 @@ +project: + type: website + +website: + title: "metadata-merge categories" + +categories: + - from-project diff --git a/tests/docs/smoke-all/metadata-merge/website-categories/index.qmd b/tests/docs/smoke-all/metadata-merge/website-categories/index.qmd new file mode 100644 index 00000000000..79c0997e309 --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/website-categories/index.qmd @@ -0,0 +1,5 @@ +--- +title: Website categories merge +--- + +Home page of the fixture. The assertions live in `posts/post.qmd`. diff --git a/tests/docs/smoke-all/metadata-merge/website-categories/posts/_metadata.yml b/tests/docs/smoke-all/metadata-merge/website-categories/posts/_metadata.yml new file mode 100644 index 00000000000..9b26cbfdc26 --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/website-categories/posts/_metadata.yml @@ -0,0 +1,4 @@ +author: + - name: from-directory +categories: + - from-directory diff --git a/tests/docs/smoke-all/metadata-merge/website-categories/posts/post.qmd b/tests/docs/smoke-all/metadata-merge/website-categories/posts/post.qmd new file mode 100644 index 00000000000..61b185cefdd --- /dev/null +++ b/tests/docs/smoke-all/metadata-merge/website-categories/posts/post.qmd @@ -0,0 +1,19 @@ +--- +title: A post +author: from-document +categories: + - from-document +_quarto: + tests: + html: + ensureFileRegexMatches: + - + - 'class="quarto-category">from-project<' + - 'class="quarto-category">from-directory<' + - 'class="quarto-category">from-document<' + - '' + - '' + - [] +--- + +In a website, `categories` from `_quarto.yml`, the directory `_metadata.yml` and the document front matter are all shown on the page. The `author` from the directory `_metadata.yml` and from the document front matter are both emitted too. diff --git a/tests/smoke/project/project-metadata-merge.test.ts b/tests/smoke/project/project-metadata-merge.test.ts new file mode 100644 index 00000000000..e5439e55422 --- /dev/null +++ b/tests/smoke/project/project-metadata-merge.test.ts @@ -0,0 +1,64 @@ +/* + * project-metadata-merge.test.ts + * + * Copyright (C) 2026 Posit Software, PBC + */ +import { existsSync } from "../../../src/deno_ral/fs.ts"; +import { join } from "../../../src/deno_ral/path.ts"; + +import { testQuartoCmd } from "../../test.ts"; +import { docs } from "../../utils.ts"; + +import { ensureFileRegexMatches, noErrorsOrWarnings } from "../../verify.ts"; + +const projectDir = docs("project/metadata-merge"); + +// keys defined in both _quarto.yml and the front matter are merged: the +// document wins shared leaves, the project's other leaves and list entries stay +const mergedKeys = [ + /KEYWORDS: \[project-keyword, shared-keyword, document-keyword\]/, + /PARAMS: \[project-value\] \[document-value\]/, + /FROM-PROJECT: \[project-value\]/, + /SHARED: \[document-value\]/, + /FROM-DOCUMENT: \[document-value\]/, + /CUSTOM-LIST: \[project-item, document-item\]/, + /
\s*

project-value<\/p>/, +]; + +testQuartoCmd( + "render", + [projectDir, "--to", "html"], + [ + noErrorsOrWarnings, + ensureFileRegexMatches(join(projectDir, "markdown.html"), mergedKeys), + ensureFileRegexMatches( + join(projectDir, "knitr.html"), + [ + ...mergedKeys, + // a key holding an inline expression takes the executed value as a + // whole, without repeating a value the project also lists + /TAGS: \[project-tag, document-tag\]/, + /EVALUATED: \[evaluated-value\]/, + ], + [/`\{r\}/], + ), + ], + { + teardown: () => { + for ( + const file of [ + "markdown.html", + "markdown_files", + "knitr.html", + "knitr_files", + ] + ) { + const path = join(projectDir, file); + if (existsSync(path)) { + Deno.removeSync(path, { recursive: true }); + } + } + return Promise.resolve(); + }, + }, +); diff --git a/tests/smoke/render/render-cli-metadata-file.test.ts b/tests/smoke/render/render-cli-metadata-file.test.ts new file mode 100644 index 00000000000..1e9075b4d44 --- /dev/null +++ b/tests/smoke/render/render-cli-metadata-file.test.ts @@ -0,0 +1,50 @@ +/* + * render-cli-metadata-file.test.ts + * + * Copyright (C) 2026 Posit Software, PBC + */ +import { existsSync } from "../../../src/deno_ral/fs.ts"; +import { join } from "../../../src/deno_ral/path.ts"; + +import { testQuartoCmd } from "../../test.ts"; +import { docs } from "../../utils.ts"; + +import { ensureFileRegexMatches, noErrorsOrWarnings } from "../../verify.ts"; + +const dir = docs("metadata-merge-cli"); +const input = join(dir, "cli-metadata-file.qmd"); +const output = join(dir, "cli-metadata-file.html"); + +// the document's front matter wins over a --metadata-file for a value both +// define, and what only the metadata file defines is kept +testQuartoCmd( + "render", + [ + input, + "--to", + "html", + "--metadata-file", + join(dir, "cli-metadata-file.yml"), + ], + [ + noErrorsOrWarnings, + ensureFileRegexMatches(output, [ + //, + /

\s*

document-value<\/p>/, + /

\s*

document-value<\/p>/, + /

\s*

document-value<\/p>/, + /

\s*

metadata-file-value<\/p>/, + ]), + ], + { + teardown: () => { + for (const file of ["cli-metadata-file.html", "cli-metadata-file_files"]) { + const path = join(dir, file); + if (existsSync(path)) { + Deno.removeSync(path, { recursive: true }); + } + } + return Promise.resolve(); + }, + }, +); diff --git a/tests/smoke/render/render-freeze.test.ts b/tests/smoke/render/render-freeze.test.ts index 82979a58444..4babf9ca3d6 100644 --- a/tests/smoke/render/render-freeze.test.ts +++ b/tests/smoke/render/render-freeze.test.ts @@ -12,7 +12,11 @@ import { Metadata } from "../../../src/config/types.ts"; import { removeIfEmptyDir, safeRemoveIfExists } from "../../../src/core/path.ts"; import { runQuarto } from "../../quarto-cmd.ts"; import { ExecuteOutput, Verify } from "../../test.ts"; -import { noErrors, outputCreated } from "../../verify.ts"; +import { + ensureHtmlElementContents, + noErrors, + outputCreated, +} from "../../verify.ts"; import { testRender } from "./render.ts"; const regex = /output file: .*\.knit\.md/m; @@ -185,3 +189,55 @@ testRender( teardown: testContext.teardown, }, ); + +// Inline R expressions in YAML front matter stay evaluated when the render +// reuses the frozen execution results instead of running knitr again +const inlineYamlFileName = "freeze-inline-yaml"; +const inlineYamlDir = Deno.makeTempDirSync(); +const inlineYamlPath = join(inlineYamlDir, `${inlineYamlFileName}.qmd`); +const inlineYamlContext = testFileContext( + inlineYamlPath, + { + title: "Title `r 1 + 1`", + "custom-key": "Custom `r 2 + 3`", + format: "html", + freeze: true, + }, + [ + "::: {#custom-key}", + "{{< meta custom-key >}}", + ":::", + "", + "```{r}", + "1 + 1", + "```", + "", + ], +); +testRender( + inlineYamlDir + "/", + "html", + false, + [ + noErrors, + useFrozen, + ensureHtmlElementContents( + join(inlineYamlDir, `${inlineYamlFileName}.html`), + { selectors: ["h1.title"], matches: [/^\s*Title 2\s*$/] }, + ), + ensureHtmlElementContents( + join(inlineYamlDir, `${inlineYamlFileName}.html`), + { selectors: ["#custom-key"], matches: [/^\s*Custom 5\s*$/] }, + ), + ], + { + name: "frozen inline R in YAML", + setup: inlineYamlContext.setup, + teardown: async () => { + await inlineYamlContext.teardown(); + const freezerDir = join(inlineYamlDir, "_freeze"); + safeRemoveIfExists(join(freezerDir, inlineYamlFileName)); + removeIfEmptyDir(freezerDir); + }, + }, +);